Skip to content

Commit

Permalink
Merge pull request #125 from jfrog/GH-122-add-aws-iam-role-resource
Browse files Browse the repository at this point in the history
Add 'platform_aws_iam_role' resource
  • Loading branch information
alexhung authored Sep 16, 2024
2 parents 3748ecc + 20e6e2a commit 2992840
Show file tree
Hide file tree
Showing 14 changed files with 415 additions and 9 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/acceptance-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.21
go-version: 1.22
- name: Install Helm
uses: azure/[email protected]
- name: Install Terraform CLI
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.21
go-version: 1.22
-
name: Import GPG key
id: import_gpg
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 1.12.0 (September 13, 2024). Tested on Artifactory 7.90.10 with Terraform 1.9.5 and OpenTofu 1.8.2

FEATURES:

**New Resource:**
* `platform_aws_iam_role` - Resource to manage AWS IAM role. PR: [#125](https://github.com/jfrog/terraform-provider-platform/pull/125)

## 1.11.1 (September 9, 2024). Tested on Artifactory 7.90.9 with Terraform 1.9.5 and OpenTofu 1.8.2

IMPROVEMENTS:
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ During the provider start up, if it finds env var `TFC_WORKLOAD_IDENTITY_TOKEN`

Follow [confgure an OIDC integration](https://jfrog.com/help/r/jfrog-platform-administration-documentation/configure-an-oidc-integration). Enter a name for the provider, e.g. `terraform-cloud`. Use `https://app.terraform.io` for "Provider URL". Choose your own value for "Audience", e.g. `jfrog-terraform-cloud`.

Then [configure an identity mapping](https://jfrog.com/help/r/jfrog-platform-administration-documentation/configure-identity-mappings) with an empty "Claims JSON" (`{}`), and select the "Token scope", "User", and "Service" as desired.
Then [configure an identity mapping](https://jfrog.com/help/r/jfrog-platform-administration-documentation/configure-identity-mappings) with appropriate "Claims JSON" (e.g. `aud`, `sub` at minimum. See [Terraform Workload Identity - Configuring Trust with your Cloud Platform](https://developer.hashicorp.com/terraform/cloud-docs/workspaces/dynamic-provider-credentials/workload-identity-tokens#configuring-trust-with-your-cloud-platform)), and select the "Token scope", "User", and "Service" as desired.

#### Set environment variable in your Terraform Workspace

Expand Down
39 changes: 39 additions & 0 deletions docs/resources/aws_iam_role.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "platform_aws_iam_role Resource - terraform-provider-platform"
subcategory: ""
description: |-
Provides a resource to manage AWS IAM roles for JFrog platform users. You can use the AWS IAM roles for passwordless access to Amazon EKS. For more information, see Passwordless Access for Amazon EKS https://jfrog.com/help/r/jfrog-installation-setup-documentation/passwordless-access-for-amazon-eks.
->Only available for Artifactory 7.90.10 or later.
---

# platform_aws_iam_role (Resource)

Provides a resource to manage AWS IAM roles for JFrog platform users. You can use the AWS IAM roles for passwordless access to Amazon EKS. For more information, see [Passwordless Access for Amazon EKS](https://jfrog.com/help/r/jfrog-installation-setup-documentation/passwordless-access-for-amazon-eks).

->Only available for Artifactory 7.90.10 or later.

## Example Usage

```terraform
resource "platform_aws_iam_role" "myuser-aws-iam-role" {
username = "myuser"
iam_role = "arn:aws:iam::000000000000:role/example"
}
```

<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `iam_role` (String) The AWS IAM role. Must follow the regex, "^arn:aws:iam::\d{12}:role/[\w+=,.@:-]+$"
- `username` (String) The JFrog Platform user name.

## Import

Import is supported using the following syntax:

```shell
terraform import platform_aws_iam_role.myuser-aws-iam-role myuser
```
1 change: 1 addition & 0 deletions examples/resources/platform_aws_iam_role/import.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
terraform import platform_aws_iam_role.myuser-aws-iam-role myuser
4 changes: 4 additions & 0 deletions examples/resources/platform_aws_iam_role/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
resource "platform_aws_iam_role" "myuser-aws-iam-role" {
username = "myuser"
iam_role = "arn:aws:iam::000000000000:role/example"
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/jfrog/terraform-provider-platform
// if you need to do local dev, literally just uncomment the line below
// replace github.com/jfrog/terraform-provider-shared => ../terraform-provider-shared

go 1.21.5
go 1.22.5

require (
github.com/go-resty/resty/v2 v2.15.0
Expand Down
1 change: 1 addition & 0 deletions pkg/platform/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ func (p *PlatformProvider) DataSources(ctx context.Context) []func() datasource.

func (p *PlatformProvider) Resources(ctx context.Context) []func() resource.Resource {
return []func() resource.Resource{
NewAWSIAMRoleResource,
NewLicenseResource,
NewGlobalRoleResource,
NewOIDCConfigurationResource,
Expand Down
243 changes: 243 additions & 0 deletions pkg/platform/resource_aws_iam_role.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
package platform

import (
"context"
"fmt"
"net/http"
"regexp"

"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/jfrog/terraform-provider-shared/util"
utilfw "github.com/jfrog/terraform-provider-shared/util/fw"
)

const (
AWSIAMRolesEndpoint = "access/api/v1/aws/iam_role"
AWSIAMRoleEndpoint = "access/api/v1/aws/iam_role/{username}"
)

func NewAWSIAMRoleResource() resource.Resource {
return &AWSIAMRoleResource{
TypeName: "platform_aws_iam_role",
}
}

type AWSIAMRoleResource struct {
ProviderData PlatformProviderMetadata
TypeName string
}

type AWSIAMRoleResourceModel struct {
Username types.String `tfsdk:"username"`
IAMRole types.String `tfsdk:"iam_role"`
}

type AWSIAMRoleAPIModel struct {
Username string `json:"username"`
IAMRole string `json:"iam_role"`
}

func (r *AWSIAMRoleResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = r.TypeName
}

func (r *AWSIAMRoleResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"username": schema.StringAttribute{
Required: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Description: "The JFrog Platform user name.",
},
"iam_role": schema.StringAttribute{
Required: true,
Validators: []validator.String{
stringvalidator.RegexMatches(regexp.MustCompile(`^arn:aws:iam::\d{12}:role/[\w+=,.@:-]+$`), "Must follow the regex, \"^arn:aws:iam::\\d{12}:role/[\\w+=,.@:-]+$\""),
},
MarkdownDescription: "The AWS IAM role. Must follow the regex, \"^arn:aws:iam::\\d{12}:role/[\\w+=,.@:-]+$\"",
},
},
MarkdownDescription: "Provides a resource to manage AWS IAM roles for JFrog platform users. You can use the AWS IAM roles for passwordless access to Amazon EKS. For more information, see [Passwordless Access for Amazon EKS](https://jfrog.com/help/r/jfrog-installation-setup-documentation/passwordless-access-for-amazon-eks).\n\n" +
"->Only available for Artifactory 7.90.10 or later.",
}
}

func (r *AWSIAMRoleResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
// Prevent panic if the provider has not been configured.
if req.ProviderData == nil {
return
}
r.ProviderData = req.ProviderData.(PlatformProviderMetadata)

supported, err := util.CheckVersion(r.ProviderData.ArtifactoryVersion, "7.90.10")
if err != nil {
resp.Diagnostics.AddError(
"Failed to check Artifactory version",
err.Error(),
)
return
}

if !supported {
resp.Diagnostics.AddError(
"Unsupported Artifactory version",
fmt.Sprintf("This resource is supported by Artifactory version 7.90.10 or later. Current version: %s", r.ProviderData.ArtifactoryVersion),
)
return
}
}

func (r *AWSIAMRoleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
go util.SendUsageResourceCreate(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName)

var plan AWSIAMRoleResourceModel

// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}

role := AWSIAMRoleAPIModel{
Username: plan.Username.ValueString(),
IAMRole: plan.IAMRole.ValueString(),
}

response, err := r.ProviderData.Client.R().
SetBody(role).
Put(AWSIAMRolesEndpoint)

if err != nil {
utilfw.UnableToCreateResourceError(resp, err.Error())
return
}

if response.IsError() {
utilfw.UnableToCreateResourceError(resp, response.String())
return
}

// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}

func (r *AWSIAMRoleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
go util.SendUsageResourceRead(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName)

var state AWSIAMRoleResourceModel

resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}

var role AWSIAMRoleAPIModel

response, err := r.ProviderData.Client.R().
SetPathParam("username", state.Username.ValueString()).
SetResult(&role).
Get(AWSIAMRoleEndpoint)

if err != nil {
utilfw.UnableToRefreshResourceError(resp, err.Error())
return
}

if response.IsError() {
utilfw.UnableToRefreshResourceError(resp, response.String())
return
}

// Treat HTTP 404 Not Found status as a signal to recreate resource
// and return early
if response.StatusCode() == http.StatusNotFound {
resp.State.RemoveResource(ctx)
return
}

// Convert from the API data model to the Terraform data model
// and refresh any attribute values.
state.Username = types.StringValue(role.Username)
state.IAMRole = types.StringValue(role.IAMRole)

resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}

func (r *AWSIAMRoleResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
go util.SendUsageResourceUpdate(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName)

var plan AWSIAMRoleResourceModel

// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}

role := AWSIAMRoleAPIModel{
Username: plan.Username.ValueString(),
IAMRole: plan.IAMRole.ValueString(),
}

response, err := r.ProviderData.Client.R().
SetBody(role).
Put(AWSIAMRolesEndpoint)

if err != nil {
utilfw.UnableToUpdateResourceError(resp, err.Error())
return
}

if response.IsError() {
utilfw.UnableToUpdateResourceError(resp, response.String())
return
}

// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}

func (r *AWSIAMRoleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
go util.SendUsageResourceDelete(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName)

var state AWSIAMRoleResourceModel

diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

response, err := r.ProviderData.Client.R().
SetPathParam("username", state.Username.ValueString()).
Delete(AWSIAMRoleEndpoint)

if err != nil {
utilfw.UnableToDeleteResourceError(resp, err.Error())
return
}

if response.IsError() {
utilfw.UnableToDeleteResourceError(resp, response.String())
return
}

// If the logic reaches here, it implicitly succeeded and will remove
// the resource from state if there are no other errors.
}

func (r *AWSIAMRoleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("username"), req, resp)
}
Loading

0 comments on commit 2992840

Please sign in to comment.