From 38bb9e6d21f77d989d47dfd5befde81d61e47fd1 Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Mon, 3 Jun 2024 13:23:10 -0700 Subject: [PATCH 1/6] Migrate project_user resource to Plugin Framework --- pkg/project/provider/framework.go | 1 + pkg/project/provider/sdkv2.go | 1 - pkg/project/resource/resource_project_user.go | 427 +++++++++++------- .../resource/resource_project_user_test.go | 102 ++++- 4 files changed, 371 insertions(+), 160 deletions(-) diff --git a/pkg/project/provider/framework.go b/pkg/project/provider/framework.go index 5d2b52c0..06405da4 100644 --- a/pkg/project/provider/framework.go +++ b/pkg/project/provider/framework.go @@ -183,6 +183,7 @@ func (p *ProjectProvider) Resources(ctx context.Context) []func() resource.Resou project.NewProjectResource, project.NewProjectEnvironmentResource, project.NewProjectGroupResource, + project.NewProjectUserResource, } } diff --git a/pkg/project/provider/sdkv2.go b/pkg/project/provider/sdkv2.go index 494da382..166a8d90 100644 --- a/pkg/project/provider/sdkv2.go +++ b/pkg/project/provider/sdkv2.go @@ -49,7 +49,6 @@ func SdkV2() *schema.Provider { productId, map[string]*schema.Resource{ "project_role": resource.ProjectRoleResource(), - "project_user": resource.ProjectUserResource(), "project_repository": resource.ProjectRepositoryResource(), }, ), diff --git a/pkg/project/resource/resource_project_user.go b/pkg/project/resource/resource_project_user.go index 76553914..fc211fd4 100644 --- a/pkg/project/resource/resource_project_user.go +++ b/pkg/project/resource/resource_project_user.go @@ -6,204 +6,327 @@ import ( "net/http" "strings" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "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/booldefault" + "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" - "github.com/jfrog/terraform-provider-shared/util/sdk" - "github.com/jfrog/terraform-provider-shared/validator" + utilfw "github.com/jfrog/terraform-provider-shared/util/fw" + validatorfw_string "github.com/jfrog/terraform-provider-shared/validator/fw/string" ) const ProjectUsersUrl = "access/api/v1/projects/{projectKey}/users/{name}" -type ProjectUser struct { - ProjectKey string `json:"-"` - Name string `json:"name"` - Roles []string `json:"roles"` - IgnoreMissingUser bool `json:"-"` +func NewProjectUserResource() resource.Resource { + return &ProjectUserResource{} } -func (m ProjectUser) Id() string { - return fmt.Sprintf(`%s:%s`, m.ProjectKey, m.Name) +type ProjectUserResource struct { + ProviderData util.ProviderMetadata + TypeName string } -func ProjectUserResource() *schema.Resource { - var projectUserSchema = map[string]*schema.Schema{ - "project_key": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - ValidateDiagFunc: validator.ProjectKey, - Description: "The key of the project to which the user should be assigned to.", - }, - "name": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotEmpty), - Description: "The name of an artifactory user.", - }, - "roles": { - Type: schema.TypeSet, - Required: true, - Elem: &schema.Schema{Type: schema.TypeString}, - MinItems: 1, - Description: "List of pre-defined Project or custom roles. Must have at least 1 role, e.g. 'Viewer'", - }, - "ignore_missing_user": { - Type: schema.TypeBool, - Optional: true, - Default: false, - Description: "When set to `true`, the resource will not fail if the user does not exist. Default to `false`. This is useful when the user is externally managed and the local account wasn't created yet.", +type ProjectUserResourceModel struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + ProjectKey types.String `tfsdk:"project_key"` + Roles types.Set `tfsdk:"roles"` + IgnoreMissingUser types.Bool `tfsdk:"ignore_missing_user"` +} + +type ProjectUserAPIModel struct { + Name string `json:"name"` + Roles []string `json:"roles"` +} + +func (r *ProjectUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_user" + r.TypeName = resp.TypeName +} + +func (r *ProjectUserResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Version: 1, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "name": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Description: "The name of an artifactory user.", + }, + "project_key": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + validatorfw_string.ProjectKey(), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Description: "The key of the project to which the user should be assigned to.", + }, + "roles": schema.SetAttribute{ + ElementType: types.StringType, + Required: true, + Validators: []validator.Set{ + setvalidator.SizeAtLeast(1), + }, + Description: "List of pre-defined Project or custom roles. Must have at least 1 role, e.g. 'Viewer'", + }, + "ignore_missing_user": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + Description: "When set to `true`, the resource will not fail if the user does not exist. Default to `false`. This is useful when the user is externally managed and the local account wasn't created yet.", + }, }, + Description: "Add a user as project member. Element has one to one mapping with the [JFrog Project Users API](https://jfrog.com/help/r/jfrog-rest-apis/add-or-update-user-in-project). Requires a user assigned with the 'Administer the Platform' role or Project Admin permissions if `admin_privileges.manage_resoures` is enabled.", } +} - var packProjectUser = func(_ context.Context, data *schema.ResourceData, m ProjectUser) diag.Diagnostics { - setValue := sdk.MkLens(data) +func (r *ProjectUserResource) 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.(util.ProviderMetadata) +} - setValue("name", m.Name) - setValue("project_key", m.ProjectKey) - setValue("roles", m.Roles) - errors := setValue("ignore_missing_user", m.IgnoreMissingUser) +func (r *ProjectUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + go util.SendUsageResourceCreate(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) - if len(errors) > 0 { - return diag.Errorf("failed to pack project member %q", errors) - } + var plan ProjectUserResourceModel - return nil + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return } - var unpackProjectUser = func(d *schema.ResourceData) ProjectUser { - return ProjectUser{ - ProjectKey: d.Get("project_key").(string), - Name: d.Get("name").(string), - Roles: sdk.CastToStringArr(d.Get("roles").(*schema.Set).List()), - IgnoreMissingUser: d.Get("ignore_missing_user").(bool), - } - } + projectKey := plan.ProjectKey.ValueString() - var readProjectUser = func(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics { - projectUser := unpackProjectUser(data) - var loadedProjectUser ProjectUser + var roles []string + resp.Diagnostics.Append(plan.Roles.ElementsAs(ctx, &roles, false)...) + if resp.Diagnostics.HasError() { + return + } - var projectError ProjectErrorsResponse - resp, err := m.(util.ProviderMetadata).Client.R(). - SetPathParams(map[string]string{ - "projectKey": projectUser.ProjectKey, - "name": projectUser.Name, - }). - SetError(&projectError). - SetResult(&loadedProjectUser). - Get(ProjectUsersUrl) + user := ProjectUserAPIModel{ + Name: plan.Name.ValueString(), + Roles: roles, + } - if err != nil { - return diag.FromErr(err) - } - if resp.StatusCode() == http.StatusNotFound && projectUser.IgnoreMissingUser { - // ignore missing user, reuse local info for state - loadedProjectUser = projectUser - } else if resp.IsError() { - return diag.Errorf("%s", projectError.String()) + var projectError ProjectErrorsResponse + response, err := r.ProviderData.Client.R(). + SetPathParams(map[string]string{ + "projectKey": projectKey, + "name": plan.Name.ValueString(), + }). + SetBody(user). + SetError(&projectError). + Put(ProjectUsersUrl) + if err != nil { + utilfw.UnableToCreateResourceError(resp, err.Error()) + } + if response.StatusCode() == http.StatusNotFound { + if plan.IgnoreMissingUser.ValueBool() { + resp.Diagnostics.AddWarning( + fmt.Sprintf("user '%s' not found", user.Name), + "but ignore_missing_user is set to true, project membership not created", + ) + } else { + resp.Diagnostics.AddError( + fmt.Sprintf("user '%s' not found", user.Name), + "project membership not created", + ) + return } + } else if response.IsError() { + utilfw.UnableToCreateResourceError(resp, projectError.String()) + } - loadedProjectUser.ProjectKey = projectUser.ProjectKey - loadedProjectUser.IgnoreMissingUser = projectUser.IgnoreMissingUser + plan.ID = types.StringValue(fmt.Sprintf("%s:%s", projectKey, user.Name)) - return packProjectUser(ctx, data, loadedProjectUser) - } + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ProjectUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + go util.SendUsageResourceRead(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) - var upsertProjectUser = func(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics { - projectUser := unpackProjectUser(data) + var state ProjectUserResourceModel + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } - var projectError ProjectErrorsResponse - resp, err := m.(util.ProviderMetadata).Client.R(). - SetPathParams(map[string]string{ - "projectKey": projectUser.ProjectKey, - "name": projectUser.Name, - }). - SetBody(&projectUser). - SetError(&projectError). - Put(ProjectUsersUrl) + projectKey := state.ProjectKey.ValueString() + + var user ProjectUserAPIModel + var projectError ProjectErrorsResponse + response, err := r.ProviderData.Client.R(). + SetPathParams(map[string]string{ + "projectKey": projectKey, + "name": state.Name.ValueString(), + }). + SetResult(&user). + SetError(&projectError). + Get(ProjectUsersUrl) + if err != nil { + utilfw.UnableToRefreshResourceError(resp, err.Error()) + return + } - // allow missing user? -> report warning and ignore error - diagnostics := diag.Diagnostics{} + updateStateValues := true + if response.StatusCode() == http.StatusNotFound { + if state.IgnoreMissingUser.ValueBool() { + updateStateValues = false + } else { + resp.State.RemoveResource(ctx) + return + } + } else if response.IsError() { + utilfw.UnableToRefreshResourceError(resp, projectError.String()) + return + } - if err != nil { - return diag.FromErr(err) + if updateStateValues { + state.ID = types.StringValue(fmt.Sprintf("%s:%s", projectKey, user.Name)) + state.Name = types.StringValue(user.Name) + state.ProjectKey = types.StringValue(projectKey) + roles, ds := types.SetValueFrom(ctx, types.StringType, user.Roles) + if ds.HasError() { + resp.Diagnostics.Append(ds...) + return } - if resp.StatusCode() == http.StatusNotFound { - if projectUser.IgnoreMissingUser { - diagnostics = append(diagnostics, diag.Diagnostic{ - Severity: diag.Warning, - Summary: fmt.Sprintf("user '%s' not found, but ignore_missing_user is set to true, project membership not created", projectUser.Name), - }) - } else { - return diag.Errorf("user '%s' not found, project membership not created", projectUser.Name) - } - } else if resp.IsError() { - return diag.Errorf("%s", projectError.String()) + state.Roles = roles + + if state.IgnoreMissingUser.IsNull() { + state.IgnoreMissingUser = types.BoolValue(false) } + } - data.SetId(projectUser.Id()) + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} - diagnostics = append(diagnostics, readProjectUser(ctx, data, m)...) +func (r *ProjectUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + go util.SendUsageResourceUpdate(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) - if len(diagnostics) > 0 { - return diagnostics - } + var plan ProjectUserResourceModel - return nil + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return } - var deleteProjectUser = func(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics { - projectUser := unpackProjectUser(data) + projectKey := plan.ProjectKey.ValueString() + + var roles []string + resp.Diagnostics.Append(plan.Roles.ElementsAs(ctx, &roles, false)...) + if resp.Diagnostics.HasError() { + return + } - var projectError ProjectErrorsResponse - resp, err := m.(util.ProviderMetadata).Client.R(). - SetPathParams(map[string]string{ - "projectKey": projectUser.ProjectKey, - "name": projectUser.Name, - }). - SetError(&projectError). - Delete(ProjectUsersUrl) + user := ProjectUserAPIModel{ + Name: plan.Name.ValueString(), + Roles: roles, + } - if err != nil { - return diag.FromErr(err) - } - if resp.IsError() && resp.StatusCode() != http.StatusNotFound { - return diag.Errorf("%s", projectError.String()) + var projectError ProjectErrorsResponse + response, err := r.ProviderData.Client.R(). + SetPathParams(map[string]string{ + "projectKey": projectKey, + "name": plan.Name.ValueString(), + }). + SetBody(user). + SetError(&projectError). + Put(ProjectUsersUrl) + if err != nil { + utilfw.UnableToUpdateResourceError(resp, err.Error()) + } + if response.StatusCode() == http.StatusNotFound { + if plan.IgnoreMissingUser.ValueBool() { + resp.Diagnostics.AddWarning( + fmt.Sprintf("user '%s' not found", user.Name), + "but ignore_missing_user is set to true, project membership not updated", + ) + } else { + resp.Diagnostics.AddError( + fmt.Sprintf("user '%s' not found", user.Name), + "project membership not updated", + ) + return } + } else if response.IsError() { + utilfw.UnableToUpdateResourceError(resp, projectError.String()) + } - data.SetId("") + plan.ID = types.StringValue(fmt.Sprintf("%s:%s", projectKey, user.Name)) - return nil - } + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} - var importForProjectKeyUserName = func(d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { - parts := strings.SplitN(d.Id(), ":", 2) - if len(parts) != 2 || parts[0] == "" || parts[1] == "" { - return nil, fmt.Errorf("unexpected format of ID (%s), expected project_key:name", d.Id()) - } +func (r *ProjectUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + go util.SendUsageResourceDelete(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) - d.Set("project_key", parts[0]) - d.Set("name", parts[1]) + var state ProjectUserResourceModel - return []*schema.ResourceData{d}, nil + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return } - return &schema.Resource{ - CreateContext: upsertProjectUser, - ReadContext: readProjectUser, - UpdateContext: upsertProjectUser, - DeleteContext: deleteProjectUser, - - Importer: &schema.ResourceImporter{ - State: importForProjectKeyUserName, - }, + projectKey := state.ProjectKey.ValueString() + + var projectError ProjectErrorsResponse + response, err := r.ProviderData.Client.R(). + SetPathParams(map[string]string{ + "projectKey": projectKey, + "name": state.Name.ValueString(), + }). + SetError(&projectError). + Delete(ProjectUsersUrl) + if err != nil { + utilfw.UnableToDeleteResourceError(resp, err.Error()) + return + } + if response.IsError() && response.StatusCode() != http.StatusNotFound { + utilfw.UnableToDeleteResourceError(resp, projectError.String()) + return + } - Schema: projectUserSchema, - SchemaVersion: 1, + // If the logic reaches here, it implicitly succeeded and will remove + // the resource from state if there are no other errors. +} - Description: "Add a user as project member. Element has one to one mapping with the [JFrog Project Users API](https://jfrog.com/help/r/jfrog-rest-apis/add-or-update-user-in-project). Requires a user assigned with the 'Administer the Platform' role or Project Admin permissions if `admin_privileges.manage_resoures` is enabled.", +// ImportState imports the resource into the Terraform state. +func (r *ProjectUserResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + parts := strings.SplitN(req.ID, ":", 2) + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + resp.Diagnostics.AddError( + "Unexpected Import Identifier", + "Expected project_key:name", + ) + return } + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_key"), parts[0])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), parts[1])...) } diff --git a/pkg/project/resource/resource_project_user_test.go b/pkg/project/resource/resource_project_user_test.go index 8fe7c004..2c15cab4 100644 --- a/pkg/project/resource/resource_project_user_test.go +++ b/pkg/project/resource/resource_project_user_test.go @@ -10,10 +10,98 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/resource" acctest "github.com/jfrog/terraform-provider-project/pkg/project/acctest" project "github.com/jfrog/terraform-provider-project/pkg/project/resource" + "github.com/jfrog/terraform-provider-shared/testutil" "github.com/jfrog/terraform-provider-shared/util" ) -func TestAccProjectUser(t *testing.T) { +func TestAccProjectUser_UpgradeFromSDKv2(t *testing.T) { + _, _, projectName := testutil.MkNames("test-project-", "project") + _, fqrn, userName := testutil.MkNames("test-project-user-", "project_user") + + projectKey := strings.ToLower(acctest.RandSeq(10)) + + email := userName + "@tempurl.org" + + params := map[string]interface{}{ + "project_name": projectName, + "project_key": projectKey, + "username": userName, + "email": email, + "roles": `["Developer","Project Admin"]`, + } + + template := ` + resource "artifactory_managed_user" "{{ .username }}" { + name = "{{ .username }}" + email = "{{ .email }}" + password = "Password1!" + admin = false + } + + resource "project" "{{ .project_name }}" { + key = "{{ .project_key }}" + display_name = "{{ .project_name }}" + description = "test description" + admin_privileges { + manage_members = true + manage_resources = true + index_resources = true + } + max_storage_in_gibibytes = 1 + block_deployments_on_limit = true + email_notification = false + + use_project_user_resource = true + } + + resource "project_user" "{{ .username }}" { + project_key = project.{{ .project_name }}.key + name = artifactory_managed_user.{{ .username }}.name + roles = {{ .roles }} + } + ` + + config := util.ExecuteTemplate("TestAccProjectUser", template, params) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + Steps: []resource.TestStep{ + { + ExternalProviders: map[string]resource.ExternalProvider{ + "artifactory": { + Source: "jfrog/artifactory", + }, + "project": { + Source: "jfrog/project", + VersionConstraint: "1.6.1", + }, + }, + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(fqrn, "project_key", fmt.Sprintf("%s", params["project_key"])), + resource.TestCheckResourceAttr(fqrn, "name", userName), + resource.TestCheckResourceAttr(fqrn, "ignore_missing_user", "false"), + resource.TestCheckResourceAttr(fqrn, "roles.#", "2"), + resource.TestCheckResourceAttr(fqrn, "roles.0", "Developer"), + resource.TestCheckResourceAttr(fqrn, "roles.1", "Project Admin"), + ), + }, + { + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + ExternalProviders: map[string]resource.ExternalProvider{ + "artifactory": { + Source: "jfrog/artifactory", + }, + }, + Config: config, + PlanOnly: true, + ConfigPlanChecks: testutil.ConfigPlanChecks(fqrn), + }, + }, + }) +} + +func TestAccProjectUser_full(t *testing.T) { projectName := fmt.Sprintf("tftestprojects%s", acctest.RandSeq(10)) projectKey := strings.ToLower(acctest.RandSeq(10)) @@ -78,7 +166,7 @@ func TestAccProjectUser(t *testing.T) { CheckDestroy: acctest.VerifyDeleted(resourceName, func(id string, request *resty.Request) (*resty.Response, error) { return verifyProjectUser(username, projectKey, request) }), - ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, ExternalProviders: map[string]resource.ExternalProvider{ "artifactory": { Source: "jfrog/artifactory", @@ -165,7 +253,7 @@ func TestAccProjectUser_invalid_roles(t *testing.T) { config := util.ExecuteTemplate("TestAccProjectUser", template, params) resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, ExternalProviders: map[string]resource.ExternalProvider{ "artifactory": { Source: "jfrog/artifactory", @@ -174,7 +262,7 @@ func TestAccProjectUser_invalid_roles(t *testing.T) { Steps: []resource.TestStep{ { Config: config, - ExpectError: regexp.MustCompile(`.*Attribute roles requires 1 item minimum, but config has only 0 declared.*`), + ExpectError: regexp.MustCompile(`.*Attribute roles set must contain at least 1 elements, got: 0.*`), }, }, }) @@ -223,11 +311,11 @@ func TestAccProjectUser_missing_user_fails(t *testing.T) { config := util.ExecuteTemplate("TestAccProjectUser", template, params) resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, Steps: []resource.TestStep{ { Config: config, - ExpectError: regexp.MustCompile(`user '.*' not found, project membership not created.*`), + ExpectError: regexp.MustCompile(`project membership not created.*`), }, }, }) @@ -281,7 +369,7 @@ func TestAccProjectMember_missing_user_ignored(t *testing.T) { CheckDestroy: acctest.VerifyDeleted(resourceName, func(id string, request *resty.Request) (*resty.Response, error) { return verifyProjectUser(username, projectKey, request) }), - ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, Steps: []resource.TestStep{ { Config: config, From 537e7af45a6569e2670baa9442f293e63a5c7102 Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Mon, 3 Jun 2024 13:24:31 -0700 Subject: [PATCH 2/6] Update CHANGELOG --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26b15dc9..530d4784 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,9 @@ -## 1.6.2 (June 3, 2024) +## 1.6.2 (June 4, 2024) IMPROVEMENTS: * resource/project_group is migrated to Plugin Framework. PR: [#131](https://github.com/jfrog/terraform-provider-project/pull/131) +* resource/project_user is migrated to Plugin Framework. PR: [#132](https://github.com/jfrog/terraform-provider-project/pull/132) ## 1.6.1 (May 31, 2024) From 8950fa381a39ddd838afcbd913ee46f884798fe7 Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Mon, 3 Jun 2024 15:06:31 -0700 Subject: [PATCH 3/6] Migrate project_role resource to Plugin Framework --- CHANGELOG.md | 2 +- pkg/project/provider/framework.go | 1 + pkg/project/provider/sdkv2.go | 1 - pkg/project/resource/resource_project_role.go | 438 +++++++++++------- .../resource/resource_project_role_test.go | 80 +++- pkg/project/resource/role.go | 16 + pkg/project/resource/validators.go | 9 - 7 files changed, 364 insertions(+), 183 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 530d4784..cb28874e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ IMPROVEMENTS: * resource/project_group is migrated to Plugin Framework. PR: [#131](https://github.com/jfrog/terraform-provider-project/pull/131) -* resource/project_user is migrated to Plugin Framework. PR: [#132](https://github.com/jfrog/terraform-provider-project/pull/132) +* resource/project_user, resource/project_role are migrated to Plugin Framework. PR: [#132](https://github.com/jfrog/terraform-provider-project/pull/132) ## 1.6.1 (May 31, 2024) diff --git a/pkg/project/provider/framework.go b/pkg/project/provider/framework.go index 06405da4..31b9dbe4 100644 --- a/pkg/project/provider/framework.go +++ b/pkg/project/provider/framework.go @@ -183,6 +183,7 @@ func (p *ProjectProvider) Resources(ctx context.Context) []func() resource.Resou project.NewProjectResource, project.NewProjectEnvironmentResource, project.NewProjectGroupResource, + project.NewProjectRoleResource, project.NewProjectUserResource, } } diff --git a/pkg/project/provider/sdkv2.go b/pkg/project/provider/sdkv2.go index 166a8d90..ac08009d 100644 --- a/pkg/project/provider/sdkv2.go +++ b/pkg/project/provider/sdkv2.go @@ -48,7 +48,6 @@ func SdkV2() *schema.Provider { ResourcesMap: sdk.AddTelemetry( productId, map[string]*schema.Resource{ - "project_role": resource.ProjectRoleResource(), "project_repository": resource.ProjectRepositoryResource(), }, ), diff --git a/pkg/project/resource/resource_project_role.go b/pkg/project/resource/resource_project_role.go index 06928489..76d1a5aa 100644 --- a/pkg/project/resource/resource_project_role.go +++ b/pkg/project/resource/resource_project_role.go @@ -6,12 +6,17 @@ import ( "net/http" "strings" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "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" - "github.com/jfrog/terraform-provider-shared/util/sdk" - "github.com/jfrog/terraform-provider-shared/validator" + utilfw "github.com/jfrog/terraform-provider-shared/util/fw" + validatorfw_string "github.com/jfrog/terraform-provider-shared/validator/fw/string" ) const ProjectRolesUrl = ProjectUrl + "/roles" @@ -59,208 +64,301 @@ var validRoleActions = []string{ "MANAGE_RESOURCES", } -type Role struct { +func NewProjectRoleResource() resource.Resource { + return &ProjectRoleResource{} +} + +type ProjectRoleResource struct { + ProviderData util.ProviderMetadata + TypeName string +} + +type ProjectRoleResourceModel struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Type types.String `tfsdk:"type"` + ProjectKey types.String `tfsdk:"project_key"` + Environments types.Set `tfsdk:"environments"` + Actions types.Set `tfsdk:"actions"` +} + +type ProjectRoleAPIModel struct { Name string `json:"name"` - Description string `json:"description"` Type string `json:"type"` Environments []string `json:"environments"` Actions []string `json:"actions"` } -func (r Role) Id() string { - return r.Name +func (r *ProjectRoleResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_role" + r.TypeName = resp.TypeName } -func (a Role) Equals(b Equatable) bool { - return a.Id() == b.Id() +func (r *ProjectRoleResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Version: 1, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "name": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 64), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "type": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.RegexMatches(customRoleTypeRegex, fmt.Sprintf(`Only "%s" is supported`, customRoleType)), + }, + Description: fmt.Sprintf(`Type of role. Only "%s" is supported`, customRoleType), + }, + "project_key": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + validatorfw_string.ProjectKey(), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Description: "Project key for this environment. This field supports only 2 - 32 lowercase alphanumeric and hyphen characters. Must begin with a letter.", + }, + "environments": schema.SetAttribute{ + ElementType: types.StringType, + Required: true, + Description: fmt.Sprintf("A repository can be available in different environments. Members with roles defined in the set environment will have access to the repository. List of pre-defined environments (%s)", strings.Join(validRoleEnvironments, ", ")), + }, + "actions": schema.SetAttribute{ + ElementType: types.StringType, + Required: true, + Description: fmt.Sprintf("List of pre-defined actions (%s)", strings.Join(validRoleActions, ", ")), + }, + }, + Description: "Create a project role. Element has one to one mapping with the [JFrog Project Roles API](https://www.jfrog.com/confluence/display/JFROG/Artifactory+REST+API#ArtifactoryRESTAPI-AddaNewRole). Requires a user assigned with the 'Administer the Platform' role or Project Admin permissions if `admin_privileges.manage_resoures` is enabled.", + } } -func ProjectRoleResource() *schema.Resource { - var projectRoleSchema = map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Required: true, - ValidateDiagFunc: validation.ToDiagFunc(validation.All( - validation.StringIsNotEmpty, - maxLength(64), - )), - }, - "type": { - Type: schema.TypeString, - Required: true, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringMatch(customRoleTypeRegex, fmt.Sprintf(`Only "%s" is supported`, customRoleType))), - Description: fmt.Sprintf(`Type of role. Only "%s" is supported`, customRoleType), - }, - "project_key": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - ValidateDiagFunc: validator.ProjectKey, - Description: "Project key for this environment. This field supports only 2 - 32 lowercase alphanumeric and hyphen characters. Must begin with a letter.", - }, - "environments": { - Type: schema.TypeSet, - Required: true, - Elem: &schema.Schema{Type: schema.TypeString}, - Description: fmt.Sprintf("A repository can be available in different environments. Members with roles defined in the set environment will have access to the repository. List of pre-defined environments (%s)", strings.Join(validRoleEnvironments, ", ")), - }, - "actions": { - Type: schema.TypeSet, - Required: true, - Elem: &schema.Schema{Type: schema.TypeString}, - Description: fmt.Sprintf("List of pre-defined actions (%s)", strings.Join(validRoleActions, ", ")), - }, +func (r *ProjectRoleResource) 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.(util.ProviderMetadata) +} + +func (r *ProjectRoleResource) 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 ProjectRoleResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + projectKey := plan.ProjectKey.ValueString() + + var environments []string + resp.Diagnostics.Append(plan.Environments.ElementsAs(ctx, &environments, false)...) + if resp.Diagnostics.HasError() { + return + } + + var actions []string + resp.Diagnostics.Append(plan.Actions.ElementsAs(ctx, &actions, false)...) + if resp.Diagnostics.HasError() { + return + } + + role := ProjectRoleAPIModel{ + Name: plan.Name.ValueString(), + Type: plan.Type.ValueString(), + Environments: environments, + Actions: actions, } - var packRole = func(_ context.Context, data *schema.ResourceData, role Role, projectKey string) diag.Diagnostics { - setValue := sdk.MkLens(data) + var projectError ProjectErrorsResponse + response, err := r.ProviderData.Client.R(). + SetPathParam("projectKey", projectKey). + SetBody(role). + SetError(&projectError). + Post(ProjectRolesUrl) + if err != nil { + utilfw.UnableToCreateResourceError(resp, err.Error()) + } + if response.IsError() { + utilfw.UnableToCreateResourceError(resp, projectError.String()) + } - setValue("name", role.Name) - setValue("type", role.Type) - setValue("project_key", projectKey) - setValue("environments", role.Environments) - errors := setValue("actions", role.Actions) + plan.ID = types.StringValue(role.Name) - if len(errors) > 0 { - return diag.Errorf("failed to pack project role %q", errors) - } + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} - return nil +func (r *ProjectRoleResource) 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 ProjectRoleResourceModel + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return } - var readProjectRole = func(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics { - var role Role - projectKey := data.Get("project_key").(string) - - var projectError ProjectErrorsResponse - resp, err := m.(util.ProviderMetadata).Client.R(). - SetPathParams(map[string]string{ - "projectKey": projectKey, - "roleName": data.Id(), - }). - SetResult(&role). - SetError(&projectError). - Get(ProjectRoleUrl) - - if err != nil { - return diag.FromErr(err) - } - if resp.StatusCode() == http.StatusNotFound { - data.SetId("") - return nil - } - if resp.IsError() { - return diag.Errorf("%s", projectError.String()) - } - - return packRole(ctx, data, role, projectKey) + projectKey := state.ProjectKey.ValueString() + + var role ProjectRoleAPIModel + var projectError ProjectErrorsResponse + response, err := r.ProviderData.Client.R(). + SetPathParams(map[string]string{ + "projectKey": projectKey, + "roleName": state.Name.ValueString(), + }). + SetResult(&role). + SetError(&projectError). + Get(ProjectRoleUrl) + if err != nil { + utilfw.UnableToRefreshResourceError(resp, err.Error()) + return } - var unpackRole = func(data *schema.ResourceData) Role { - d := &sdk.ResourceData{ResourceData: data} + if response.StatusCode() == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + if response.IsError() { + utilfw.UnableToRefreshResourceError(resp, projectError.String()) + return + } - return Role{ - Name: d.GetString("name", false), - Type: d.GetString("type", false), - Environments: d.GetSet("environments"), - Actions: d.GetSet("actions"), - } + state.ID = types.StringValue(role.Name) + state.Name = types.StringValue(role.Name) + state.Type = types.StringValue(role.Type) + state.ProjectKey = types.StringValue(projectKey) + + environments, ds := types.SetValueFrom(ctx, types.StringType, role.Environments) + if ds.HasError() { + resp.Diagnostics.Append(ds...) + return } + state.Environments = environments - var createProjectRole = func(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics { - projectKey := data.Get("project_key").(string) - role := unpackRole(data) + actions, ds := types.SetValueFrom(ctx, types.StringType, role.Actions) + if ds.HasError() { + resp.Diagnostics.Append(ds...) + return + } + state.Actions = actions - var projectError ProjectErrorsResponse - resp, err := m.(util.ProviderMetadata).Client.R(). - SetPathParam("projectKey", projectKey). - SetBody(role). - SetError(&projectError). - Post(ProjectRolesUrl) + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} - if err != nil { - return diag.FromErr(err) - } - if resp.IsError() { - return diag.Errorf("%s", projectError.String()) - } +func (r *ProjectRoleResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + go util.SendUsageResourceUpdate(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) - data.SetId(role.Id()) + var plan ProjectRoleResourceModel - return readProjectRole(ctx, data, m) + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return } - var updateProjectRole = func(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics { - projectKey := data.Get("project_key").(string) - role := unpackRole(data) - - var projectError ProjectErrorsResponse - resp, err := m.(util.ProviderMetadata).Client.R(). - SetPathParams(map[string]string{ - "projectKey": projectKey, - "roleName": role.Name, - }). - SetError(&projectError). - SetBody(role). - Put(ProjectRoleUrl) - - if err != nil { - return diag.FromErr(err) - } - if resp.IsError() { - return diag.Errorf("%s", projectError.String()) - } - - data.SetId(role.Id()) - - return readProjectRole(ctx, data, m) + projectKey := plan.ProjectKey.ValueString() + + var environments []string + resp.Diagnostics.Append(plan.Environments.ElementsAs(ctx, &environments, false)...) + if resp.Diagnostics.HasError() { + return } - var deleteProjectRole = func(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics { - var projectError ProjectErrorsResponse - resp, err := m.(util.ProviderMetadata).Client.R(). - SetPathParams(map[string]string{ - "roleName": data.Id(), - "projectKey": data.Get("project_key").(string), - }). - SetError(&projectError). - Delete(ProjectRoleUrl) - - if err != nil { - return diag.FromErr(err) - } - if resp.IsError() && resp.StatusCode() != http.StatusNotFound { - return diag.Errorf("%s", projectError.String()) - } - - return nil + var actions []string + resp.Diagnostics.Append(plan.Actions.ElementsAs(ctx, &actions, false)...) + if resp.Diagnostics.HasError() { + return } - var importForProjectKeyRoleName = func(d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { - parts := strings.SplitN(d.Id(), ":", 2) - if len(parts) != 2 || parts[0] == "" || parts[1] == "" { - return nil, fmt.Errorf("unexpected format of ID (%s), expected project_key:role_name", d.Id()) - } + role := ProjectRoleAPIModel{ + Name: plan.Name.ValueString(), + Type: plan.Type.ValueString(), + Environments: environments, + Actions: actions, + } - d.Set("project_key", parts[0]) - d.Set("name", parts[1]) - d.SetId(parts[1]) + var projectError ProjectErrorsResponse + response, err := r.ProviderData.Client.R(). + SetPathParams(map[string]string{ + "projectKey": projectKey, + "roleName": role.Name, + }). + SetBody(role). + SetError(&projectError). + Put(ProjectRoleUrl) + if err != nil { + utilfw.UnableToUpdateResourceError(resp, err.Error()) + } + if response.IsError() { + utilfw.UnableToUpdateResourceError(resp, projectError.String()) + } + + plan.ID = types.StringValue(role.Name) + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ProjectRoleResource) 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 ProjectRoleResourceModel - return []*schema.ResourceData{d}, nil + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return } - return &schema.Resource{ - SchemaVersion: 1, - CreateContext: createProjectRole, - ReadContext: readProjectRole, - UpdateContext: updateProjectRole, - DeleteContext: deleteProjectRole, + projectKey := state.ProjectKey.ValueString() + + var projectError ProjectErrorsResponse + response, err := r.ProviderData.Client.R(). + SetPathParams(map[string]string{ + "projectKey": projectKey, + "roleName": state.Name.ValueString(), + }). + SetError(&projectError). + Delete(ProjectRoleUrl) + if err != nil { + utilfw.UnableToDeleteResourceError(resp, err.Error()) + return + } + if response.IsError() && response.StatusCode() != http.StatusNotFound { + utilfw.UnableToDeleteResourceError(resp, projectError.String()) + return + } - Importer: &schema.ResourceImporter{ - State: importForProjectKeyRoleName, - }, + // If the logic reaches here, it implicitly succeeded and will remove + // the resource from state if there are no other errors. +} - Schema: projectRoleSchema, - Description: "Create a project role. Element has one to one mapping with the [JFrog Project Roles API](https://www.jfrog.com/confluence/display/JFROG/Artifactory+REST+API#ArtifactoryRESTAPI-AddaNewRole). Requires a user assigned with the 'Administer the Platform' role or Project Admin permissions if `admin_privileges.manage_resoures` is enabled.", +// ImportState imports the resource into the Terraform state. +func (r *ProjectRoleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + parts := strings.SplitN(req.ID, ":", 2) + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + resp.Diagnostics.AddError( + "Unexpected Import Identifier", + "Expected project_key:role_name", + ) + return } + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_key"), parts[0])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), parts[1])...) } diff --git a/pkg/project/resource/resource_project_role_test.go b/pkg/project/resource/resource_project_role_test.go index 2e1e9b45..453925e3 100644 --- a/pkg/project/resource/resource_project_role_test.go +++ b/pkg/project/resource/resource_project_role_test.go @@ -9,9 +9,79 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/resource" acctest "github.com/jfrog/terraform-provider-project/pkg/project/acctest" project "github.com/jfrog/terraform-provider-project/pkg/project/resource" + "github.com/jfrog/terraform-provider-shared/testutil" "github.com/jfrog/terraform-provider-shared/util" ) +func TestAccProjectRole_UpgradeFromSDKv2(t *testing.T) { + _, _, projectName := testutil.MkNames("test-project-", "project") + _, fqrn, roleName := testutil.MkNames("test-project-role-", "project_role") + + projectKey := strings.ToLower(acctest.RandSeq(10)) + + template := ` + resource "project" "{{ .project_name }}" { + key = "{{ .project_key }}" + display_name = "{{ .project_name }}" + admin_privileges { + manage_members = true + manage_resources = true + index_resources = true + } + } + + resource "project_role" "{{ .name }}" { + name = "{{ .name }}" + type = "{{ .type }}" + project_key = project.{{ .project_name }}.key + + environments = ["{{ .environment }}"] + actions = ["{{ .action }}"] + } + ` + + testData := map[string]string{ + "name": roleName, + "project_name": projectName, + "project_key": projectKey, + "type": "CUSTOM", + "environment": "DEV", + "action": "READ_REPOSITORY", + } + + config := util.ExecuteTemplate("TestAccProjectRole", template, testData) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + Steps: []resource.TestStep{ + { + ExternalProviders: map[string]resource.ExternalProvider{ + "project": { + Source: "jfrog/project", + VersionConstraint: "1.6.1", + }, + }, + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(fqrn, "name", testData["name"]), + resource.TestCheckResourceAttr(fqrn, "project_key", testData["project_key"]), + resource.TestCheckResourceAttr(fqrn, "type", testData["type"]), + resource.TestCheckResourceAttr(fqrn, "environments.#", "1"), + resource.TestCheckResourceAttr(fqrn, "environments.0", testData["environment"]), + resource.TestCheckResourceAttr(fqrn, "actions.#", "1"), + resource.TestCheckResourceAttr(fqrn, "actions.0", testData["action"]), + ), + }, + { + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + Config: config, + PlanOnly: true, + ConfigPlanChecks: testutil.ConfigPlanChecks(fqrn), + }, + }, + }) +} + func TestAccProjectRole_full(t *testing.T) { name := acctest.RandSeq(20) resourceName := fmt.Sprintf("project_role.%s", name) @@ -64,7 +134,7 @@ func TestAccProjectRole_full(t *testing.T) { CheckDestroy: acctest.VerifyDeleted(resourceName, func(id string, request *resty.Request) (*resty.Response, error) { return verifyRole(id, projectKey, request) }), - ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, Steps: []resource.TestStep{ { Config: config, @@ -90,6 +160,12 @@ func TestAccProjectRole_full(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "actions.0", testUpdatedData["action"]), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateId: fmt.Sprintf("%s:%s", testUpdatedData["project_key"], testUpdatedData["name"]), + ImportStateVerify: true, + }, }, }) } @@ -138,7 +214,7 @@ func TestAccProjectRole_conflict_with_project(t *testing.T) { CheckDestroy: acctest.VerifyDeleted(resourceName, func(id string, request *resty.Request) (*resty.Response, error) { return verifyRole(id, projectKey, request) }), - ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, Steps: []resource.TestStep{ { Config: config, diff --git a/pkg/project/resource/role.go b/pkg/project/resource/role.go index 8eeb8c22..3af66845 100644 --- a/pkg/project/resource/role.go +++ b/pkg/project/resource/role.go @@ -9,6 +9,22 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" ) +type Role struct { + Name string `json:"name"` + Description string `json:"description"` + Type string `json:"type"` + Environments []string `json:"environments"` + Actions []string `json:"actions"` +} + +func (r Role) Id() string { + return r.Name +} + +func (a Role) Equals(b Equatable) bool { + return a.Id() == b.Id() +} + func filterRoles(roles []Role, roleType string) []Role { filteredRoles := roles[:0] for _, role := range roles { diff --git a/pkg/project/resource/validators.go b/pkg/project/resource/validators.go index bacf42e2..814b0d2d 100644 --- a/pkg/project/resource/validators.go +++ b/pkg/project/resource/validators.go @@ -6,15 +6,6 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) -func maxLength(length int) func(i interface{}, k string) ([]string, []error) { - return func(value interface{}, k string) ([]string, []error) { - if len(value.(string)) > length { - return nil, []error{fmt.Errorf("string must be less than or equal %d characters long", length)} - } - return nil, nil - } -} - func int64Between(min, max int64) schema.SchemaValidateFunc { return func(i interface{}, k string) (warnings []string, errors []error) { v1, ok := i.(int) From a37b36231818fe91269fbc885e6b6b481b423ee6 Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Tue, 4 Jun 2024 10:18:18 -0700 Subject: [PATCH 4/6] Migrate project_repository resource to Plugin Framework Remove SDKv2 references from provider --- CHANGELOG.md | 2 +- main.go | 44 +-- pkg/project/acctest/test.go | 61 +--- pkg/project/provider/framework.go | 7 +- pkg/project/provider/sdkv2.go | 126 ------- pkg/project/provider/sdkv2_test.go | 17 - pkg/project/resource/membership_test.go | 4 +- pkg/project/resource/repo_test.go | 6 +- .../resource_project_environment_test.go | 2 +- .../resource/resource_project_repository.go | 327 ++++++++++-------- .../resource_project_repository_test.go | 96 ++++- pkg/project/resource/role_test.go | 2 +- pkg/project/resource/util.go | 4 +- pkg/project/resource/validators.go | 25 -- 14 files changed, 314 insertions(+), 409 deletions(-) delete mode 100644 pkg/project/provider/sdkv2.go delete mode 100644 pkg/project/provider/sdkv2_test.go delete mode 100644 pkg/project/resource/validators.go diff --git a/CHANGELOG.md b/CHANGELOG.md index cb28874e..8fd87db0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ IMPROVEMENTS: * resource/project_group is migrated to Plugin Framework. PR: [#131](https://github.com/jfrog/terraform-provider-project/pull/131) -* resource/project_user, resource/project_role are migrated to Plugin Framework. PR: [#132](https://github.com/jfrog/terraform-provider-project/pull/132) +* resource/project_user, resource/project_role, and resource/project_repository are migrated to Plugin Framework. PR: [#132](https://github.com/jfrog/terraform-provider-project/pull/132) ## 1.6.1 (May 31, 2024) diff --git a/main.go b/main.go index 6ef44940..fee77772 100644 --- a/main.go +++ b/main.go @@ -6,10 +6,6 @@ import ( "log" "github.com/hashicorp/terraform-plugin-framework/providerserver" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" - "github.com/hashicorp/terraform-plugin-go/tfprotov6/tf6server" - "github.com/hashicorp/terraform-plugin-mux/tf5to6server" - "github.com/hashicorp/terraform-plugin-mux/tf6muxserver" provider "github.com/jfrog/terraform-provider-project/pkg/project/provider" ) @@ -18,46 +14,18 @@ import ( //go:generate go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs func main() { - ctx := context.Background() - var debug bool flag.BoolVar(&debug, "debug", false, "set to true to run the provider with support for debuggers like delve") flag.Parse() - upgradedSdkServer, err := tf5to6server.UpgradeServer( - ctx, - provider.SdkV2().GRPCProvider, // terraform-plugin-sdk provider - ) - if err != nil { - log.Fatal(err) - } - - providers := []func() tfprotov6.ProviderServer{ - providerserver.NewProtocol6(provider.Framework()()), // terraform-plugin-framework provider - func() tfprotov6.ProviderServer { - return upgradedSdkServer - }, - } - - muxServer, err := tf6muxserver.NewMuxServer(ctx, providers...) - if err != nil { - log.Fatal(err) - } - - var serveOpts []tf6server.ServeOpt - - if debug { - serveOpts = append(serveOpts, tf6server.WithManagedDebug()) + opts := providerserver.ServeOpts{ + // TODO: Update this string with the published name of your provider. + Address: "registry.terraform.io/jfrog/project", + Debug: debug, } - - err = tf6server.Serve( - "registry.terraform.io/jfrog/project", - muxServer.ProviderServer, - serveOpts..., - ) - + err := providerserver.Serve(context.Background(), provider.Framework(), opts) if err != nil { - log.Fatal(err) + log.Fatal(err.Error()) } } diff --git a/pkg/project/acctest/test.go b/pkg/project/acctest/test.go index 37861074..2d8d78ad 100644 --- a/pkg/project/acctest/test.go +++ b/pkg/project/acctest/test.go @@ -1,7 +1,6 @@ package acctest import ( - "context" "fmt" "math/rand" "net/http" @@ -10,23 +9,15 @@ import ( "testing" "github.com/go-resty/resty/v2" + "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/providerserver" "github.com/hashicorp/terraform-plugin-go/tfprotov6" - "github.com/hashicorp/terraform-plugin-mux/tf5to6server" - "github.com/hashicorp/terraform-plugin-mux/tf6muxserver" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - terraform2 "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" "github.com/hashicorp/terraform-plugin-testing/terraform" - "github.com/jfrog/terraform-provider-project/pkg/project/provider" + project "github.com/jfrog/terraform-provider-project/pkg/project/provider" "github.com/jfrog/terraform-provider-shared/client" "github.com/jfrog/terraform-provider-shared/testutil" - "github.com/jfrog/terraform-provider-shared/util" ) -// Provider PreCheck(t) must be called before using this provider instance. -var Provider *schema.Provider -var ProviderFactories map[string]func() (*schema.Provider, error) - // testAccProviderConfigure ensures Provider is only configured once // // The PreCheck(t) function is invoked for every test and this prevents @@ -35,50 +26,15 @@ var ProviderFactories map[string]func() (*schema.Provider, error) // Provider be errantly reused in ProviderFactories. var testAccProviderConfigure sync.Once -// ProtoV6MuxProviderFactories is used to instantiate both SDK v2 and Framework providers -// during acceptance tests. Use it only if you need to combine resources from SDK v2 and the Framework in the same test. -var ProtoV6MuxProviderFactories map[string]func() (tfprotov6.ProviderServer, error) +var Provider provider.Provider var ProtoV6ProviderFactories map[string]func() (tfprotov6.ProviderServer, error) func init() { - Provider = provider.SdkV2() - - ProviderFactories = map[string]func() (*schema.Provider, error){ - "project": func() (*schema.Provider, error) { return Provider, nil }, - } + Provider = project.Framework()() ProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){ - "project": providerserver.NewProtocol6WithError(provider.Framework()()), - } - - ProtoV6MuxProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){ - "project": func() (tfprotov6.ProviderServer, error) { - ctx := context.Background() - - upgradedSdkServer, err := tf5to6server.UpgradeServer( - ctx, - provider.SdkV2().GRPCProvider, // terraform-plugin-sdk provider - ) - if err != nil { - return nil, err - } - - providers := []func() tfprotov6.ProviderServer{ - providerserver.NewProtocol6(provider.Framework()()), // terraform-plugin-framework provider - func() tfprotov6.ProviderServer { - return upgradedSdkServer - }, - } - - muxServer, err := tf6muxserver.NewMuxServer(ctx, providers...) - - if err != nil { - return nil, err - } - - return muxServer.ProviderServer(), nil - }, + "project": providerserver.NewProtocol6WithError(Provider), } } @@ -99,11 +55,6 @@ func PreCheck(t *testing.T) { if err != nil { t.Fatalf("failed to set custom base URL: %v", err) } - - configErr := Provider.Configure(context.Background(), (*terraform2.ResourceConfig)(terraform2.NewResourceConfigRaw(nil))) - if configErr != nil && configErr.HasError() { - t.Fatalf("failed to configure provider %v", configErr) - } }) } @@ -120,7 +71,7 @@ func VerifyDeleted(id string, check CheckFun) func(*terraform.State) error { return fmt.Errorf("error: Resource id [%s] not found", id) } - client := Provider.Meta().(util.ProviderMetadata).Client + client := Provider.(*project.ProjectProvider).Meta.Client resp, err := check(rs.Primary.ID, client.R()) if err != nil { return err diff --git a/pkg/project/provider/framework.go b/pkg/project/provider/framework.go index 31b9dbe4..b8bc4f25 100644 --- a/pkg/project/provider/framework.go +++ b/pkg/project/provider/framework.go @@ -20,7 +20,9 @@ import ( // Ensure the implementation satisfies the provider.Provider interface. var _ provider.Provider = &ProjectProvider{} -type ProjectProvider struct{} +type ProjectProvider struct { + Meta util.ProviderMetadata +} // ProjectProviderModel describes the provider data model. type ProjectProviderModel struct { @@ -173,6 +175,8 @@ func (p *ProjectProvider) Configure(ctx context.Context, req provider.ConfigureR ArtifactoryVersion: version, } + p.Meta = meta + resp.DataSourceData = meta resp.ResourceData = meta } @@ -183,6 +187,7 @@ func (p *ProjectProvider) Resources(ctx context.Context) []func() resource.Resou project.NewProjectResource, project.NewProjectEnvironmentResource, project.NewProjectGroupResource, + project.NewProjectRepositoryResource, project.NewProjectRoleResource, project.NewProjectUserResource, } diff --git a/pkg/project/provider/sdkv2.go b/pkg/project/provider/sdkv2.go deleted file mode 100644 index ac08009d..00000000 --- a/pkg/project/provider/sdkv2.go +++ /dev/null @@ -1,126 +0,0 @@ -package provider - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - resource "github.com/jfrog/terraform-provider-project/pkg/project/resource" - "github.com/jfrog/terraform-provider-shared/client" - "github.com/jfrog/terraform-provider-shared/util" - "github.com/jfrog/terraform-provider-shared/util/sdk" - "github.com/jfrog/terraform-provider-shared/validator" -) - -// Provider Projects provider that supports configuration via a token -// Supported resources are repos, users, groups, replications, and permissions -func SdkV2() *schema.Provider { - p := &schema.Provider{ - Schema: map[string]*schema.Schema{ - "url": { - Type: schema.TypeString, - Optional: true, - ValidateFunc: validation.IsURLWithHTTPorHTTPS, - Description: "URL of Artifactory. This can also be sourced from the `PROJECT_URL` or `JFROG_URL` environment variable. Default to 'http://localhost:8081' if not set.", - }, - "access_token": { - Type: schema.TypeString, - Optional: true, - Sensitive: true, - Description: "This is a Bearer token that can be given to you by your admin under `Identity and Access`. This can also be sourced from the `PROJECT_ACCESS_TOKEN` or `JFROG_ACCESS_TOKEN` environment variable. Defauult to empty string if not set.", - }, - "oidc_provider_name": { - Type: schema.TypeString, - Optional: true, - ValidateDiagFunc: validator.StringIsNotEmpty, - Description: "OIDC provider name. See [Configure an OIDC Integration](https://jfrog.com/help/r/jfrog-platform-administration-documentation/configure-an-oidc-integration) for more details.", - }, - "check_license": { - Type: schema.TypeBool, - Optional: true, - Default: true, - Description: "Toggle for pre-flight checking of Artifactory Enterprise license. Default to `true`.", - }, - }, - - ResourcesMap: sdk.AddTelemetry( - productId, - map[string]*schema.Resource{ - "project_repository": resource.ProjectRepositoryResource(), - }, - ), - } - - p.ConfigureContextFunc = func(ctx context.Context, data *schema.ResourceData) (interface{}, diag.Diagnostics) { - return providerConfigure(ctx, data, p.TerraformVersion) - } - - return p -} - -func providerConfigure(ctx context.Context, d *schema.ResourceData, terraformVersion string) (interface{}, diag.Diagnostics) { - url := util.CheckEnvVars([]string{"JFROG_URL", "PROJECT_URL"}, "") - accessToken := util.CheckEnvVars([]string{"JFROG_ACCESS_TOKEN", "PROJECT_ACCESS_TOKEN"}, "") - - if v, ok := d.GetOk("url"); ok { - url = v.(string) - } - if url == "" { - return nil, diag.Errorf("missing URL Configuration") - } - - restyClient, err := client.Build(url, productId) - if err != nil { - return nil, diag.FromErr(err) - } - - if v, ok := d.GetOk("oidc_provider_name"); ok { - oidcAccessToken, err := util.OIDCTokenExchange(ctx, restyClient, v.(string)) - if err != nil { - return nil, diag.FromErr(err) - } - - if oidcAccessToken != "" { - accessToken = oidcAccessToken - } - } - - if v, ok := d.GetOk("access_token"); ok && v != "" { - accessToken = v.(string) - } - - if accessToken == "" { - return nil, diag.Errorf("Missing JFrog Access Token\n" + - "While configuring the provider, the Access Token was not found in " + - "the JFROG_ACCESS_TOKEN/PROJECT_ACCESS_TOKEN environment variable or provider " + - "configuration block access_token attribute.") - } - - restyClient, err = client.AddAuth(restyClient, "", accessToken) - if err != nil { - return nil, diag.FromErr(err) - } - - checkLicense := d.Get("check_license").(bool) - if checkLicense { - licenseErr := util.CheckArtifactoryLicense(restyClient, "Enterprise", "Commercial", "Edge") - if licenseErr != nil { - return nil, diag.FromErr(licenseErr) - } - } - - version, err := util.GetArtifactoryVersion(restyClient) - if err != nil { - return nil, diag.FromErr(err) - } - - featureUsage := fmt.Sprintf("Terraform/%s", terraformVersion) - go util.SendUsage(ctx, restyClient.R(), productId, featureUsage) - - return util.ProviderMetadata{ - Client: restyClient, - ArtifactoryVersion: version, - }, nil -} diff --git a/pkg/project/provider/sdkv2_test.go b/pkg/project/provider/sdkv2_test.go deleted file mode 100644 index 0c01dffd..00000000 --- a/pkg/project/provider/sdkv2_test.go +++ /dev/null @@ -1,17 +0,0 @@ -package provider_test - -import ( - "testing" - - provider "github.com/jfrog/terraform-provider-project/pkg/project/provider" -) - -func TestProvider_validate(t *testing.T) { - if err := provider.SdkV2().InternalValidate(); err != nil { - t.Fatalf("err: %s", err) - } -} - -func TestProvider_impl(t *testing.T) { - var _ = provider.SdkV2() -} diff --git a/pkg/project/resource/membership_test.go b/pkg/project/resource/membership_test.go index 98cc0bd3..4e173f26 100644 --- a/pkg/project/resource/membership_test.go +++ b/pkg/project/resource/membership_test.go @@ -110,7 +110,7 @@ func TestAccProject_membership(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, CheckDestroy: acctest.VerifyDeleted(resourceName, verifyProject), - ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, ExternalProviders: map[string]resource.ExternalProvider{ "artifactory": { Source: "jfrog/artifactory", @@ -264,7 +264,7 @@ func TestAccProject_group(t *testing.T) { PreCheck: func() { acctest.PreCheck(t) }, CheckDestroy: acctest.VerifyDeleted(resourceName, verifyProject), - ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, ExternalProviders: map[string]resource.ExternalProvider{ "artifactory": { Source: "jfrog/artifactory", diff --git a/pkg/project/resource/repo_test.go b/pkg/project/resource/repo_test.go index b0e9bac6..0e452f69 100644 --- a/pkg/project/resource/repo_test.go +++ b/pkg/project/resource/repo_test.go @@ -105,7 +105,7 @@ func TestAccProject_repo(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, CheckDestroy: acctest.VerifyDeleted(resourceName, verifyProject), - ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, ExternalProviders: map[string]resource.ExternalProvider{ "artifactory": { Source: "jfrog/artifactory", @@ -250,7 +250,7 @@ func TestAccProject_repoAssignMultipleRepos(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, CheckDestroy: acctest.VerifyDeleted(resourceName, verifyProject), - ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, ExternalProviders: map[string]resource.ExternalProvider{ "artifactory": { Source: "jfrog/artifactory", @@ -361,7 +361,7 @@ func TestAccProject_repoUnassignNonexistantRepo(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, CheckDestroy: acctest.VerifyDeleted(resourceName, verifyProject), - ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, ExternalProviders: map[string]resource.ExternalProvider{ "artifactory": { Source: "jfrog/artifactory", diff --git a/pkg/project/resource/resource_project_environment_test.go b/pkg/project/resource/resource_project_environment_test.go index 12f8e823..9c52dcb4 100644 --- a/pkg/project/resource/resource_project_environment_test.go +++ b/pkg/project/resource/resource_project_environment_test.go @@ -63,7 +63,7 @@ func TestAccProjectEnvironment_UpgradeFromSDKv2(t *testing.T) { ), }, { - ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, Config: config, PlanOnly: true, ConfigPlanChecks: testutil.ConfigPlanChecks(fqrn), diff --git a/pkg/project/resource/resource_project_repository.go b/pkg/project/resource/resource_project_repository.go index ddf4c00a..04fcb229 100644 --- a/pkg/project/resource/resource_project_repository.go +++ b/pkg/project/resource/resource_project_repository.go @@ -5,192 +5,249 @@ import ( "fmt" "net/http" "strings" - + "time" + + "github.com/cenkalti/backoff/v4" + "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/hashicorp/terraform-plugin-log/tflog" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/jfrog/terraform-provider-shared/util" - "github.com/jfrog/terraform-provider-shared/util/sdk" - "github.com/jfrog/terraform-provider-shared/validator" + utilfw "github.com/jfrog/terraform-provider-shared/util/fw" + validatorfw_string "github.com/jfrog/terraform-provider-shared/validator/fw/string" ) const repositoryEndpoint = "/artifactory/api/repositories/{key}" -type Repository struct { +func NewProjectRepositoryResource() resource.Resource { + return &ProjectRepositoryResource{} +} + +type ProjectRepositoryResource struct { + ProviderData util.ProviderMetadata + TypeName string +} + +type ProjectRepositoryResourceModel struct { + ID types.String `tfsdk:"id"` + Key types.String `tfsdk:"key"` + ProjectKey types.String `tfsdk:"project_key"` +} + +type ProjectRepositoryAPIModel struct { Key string `json:"key"` ProjectKey string `json:"projectKey"` } -func ProjectRepositoryResource() *schema.Resource { - var projectRepositoryID = func(projectKey, repoKey string) string { - return fmt.Sprintf("%s-%s", projectKey, repoKey) - } +func (r *ProjectRepositoryResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_repository" + r.TypeName = resp.TypeName +} - var projectRepositorySchema = map[string]*schema.Schema{ - "project_key": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - ValidateDiagFunc: validator.ProjectKey, - Description: "The key of the project to which the repository should be assigned to.", - }, - "key": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - ValidateDiagFunc: validator.RepoKey, - Description: "The key of the repository.", +func (r *ProjectRepositoryResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Version: 1, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "key": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + validatorfw_string.RepoKey(), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Description: "The key of the repository.", + }, + "project_key": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + validatorfw_string.ProjectKey(), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Description: "The key of the project to which the repository should be assigned to.", + }, }, + Description: "Assign a repository to a project. Requires a user assigned with the 'Administer the Platform' role or Project Admin permissions if `admin_privileges.manage_resoures` is enabled.", } +} - var packProjectRepository = func(repo Repository, data *schema.ResourceData) error { - setValue := sdk.MkLens(data) +func (r *ProjectRepositoryResource) 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.(util.ProviderMetadata) +} - setValue("project_key", repo.ProjectKey) - errors := setValue("key", repo.Key) - if len(errors) > 0 { - return fmt.Errorf("failed to pack project repository %q", errors) - } +func (r *ProjectRepositoryResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + go util.SendUsageResourceCreate(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) - data.SetId(projectRepositoryID(repo.ProjectKey, repo.Key)) + var plan ProjectRepositoryResourceModel - return nil + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return } - var readProjectRepository = func(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics { - repoKey := data.Get("key").(string) + projectKey := plan.ProjectKey.ValueString() + repoKey := plan.Key.ValueString() + + var projectError ProjectErrorsResponse + response, err := r.ProviderData.Client.R(). + SetPathParams(map[string]string{ + "projectKey": projectKey, + "repoKey": repoKey, + }). + SetError(&projectError). + Put("/access/api/v1/projects/_/attach/repositories/{repoKey}/{projectKey}?force=true") + if err != nil { + utilfw.UnableToCreateResourceError(resp, err.Error()) + return + } + if response.IsError() { + utilfw.UnableToCreateResourceError(resp, projectError.String()) + return + } - var repo Repository - resp, err := m.(util.ProviderMetadata).Client.R(). + var retryFunc = func() error { + var repo ProjectRepositoryAPIModel + resp, err := r.ProviderData.Client.R(). SetResult(&repo). SetPathParam("key", repoKey). Get(repositoryEndpoint) if err != nil { - return diag.FromErr(err) - } - if resp.StatusCode() == http.StatusBadRequest || resp.StatusCode() == http.StatusNotFound { - data.SetId("") - return nil + return fmt.Errorf("error getting repository: %s", err) } if resp.IsError() { - return diag.Errorf("%s", resp.String()) + return fmt.Errorf("error getting repository: %s", resp.String()) } if repo.ProjectKey == "" { - tflog.Warn(ctx, "no project_key for repo", map[string]any{"repoKey": repoKey}) - data.SetId("") - return nil - } - - err = packProjectRepository(repo, data) - if err != nil { - return diag.FromErr(err) + return fmt.Errorf("expected repository to be assigned to project but currently not") } return nil } - var createProjectRepository = func(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics { - projectKey := data.Get("project_key").(string) - repoKey := data.Get("key").(string) + bf := backoff.WithContext( + backoff.NewExponentialBackOff(backoff.WithMaxElapsedTime(20*time.Minute)), + ctx, + ) + retryError := backoff.Retry(retryFunc, bf) + if retryError != nil { + utilfw.UnableToCreateResourceError(resp, retryError.Error()) + return + } - var projectError ProjectErrorsResponse - resp, err := m.(util.ProviderMetadata).Client.R(). - SetPathParams(map[string]string{ - "projectKey": projectKey, - "repoKey": repoKey, - }). - SetError(&projectError). - Put("/access/api/v1/projects/_/attach/repositories/{repoKey}/{projectKey}?force=true") + plan.ID = types.StringValue(fmt.Sprintf("%s-%s", projectKey, repoKey)) - if err != nil { - return diag.FromErr(err) - } - if resp.IsError() { - return diag.Errorf("%s", projectError.String()) - } + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} - retryError := retry.RetryContext(ctx, data.Timeout(schema.TimeoutCreate), func() *retry.RetryError { - var repo Repository - resp, err := m.(util.ProviderMetadata).Client.R(). - SetResult(&repo). - SetPathParam("key", repoKey). - Get(repositoryEndpoint) - - if err != nil { - return retry.NonRetryableError(fmt.Errorf("error getting repository: %s", err)) - } - if resp.IsError() { - return retry.NonRetryableError(fmt.Errorf("error getting repository: %s", resp.String())) - } - - if repo.ProjectKey == "" { - return retry.RetryableError(fmt.Errorf("expected repository to be assigned to project but currently not")) - } - - err = packProjectRepository(repo, data) - if err != nil { - return retry.NonRetryableError(err) - } - - return nil - }) - - if retryError != nil { - return diag.FromErr(retryError) - } +func (r *ProjectRepositoryResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + go util.SendUsageResourceRead(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) - return nil + var state ProjectRepositoryResourceModel + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return } - var deleteProjectRepository = func(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics { - repoKey := data.Get("key").(string) + projectKey := state.ProjectKey.ValueString() + repoKey := state.Key.ValueString() + + var repo ProjectRepositoryAPIModel + var projectError ProjectErrorsResponse + response, err := r.ProviderData.Client.R(). + SetResult(&repo). + SetPathParam("key", repoKey). + Get(repositoryEndpoint) + if err != nil { + utilfw.UnableToRefreshResourceError(resp, err.Error()) + return + } - var projectError ProjectErrorsResponse - resp, err := m.(util.ProviderMetadata).Client.R(). - SetPathParam("repoKey", repoKey). - SetError(&projectError). - Delete("/access/api/v1/projects/_/attach/repositories/{repoKey}") + if response.StatusCode() == http.StatusBadRequest || response.StatusCode() == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + if response.IsError() { + utilfw.UnableToRefreshResourceError(resp, projectError.String()) + return + } + if repo.ProjectKey == "" { + tflog.Warn(ctx, "no project_key for repo", map[string]any{"repoKey": repoKey}) + resp.State.RemoveResource(ctx) + return + } - if err != nil { - return diag.FromErr(err) - } - if resp.IsError() && resp.StatusCode() != http.StatusNotFound { - return diag.Errorf("%s", projectError.String()) - } + state.ID = types.StringValue(fmt.Sprintf("%s-%s", projectKey, repoKey)) + state.ProjectKey = types.StringValue(projectKey) - data.SetId("") + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} - return nil - } +func (r *ProjectRepositoryResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddWarning( + "Update not supported", + "Repository assignment to project cannnot be updated.", + ) +} - var importForProjectKeyRepositoryKey = func(d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { - parts := strings.SplitN(d.Id(), ":", 2) - if len(parts) != 2 || parts[0] == "" || parts[1] == "" { - return nil, fmt.Errorf("unexpected format of ID (%s), expected project_key:repository_key", d.Id()) - } +func (r *ProjectRepositoryResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + go util.SendUsageResourceDelete(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) - d.Set("project_key", parts[0]) - d.Set("key", parts[1]) - d.SetId(projectRepositoryID(parts[0], parts[1])) + var state ProjectRepositoryResourceModel - return []*schema.ResourceData{d}, nil + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return } - return &schema.Resource{ - CreateContext: createProjectRepository, - ReadContext: readProjectRepository, - DeleteContext: deleteProjectRepository, - - Importer: &schema.ResourceImporter{ - State: importForProjectKeyRepositoryKey, - }, + var projectError ProjectErrorsResponse + response, err := r.ProviderData.Client.R(). + SetPathParam("repoKey", state.Key.ValueString()). + SetError(&projectError). + Delete("/access/api/v1/projects/_/attach/repositories/{repoKey}") + if err != nil { + utilfw.UnableToDeleteResourceError(resp, err.Error()) + return + } + if response.IsError() && response.StatusCode() != http.StatusNotFound { + utilfw.UnableToDeleteResourceError(resp, projectError.String()) + return + } - Schema: projectRepositorySchema, - SchemaVersion: 1, + // If the logic reaches here, it implicitly succeeded and will remove + // the resource from state if there are no other errors. +} - Description: "Assign a repository to a project. Requires a user assigned with the 'Administer the Platform' role or Project Admin permissions if `admin_privileges.manage_resoures` is enabled.", +// ImportState imports the resource into the Terraform state. +func (r *ProjectRepositoryResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + parts := strings.SplitN(req.ID, ":", 2) + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + resp.Diagnostics.AddError( + "Unexpected Import Identifier", + "Expected project_key:repository_key", + ) + return } + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_key"), parts[0])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("key"), parts[1])...) } diff --git a/pkg/project/resource/resource_project_repository_test.go b/pkg/project/resource/resource_project_repository_test.go index 8bfde7ca..af75308e 100644 --- a/pkg/project/resource/resource_project_repository_test.go +++ b/pkg/project/resource/resource_project_repository_test.go @@ -11,7 +11,99 @@ import ( "github.com/jfrog/terraform-provider-shared/util" ) -func TestAccProjectRepository(t *testing.T) { +func TestAccProjectRepository_UpgradeFromSDKv2(t *testing.T) { + _, _, projectName := testutil.MkNames("test-project-", "project") + _, fqrn, projectRepoName := testutil.MkNames("test-project-repo-", "project_repository") + + projectKey := strings.ToLower(acctest.RandSeq(10)) + repoKey1 := fmt.Sprintf("repo%d", testutil.RandomInt()) + repoKey2 := fmt.Sprintf("repo%d", testutil.RandomInt()) + + params := map[string]interface{}{ + "project_name": projectName, + "project_key": projectKey, + "repo_key": repoKey1, + "repo_key_1": repoKey1, + "repo_key_2": repoKey2, + "project_repo_name": projectRepoName, + } + + template := ` + resource "artifactory_local_generic_repository" "{{ .repo_key_1 }}" { + key = "{{ .repo_key_1 }}" + + lifecycle { + ignore_changes = ["project_key"] + } + } + + resource "artifactory_local_generic_repository" "{{ .repo_key_2 }}" { + key = "{{ .repo_key_2 }}" + + lifecycle { + ignore_changes = ["project_key"] + } + } + + resource "project" "{{ .project_name }}" { + key = "{{ .project_key }}" + display_name = "{{ .project_name }}" + description = "test description" + admin_privileges { + manage_members = true + manage_resources = true + index_resources = true + } + max_storage_in_gibibytes = 1 + block_deployments_on_limit = true + email_notification = false + } + + resource "project_repository" "{{ .project_repo_name }}" { + project_key = project.{{ .project_name }}.key + key = artifactory_local_generic_repository.{{ .repo_key }}.key + } + ` + + config := util.ExecuteTemplate("TestAccProjectRepository", template, params) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + Steps: []resource.TestStep{ + { + ExternalProviders: map[string]resource.ExternalProvider{ + "artifactory": { + Source: "jfrog/artifactory", + VersionConstraint: "10.1.4", + }, + "project": { + Source: "jfrog/project", + VersionConstraint: "1.6.1", + }, + }, + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(fqrn, "project_key", params["project_key"].(string)), + resource.TestCheckResourceAttr(fqrn, "key", params["repo_key"].(string)), + ), + }, + { + ExternalProviders: map[string]resource.ExternalProvider{ + "artifactory": { + Source: "jfrog/artifactory", + VersionConstraint: "10.1.4", + }, + }, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + Config: config, + PlanOnly: true, + ConfigPlanChecks: testutil.ConfigPlanChecks(fqrn), + }, + }, + }) +} + +func TestAccProjectRepository_full(t *testing.T) { projectKey := strings.ToLower(acctest.RandSeq(10)) projectName := fmt.Sprintf("tftestprojects%s", projectKey) @@ -80,7 +172,7 @@ func TestAccProjectRepository(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, ExternalProviders: map[string]resource.ExternalProvider{ "artifactory": { Source: "jfrog/artifactory", diff --git a/pkg/project/resource/role_test.go b/pkg/project/resource/role_test.go index ee057031..7754f3c2 100644 --- a/pkg/project/resource/role_test.go +++ b/pkg/project/resource/role_test.go @@ -115,7 +115,7 @@ func TestAccProject_role(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, CheckDestroy: acctest.VerifyDeleted(resourceName, verifyProject), - ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, Steps: []resource.TestStep{ { Config: initialConfig, diff --git a/pkg/project/resource/util.go b/pkg/project/resource/util.go index 17feea95..7d6eb3d7 100644 --- a/pkg/project/resource/util.go +++ b/pkg/project/resource/util.go @@ -5,12 +5,12 @@ import ( "regexp" "github.com/go-resty/resty/v2" - "github.com/jfrog/terraform-provider-shared/util/sdk" + "github.com/jfrog/terraform-provider-shared/util" "github.com/samber/lo" ) type Equatable interface { - sdk.Identifiable + util.Identifiable Equals(other Equatable) bool } diff --git a/pkg/project/resource/validators.go b/pkg/project/resource/validators.go deleted file mode 100644 index 814b0d2d..00000000 --- a/pkg/project/resource/validators.go +++ /dev/null @@ -1,25 +0,0 @@ -package project - -import ( - "fmt" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" -) - -func int64Between(min, max int64) schema.SchemaValidateFunc { - return func(i interface{}, k string) (warnings []string, errors []error) { - v1, ok := i.(int) - if !ok { - errors = append(errors, fmt.Errorf("expected type of %s to be integer", k)) - return warnings, errors - } - - v2 := int64(v1) - if v2 < min || v2 > max { - errors = append(errors, fmt.Errorf("expected %s to be in the range (%d - %d), got %d", k, min, max, v2)) - return warnings, errors - } - - return warnings, errors - } -} From b33d06ffb4800a4291a25f40777e30b6cc318749 Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Tue, 4 Jun 2024 10:22:16 -0700 Subject: [PATCH 5/6] Update shard package Add Backoff package to replace one from TF SDKv2 --- go.mod | 9 ++++----- go.sum | 16 ++++++---------- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index f00b93e8..78fccd3c 100644 --- a/go.mod +++ b/go.mod @@ -9,13 +9,14 @@ require ( github.com/hashicorp/terraform-plugin-framework v1.8.0 github.com/hashicorp/terraform-plugin-framework-validators v0.12.0 github.com/hashicorp/terraform-plugin-log v0.9.0 - github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0 github.com/hashicorp/terraform-plugin-testing v1.8.0 - github.com/jfrog/terraform-provider-shared v1.25.3 + github.com/jfrog/terraform-provider-shared v1.25.4 github.com/samber/lo v1.39.0 golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 ) +require github.com/hashicorp/terraform-plugin-sdk/v2 v2.33.0 // indirect + require ( github.com/BurntSushi/toml v1.2.1 // indirect github.com/Kunde21/markdownfmt/v3 v3.1.0 // indirect @@ -28,6 +29,7 @@ require ( github.com/armon/go-radix v1.0.0 // indirect github.com/bgentry/speakeasy v0.1.0 // indirect github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 github.com/cloudflare/circl v1.3.7 // indirect github.com/fatih/color v1.16.0 // indirect github.com/golang/protobuf v1.5.4 // indirect @@ -50,7 +52,6 @@ require ( github.com/hashicorp/terraform-exec v0.21.0 // indirect github.com/hashicorp/terraform-json v0.22.1 // indirect github.com/hashicorp/terraform-plugin-go v0.23.0 - github.com/hashicorp/terraform-plugin-mux v0.16.0 github.com/hashicorp/terraform-registry-address v0.2.3 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect github.com/hashicorp/yamux v0.1.1 // indirect @@ -85,9 +86,7 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect google.golang.org/grpc v1.63.2 // indirect google.golang.org/protobuf v1.34.0 // indirect - gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect - gopkg.in/ldap.v2 v2.5.1 // indirect gopkg.in/yaml.v2 v2.3.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 5deaa7f0..d357be72 100644 --- a/go.sum +++ b/go.sum @@ -27,6 +27,8 @@ github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwN github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= @@ -111,10 +113,8 @@ github.com/hashicorp/terraform-plugin-go v0.23.0 h1:AALVuU1gD1kPb48aPQUjug9Ir/12 github.com/hashicorp/terraform-plugin-go v0.23.0/go.mod h1:1E3Cr9h2vMlahWMbsSEcNrOCxovCZhOOIXjFHbjc/lQ= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= -github.com/hashicorp/terraform-plugin-mux v0.16.0 h1:RCzXHGDYwUwwqfYYWJKBFaS3fQsWn/ZECEiW7p2023I= -github.com/hashicorp/terraform-plugin-mux v0.16.0/go.mod h1:PF79mAsPc8CpusXPfEVa4X8PtkB+ngWoiUClMrNZlYo= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0 h1:kJiWGx2kiQVo97Y5IOGR4EMcZ8DtMswHhUuFibsCQQE= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0/go.mod h1:sl/UoabMc37HA6ICVMmGO+/0wofkVIRxf+BMb/dnoIg= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.33.0 h1:qHprzXy/As0rxedphECBEQAh3R4yp6pKksKHcqZx5G8= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.33.0/go.mod h1:H+8tjs9TjV2w57QFVSMBQacf8k/E1XwLXGCARgViC6A= github.com/hashicorp/terraform-plugin-testing v1.8.0 h1:wdYIgwDk4iO933gC4S8KbKdnMQShu6BXuZQPScmHvpk= github.com/hashicorp/terraform-plugin-testing v1.8.0/go.mod h1:o2kOgf18ADUaZGhtOl0YCkfIxg01MAiMATT2EtIHlZk= github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= @@ -130,8 +130,8 @@ github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/jfrog/terraform-provider-shared v1.25.3 h1:0P1H5WkNmhrXvXAo5pY1XkVn81CkZxcttDqZTESlKBw= -github.com/jfrog/terraform-provider-shared v1.25.3/go.mod h1:QthwPRUALElMt2RTGqoeB/3Vztx626YPBzIAoqEp0w0= +github.com/jfrog/terraform-provider-shared v1.25.4 h1:+rx+/7dbJPNzdGs5rNIHtHvN+9TlbHuqGr+/idW2ozw= +github.com/jfrog/terraform-provider-shared v1.25.4/go.mod h1:QthwPRUALElMt2RTGqoeB/3Vztx626YPBzIAoqEp0w0= github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= @@ -301,14 +301,10 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4= google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d h1:TxyelI5cVkbREznMhfzycHdkp5cLA7DpE+GKjSslYhM= -gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/ldap.v2 v2.5.1 h1:wiu0okdNfjlBzg6UWvd1Hn8Y+Ux17/u/4nlk4CQr6tU= -gopkg.in/ldap.v2 v2.5.1/go.mod h1:oI0cpe/D7HRtBQl8aTg+ZmzFUAvu4lsv3eLXMLGFxWk= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= From b6f334d51a72d6bbf690e232659f37678c7a8802 Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Tue, 4 Jun 2024 10:30:45 -0700 Subject: [PATCH 6/6] Update CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fd87db0..6b01d9df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ IMPROVEMENTS: * resource/project_group is migrated to Plugin Framework. PR: [#131](https://github.com/jfrog/terraform-provider-project/pull/131) -* resource/project_user, resource/project_role, and resource/project_repository are migrated to Plugin Framework. PR: [#132](https://github.com/jfrog/terraform-provider-project/pull/132) +* resource/project_user, resource/project_role, and resource/project_repository are migrated to Plugin Framework. PR: [#133](https://github.com/jfrog/terraform-provider-project/pull/133) ## 1.6.1 (May 31, 2024)