From fa99d33bd2f16411dbd0deb347682e72425c520f Mon Sep 17 00:00:00 2001 From: ThomasZalewski Date: Wed, 3 Jul 2024 17:01:54 -0400 Subject: [PATCH 01/17] Added support for datazone project resource. --- internal/service/datazone/project.go | 744 ++++++++++++++++++ internal/service/datazone/project_test.go | 238 ++++++ website/docs/r/datazone_project.html.markdown | 69 ++ 3 files changed, 1051 insertions(+) create mode 100644 internal/service/datazone/project.go create mode 100644 internal/service/datazone/project_test.go create mode 100644 website/docs/r/datazone_project.html.markdown diff --git a/internal/service/datazone/project.go b/internal/service/datazone/project.go new file mode 100644 index 00000000000..caeeff8c0d3 --- /dev/null +++ b/internal/service/datazone/project.go @@ -0,0 +1,744 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package datazone + +// **PLEASE DELETE THIS AND ALL TIP COMMENTS BEFORE SUBMITTING A PR FOR REVIEW!** +// +// TIP: ==== INTRODUCTION ==== +// Thank you for trying the skaff tool! +// +// You have opted to include these helpful comments. They all include "TIP:" +// to help you find and remove them when you're done with them. +// +// While some aspects of this file are customized to your input, the +// scaffold tool does *not* look at the AWS API and ensure it has correct +// function, structure, and variable names. It makes guesses based on +// commonalities. You will need to make significant adjustments. +// +// In other words, as generated, this is a rough outline of the work you will +// need to do. If something doesn't make sense for your situation, get rid of +// it. + +import ( + // TIP: ==== IMPORTS ==== + // This is a common set of imports but not customized to your code since + // your code hasn't been written yet. Make sure you, your IDE, or + // goimports -w fixes these imports. + // + // The provider linter wants your imports to be in two groups: first, + // standard library (i.e., "fmt" or "strings"), second, everything else. + // + // Also, AWS Go SDK v2 may handle nested structures differently than v1, + // using the services/datazone/types package. If so, you'll + // need to import types and reference the nested types, e.g., as + // awstypes.. + "context" + "errors" + "time" + + "github.com/YakDriver/regexache" + "github.com/aws/aws-sdk-go-v2/aws" + //"github.com/aws/aws-sdk-go-v2/aws/middleware" + "github.com/aws/aws-sdk-go-v2/service/datazone" + awstypes "github.com/aws/aws-sdk-go-v2/service/datazone/types" + //"github.com/aws/smithy-go/middleware" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "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-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-provider-aws/internal/create" + //"github.com/hashicorp/terraform-provider-aws/internal/enum" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// TIP: ==== FILE STRUCTURE ==== +// All resources should follow this basic outline. Improve this resource's +// maintainability by sticking to it. +// +// 1. Package declaration +// 2. Imports +// 3. Main resource struct with schema method +// 4. Create, read, update, delete methods (in that order) +// 5. Other functions (flatteners, expanders, waiters, finders, etc.) + +// Function annotations are used for resource registration to the Provider. DO NOT EDIT. +// @FrameworkResource("aws_datazone_project", name="Project") +func newResourceProject(_ context.Context) (resource.ResourceWithConfigure, error) { + r := &resourceProject{} + + // TIP: ==== CONFIGURABLE TIMEOUTS ==== + // Users can configure timeout lengths but you need to use the times they + // provide. Access the timeout they configure (or the defaults) using, + // e.g., r.CreateTimeout(ctx, plan.Timeouts) (see below). The times here are + // the defaults if they don't configure timeouts. + r.SetDefaultCreateTimeout(30 * time.Minute) + r.SetDefaultUpdateTimeout(30 * time.Minute) + r.SetDefaultDeleteTimeout(30 * time.Minute) + + return r, nil +} + +const ( + ResNameProject = "Project" +) + +type resourceProject struct { + framework.ResourceWithConfigure + framework.WithTimeouts +} + +func (r *resourceProject) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "aws_datazone_project" +} + +// TIP: ==== SCHEMA ==== +// In the schema, add each of the attributes in snake case (e.g., +// delete_automated_backups). +// +// Formatting rules: +// * Alphabetize attributes to make them easier to find. +// * Do not add a blank line between attributes. +// +// Attribute basics: +// - If a user can provide a value ("configure a value") for an +// attribute (e.g., instances = 5), we call the attribute an +// "argument." +// - You change the way users interact with attributes using: +// - Required +// - Optional +// - Computed +// - There are only four valid combinations: +// +// 1. Required only - the user must provide a value +// Required: true, +// +// 2. Optional only - the user can configure or omit a value; do not +// use Default or DefaultFunc +// +// Optional: true, +// +// 3. Computed only - the provider can provide a value but the user +// cannot, i.e., read-only +// +// Computed: true, +// +// 4. Optional AND Computed - the provider or user can provide a value; +// use this combination if you are using Default +// +// Optional: true, +// Computed: true, +// +// You will typically find arguments in the input struct +// (e.g., CreateDBInstanceInput) for the create operation. Sometimes +// they are only in the input struct (e.g., ModifyDBInstanceInput) for +// the modify operation. +// +// For more about schema options, visit +// https://developer.hashicorp.com/terraform/plugin/framework/handling-data/schemas?page=schemas +func (r *resourceProject) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + names.AttrDescription: schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthAtMost(2048), + }, + }, + "domain_identifier": schema.StringAttribute{ + CustomType: fwtypes.RegexpType, + Optional: true, + Computed: true, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexache.MustCompile(`^dzd[-_][a-zA-Z0-9_-]{1,36}$`), "must conform to: ^dzd[-_][a-zA-Z0-9_-]{1,36}$ "), + }, + }, + "glossary_terms": schema.ListAttribute{ + CustomType: fwtypes.NewListNestedObjectTypeOf[types.String](ctx), + Validators: []validator.List{ + listvalidator.ValueStringsAre(stringvalidator.LengthBetween(1, 20)), + }, + }, + names.AttrName: schema.StringAttribute{ + CustomType: fwtypes.RegexpType, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexache.MustCompile(`^[\w -]+$`), "must conform to: ^[\\w -]+$ "), + stringvalidator.LengthBetween(1, 64), + }, + }, + "created_by": schema.StringAttribute{ + Computed: true, + }, + "id": schema.StringAttribute{ + Computed: true, + }, + "created_at": schema.StringAttribute{ + CustomType: timetypes.RFC3339Type{}, + Computed: true, + }, + "failure_reasons": schema.ListAttribute{ + CustomType: fwtypes.NewListNestedObjectTypeOf[awstypes.ProjectDeletionError](ctx), + Computed: true, + }, + "last_updated_at": schema.StringAttribute{ + Computed: true, + }, + "project_status": schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.ProjectStatus](), + Computed: true, + }, + "result_metadata": schema.ListAttribute{ + CustomType: fwtypes.NewListNestedObjectTypeOf[middleware.Metadata](ctx), + Computed: true, + }, + }, + } +} + +func (r *resourceProject) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // TIP: ==== RESOURCE CREATE ==== + // Generally, the Create function should do the following things. Make + // sure there is a good reason if you don't do one of these. + // + // 1. Get a client connection to the relevant service + // 2. Fetch the plan + // 3. Populate a create input structure + // 4. Call the AWS create/put function + // 5. Using the output from the create function, set the minimum arguments + // and attributes for the Read function to work, as well as any computed + // only attributes. + // 6. Use a waiter to wait for create to complete + // 7. Save the request plan to response state + + // TIP: -- 1. Get a client connection to the relevant service + conn := r.Meta().DataZoneClient(ctx) + + // TIP: -- 2. Fetch the plan + var plan resourceProjectData + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + // TIP: -- 3. Populate a create input structure + in := &datazone.CreateProjectInput{ + Name: aws.String(plan.Name.ValueString()), + } + if !plan.Description.IsNull() { + in.Description = aws.String(plan.Description.ValueString()) + } + if !plan.DomainIdentifier.IsNull() { + in.DomainIdentifier = aws.String(plan.DomainIdentifier.ValueString()) + } + if !plan.GlossaryTerms.IsNull() { + // do later + } + + // TIP: -- 4. Call the AWS create function + out, err := conn.CreateProject(ctx, in) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.DataZone, create.ErrActionCreating, ResNameProject, plan.Name.String(), err), + err.Error(), + ) + return + } + if out == nil || !(out.FailureReasons == nil) { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.DataZone, create.ErrActionCreating, ResNameProject, plan.Name.String(), nil), + errors.New("empty output").Error(), + ) + return + } + + // TIP: -- 5. Using the output from the create function, set the minimum attributes + + // can i autoflex this? + resp.Diagnostics.Append(flex.Flatten(ctx, &out, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + // TIP: -- 6. Use a waiter to wait for create to complete + createTimeout := r.CreateTimeout(ctx, plan.Timeouts) + _, err = waitProjectCreated(ctx, conn, plan.DomainIdentifier.ValueString(),plan.ID.ValueString(),createTimeout) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.DataZone, create.ErrActionWaitingForCreation, ResNameProject, plan.Name.String(), err), + err.Error(), + ) + return + } + + // TIP: -- 7. Save the request plan to response state + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} + +func (r *resourceProject) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // TIP: ==== RESOURCE READ ==== + // Generally, the Read function should do the following things. Make + // sure there is a good reason if you don't do one of these. + // + // 1. Get a client connection to the relevant service + // 2. Fetch the state + // 3. Get the resource from AWS + // 4. Remove resource from state if it is not found + // 5. Set the arguments and attributes + // 6. Set the state + + // TIP: -- 1. Get a client connection to the relevant service + conn := r.Meta().DataZoneClient(ctx) + + // TIP: -- 2. Fetch the state + var state resourceProjectData + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + // TIP: -- 3. Get the resource from AWS using an API Get, List, or Describe- + // type function, or, better yet, using a finder. + var input datazone.GetProjectInput + if !state.DomainIdentifier.IsNull(){ + input.DomainIdentifier = state.DomainIdentifier.ValueStringPointer() + } + if !state.ID.IsNull(){ + input.Identifier = state.ID.ValueStringPointer() + } + out, err := conn.GetProject(ctx,&input) + // TIP: -- 4. Remove resource from state if it is not found + if tfresource.NotFound(err) { + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.DataZone, create.ErrActionSetting, ResNameProject, state.ID.String(), err), + err.Error(), + ) + return + } + + resp.Diagnostics.Append(flex.Flatten(ctx, &out, &state)...) + if resp.Diagnostics.HasError() { + return + } + + // TIP: -- 6. Set the state + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *resourceProject) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // TIP: ==== RESOURCE UPDATE ==== + // Not all resources have Update functions. There are a few reasons: + // a. The AWS API does not support changing a resource + // b. All arguments have RequiresReplace() plan modifiers + // c. The AWS API uses a create call to modify an existing resource + // + // In the cases of a. and b., the resource will not have an update method + // defined. In the case of c., Update and Create can be refactored to call + // the same underlying function. + // + // The rest of the time, there should be an Update function and it should + // do the following things. Make sure there is a good reason if you don't + // do one of these. + // + // 1. Get a client connection to the relevant service + // 2. Fetch the plan and state + // 3. Populate a modify input structure and check for changes + // 4. Call the AWS modify/update function + // 5. Use a waiter to wait for update to complete + // 6. Save the request plan to response state + // TIP: -- 1. Get a client connection to the relevant service + conn := r.Meta().DataZoneClient(ctx) + + // TIP: -- 2. Fetch the plan + var plan, state resourceProjectData + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) /// get the plan + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) // get the state + if resp.Diagnostics.HasError() { + return + } + + if plan.DomainIdentifier != state.DomainIdentifier { // Do i have to error check this + return // add error here + } + + // TIP: -- 3. Populate a modify input structure and check for changes + /*/ + if !plan.Name.Equal(state.Name) || + !plan.Description.Equal(state.Description) || + !plan.ComplexArgument.Equal(state.ComplexArgument) || + !plan.Type.Equal(state.Type) { +*/ + + in := &datazone.UpdateProjectInput{ + DomainIdentifier: aws.String(plan.DomainIdentifier.ValueString()), // This can't change + Identifier: aws.String(plan.ID.ValueString()), + } + + if !plan.Description.IsNull() { + in.Description = aws.String(plan.Description.ValueString()) + } + + if !plan.GlossaryTerms.IsNull() { + in.GlossaryTerms = aws.String(plan.Description.ValueString()) // don't know how to do + } + + if !plan.Name.IsNull() { + in.Name = aws.String(plan.Name.ValueString()) + } + + + + // TIP: -- 4. Call the AWS modify/update function + out, err := conn.UpdateProject(ctx, in) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.DataZone, create.ErrActionUpdating, ResNameProject, plan.ID.String(), err), + err.Error(), + ) + return + } + if out == nil || !(out.FailureReasons == nil) { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.DataZone, create.ErrActionUpdating, ResNameProject, plan.ID.String(), nil), + errors.New("empty output").Error(), + ) + return + } + + resp.Diagnostics.Append(flex.Flatten(ctx, &out, &state)...) + if resp.Diagnostics.HasError() { + return + } + + // TIP: -- 5. Use a waiter to wait for update to complete + updateTimeout := r.UpdateTimeout(ctx, plan.Timeouts) + _, err = waitProjectUpdated(ctx, conn, plan.ID.ValueString(), updateTimeout) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.DataZone, create.ErrActionWaitingForUpdate, ResNameProject, plan.ID.String(), err), + err.Error(), + ) + return + } + + // TIP: -- 6. Save the request plan to response state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *resourceProject) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // TIP: ==== RESOURCE DELETE ==== + // Most resources have Delete functions. There are rare situations + // where you might not need a delete: + // a. The AWS API does not provide a way to delete the resource + // b. The point of your resource is to perform an action (e.g., reboot a + // server) and deleting serves no purpose. + // + // The Delete function should do the following things. Make sure there + // is a good reason if you don't do one of these. + // + // 1. Get a client connection to the relevant service + // 2. Fetch the state + // 3. Populate a delete input structure + // 4. Call the AWS delete function + // 5. Use a waiter to wait for delete to complete + // TIP: -- 1. Get a client connection to the relevant service + conn := r.Meta().DataZoneClient(ctx) + + // TIP: -- 2. Fetch the state + var state resourceProjectData + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + // TIP: -- 3. Populate a delete input structure + in := &datazone.DeleteProjectInput{ + DomainIdentifier: aws.String((state.DomainIdentifier.ValueString())), + Identifier: aws.String((state.ID.ValueString())), + } + if !state.SkipDeletionCheck.IsNull() { + in.SkipDeletionCheck = state.SkipDeletionCheck.ValueBoolPointer() + } + + // TIP: -- 4. Call the AWS delete function + _, err := conn.DeleteProject(ctx, in) + // TIP: On rare occassions, the API returns a not found error after deleting a + // resource. If that happens, we don't want it to show up as an error. + if err != nil { + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return + } + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.DataZone, create.ErrActionDeleting, ResNameProject, state.ID.String(), err), + err.Error(), + ) + return + } + + deleteTimeout := r.DeleteTimeout(ctx, state.Timeouts) + _, err = waitProjectDeleted(ctx, conn,state.DomainIdentifier.ValueString(),state.ID.ValueString(), deleteTimeout) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.DataZone, create.ErrActionWaitingForDeletion, ResNameProject, state.ID.String(), err), + err.Error(), + ) + return + } +} + +// TIP: ==== TERRAFORM IMPORTING ==== +// If Read can get all the information it needs from the Identifier +// (i.e., path.Root("id")), you can use the PassthroughID importer. Otherwise, +// you'll need a custom import function. +// +// See more: +// https://developer.hashicorp.com/terraform/plugin/framework/resources/import +func (r *resourceProject) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +// TIP: ==== STATUS CONSTANTS ==== +// Create constants for states and statuses if the service does not +// already have suitable constants. We prefer that you use the constants +// provided in the service if available (e.g., awstypes.StatusInProgress). +const ( + statusChangePending = "PENDING" // I changes these to be more inline with aws types + statusUpdated = "UPDATED" // ^ +) + +// TIP: ==== WAITERS ==== +// Some resources of some services have waiters provided by the AWS API. +// Unless they do not work properly, use them rather than defining new ones +// here. +// +// Sometimes we define the wait, status, and find functions in separate +// files, wait.go, status.go, and find.go. Follow the pattern set out in the +// service and define these where it makes the most sense. +// +// If these functions are used in the _test.go file, they will need to be +// exported (i.e., capitalized). +// +// You will need to adjust the parameters and names to fit the service. +func waitProjectCreated(ctx context.Context, conn *datazone.Client, domain string, identifier string, timeout time.Duration) (*datazone.GetProjectOutput, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{}, + Target: []string{string(awstypes.ProjectStatusActive)}, + Refresh: statusProject(ctx, conn, domain,identifier), + Timeout: timeout, + NotFoundChecks: 20, + ContinuousTargetOccurence: 2, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*datazone.GetProjectOutput); ok { + return out, err + } + + return nil, err +} + +// TIP: It is easier to determine whether a resource is updated for some +// resources than others. The best case is a status flag that tells you when +// the update has been fully realized. Other times, you can check to see if a +// key resource argument is updated to a new value or not. +func waitProjectUpdated(ctx context.Context, conn *datazone.Client, domain string, identifier string, timeout time.Duration) (*datazone.GetProjectOutput, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{statusChangePending}, + Target: []string{statusUpdated}, + Refresh: statusProject(ctx, conn, domain,identifier), + Timeout: timeout, + NotFoundChecks: 20, + ContinuousTargetOccurence: 2, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*datazone.GetProjectOutput); ok { // not too sure if this is correct + return out, err + } + + return nil, err +} + +// TIP: A deleted waiter is almost like a backwards created waiter. There may +// be additional pending states, however. +func waitProjectDeleted(ctx context.Context, conn *datazone.Client, domain string, identifier string, timeout time.Duration) (*datazone.GetProjectOutput, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{string(awstypes.ProjectStatusDeleting), string(awstypes.ProjectStatusActive)}, + Target: []string{}, + Refresh: statusProject(ctx, conn, domain,identifier), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*datazone.GetProjectOutput); ok { // not too sure if this is correct + return out, err + } + + return nil, err +} + +func statusProject(ctx context.Context, conn *datazone.Client, domain string, identifier string) retry.StateRefreshFunc { + return func() (interface{}, string, error) { + out, err := findProjectByID(ctx, conn, domain,identifier) + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return out, aws.ToString((*string)(&out.ProjectStatus)), nil + } +} + +// TIP: ==== FINDERS ==== +func findProjectByID(ctx context.Context, conn *datazone.Client, domain string, identifier string) (*datazone.GetProjectOutput, error) { + in := &datazone.GetProjectInput{ + DomainIdentifier: aws.String(domain), + Identifier: aws.String(identifier), + } + + out, err := conn.GetProject(ctx, in) // add finder here later + if err != nil { + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: in, + } + } + + return nil, err + } + + if out == nil || !(out.FailureReasons == nil) { + return nil, tfresource.NewEmptyResultError(in) + } + + return out, nil +} + +// TIP: ==== FLEX ==== +// Flatteners and expanders ("flex" functions) help handle complex data +// types. Flatteners take an API data type and return the equivalent Plugin-Framework +// type. In other words, flatteners translate from AWS -> Terraform. +// +// On the other hand, expanders take a Terraform data structure and return +// something that you can send to the AWS API. In other words, expanders +// translate from Terraform -> AWS. +// +// See more: +// https://hashicorp.github.io/terraform-provider-aws/data-handling-and-conversion/ +func flattenComplexArgument(ctx context.Context, apiObject *awstypes.ComplexArgument) (types.List, diag.Diagnostics) { + var diags diag.Diagnostics + elemType := types.ObjectType{AttrTypes: complexArgumentAttrTypes} + + if apiObject == nil { + return types.ListNull(elemType), diags + } + + obj := map[string]attr.Value{ + "nested_required": flex.StringValueToFramework(ctx, apiObject.NestedRequired), + "nested_optional": flex.StringValueToFramework(ctx, apiObject.NestedOptional), + } + objVal, d := types.ObjectValue(complexArgumentAttrTypes, obj) + diags.Append(d...) + + listVal, d := types.ListValue(elemType, []attr.Value{objVal}) + diags.Append(d...) + + return listVal, diags +} + +// TIP: Often the AWS API will return a slice of structures in response to a +// request for information. Sometimes you will have set criteria (e.g., the ID) +// that means you'll get back a one-length slice. This plural function works +// brilliantly for that situation too. +/*/ +func flattenComplexArguments(ctx context.Context, apiObjects []*awstypes.ComplexArgument) (types.List, diag.Diagnostics) { + var diags diag.Diagnostics + elemType := types.ObjectType{AttrTypes: complexArgumentAttrTypes} + + if len(apiObjects) == 0 { + return types.ListNull(elemType), diags + } + + elems := []attr.Value{} + for _, apiObject := range apiObjects { + if apiObject == nil { + continue + } + + obj := map[string]attr.Value{ + "nested_required": flex.StringValueToFramework(ctx, apiObject.NestedRequired), + "nested_optional": flex.StringValueToFramework(ctx, apiObject.NestedOptional), + } + objVal, d := types.ObjectValue(complexArgumentAttrTypes, obj) + diags.Append(d...) + + elems = append(elems, objVal) + } + + listVal, d := types.ListValue(elemType, elems) + diags.Append(d...) + + return listVal, diags +} +*/ +// TIP: ==== DATA STRUCTURES ==== +// With Terraform Plugin-Framework configurations are deserialized into +// Go types, providing type safety without the need for type assertions. +// These structs should match the schema definition exactly, and the `tfsdk` +// tag value should match the attribute name. +// +// Nested objects are represented in their own data struct. These will +// also have a corresponding attribute type mapping for use inside flex +// functions. +// +// See more: +// https://developer.hashicorp.com/terraform/plugin/framework/handling-data/accessing-values +type resourceProjectData struct { + Description types.String `tfsdk:"description"` + DomainIdentifier types.String `tfsdk:"domain_identifier"` + GlossaryTerms fwtypes.ListNestedObjectValueOf[types.String] `tfsdk:"glossary_terms"` + Name types.String `tfsdk:"name"` + CreatedBy types.String `tfsdk:"created_by"` + ID types.String `tfsdk:"id"` + CreatedAt timetypes.RFC3339 `tfsdk:"created_at"` + FailureReasons fwtypes.ListNestedObjectValueOf[awstypes.ProjectDeletionError] `tfsdk:"failure_reasons"` + ProjectStatus fwtypes.StringEnum[awstypes.ProjectStatus] `tfsdk:"project_status"` + ResultMetadata middleware.Metadata `tfsdk:"result_metadata"` + Timeouts timeouts.Value `tfsdk:"timeouts"` + SkipDeletionCheck types.Bool `tfsdk:"skip_deletion_check"` +} + +// DomainId types.String `tfsdk:"domain_id"` // is this the same thing as domainIdent + +type complexArgumentData struct { + NestedRequired types.String `tfsdk:"nested_required"` + NestedOptional types.String `tfsdk:"nested_optional"` +} + +var complexArgumentAttrTypes = map[string]attr.Type{ + "nested_required": types.StringType, + "nested_optional": types.StringType, +} diff --git a/internal/service/datazone/project_test.go b/internal/service/datazone/project_test.go new file mode 100644 index 00000000000..82d108ced58 --- /dev/null +++ b/internal/service/datazone/project_test.go @@ -0,0 +1,238 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package datazone_test + +// **PLEASE DELETE THIS AND ALL TIP COMMENTS BEFORE SUBMITTING A PR FOR REVIEW!** +// +// TIP: ==== INTRODUCTION ==== +// Thank you for trying the skaff tool! +// +// You have opted to include these helpful comments. They all include "TIP:" +// to help you find and remove them when you're done with them. +// +// While some aspects of this file are customized to your input, the +// scaffold tool does *not* look at the AWS API and ensure it has correct +// function, structure, and variable names. It makes guesses based on +// commonalities. You will need to make significant adjustments. +// +// In other words, as generated, this is a rough outline of the work you will +// need to do. If something doesn't make sense for your situation, get rid of +// it. + +import ( + // TIP: ==== IMPORTS ==== + // This is a common set of imports but not customized to your code since + // your code hasn't been written yet. Make sure you, your IDE, or + // goimports -w fixes these imports. + // + // The provider linter wants your imports to be in two groups: first, + // standard library (i.e., "fmt" or "strings"), second, everything else. + // + // Also, AWS Go SDK v2 may handle nested structures differently than v1, + // using the services/datazone/types package. If so, you'll + // need to import types and reference the nested types, e.g., as + // types.. + "context" + "errors" + "fmt" + "testing" + + "github.com/YakDriver/regexache" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/datazone" + "github.com/aws/aws-sdk-go-v2/service/datazone/types" + sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/create" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + "github.com/hashicorp/terraform-provider-aws/names" + + // TIP: You will often need to import the package that this test file lives + // in. Since it is in the "test" context, it must import the package to use + // any normal context constants, variables, or functions. + tfdatazone "github.com/hashicorp/terraform-provider-aws/internal/service/datazone" +) + +func TestAccDataZoneProject_basic(t *testing.T) { + ctx := acctest.Context(t) + // TIP: This is a long-running test guard for tests that run longer than + // 300s (5 min) generally. + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var project datazone.GetProjectOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_datazone_project.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + //acctest.PreCheckPartitionHasService(t, names.DataZoneEndpointID) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.DataZoneServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckProjectDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccProjectConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckProjectExists(ctx, resourceName, &project), + resource.TestCheckResourceAttr(resourceName, "auto_minor_version_upgrade", "false"), + resource.TestCheckResourceAttrSet(resourceName, "maintenance_window_start_time.0.day_of_week"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "user.*", map[string]string{ + "console_access": "false", + "groups.#": "0", + "username": "Test", + "password": "TestTest1234", + }), + acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "datazone", regexache.MustCompile(`project:+.`)), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"apply_immediately", "user"}, + }, + }, + }) +} + +func TestAccDataZoneProject_disappears(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var project datazone.DescribeProjectResponse + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_datazone_project.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.DataZoneEndpointID) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.DataZoneServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckProjectDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccProjectConfig_basic(rName, testAccProjectVersionNewer), + Check: resource.ComposeTestCheckFunc( + testAccCheckProjectExists(ctx, resourceName, &project), + // TIP: The Plugin-Framework disappears helper is similar to the Plugin-SDK version, + // but expects a new resource factory function as the third argument. To expose this + // private function to the testing package, you may need to add a line like the following + // to exports_test.go: + // + // var ResourceProject = newResourceProject + acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tfdatazone.ResourceProject, resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckProjectDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).DataZoneClient(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_datazone_project" { + continue + } + + input := &datazone.DescribeProjectInput{ + ProjectId: aws.String(rs.Primary.ID), + } + _, err := conn.DescribeProject(ctx, &datazone.DescribeProjectInput{ + ProjectId: aws.String(rs.Primary.ID), + }) + if errs.IsA[*types.ResourceNotFoundException](err) { + return nil + } + if err != nil { + return create.Error(names.DataZone, create.ErrActionCheckingDestroyed, tfdatazone.ResNameProject, rs.Primary.ID, err) + } + + return create.Error(names.DataZone, create.ErrActionCheckingDestroyed, tfdatazone.ResNameProject, rs.Primary.ID, errors.New("not destroyed")) + } + + return nil + } +} + +func testAccCheckProjectExists(ctx context.Context, name string, project *datazone.GetProjectOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return create.Error(names.DataZone, create.ErrActionCheckingExistence, tfdatazone.ResNameProject, name, errors.New("not found")) + } + + if rs.Primary.ID == "" { + return create.Error(names.DataZone, create.ErrActionCheckingExistence, tfdatazone.ResNameProject, name, errors.New("not set")) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).DataZoneClient(ctx) + resp, err := conn.GetProject(ctx, &datazone.GetProjectInput{ + DomainIdentifier: project.DomainId, + Identifier: project.Id, + }) + + if err != nil { + return create.Error(names.DataZone, create.ErrActionCheckingExistence, tfdatazone.ResNameProject, rs.Primary.ID, err) + } + + *project = *resp + + return nil + } +} + +func testAccPreCheck(ctx context.Context, t *testing.T) { + conn := acctest.Provider.Meta().(*conns.AWSClient).DataZoneClient(ctx) + + input := &datazone.ListProjectsInput{} + _, err := conn.ListProjects(ctx, input) + + if acctest.PreCheckSkipError(err) { + t.Skipf("skipping acceptance testing: %s", err) + } + if err != nil { + t.Fatalf("unexpected PreCheck error: %s", err) + } +} + +func testAccCheckProjectNotRecreated(before, after *datazone.DescribeProjectResponse) resource.TestCheckFunc { + return func(s *terraform.State) error { + if before, after := aws.ToString(before.ProjectId), aws.ToString(after.ProjectId); before != after { + return create.Error(names.DataZone, create.ErrActionCheckingNotRecreated, tfdatazone.ResNameProject, aws.ToString(before.ProjectId), errors.New("recreated")) + } + + return nil + } +} + +func testAccProjectConfig_basic(rName string) string { + return fmt.Sprintf(` +resource "aws_security_group" "test" { + name = %[1]q +} + +resource "aws_datazone_project" "test" { + domain_identifier = "identifier" + name = %[1]q + description = "" + glossary_terms = ["glossary","terms"] +} +`, rName) +} diff --git a/website/docs/r/datazone_project.html.markdown b/website/docs/r/datazone_project.html.markdown new file mode 100644 index 00000000000..eee1fc3f870 --- /dev/null +++ b/website/docs/r/datazone_project.html.markdown @@ -0,0 +1,69 @@ +--- +subcategory: "DataZone" +layout: "aws" +page_title: "AWS: aws_datazone_project" +description: |- + Terraform resource for managing an AWS DataZone Project. +--- +` +# Resource: aws_datazone_project + +Terraform resource for managing an AWS DataZone Project. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_datazone_project" "example" { +} +``` + +## Argument Reference + +The following arguments are required: + +* `example_arg` - (Required) Concise argument description. Do not begin the description with "An", "The", "Defines", "Indicates", or "Specifies," as these are verbose. In other words, "Indicates the amount of storage," can be rewritten as "Amount of storage," without losing any information. + +The following arguments are optional: + +* `optional_arg` - (Optional) Concise argument description. Do not begin the description with "An", "The", "Defines", "Indicates", or "Specifies," as these are verbose. In other words, "Indicates the amount of storage," can be rewritten as "Amount of storage," without losing any information. + +## Attribute Reference + +This resource exports the following attributes in addition to the arguments above: + +* `arn` - ARN of the Project. Do not begin the description with "An", "The", "Defines", "Indicates", or "Specifies," as these are verbose. In other words, "Indicates the amount of storage," can be rewritten as "Amount of storage," without losing any information. +* `example_attribute` - Concise description. Do not begin the description with "An", "The", "Defines", "Indicates", or "Specifies," as these are verbose. In other words, "Indicates the amount of storage," can be rewritten as "Amount of storage," without losing any information. + +## Timeouts + +[Configuration options](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts): + +* `create` - (Default `60m`) +* `update` - (Default `180m`) +* `delete` - (Default `90m`) + +## Import + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import DataZone Project using the `example_id_arg`. For example: + +```terraform +import { + to = aws_datazone_project.example + id = "project-id-12345678" +} +``` + +Using `terraform import`, import DataZone Project using the `example_id_arg`. For example: + +```console +% terraform import aws_datazone_project.example project-id-12345678 +``` From c60c691afe93f83c6f28f16cdae91a5fabc8d509 Mon Sep 17 00:00:00 2001 From: ThomasZalewski Date: Fri, 5 Jul 2024 17:03:46 -0400 Subject: [PATCH 02/17] Wrote tests and improved resource. --- internal/service/datazone/exports_test.go | 1 + internal/service/datazone/project.go | 157 ++++++++++++------ internal/service/datazone/project_test.go | 105 ++++++++---- .../service/datazone/service_package_gen.go | 4 + 4 files changed, 178 insertions(+), 89 deletions(-) diff --git a/internal/service/datazone/exports_test.go b/internal/service/datazone/exports_test.go index bb4638c851a..58f3e4834fd 100644 --- a/internal/service/datazone/exports_test.go +++ b/internal/service/datazone/exports_test.go @@ -8,4 +8,5 @@ var ( ResourceDomain = newResourceDomain ResourceEnvironmentBlueprintConfiguration = newResourceEnvironmentBlueprintConfiguration IsResourceMissing = isResourceMissing + ResourceProject = newResourceProject ) diff --git a/internal/service/datazone/project.go b/internal/service/datazone/project.go index caeeff8c0d3..31e594ee933 100644 --- a/internal/service/datazone/project.go +++ b/internal/service/datazone/project.go @@ -35,20 +35,26 @@ import ( // awstypes.. "context" "errors" + //"regexp" "time" "github.com/YakDriver/regexache" "github.com/aws/aws-sdk-go-v2/aws" + + //"github.com/aws/smithy-go/middleware" + //"github.com/aws/aws-sdk-go-v2/aws/middleware" "github.com/aws/aws-sdk-go-v2/service/datazone" awstypes "github.com/aws/aws-sdk-go-v2/service/datazone/types" + //"github.com/aws/smithy-go/middleware" "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes" "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" + + //"github.com/hashicorp/terraform-plugin-framework/attr" + //"github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -59,7 +65,11 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "github.com/hashicorp/terraform-provider-aws/internal/create" + //"github.com/hashicorp/terraform-provider-aws/internal/enum" + //awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-provider-aws/internal/errs" "github.com/hashicorp/terraform-provider-aws/internal/framework" "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" @@ -168,19 +178,29 @@ func (r *resourceProject) Schema(ctx context.Context, req resource.SchemaRequest Validators: []validator.String{ stringvalidator.RegexMatches(regexache.MustCompile(`^dzd[-_][a-zA-Z0-9_-]{1,36}$`), "must conform to: ^dzd[-_][a-zA-Z0-9_-]{1,36}$ "), }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, + "glossary_terms": schema.ListAttribute{ - CustomType: fwtypes.NewListNestedObjectTypeOf[types.String](ctx), + CustomType: fwtypes.ListOfStringType, + ElementType: fwtypes.RegexpType.StringType, + Validators: []validator.List{ listvalidator.ValueStringsAre(stringvalidator.LengthBetween(1, 20)), + listvalidator.ValueStringsAre(stringvalidator.RegexMatches(regexache.MustCompile(`^[a-zA-Z0-9_-]{1,36}]$`), "must conform to: ^[a-zA-Z0-9_-]{1,36}]$ ")), }, + Optional: true, }, + names.AttrName: schema.StringAttribute{ CustomType: fwtypes.RegexpType, Validators: []validator.String{ stringvalidator.RegexMatches(regexache.MustCompile(`^[\w -]+$`), "must conform to: ^[\\w -]+$ "), stringvalidator.LengthBetween(1, 64), }, + Required: true, }, "created_by": schema.StringAttribute{ Computed: true, @@ -192,21 +212,41 @@ func (r *resourceProject) Schema(ctx context.Context, req resource.SchemaRequest CustomType: timetypes.RFC3339Type{}, Computed: true, }, + "failure_reasons": schema.ListAttribute{ - CustomType: fwtypes.NewListNestedObjectTypeOf[awstypes.ProjectDeletionError](ctx), + CustomType: fwtypes.NewListNestedObjectTypeOf[dsProjectDeletionError](ctx), Computed: true, }, + "last_updated_at": schema.StringAttribute{ - Computed: true, + CustomType: timetypes.RFC3339Type{}, + Computed: true, }, "project_status": schema.StringAttribute{ CustomType: fwtypes.StringEnumType[awstypes.ProjectStatus](), Computed: true, }, - "result_metadata": schema.ListAttribute{ - CustomType: fwtypes.NewListNestedObjectTypeOf[middleware.Metadata](ctx), + /* + "result_metadata": schema.ListAttribute{ + CustomType: fwtypes.NewListNestedObjectTypeOf[middleware.Metadata](ctx), + Computed: true, + }, + */ + "domain_id": schema.StringAttribute{ + CustomType: fwtypes.RegexpType, Computed: true, }, + + "skip_deletion_check": schema.BoolAttribute{ + Required: true, + }, + }, + Blocks: map[string]schema.Block{ + names.AttrTimeouts: timeouts.Block(ctx, timeouts.Opts{ + Create: true, + Update: true, + Delete: true, + }), }, } } @@ -214,7 +254,7 @@ func (r *resourceProject) Schema(ctx context.Context, req resource.SchemaRequest func (r *resourceProject) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // TIP: ==== RESOURCE CREATE ==== // Generally, the Create function should do the following things. Make - // sure there is a good reason if you don't do one of these. + // sure there is a good reason if you don't do one of these. // // 1. Get a client connection to the relevant service // 2. Fetch the plan @@ -235,6 +275,17 @@ func (r *resourceProject) Create(ctx context.Context, req resource.CreateRequest if resp.Diagnostics.HasError() { return } + // validate that domain exists + var validateDomain datazone.GetDomainInput + validateDomain.Identifier = plan.DomainIdentifier.ValueStringPointer() + _, err := conn.GetDomain(ctx, &validateDomain) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.DataZone, create.ErrActionCreating, ResNameProject, plan.Name.String(), err), + err.Error(), + ) + return + } // TIP: -- 3. Populate a create input structure in := &datazone.CreateProjectInput{ @@ -246,9 +297,6 @@ func (r *resourceProject) Create(ctx context.Context, req resource.CreateRequest if !plan.DomainIdentifier.IsNull() { in.DomainIdentifier = aws.String(plan.DomainIdentifier.ValueString()) } - if !plan.GlossaryTerms.IsNull() { - // do later - } // TIP: -- 4. Call the AWS create function out, err := conn.CreateProject(ctx, in) @@ -270,14 +318,17 @@ func (r *resourceProject) Create(ctx context.Context, req resource.CreateRequest // TIP: -- 5. Using the output from the create function, set the minimum attributes // can i autoflex this? + resp.Diagnostics.Append(flex.Flatten(ctx, &out, &plan)...) if resp.Diagnostics.HasError() { return } + //plan.ResultMetadata = out.ResultMetadata.Clone() + // TIP: -- 6. Use a waiter to wait for create to complete createTimeout := r.CreateTimeout(ctx, plan.Timeouts) - _, err = waitProjectCreated(ctx, conn, plan.DomainIdentifier.ValueString(),plan.ID.ValueString(),createTimeout) + _, err = waitProjectCreated(ctx, conn, plan.DomainIdentifier.ValueString(), plan.ID.ValueString(), createTimeout) if err != nil { resp.Diagnostics.AddError( create.ProblemStandardMessage(names.DataZone, create.ErrActionWaitingForCreation, ResNameProject, plan.Name.String(), err), @@ -311,17 +362,16 @@ func (r *resourceProject) Read(ctx context.Context, req resource.ReadRequest, re if resp.Diagnostics.HasError() { return } - // TIP: -- 3. Get the resource from AWS using an API Get, List, or Describe- // type function, or, better yet, using a finder. var input datazone.GetProjectInput - if !state.DomainIdentifier.IsNull(){ + if !state.DomainIdentifier.IsNull() { input.DomainIdentifier = state.DomainIdentifier.ValueStringPointer() } - if !state.ID.IsNull(){ + if !state.ID.IsNull() { input.Identifier = state.ID.ValueStringPointer() } - out, err := conn.GetProject(ctx,&input) + out, err := conn.GetProject(ctx, &input) // TIP: -- 4. Remove resource from state if it is not found if tfresource.NotFound(err) { resp.State.RemoveResource(ctx) @@ -339,6 +389,7 @@ func (r *resourceProject) Read(ctx context.Context, req resource.ReadRequest, re if resp.Diagnostics.HasError() { return } + //state.ResultMetadata = out.ResultMetadata.Clone() // TIP: -- 6. Set the state resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) @@ -370,13 +421,13 @@ func (r *resourceProject) Update(ctx context.Context, req resource.UpdateRequest // TIP: -- 2. Fetch the plan var plan, state resourceProjectData - resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) /// get the plan + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) /// get the plan resp.Diagnostics.Append(req.State.Get(ctx, &state)...) // get the state if resp.Diagnostics.HasError() { return } - if plan.DomainIdentifier != state.DomainIdentifier { // Do i have to error check this + if plan.DomainIdentifier != state.DomainIdentifier { // I should error check this return // add error here } @@ -386,27 +437,21 @@ func (r *resourceProject) Update(ctx context.Context, req resource.UpdateRequest !plan.Description.Equal(state.Description) || !plan.ComplexArgument.Equal(state.ComplexArgument) || !plan.Type.Equal(state.Type) { -*/ + */ in := &datazone.UpdateProjectInput{ DomainIdentifier: aws.String(plan.DomainIdentifier.ValueString()), // This can't change - Identifier: aws.String(plan.ID.ValueString()), + Identifier: aws.String(plan.ID.ValueString()), } if !plan.Description.IsNull() { in.Description = aws.String(plan.Description.ValueString()) } - if !plan.GlossaryTerms.IsNull() { - in.GlossaryTerms = aws.String(plan.Description.ValueString()) // don't know how to do - } - if !plan.Name.IsNull() { in.Name = aws.String(plan.Name.ValueString()) } - - // TIP: -- 4. Call the AWS modify/update function out, err := conn.UpdateProject(ctx, in) if err != nil { @@ -428,10 +473,10 @@ func (r *resourceProject) Update(ctx context.Context, req resource.UpdateRequest if resp.Diagnostics.HasError() { return } - + //state.ResultMetadata = out.ResultMetadata.Clone() // TIP: -- 5. Use a waiter to wait for update to complete updateTimeout := r.UpdateTimeout(ctx, plan.Timeouts) - _, err = waitProjectUpdated(ctx, conn, plan.ID.ValueString(), updateTimeout) + _, err = waitProjectUpdated(ctx, conn, plan.ID.ValueString(), updateTimeout.String(), r.ReadTimeout(ctx, plan.Timeouts)) if err != nil { resp.Diagnostics.AddError( create.ProblemStandardMessage(names.DataZone, create.ErrActionWaitingForUpdate, ResNameProject, plan.ID.String(), err), @@ -475,7 +520,7 @@ func (r *resourceProject) Delete(ctx context.Context, req resource.DeleteRequest DomainIdentifier: aws.String((state.DomainIdentifier.ValueString())), Identifier: aws.String((state.ID.ValueString())), } - if !state.SkipDeletionCheck.IsNull() { + if !state.SkipDeletionCheck.IsNull() { // N in.SkipDeletionCheck = state.SkipDeletionCheck.ValueBoolPointer() } @@ -495,7 +540,7 @@ func (r *resourceProject) Delete(ctx context.Context, req resource.DeleteRequest } deleteTimeout := r.DeleteTimeout(ctx, state.Timeouts) - _, err = waitProjectDeleted(ctx, conn,state.DomainIdentifier.ValueString(),state.ID.ValueString(), deleteTimeout) + _, err = waitProjectDeleted(ctx, conn, state.DomainIdentifier.ValueString(), state.ID.ValueString(), deleteTimeout) if err != nil { resp.Diagnostics.AddError( create.ProblemStandardMessage(names.DataZone, create.ErrActionWaitingForDeletion, ResNameProject, state.ID.String(), err), @@ -542,7 +587,7 @@ func waitProjectCreated(ctx context.Context, conn *datazone.Client, domain strin stateConf := &retry.StateChangeConf{ Pending: []string{}, Target: []string{string(awstypes.ProjectStatusActive)}, - Refresh: statusProject(ctx, conn, domain,identifier), + Refresh: statusProject(ctx, conn, domain, identifier), Timeout: timeout, NotFoundChecks: 20, ContinuousTargetOccurence: 2, @@ -564,7 +609,7 @@ func waitProjectUpdated(ctx context.Context, conn *datazone.Client, domain strin stateConf := &retry.StateChangeConf{ Pending: []string{statusChangePending}, Target: []string{statusUpdated}, - Refresh: statusProject(ctx, conn, domain,identifier), + Refresh: statusProject(ctx, conn, domain, identifier), Timeout: timeout, NotFoundChecks: 20, ContinuousTargetOccurence: 2, @@ -584,7 +629,7 @@ func waitProjectDeleted(ctx context.Context, conn *datazone.Client, domain strin stateConf := &retry.StateChangeConf{ Pending: []string{string(awstypes.ProjectStatusDeleting), string(awstypes.ProjectStatusActive)}, Target: []string{}, - Refresh: statusProject(ctx, conn, domain,identifier), + Refresh: statusProject(ctx, conn, domain, identifier), Timeout: timeout, } @@ -598,7 +643,7 @@ func waitProjectDeleted(ctx context.Context, conn *datazone.Client, domain strin func statusProject(ctx context.Context, conn *datazone.Client, domain string, identifier string) retry.StateRefreshFunc { return func() (interface{}, string, error) { - out, err := findProjectByID(ctx, conn, domain,identifier) + out, err := findProjectByID(ctx, conn, domain, identifier) if tfresource.NotFound(err) { return nil, "", nil } @@ -615,7 +660,7 @@ func statusProject(ctx context.Context, conn *datazone.Client, domain string, id func findProjectByID(ctx context.Context, conn *datazone.Client, domain string, identifier string) (*datazone.GetProjectOutput, error) { in := &datazone.GetProjectInput{ DomainIdentifier: aws.String(domain), - Identifier: aws.String(identifier), + Identifier: aws.String(identifier), } out, err := conn.GetProject(ctx, in) // add finder here later @@ -648,6 +693,7 @@ func findProjectByID(ctx context.Context, conn *datazone.Client, domain string, // // See more: // https://hashicorp.github.io/terraform-provider-aws/data-handling-and-conversion/ +/* func flattenComplexArgument(ctx context.Context, apiObject *awstypes.ComplexArgument) (types.List, diag.Diagnostics) { var diags diag.Diagnostics elemType := types.ObjectType{AttrTypes: complexArgumentAttrTypes} @@ -668,6 +714,7 @@ func flattenComplexArgument(ctx context.Context, apiObject *awstypes.ComplexArgu return listVal, diags } +*/ // TIP: Often the AWS API will return a slice of structures in response to a // request for information. Sometimes you will have set criteria (e.g., the ID) @@ -717,28 +764,32 @@ func flattenComplexArguments(ctx context.Context, apiObjects []*awstypes.Complex // See more: // https://developer.hashicorp.com/terraform/plugin/framework/handling-data/accessing-values type resourceProjectData struct { - Description types.String `tfsdk:"description"` - DomainIdentifier types.String `tfsdk:"domain_identifier"` - GlossaryTerms fwtypes.ListNestedObjectValueOf[types.String] `tfsdk:"glossary_terms"` - Name types.String `tfsdk:"name"` - CreatedBy types.String `tfsdk:"created_by"` - ID types.String `tfsdk:"id"` - CreatedAt timetypes.RFC3339 `tfsdk:"created_at"` - FailureReasons fwtypes.ListNestedObjectValueOf[awstypes.ProjectDeletionError] `tfsdk:"failure_reasons"` - ProjectStatus fwtypes.StringEnum[awstypes.ProjectStatus] `tfsdk:"project_status"` - ResultMetadata middleware.Metadata `tfsdk:"result_metadata"` - Timeouts timeouts.Value `tfsdk:"timeouts"` - SkipDeletionCheck types.Bool `tfsdk:"skip_deletion_check"` + Description types.String `tfsdk:"description"` + DomainIdentifier fwtypes.Regexp `tfsdk:"domain_identifier"` + DomainId fwtypes.Regexp `tfsdk:"domain_id"` + Name fwtypes.Regexp `tfsdk:"name"` + CreatedBy types.String `tfsdk:"created_by"` + ID types.String `tfsdk:"id"` + CreatedAt timetypes.RFC3339 `tfsdk:"created_at"` + FailureReasons fwtypes.ListNestedObjectValueOf[dsProjectDeletionError] `tfsdk:"failure_reasons"` + LastUpdatedAt timetypes.RFC3339 `tfsdk:"last_updated_at"` + ProjectStatus fwtypes.StringEnum[awstypes.ProjectStatus] `tfsdk:"project_status"` + Timeouts timeouts.Value `tfsdk:"timeouts"` + SkipDeletionCheck types.Bool `tfsdk:"skip_deletion_check"` + GlossaryTerms fwtypes.ListValueOf[types.String] `tfsdk:"glossary_terms"` } // DomainId types.String `tfsdk:"domain_id"` // is this the same thing as domainIdent -type complexArgumentData struct { - NestedRequired types.String `tfsdk:"nested_required"` - NestedOptional types.String `tfsdk:"nested_optional"` +type dsProjectDeletionError struct { + Code types.String `tfsdk:"code"` + Message types.String `tfsdk:"message"` } -var complexArgumentAttrTypes = map[string]attr.Type{ - "nested_required": types.StringType, - "nested_optional": types.StringType, +/* +type Metadata struct { + values map[interface{}]interface{} `tfsdk:"metadata"` } + +ResultMetadata types.MapType `tfsdk:"result_metadata"` +*/ diff --git a/internal/service/datazone/project_test.go b/internal/service/datazone/project_test.go index 82d108ced58..57c1a2d032a 100644 --- a/internal/service/datazone/project_test.go +++ b/internal/service/datazone/project_test.go @@ -38,7 +38,6 @@ import ( "fmt" "testing" - "github.com/YakDriver/regexache" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/datazone" "github.com/aws/aws-sdk-go-v2/service/datazone/types" @@ -66,39 +65,45 @@ func TestAccDataZoneProject_basic(t *testing.T) { } var project datazone.GetProjectOutput - rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) // Name for project + dName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) // Name for domain resourceName := "aws_datazone_project.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t) //acctest.PreCheckPartitionHasService(t, names.DataZoneEndpointID) - testAccPreCheck(ctx, t) + //testAccPreCheckProject(ctx, t) }, ErrorCheck: acctest.ErrorCheck(t, names.DataZoneServiceID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, CheckDestroy: testAccCheckProjectDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccProjectConfig_basic(rName), + Config: testAccProjectConfig_basic(rName, dName), Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists(ctx, resourceName, &project), - resource.TestCheckResourceAttr(resourceName, "auto_minor_version_upgrade", "false"), - resource.TestCheckResourceAttrSet(resourceName, "maintenance_window_start_time.0.day_of_week"), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "user.*", map[string]string{ - "console_access": "false", - "groups.#": "0", - "username": "Test", - "password": "TestTest1234", - }), - acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "datazone", regexache.MustCompile(`project:+.`)), + // inputed + resource.TestCheckResourceAttr(resourceName, "domain_identifier", dName), // find a domain identifier + resource.TestCheckResourceAttrSet(resourceName, "glossary_terms.#"), + resource.TestCheckResourceAttr(resourceName, "description", "sample description"), + resource.TestCheckResourceAttr(resourceName, "name", resourceName), + resource.TestCheckResourceAttrSet(resourceName, "skip_deletion_check"), + // computed + resource.TestCheckResourceAttrSet(resourceName, "created_by"), + resource.TestCheckResourceAttrSet(resourceName, "id"), + resource.TestCheckResourceAttrSet(resourceName, "created_at"), + resource.TestCheckResourceAttrSet(resourceName, "failure_reasons.0.#"), + resource.TestCheckResourceAttrSet(resourceName, "last_updated_at"), + resource.TestCheckResourceAttrSet(resourceName, "project_status"), + resource.TestCheckResourceAttrSet(resourceName, "result_metadata"), ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"apply_immediately", "user"}, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + //ImportStateVerifyIgnore: []string{"apply_immediately", "user"}, }, }, }) @@ -110,22 +115,23 @@ func TestAccDataZoneProject_disappears(t *testing.T) { t.Skip("skipping long-running test in short mode") } - var project datazone.DescribeProjectResponse + var project datazone.GetProjectOutput rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + dName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) // Name for domain resourceName := "aws_datazone_project.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t) - acctest.PreCheckPartitionHasService(t, names.DataZoneEndpointID) - testAccPreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.AttrDomainName) + testAccPreCheckProject(ctx, t) }, ErrorCheck: acctest.ErrorCheck(t, names.DataZoneServiceID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, CheckDestroy: testAccCheckProjectDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccProjectConfig_basic(rName, testAccProjectVersionNewer), + Config: testAccProjectConfig_basic(rName, dName), Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists(ctx, resourceName, &project), // TIP: The Plugin-Framework disappears helper is similar to the Plugin-SDK version, @@ -151,12 +157,11 @@ func testAccCheckProjectDestroy(ctx context.Context) resource.TestCheckFunc { continue } - input := &datazone.DescribeProjectInput{ - ProjectId: aws.String(rs.Primary.ID), + input := &datazone.GetProjectInput{ + DomainIdentifier: aws.String(rs.Primary.Attributes["${aws_datazone_domain.test.id}"]), // can this be hardcoded in? + Identifier: aws.String(rs.Primary.ID), } - _, err := conn.DescribeProject(ctx, &datazone.DescribeProjectInput{ - ProjectId: aws.String(rs.Primary.ID), - }) + _, err := conn.GetProject(ctx, input) if errs.IsA[*types.ResourceNotFoundException](err) { return nil } @@ -167,9 +172,35 @@ func testAccCheckProjectDestroy(ctx context.Context) resource.TestCheckFunc { return create.Error(names.DataZone, create.ErrActionCheckingDestroyed, tfdatazone.ResNameProject, rs.Primary.ID, errors.New("not destroyed")) } - return nil + return testAccCheckProjectDomainDestroy(ctx, s) } } +func testAccCheckProjectDomainDestroy(ctx context.Context, s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).DataZoneClient(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_datazone_domain" { + continue + } + + _, err := conn.GetDomain(ctx, &datazone.GetDomainInput{ + Identifier: aws.String(rs.Primary.ID), + }) + + if tfdatazone.IsResourceMissing(err) { + return nil + } + + if err != nil { + return create.Error(names.DataZone, create.ErrActionCheckingDestroyed, tfdatazone.ResNameDomain, rs.Primary.ID, err) + } + + return create.Error(names.DataZone, create.ErrActionCheckingDestroyed, tfdatazone.ResNameDomain, rs.Primary.ID, errors.New("not destroyed")) + } + + return nil + +} func testAccCheckProjectExists(ctx context.Context, name string, project *datazone.GetProjectOutput) resource.TestCheckFunc { return func(s *terraform.State) error { @@ -198,7 +229,7 @@ func testAccCheckProjectExists(ctx context.Context, name string, project *datazo } } -func testAccPreCheck(ctx context.Context, t *testing.T) { +func testAccPreCheckProject(ctx context.Context, t *testing.T) { conn := acctest.Provider.Meta().(*conns.AWSClient).DataZoneClient(ctx) input := &datazone.ListProjectsInput{} @@ -212,27 +243,29 @@ func testAccPreCheck(ctx context.Context, t *testing.T) { } } -func testAccCheckProjectNotRecreated(before, after *datazone.DescribeProjectResponse) resource.TestCheckFunc { +func testAccCheckProjectNotRecreated(before, after *datazone.GetProjectOutput) resource.TestCheckFunc { return func(s *terraform.State) error { - if before, after := aws.ToString(before.ProjectId), aws.ToString(after.ProjectId); before != after { - return create.Error(names.DataZone, create.ErrActionCheckingNotRecreated, tfdatazone.ResNameProject, aws.ToString(before.ProjectId), errors.New("recreated")) + if before, after := aws.ToString(before.Id), aws.ToString(after.Id); before != after { + return create.Error(names.DataZone, create.ErrActionCheckingNotRecreated, tfdatazone.ResNameProject, before, errors.New("recreated")) } return nil } } -func testAccProjectConfig_basic(rName string) string { - return fmt.Sprintf(` +func testAccProjectConfig_basic(rName, dName string) string { + return acctest.ConfigCompose(testAccDomainConfig_basic(dName), fmt.Sprintf(` resource "aws_security_group" "test" { name = %[1]q } resource "aws_datazone_project" "test" { - domain_identifier = "identifier" + domain_identifier = aws_datazone_domain.test.id name = %[1]q description = "" - glossary_terms = ["glossary","terms"] + skip_deletion_check = true } -`, rName) +`, rName)) } + +// glossary_terms = ["abababab]"] diff --git a/internal/service/datazone/service_package_gen.go b/internal/service/datazone/service_package_gen.go index 7f0e80c975a..a2745588aad 100644 --- a/internal/service/datazone/service_package_gen.go +++ b/internal/service/datazone/service_package_gen.go @@ -36,6 +36,10 @@ func (p *servicePackage) FrameworkResources(ctx context.Context) []*types.Servic Factory: newResourceEnvironmentBlueprintConfiguration, Name: "Environment Blueprint Configuration", }, + { + Factory: newResourceProject, + Name: "Project", + }, } } From 059df7e5ad0d5fc36b291232cbf02a6db05e73b2 Mon Sep 17 00:00:00 2001 From: ThomasZalewski Date: Tue, 9 Jul 2024 13:22:31 -0400 Subject: [PATCH 03/17] Added support for C,R, and D actions. --- internal/service/datazone/project.go | 142 +++++++++++--------- internal/service/datazone/project_test.go | 155 ++++++++++++++++++---- 2 files changed, 207 insertions(+), 90 deletions(-) diff --git a/internal/service/datazone/project.go b/internal/service/datazone/project.go index 31e594ee933..1c9eebdb57c 100644 --- a/internal/service/datazone/project.go +++ b/internal/service/datazone/project.go @@ -35,7 +35,9 @@ import ( // awstypes.. "context" "errors" + //"regexp" + "fmt" "time" "github.com/YakDriver/regexache" @@ -171,7 +173,7 @@ func (r *resourceProject) Schema(ctx context.Context, req resource.SchemaRequest stringvalidator.LengthAtMost(2048), }, }, - "domain_identifier": schema.StringAttribute{ + "domain_id": schema.StringAttribute{ CustomType: fwtypes.RegexpType, Optional: true, Computed: true, @@ -189,7 +191,7 @@ func (r *resourceProject) Schema(ctx context.Context, req resource.SchemaRequest Validators: []validator.List{ listvalidator.ValueStringsAre(stringvalidator.LengthBetween(1, 20)), - listvalidator.ValueStringsAre(stringvalidator.RegexMatches(regexache.MustCompile(`^[a-zA-Z0-9_-]{1,36}]$`), "must conform to: ^[a-zA-Z0-9_-]{1,36}]$ ")), + listvalidator.ValueStringsAre(stringvalidator.RegexMatches(regexache.MustCompile(`^[a-zA-Z0-9_-]{1,36}$`), "must conform to: ^[a-zA-Z0-9_-]{1,36}$ ")), }, Optional: true, }, @@ -227,15 +229,17 @@ func (r *resourceProject) Schema(ctx context.Context, req resource.SchemaRequest Computed: true, }, /* - "result_metadata": schema.ListAttribute{ - CustomType: fwtypes.NewListNestedObjectTypeOf[middleware.Metadata](ctx), + "result_metadata": schema.ListAttribute{ + CustomType: fwtypes.NewListNestedObjectTypeOf[middleware.Metadata](ctx), + Computed: true, + }, + + + "domain_id": schema.StringAttribute{ + CustomType: fwtypes.RegexpType, Computed: true, }, */ - "domain_id": schema.StringAttribute{ - CustomType: fwtypes.RegexpType, - Computed: true, - }, "skip_deletion_check": schema.BoolAttribute{ Required: true, @@ -252,6 +256,7 @@ func (r *resourceProject) Schema(ctx context.Context, req resource.SchemaRequest } func (r *resourceProject) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + fmt.Println("create") // TIP: ==== RESOURCE CREATE ==== // Generally, the Create function should do the following things. Make // sure there is a good reason if you don't do one of these. @@ -277,7 +282,7 @@ func (r *resourceProject) Create(ctx context.Context, req resource.CreateRequest } // validate that domain exists var validateDomain datazone.GetDomainInput - validateDomain.Identifier = plan.DomainIdentifier.ValueStringPointer() + validateDomain.Identifier = plan.DomainId.ValueStringPointer() _, err := conn.GetDomain(ctx, &validateDomain) if err != nil { resp.Diagnostics.AddError( @@ -288,14 +293,18 @@ func (r *resourceProject) Create(ctx context.Context, req resource.CreateRequest } // TIP: -- 3. Populate a create input structure + // we probably want to error here if there is null but not rn in := &datazone.CreateProjectInput{ Name: aws.String(plan.Name.ValueString()), } if !plan.Description.IsNull() { in.Description = aws.String(plan.Description.ValueString()) } - if !plan.DomainIdentifier.IsNull() { - in.DomainIdentifier = aws.String(plan.DomainIdentifier.ValueString()) + if !plan.DomainId.IsNull() { + in.DomainIdentifier = aws.String(plan.DomainId.ValueString()) + } + if !plan.GlossaryTerms.IsNull() { + in.GlossaryTerms = aws.ToStringSlice(flex.ExpandFrameworkStringList(ctx, plan.GlossaryTerms)) } // TIP: -- 4. Call the AWS create function @@ -319,16 +328,18 @@ func (r *resourceProject) Create(ctx context.Context, req resource.CreateRequest // can i autoflex this? - resp.Diagnostics.Append(flex.Flatten(ctx, &out, &plan)...) + resp.Diagnostics.Append(flex.Flatten(ctx, out, &plan)...) if resp.Diagnostics.HasError() { return } + // data.Type = fwtypes.StringEnumValue[awstypes.VisibilityType](image.Visibility) + plan.ProjectStatus = fwtypes.StringEnumValue[awstypes.ProjectStatus](out.ProjectStatus) //plan.ResultMetadata = out.ResultMetadata.Clone() // TIP: -- 6. Use a waiter to wait for create to complete createTimeout := r.CreateTimeout(ctx, plan.Timeouts) - _, err = waitProjectCreated(ctx, conn, plan.DomainIdentifier.ValueString(), plan.ID.ValueString(), createTimeout) + _, err = waitProjectCreated(ctx, conn, plan.DomainId.ValueString(), plan.ID.ValueString(), createTimeout) if err != nil { resp.Diagnostics.AddError( create.ProblemStandardMessage(names.DataZone, create.ErrActionWaitingForCreation, ResNameProject, plan.Name.String(), err), @@ -338,42 +349,25 @@ func (r *resourceProject) Create(ctx context.Context, req resource.CreateRequest } // TIP: -- 7. Save the request plan to response state + fmt.Println("create di: ", plan.DomainId) resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) } func (r *resourceProject) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - // TIP: ==== RESOURCE READ ==== - // Generally, the Read function should do the following things. Make - // sure there is a good reason if you don't do one of these. - // - // 1. Get a client connection to the relevant service - // 2. Fetch the state - // 3. Get the resource from AWS - // 4. Remove resource from state if it is not found - // 5. Set the arguments and attributes - // 6. Set the state - - // TIP: -- 1. Get a client connection to the relevant service + fmt.Println("read") conn := r.Meta().DataZoneClient(ctx) - - // TIP: -- 2. Fetch the state var state resourceProjectData resp.Diagnostics.Append(req.State.Get(ctx, &state)...) if resp.Diagnostics.HasError() { return } - // TIP: -- 3. Get the resource from AWS using an API Get, List, or Describe- - // type function, or, better yet, using a finder. - var input datazone.GetProjectInput - if !state.DomainIdentifier.IsNull() { - input.DomainIdentifier = state.DomainIdentifier.ValueStringPointer() - } - if !state.ID.IsNull() { - input.Identifier = state.ID.ValueStringPointer() + in := &datazone.GetProjectInput{ + DomainIdentifier: state.DomainId.ValueStringPointer(), + Identifier: state.ID.ValueStringPointer(), } - out, err := conn.GetProject(ctx, &input) - // TIP: -- 4. Remove resource from state if it is not found + out, err := conn.GetProject(ctx, in) if tfresource.NotFound(err) { + fmt.Println("error") resp.State.RemoveResource(ctx) return } @@ -384,18 +378,26 @@ func (r *resourceProject) Read(ctx context.Context, req resource.ReadRequest, re ) return } - - resp.Diagnostics.Append(flex.Flatten(ctx, &out, &state)...) + fmt.Println(state.Description) + var stateNew resourceProjectData + resp.Diagnostics.Append(flex.Flatten(ctx, out, &stateNew)...) if resp.Diagnostics.HasError() { return } - //state.ResultMetadata = out.ResultMetadata.Clone() + if state.DomainId.ValueString() != "" { + fmt.Println(state.DomainId.ValueString()) + out.DomainId = state.DomainId.ValueStringPointer() + } + fmt.Println("Statenew: ", stateNew.DomainId) // TIP: -- 6. Set the state - resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) + stateNew.Timeouts = state.Timeouts + + resp.Diagnostics.Append(resp.State.Set(ctx, &stateNew)...) } func (r *resourceProject) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + fmt.Println("update") // TIP: ==== RESOURCE UPDATE ==== // Not all resources have Update functions. There are a few reasons: // a. The AWS API does not support changing a resource @@ -427,21 +429,22 @@ func (r *resourceProject) Update(ctx context.Context, req resource.UpdateRequest return } - if plan.DomainIdentifier != state.DomainIdentifier { // I should error check this + if plan.DomainId != state.DomainId { // I should error check this return // add error here } // TIP: -- 3. Populate a modify input structure and check for changes /*/ - if !plan.Name.Equal(state.Name) || + if !plan.Name.Equal(state.Nam||e) !plan.Description.Equal(state.Description) || !plan.ComplexArgument.Equal(state.ComplexArgument) || !plan.Type.Equal(state.Type) { */ in := &datazone.UpdateProjectInput{ - DomainIdentifier: aws.String(plan.DomainIdentifier.ValueString()), // This can't change + DomainIdentifier: aws.String(plan.DomainId.ValueString()), // This can't change Identifier: aws.String(plan.ID.ValueString()), + GlossaryTerms: aws.ToStringSlice(flex.ExpandFrameworkStringList(ctx, plan.GlossaryTerms)), } if !plan.Description.IsNull() { @@ -455,38 +458,41 @@ func (r *resourceProject) Update(ctx context.Context, req resource.UpdateRequest // TIP: -- 4. Call the AWS modify/update function out, err := conn.UpdateProject(ctx, in) if err != nil { + fmt.Println("first update error") resp.Diagnostics.AddError( create.ProblemStandardMessage(names.DataZone, create.ErrActionUpdating, ResNameProject, plan.ID.String(), err), err.Error(), ) return } - if out == nil || !(out.FailureReasons == nil) { + if out == nil { resp.Diagnostics.AddError( create.ProblemStandardMessage(names.DataZone, create.ErrActionUpdating, ResNameProject, plan.ID.String(), nil), errors.New("empty output").Error(), ) return } - - resp.Diagnostics.Append(flex.Flatten(ctx, &out, &state)...) + var stateNew resourceProjectData + resp.Diagnostics.Append(flex.Flatten(ctx, out, &stateNew)...) if resp.Diagnostics.HasError() { return } //state.ResultMetadata = out.ResultMetadata.Clone() // TIP: -- 5. Use a waiter to wait for update to complete - updateTimeout := r.UpdateTimeout(ctx, plan.Timeouts) - _, err = waitProjectUpdated(ctx, conn, plan.ID.ValueString(), updateTimeout.String(), r.ReadTimeout(ctx, plan.Timeouts)) - if err != nil { - resp.Diagnostics.AddError( - create.ProblemStandardMessage(names.DataZone, create.ErrActionWaitingForUpdate, ResNameProject, plan.ID.String(), err), - err.Error(), - ) - return - } + //updateTimeout := r.UpdateTimeout(ctx, plan.Timeouts) + /* + _, err = waitProjectUpdated(ctx, conn, plan.ID.ValueString(), updateTimeout.String(), r.ReadTimeout(ctx, plan.Timeouts)) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.DataZone, create.ErrActionWaitingForUpdate, ResNameProject, plan.ID.String(), err), + err.Error(), + ) + return + } + */ // TIP: -- 6. Save the request plan to response state - resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &stateNew)...) } func (r *resourceProject) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { @@ -516,31 +522,42 @@ func (r *resourceProject) Delete(ctx context.Context, req resource.DeleteRequest } // TIP: -- 3. Populate a delete input structure + fmt.Println("hello") + fmt.Println(state.DomainId.ValueString()) + fmt.Println(state.ID.ValueString()) + in := &datazone.DeleteProjectInput{ - DomainIdentifier: aws.String((state.DomainIdentifier.ValueString())), - Identifier: aws.String((state.ID.ValueString())), + DomainIdentifier: aws.String((*state.DomainId.ValueStringPointer())), + Identifier: aws.String((*state.ID.ValueStringPointer())), } - if !state.SkipDeletionCheck.IsNull() { // N + if !state.SkipDeletionCheck.IsNull() { in.SkipDeletionCheck = state.SkipDeletionCheck.ValueBoolPointer() } + fmt.Println("-----") + fmt.Println(state.SkipDeletionCheck) + fmt.Println(*in.SkipDeletionCheck) // TIP: -- 4. Call the AWS delete function _, err := conn.DeleteProject(ctx, in) // TIP: On rare occassions, the API returns a not found error after deleting a // resource. If that happens, we don't want it to show up as an error. if err != nil { + fmt.Println("error at deleting") if errs.IsA[*awstypes.ResourceNotFoundException](err) { + fmt.Println(err) return } resp.Diagnostics.AddError( create.ProblemStandardMessage(names.DataZone, create.ErrActionDeleting, ResNameProject, state.ID.String(), err), err.Error(), ) + fmt.Println("2nd deleting error") return } deleteTimeout := r.DeleteTimeout(ctx, state.Timeouts) - _, err = waitProjectDeleted(ctx, conn, state.DomainIdentifier.ValueString(), state.ID.ValueString(), deleteTimeout) + //var t time.Duration = (1000000000 * 100) + _, err = waitProjectDeleted(ctx, conn, state.DomainId.ValueString(), state.ID.ValueString(), deleteTimeout) if err != nil { resp.Diagnostics.AddError( create.ProblemStandardMessage(names.DataZone, create.ErrActionWaitingForDeletion, ResNameProject, state.ID.String(), err), @@ -605,6 +622,7 @@ func waitProjectCreated(ctx context.Context, conn *datazone.Client, domain strin // resources than others. The best case is a status flag that tells you when // the update has been fully realized. Other times, you can check to see if a // key resource argument is updated to a new value or not. +/* func waitProjectUpdated(ctx context.Context, conn *datazone.Client, domain string, identifier string, timeout time.Duration) (*datazone.GetProjectOutput, error) { stateConf := &retry.StateChangeConf{ Pending: []string{statusChangePending}, @@ -622,6 +640,7 @@ func waitProjectUpdated(ctx context.Context, conn *datazone.Client, domain strin return nil, err } +*/ // TIP: A deleted waiter is almost like a backwards created waiter. There may // be additional pending states, however. @@ -765,7 +784,6 @@ func flattenComplexArguments(ctx context.Context, apiObjects []*awstypes.Complex // https://developer.hashicorp.com/terraform/plugin/framework/handling-data/accessing-values type resourceProjectData struct { Description types.String `tfsdk:"description"` - DomainIdentifier fwtypes.Regexp `tfsdk:"domain_identifier"` DomainId fwtypes.Regexp `tfsdk:"domain_id"` Name fwtypes.Regexp `tfsdk:"name"` CreatedBy types.String `tfsdk:"created_by"` diff --git a/internal/service/datazone/project_test.go b/internal/service/datazone/project_test.go index 57c1a2d032a..0dab5e198a5 100644 --- a/internal/service/datazone/project_test.go +++ b/internal/service/datazone/project_test.go @@ -84,31 +84,32 @@ func TestAccDataZoneProject_basic(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists(ctx, resourceName, &project), // inputed - resource.TestCheckResourceAttr(resourceName, "domain_identifier", dName), // find a domain identifier - resource.TestCheckResourceAttrSet(resourceName, "glossary_terms.#"), - resource.TestCheckResourceAttr(resourceName, "description", "sample description"), - resource.TestCheckResourceAttr(resourceName, "name", resourceName), + resource.TestCheckResourceAttrSet(resourceName, "domain_id"), // find a domain identifier + //resource.TestCheckResourceAttrSet(resourceName, "glossary_terms.#"), + resource.TestCheckResourceAttrSet(resourceName, "description"), + resource.TestCheckResourceAttrSet(resourceName, "name"), resource.TestCheckResourceAttrSet(resourceName, "skip_deletion_check"), // computed resource.TestCheckResourceAttrSet(resourceName, "created_by"), resource.TestCheckResourceAttrSet(resourceName, "id"), resource.TestCheckResourceAttrSet(resourceName, "created_at"), - resource.TestCheckResourceAttrSet(resourceName, "failure_reasons.0.#"), + //resource.TestCheckResourceAttrSet(resourceName, "failure_reasons.#"), resource.TestCheckResourceAttrSet(resourceName, "last_updated_at"), - resource.TestCheckResourceAttrSet(resourceName, "project_status"), - resource.TestCheckResourceAttrSet(resourceName, "result_metadata"), + // should this ever be empty?? resource.TestCheckResourceAttrSet(resourceName, "project_status"), + //resource.TestCheckResourceAttrSet(resourceName, "result_metadata"), ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - //ImportStateVerifyIgnore: []string{"apply_immediately", "user"}, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ExpectNonEmptyPlan: true, + + ImportStateVerifyIgnore: []string{"apply_immediately", "user"}, }, }, }) } - func TestAccDataZoneProject_disappears(t *testing.T) { ctx := acctest.Context(t) if testing.Short() { @@ -147,7 +148,6 @@ func TestAccDataZoneProject_disappears(t *testing.T) { }, }) } - func testAccCheckProjectDestroy(ctx context.Context) resource.TestCheckFunc { return func(s *terraform.State) error { conn := acctest.Provider.Meta().(*conns.AWSClient).DataZoneClient(ctx) @@ -156,9 +156,10 @@ func testAccCheckProjectDestroy(ctx context.Context) resource.TestCheckFunc { if rs.Type != "aws_datazone_project" { continue } + t := rs.Primary.Attributes["domain_id"] input := &datazone.GetProjectInput{ - DomainIdentifier: aws.String(rs.Primary.Attributes["${aws_datazone_domain.test.id}"]), // can this be hardcoded in? + DomainIdentifier: &t, Identifier: aws.String(rs.Primary.ID), } _, err := conn.GetProject(ctx, input) @@ -182,10 +183,20 @@ func testAccCheckProjectDomainDestroy(ctx context.Context, s *terraform.State) e if rs.Type != "aws_datazone_domain" { continue } - - _, err := conn.GetDomain(ctx, &datazone.GetDomainInput{ + in := &datazone.GetDomainInput{ Identifier: aws.String(rs.Primary.ID), - }) + } + del := &datazone.DeleteDomainInput{ + Identifier: aws.String(rs.Primary.ID), + } + _, err := conn.DeleteDomain(ctx, del) + + if err != nil { + return create.Error(names.DataZone, create.ErrActionCheckingDestroyed, tfdatazone.ResNameDomain, rs.Primary.ID, err) + + } + + _, err = conn.GetDomain(ctx, in) if tfdatazone.IsResourceMissing(err) { return nil @@ -199,9 +210,7 @@ func testAccCheckProjectDomainDestroy(ctx context.Context, s *terraform.State) e } return nil - } - func testAccCheckProjectExists(ctx context.Context, name string, project *datazone.GetProjectOutput) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[name] @@ -212,11 +221,16 @@ func testAccCheckProjectExists(ctx context.Context, name string, project *datazo if rs.Primary.ID == "" { return create.Error(names.DataZone, create.ErrActionCheckingExistence, tfdatazone.ResNameProject, name, errors.New("not set")) } + if rs.Primary.Attributes["domain_id"] == "" { + return create.Error(names.DataZone, create.ErrActionCheckingExistence, tfdatazone.ResNameProject, name, errors.New("domain identifer not set")) + + } + t := rs.Primary.Attributes["domain_id"] conn := acctest.Provider.Meta().(*conns.AWSClient).DataZoneClient(ctx) resp, err := conn.GetProject(ctx, &datazone.GetProjectInput{ - DomainIdentifier: project.DomainId, - Identifier: project.Id, + DomainIdentifier: &t, + Identifier: &rs.Primary.ID, }) if err != nil { @@ -228,7 +242,6 @@ func testAccCheckProjectExists(ctx context.Context, name string, project *datazo return nil } } - func testAccPreCheckProject(ctx context.Context, t *testing.T) { conn := acctest.Provider.Meta().(*conns.AWSClient).DataZoneClient(ctx) @@ -242,7 +255,6 @@ func testAccPreCheckProject(ctx context.Context, t *testing.T) { t.Fatalf("unexpected PreCheck error: %s", err) } } - func testAccCheckProjectNotRecreated(before, after *datazone.GetProjectOutput) resource.TestCheckFunc { return func(s *terraform.State) error { if before, after := aws.ToString(before.Id), aws.ToString(after.Id); before != after { @@ -252,20 +264,107 @@ func testAccCheckProjectNotRecreated(before, after *datazone.GetProjectOutput) r return nil } } +func TestAccCheckProjectUpdate(t *testing.T) { + ctx := acctest.Context(t) + // TIP: This is a long-running test guard for tests that run longer than + // 300s (5 min) generally. + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var project datazone.GetProjectOutput + pName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) // Name for datazone project + dName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) // Name for datazone domain + resourceName := "aws_datazone_project.test" -func testAccProjectConfig_basic(rName, dName string) string { + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + //acctest.PreCheckPartitionHasService(t, names.DataZoneEndpointID) + //testAccPreCheckProject(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.DataZoneServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckProjectDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccProjectConfig_basic(pName, dName), + Check: resource.ComposeTestCheckFunc( + testAccCheckProjectExists(ctx, resourceName, &project), + // inputed + resource.TestCheckResourceAttrSet(resourceName, "domain_id"), // find a domain identifier + //resource.TestCheckResourceAttrSet(resourceName, "glossary_terms.#"), + resource.TestCheckResourceAttrSet(resourceName, "description"), + resource.TestCheckResourceAttrSet(resourceName, "name"), + resource.TestCheckResourceAttrSet(resourceName, "skip_deletion_check"), + // computed + resource.TestCheckResourceAttrSet(resourceName, "created_by"), + resource.TestCheckResourceAttrSet(resourceName, "id"), + resource.TestCheckResourceAttrSet(resourceName, "created_at"), + //resource.TestCheckResourceAttrSet(resourceName, "failure_reasons.#"), + resource.TestCheckResourceAttrSet(resourceName, "last_updated_at"), + // should this ever be empty?? resource.TestCheckResourceAttrSet(resourceName, "project_status"), + //resource.TestCheckResourceAttrSet(resourceName, "result_metadata"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + //ImportStateVerifyIgnore: []string{"apply_immediately", "user"}, + }, + { + Config: testAccProjectConfig_basic(pName, dName), + Check: resource.ComposeTestCheckFunc( + testAccCheckProjectExists(ctx, resourceName, &project), + // inputed + resource.TestCheckResourceAttrSet(resourceName, "domain_id"), // find a domain identifier + //resource.TestCheckResourceAttrSet(resourceName, "glossary_terms.#"), + resource.TestCheckResourceAttrSet(resourceName, "description"), + resource.TestCheckResourceAttrSet(resourceName, "name"), + resource.TestCheckResourceAttrSet(resourceName, "skip_deletion_check"), + // computed + resource.TestCheckResourceAttrSet(resourceName, "created_by"), + resource.TestCheckResourceAttrSet(resourceName, "id"), + resource.TestCheckResourceAttrSet(resourceName, "created_at"), + //resource.TestCheckResourceAttrSet(resourceName, "failure_reasons.#"), + resource.TestCheckResourceAttrSet(resourceName, "last_updated_at"), + // should this ever be empty?? resource.TestCheckResourceAttrSet(resourceName, "project_status"), + //resource.TestCheckResourceAttrSet(resourceName, "result_metadata"), + ), + }, + }, + }) + +} + +func testAccProjectConfig_basic(pName, dName string) string { + return acctest.ConfigCompose(testAccDomainConfig_basic(dName), fmt.Sprintf(` +resource "aws_security_group" "test" { + name = %[1]q +} + +resource "aws_datazone_project" "test" { + domain_id = aws_datazone_domain.test.id + name = %[1]q + description = "desc" + skip_deletion_check = true +} +`, pName)) +} +func testAccProjectConfigBasicUpdate(pName, dName string) string { return acctest.ConfigCompose(testAccDomainConfig_basic(dName), fmt.Sprintf(` resource "aws_security_group" "test" { name = %[1]q } resource "aws_datazone_project" "test" { - domain_identifier = aws_datazone_domain.test.id + domain_id = aws_datazone_domain.test.id name = %[1]q - description = "" + description = "description" skip_deletion_check = true } -`, rName)) +`, pName)) } -// glossary_terms = ["abababab]"] +// glossary_terms = ["2N8w6XJCwZf"] From 34adb64711d8a718e7ae87bf9aab4934e5d9240c Mon Sep 17 00:00:00 2001 From: ThomasZalewski Date: Thu, 11 Jul 2024 11:54:46 -0400 Subject: [PATCH 04/17] CI checks + finished CRUD functions --- internal/service/datazone/project.go | 505 +++--------------- internal/service/datazone/project_test.go | 236 +++----- website/docs/r/datazone_project.html.markdown | 58 +- 3 files changed, 202 insertions(+), 597 deletions(-) diff --git a/internal/service/datazone/project.go b/internal/service/datazone/project.go index 1c9eebdb57c..3eaf13e10e7 100644 --- a/internal/service/datazone/project.go +++ b/internal/service/datazone/project.go @@ -3,75 +3,33 @@ package datazone -// **PLEASE DELETE THIS AND ALL TIP COMMENTS BEFORE SUBMITTING A PR FOR REVIEW!** -// -// TIP: ==== INTRODUCTION ==== -// Thank you for trying the skaff tool! -// -// You have opted to include these helpful comments. They all include "TIP:" -// to help you find and remove them when you're done with them. -// -// While some aspects of this file are customized to your input, the -// scaffold tool does *not* look at the AWS API and ensure it has correct -// function, structure, and variable names. It makes guesses based on -// commonalities. You will need to make significant adjustments. -// -// In other words, as generated, this is a rough outline of the work you will -// need to do. If something doesn't make sense for your situation, get rid of -// it. - import ( - // TIP: ==== IMPORTS ==== - // This is a common set of imports but not customized to your code since - // your code hasn't been written yet. Make sure you, your IDE, or - // goimports -w fixes these imports. - // - // The provider linter wants your imports to be in two groups: first, - // standard library (i.e., "fmt" or "strings"), second, everything else. - // - // Also, AWS Go SDK v2 may handle nested structures differently than v1, - // using the services/datazone/types package. If so, you'll - // need to import types and reference the nested types, e.g., as - // awstypes.. "context" "errors" - - //"regexp" "fmt" + "reflect" + "strings" "time" "github.com/YakDriver/regexache" "github.com/aws/aws-sdk-go-v2/aws" - - //"github.com/aws/smithy-go/middleware" - - //"github.com/aws/aws-sdk-go-v2/aws/middleware" "github.com/aws/aws-sdk-go-v2/service/datazone" awstypes "github.com/aws/aws-sdk-go-v2/service/datazone/types" - - //"github.com/aws/smithy-go/middleware" "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes" "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - - //"github.com/hashicorp/terraform-plugin-framework/attr" - //"github.com/hashicorp/terraform-plugin-framework/diag" "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/resource/schema/boolplanmodifier" + "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-sdk/v2/helper/retry" "github.com/hashicorp/terraform-provider-aws/internal/create" - - //"github.com/hashicorp/terraform-provider-aws/internal/enum" - //awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-provider-aws/internal/enum" "github.com/hashicorp/terraform-provider-aws/internal/errs" "github.com/hashicorp/terraform-provider-aws/internal/framework" "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" @@ -80,30 +38,11 @@ import ( "github.com/hashicorp/terraform-provider-aws/names" ) -// TIP: ==== FILE STRUCTURE ==== -// All resources should follow this basic outline. Improve this resource's -// maintainability by sticking to it. -// -// 1. Package declaration -// 2. Imports -// 3. Main resource struct with schema method -// 4. Create, read, update, delete methods (in that order) -// 5. Other functions (flatteners, expanders, waiters, finders, etc.) - -// Function annotations are used for resource registration to the Provider. DO NOT EDIT. -// @FrameworkResource("aws_datazone_project", name="Project") func newResourceProject(_ context.Context) (resource.ResourceWithConfigure, error) { r := &resourceProject{} - - // TIP: ==== CONFIGURABLE TIMEOUTS ==== - // Users can configure timeout lengths but you need to use the times they - // provide. Access the timeout they configure (or the defaults) using, - // e.g., r.CreateTimeout(ctx, plan.Timeouts) (see below). The times here are - // the defaults if they don't configure timeouts. - r.SetDefaultCreateTimeout(30 * time.Minute) + r.SetDefaultCreateTimeout(3 * time.Minute) r.SetDefaultUpdateTimeout(30 * time.Minute) - r.SetDefaultDeleteTimeout(30 * time.Minute) - + r.SetDefaultDeleteTimeout(3 * time.Minute) return r, nil } @@ -120,50 +59,6 @@ func (r *resourceProject) Metadata(_ context.Context, req resource.MetadataReque resp.TypeName = "aws_datazone_project" } -// TIP: ==== SCHEMA ==== -// In the schema, add each of the attributes in snake case (e.g., -// delete_automated_backups). -// -// Formatting rules: -// * Alphabetize attributes to make them easier to find. -// * Do not add a blank line between attributes. -// -// Attribute basics: -// - If a user can provide a value ("configure a value") for an -// attribute (e.g., instances = 5), we call the attribute an -// "argument." -// - You change the way users interact with attributes using: -// - Required -// - Optional -// - Computed -// - There are only four valid combinations: -// -// 1. Required only - the user must provide a value -// Required: true, -// -// 2. Optional only - the user can configure or omit a value; do not -// use Default or DefaultFunc -// -// Optional: true, -// -// 3. Computed only - the provider can provide a value but the user -// cannot, i.e., read-only -// -// Computed: true, -// -// 4. Optional AND Computed - the provider or user can provide a value; -// use this combination if you are using Default -// -// Optional: true, -// Computed: true, -// -// You will typically find arguments in the input struct -// (e.g., CreateDBInstanceInput) for the create operation. Sometimes -// they are only in the input struct (e.g., ModifyDBInstanceInput) for -// the modify operation. -// -// For more about schema options, visit -// https://developer.hashicorp.com/terraform/plugin/framework/handling-data/schemas?page=schemas func (r *resourceProject) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ Attributes: map[string]schema.Attribute{ @@ -174,30 +69,27 @@ func (r *resourceProject) Schema(ctx context.Context, req resource.SchemaRequest }, }, "domain_id": schema.StringAttribute{ - CustomType: fwtypes.RegexpType, - Optional: true, - Computed: true, + Required: true, Validators: []validator.String{ stringvalidator.RegexMatches(regexache.MustCompile(`^dzd[-_][a-zA-Z0-9_-]{1,36}$`), "must conform to: ^dzd[-_][a-zA-Z0-9_-]{1,36}$ "), }, PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), + stringplanmodifier.UseStateForUnknown(), }, }, "glossary_terms": schema.ListAttribute{ CustomType: fwtypes.ListOfStringType, - ElementType: fwtypes.RegexpType.StringType, + ElementType: types.StringType, Validators: []validator.List{ - listvalidator.ValueStringsAre(stringvalidator.LengthBetween(1, 20)), + listvalidator.SizeBetween(1, 20), listvalidator.ValueStringsAre(stringvalidator.RegexMatches(regexache.MustCompile(`^[a-zA-Z0-9_-]{1,36}$`), "must conform to: ^[a-zA-Z0-9_-]{1,36}$ ")), }, Optional: true, }, names.AttrName: schema.StringAttribute{ - CustomType: fwtypes.RegexpType, Validators: []validator.String{ stringvalidator.RegexMatches(regexache.MustCompile(`^[\w -]+$`), "must conform to: ^[\\w -]+$ "), stringvalidator.LengthBetween(1, 64), @@ -207,12 +99,14 @@ func (r *resourceProject) Schema(ctx context.Context, req resource.SchemaRequest "created_by": schema.StringAttribute{ Computed: true, }, - "id": schema.StringAttribute{ - Computed: true, - }, - "created_at": schema.StringAttribute{ + names.AttrID: framework.IDAttribute(), + + names.AttrCreatedAt: schema.StringAttribute{ CustomType: timetypes.RFC3339Type{}, Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "failure_reasons": schema.ListAttribute{ @@ -227,22 +121,15 @@ func (r *resourceProject) Schema(ctx context.Context, req resource.SchemaRequest "project_status": schema.StringAttribute{ CustomType: fwtypes.StringEnumType[awstypes.ProjectStatus](), Computed: true, - }, - /* - "result_metadata": schema.ListAttribute{ - CustomType: fwtypes.NewListNestedObjectTypeOf[middleware.Metadata](ctx), - Computed: true, - }, - - - "domain_id": schema.StringAttribute{ - CustomType: fwtypes.RegexpType, - Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), }, - */ - + }, "skip_deletion_check": schema.BoolAttribute{ - Required: true, + Optional: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.UseStateForUnknown(), + }, }, }, Blocks: map[string]schema.Block{ @@ -256,31 +143,12 @@ func (r *resourceProject) Schema(ctx context.Context, req resource.SchemaRequest } func (r *resourceProject) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - fmt.Println("create") - // TIP: ==== RESOURCE CREATE ==== - // Generally, the Create function should do the following things. Make - // sure there is a good reason if you don't do one of these. - // - // 1. Get a client connection to the relevant service - // 2. Fetch the plan - // 3. Populate a create input structure - // 4. Call the AWS create/put function - // 5. Using the output from the create function, set the minimum arguments - // and attributes for the Read function to work, as well as any computed - // only attributes. - // 6. Use a waiter to wait for create to complete - // 7. Save the request plan to response state - - // TIP: -- 1. Get a client connection to the relevant service conn := r.Meta().DataZoneClient(ctx) - - // TIP: -- 2. Fetch the plan var plan resourceProjectData resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) if resp.Diagnostics.HasError() { return } - // validate that domain exists var validateDomain datazone.GetDomainInput validateDomain.Identifier = plan.DomainId.ValueStringPointer() _, err := conn.GetDomain(ctx, &validateDomain) @@ -292,8 +160,6 @@ func (r *resourceProject) Create(ctx context.Context, req resource.CreateRequest return } - // TIP: -- 3. Populate a create input structure - // we probably want to error here if there is null but not rn in := &datazone.CreateProjectInput{ Name: aws.String(plan.Name.ValueString()), } @@ -307,7 +173,6 @@ func (r *resourceProject) Create(ctx context.Context, req resource.CreateRequest in.GlossaryTerms = aws.ToStringSlice(flex.ExpandFrameworkStringList(ctx, plan.GlossaryTerms)) } - // TIP: -- 4. Call the AWS create function out, err := conn.CreateProject(ctx, in) if err != nil { resp.Diagnostics.AddError( @@ -319,25 +184,15 @@ func (r *resourceProject) Create(ctx context.Context, req resource.CreateRequest if out == nil || !(out.FailureReasons == nil) { resp.Diagnostics.AddError( create.ProblemStandardMessage(names.DataZone, create.ErrActionCreating, ResNameProject, plan.Name.String(), nil), - errors.New("empty output").Error(), + errors.New("failure reasons populated").Error(), ) return } - // TIP: -- 5. Using the output from the create function, set the minimum attributes - - // can i autoflex this? - resp.Diagnostics.Append(flex.Flatten(ctx, out, &plan)...) if resp.Diagnostics.HasError() { return } - // data.Type = fwtypes.StringEnumValue[awstypes.VisibilityType](image.Visibility) - plan.ProjectStatus = fwtypes.StringEnumValue[awstypes.ProjectStatus](out.ProjectStatus) - - //plan.ResultMetadata = out.ResultMetadata.Clone() - - // TIP: -- 6. Use a waiter to wait for create to complete createTimeout := r.CreateTimeout(ctx, plan.Timeouts) _, err = waitProjectCreated(ctx, conn, plan.DomainId.ValueString(), plan.ID.ValueString(), createTimeout) if err != nil { @@ -348,29 +203,27 @@ func (r *resourceProject) Create(ctx context.Context, req resource.CreateRequest return } - // TIP: -- 7. Save the request plan to response state - fmt.Println("create di: ", plan.DomainId) - resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) } func (r *resourceProject) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - fmt.Println("read") conn := r.Meta().DataZoneClient(ctx) var state resourceProjectData resp.Diagnostics.Append(req.State.Get(ctx, &state)...) if resp.Diagnostics.HasError() { return } + in := &datazone.GetProjectInput{ DomainIdentifier: state.DomainId.ValueStringPointer(), Identifier: state.ID.ValueStringPointer(), } out, err := conn.GetProject(ctx, in) if tfresource.NotFound(err) { - fmt.Println("error") resp.State.RemoveResource(ctx) return } + if err != nil { resp.Diagnostics.AddError( create.ProblemStandardMessage(names.DataZone, create.ErrActionSetting, ResNameProject, state.ID.String(), err), @@ -378,87 +231,57 @@ func (r *resourceProject) Read(ctx context.Context, req resource.ReadRequest, re ) return } - fmt.Println(state.Description) - var stateNew resourceProjectData - resp.Diagnostics.Append(flex.Flatten(ctx, out, &stateNew)...) + resp.Diagnostics.Append(flex.Flatten(ctx, out, &state)...) if resp.Diagnostics.HasError() { return } - if state.DomainId.ValueString() != "" { - fmt.Println(state.DomainId.ValueString()) - out.DomainId = state.DomainId.ValueStringPointer() - } - fmt.Println("Statenew: ", stateNew.DomainId) - - // TIP: -- 6. Set the state - stateNew.Timeouts = state.Timeouts - resp.Diagnostics.Append(resp.State.Set(ctx, &stateNew)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) } func (r *resourceProject) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - fmt.Println("update") - // TIP: ==== RESOURCE UPDATE ==== - // Not all resources have Update functions. There are a few reasons: - // a. The AWS API does not support changing a resource - // b. All arguments have RequiresReplace() plan modifiers - // c. The AWS API uses a create call to modify an existing resource - // - // In the cases of a. and b., the resource will not have an update method - // defined. In the case of c., Update and Create can be refactored to call - // the same underlying function. - // - // The rest of the time, there should be an Update function and it should - // do the following things. Make sure there is a good reason if you don't - // do one of these. - // - // 1. Get a client connection to the relevant service - // 2. Fetch the plan and state - // 3. Populate a modify input structure and check for changes - // 4. Call the AWS modify/update function - // 5. Use a waiter to wait for update to complete - // 6. Save the request plan to response state - // TIP: -- 1. Get a client connection to the relevant service conn := r.Meta().DataZoneClient(ctx) - // TIP: -- 2. Fetch the plan var plan, state resourceProjectData - resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) /// get the plan - resp.Diagnostics.Append(req.State.Get(ctx, &state)...) // get the state + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) if resp.Diagnostics.HasError() { return } - if plan.DomainId != state.DomainId { // I should error check this - return // add error here + if plan.DomainId != state.DomainId { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.DataZone, create.ErrActionUpdating, ResNameProject, plan.ID.String(), nil), + errors.New("domain_id should not change with updates").Error(), + ) + return } - // TIP: -- 3. Populate a modify input structure and check for changes - /*/ - if !plan.Name.Equal(state.Nam||e) - !plan.Description.Equal(state.Description) || - !plan.ComplexArgument.Equal(state.ComplexArgument) || - !plan.Type.Equal(state.Type) { - */ - in := &datazone.UpdateProjectInput{ - DomainIdentifier: aws.String(plan.DomainId.ValueString()), // This can't change + DomainIdentifier: aws.String(plan.DomainId.ValueString()), Identifier: aws.String(plan.ID.ValueString()), - GlossaryTerms: aws.ToStringSlice(flex.ExpandFrameworkStringList(ctx, plan.GlossaryTerms)), + } + + if plan.GlossaryTerms.IsNull() { + if !reflect.DeepEqual(plan.GlossaryTerms, state.GlossaryTerms) { + in.GlossaryTerms = aws.ToStringSlice(flex.ExpandFrameworkStringList(ctx, plan.GlossaryTerms)) + } } if !plan.Description.IsNull() { - in.Description = aws.String(plan.Description.ValueString()) + if plan.Description.ValueString() != state.Description.ValueString() { + in.Description = aws.String(plan.Description.ValueString()) + } } if !plan.Name.IsNull() { - in.Name = aws.String(plan.Name.ValueString()) + if plan.Name != state.Name { + in.Name = aws.String(plan.Name.ValueString()) + } } - // TIP: -- 4. Call the AWS modify/update function out, err := conn.UpdateProject(ctx, in) if err != nil { - fmt.Println("first update error") resp.Diagnostics.AddError( create.ProblemStandardMessage(names.DataZone, create.ErrActionUpdating, ResNameProject, plan.ID.String(), err), err.Error(), @@ -468,64 +291,36 @@ func (r *resourceProject) Update(ctx context.Context, req resource.UpdateRequest if out == nil { resp.Diagnostics.AddError( create.ProblemStandardMessage(names.DataZone, create.ErrActionUpdating, ResNameProject, plan.ID.String(), nil), - errors.New("empty output").Error(), + errors.New("empty output from project update").Error(), ) return } - var stateNew resourceProjectData - resp.Diagnostics.Append(flex.Flatten(ctx, out, &stateNew)...) - if resp.Diagnostics.HasError() { + + _, err = waitProjectUpdated(ctx, conn, plan.DomainId.ValueString(), plan.ID.ValueString(), r.UpdateTimeout(ctx, plan.Timeouts)) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.DataZone, create.ErrActionWaitingForUpdate, ResNameProject, plan.ID.String(), err), + err.Error(), + ) return } - //state.ResultMetadata = out.ResultMetadata.Clone() - // TIP: -- 5. Use a waiter to wait for update to complete - //updateTimeout := r.UpdateTimeout(ctx, plan.Timeouts) - /* - _, err = waitProjectUpdated(ctx, conn, plan.ID.ValueString(), updateTimeout.String(), r.ReadTimeout(ctx, plan.Timeouts)) - if err != nil { - resp.Diagnostics.AddError( - create.ProblemStandardMessage(names.DataZone, create.ErrActionWaitingForUpdate, ResNameProject, plan.ID.String(), err), - err.Error(), - ) - return - } - */ - // TIP: -- 6. Save the request plan to response state - resp.Diagnostics.Append(resp.State.Set(ctx, &stateNew)...) + resp.Diagnostics.Append(flex.Flatten(ctx, out, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) } func (r *resourceProject) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { - // TIP: ==== RESOURCE DELETE ==== - // Most resources have Delete functions. There are rare situations - // where you might not need a delete: - // a. The AWS API does not provide a way to delete the resource - // b. The point of your resource is to perform an action (e.g., reboot a - // server) and deleting serves no purpose. - // - // The Delete function should do the following things. Make sure there - // is a good reason if you don't do one of these. - // - // 1. Get a client connection to the relevant service - // 2. Fetch the state - // 3. Populate a delete input structure - // 4. Call the AWS delete function - // 5. Use a waiter to wait for delete to complete - // TIP: -- 1. Get a client connection to the relevant service conn := r.Meta().DataZoneClient(ctx) - // TIP: -- 2. Fetch the state var state resourceProjectData resp.Diagnostics.Append(req.State.Get(ctx, &state)...) if resp.Diagnostics.HasError() { return } - // TIP: -- 3. Populate a delete input structure - fmt.Println("hello") - fmt.Println(state.DomainId.ValueString()) - fmt.Println(state.ID.ValueString()) - in := &datazone.DeleteProjectInput{ DomainIdentifier: aws.String((*state.DomainId.ValueStringPointer())), Identifier: aws.String((*state.ID.ValueStringPointer())), @@ -533,32 +328,23 @@ func (r *resourceProject) Delete(ctx context.Context, req resource.DeleteRequest if !state.SkipDeletionCheck.IsNull() { in.SkipDeletionCheck = state.SkipDeletionCheck.ValueBoolPointer() } - fmt.Println("-----") - fmt.Println(state.SkipDeletionCheck) - fmt.Println(*in.SkipDeletionCheck) - // TIP: -- 4. Call the AWS delete function _, err := conn.DeleteProject(ctx, in) - // TIP: On rare occassions, the API returns a not found error after deleting a - // resource. If that happens, we don't want it to show up as an error. if err != nil { - fmt.Println("error at deleting") - if errs.IsA[*awstypes.ResourceNotFoundException](err) { - fmt.Println(err) + if errs.IsA[*awstypes.ResourceNotFoundException](err) || errs.IsA[*awstypes.AccessDeniedException](err) { return } resp.Diagnostics.AddError( create.ProblemStandardMessage(names.DataZone, create.ErrActionDeleting, ResNameProject, state.ID.String(), err), err.Error(), ) - fmt.Println("2nd deleting error") return } deleteTimeout := r.DeleteTimeout(ctx, state.Timeouts) - //var t time.Duration = (1000000000 * 100) _, err = waitProjectDeleted(ctx, conn, state.DomainId.ValueString(), state.ID.ValueString(), deleteTimeout) - if err != nil { + + if err != nil && !errs.IsA[*awstypes.AccessDeniedException](err) { resp.Diagnostics.AddError( create.ProblemStandardMessage(names.DataZone, create.ErrActionWaitingForDeletion, ResNameProject, state.ID.String(), err), err.Error(), @@ -567,43 +353,20 @@ func (r *resourceProject) Delete(ctx context.Context, req resource.DeleteRequest } } -// TIP: ==== TERRAFORM IMPORTING ==== -// If Read can get all the information it needs from the Identifier -// (i.e., path.Root("id")), you can use the PassthroughID importer. Otherwise, -// you'll need a custom import function. -// -// See more: -// https://developer.hashicorp.com/terraform/plugin/framework/resources/import func (r *resourceProject) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) -} + parts := strings.Split(req.ID, ":") -// TIP: ==== STATUS CONSTANTS ==== -// Create constants for states and statuses if the service does not -// already have suitable constants. We prefer that you use the constants -// provided in the service if available (e.g., awstypes.StatusInProgress). -const ( - statusChangePending = "PENDING" // I changes these to be more inline with aws types - statusUpdated = "UPDATED" // ^ -) + if len(parts) != 2 { + resp.Diagnostics.AddError("Resource Import Invalid ID", fmt.Sprintf(`Unexpected format for import ID (%s), use: "DomainIdentifier:Id"`, req.ID)) + } + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("domain_id"), parts[0])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root(names.AttrID), parts[1])...) +} -// TIP: ==== WAITERS ==== -// Some resources of some services have waiters provided by the AWS API. -// Unless they do not work properly, use them rather than defining new ones -// here. -// -// Sometimes we define the wait, status, and find functions in separate -// files, wait.go, status.go, and find.go. Follow the pattern set out in the -// service and define these where it makes the most sense. -// -// If these functions are used in the _test.go file, they will need to be -// exported (i.e., capitalized). -// -// You will need to adjust the parameters and names to fit the service. func waitProjectCreated(ctx context.Context, conn *datazone.Client, domain string, identifier string, timeout time.Duration) (*datazone.GetProjectOutput, error) { stateConf := &retry.StateChangeConf{ Pending: []string{}, - Target: []string{string(awstypes.ProjectStatusActive)}, + Target: enum.Slice[awstypes.ProjectStatus](awstypes.ProjectStatusActive), Refresh: statusProject(ctx, conn, domain, identifier), Timeout: timeout, NotFoundChecks: 20, @@ -618,15 +381,10 @@ func waitProjectCreated(ctx context.Context, conn *datazone.Client, domain strin return nil, err } -// TIP: It is easier to determine whether a resource is updated for some -// resources than others. The best case is a status flag that tells you when -// the update has been fully realized. Other times, you can check to see if a -// key resource argument is updated to a new value or not. -/* func waitProjectUpdated(ctx context.Context, conn *datazone.Client, domain string, identifier string, timeout time.Duration) (*datazone.GetProjectOutput, error) { stateConf := &retry.StateChangeConf{ - Pending: []string{statusChangePending}, - Target: []string{statusUpdated}, + Pending: []string{}, + Target: enum.Slice[awstypes.ProjectStatus](awstypes.ProjectStatusActive), Refresh: statusProject(ctx, conn, domain, identifier), Timeout: timeout, NotFoundChecks: 20, @@ -634,26 +392,23 @@ func waitProjectUpdated(ctx context.Context, conn *datazone.Client, domain strin } outputRaw, err := stateConf.WaitForStateContext(ctx) - if out, ok := outputRaw.(*datazone.GetProjectOutput); ok { // not too sure if this is correct + if out, ok := outputRaw.(*datazone.GetProjectOutput); ok { return out, err } return nil, err } -*/ -// TIP: A deleted waiter is almost like a backwards created waiter. There may -// be additional pending states, however. func waitProjectDeleted(ctx context.Context, conn *datazone.Client, domain string, identifier string, timeout time.Duration) (*datazone.GetProjectOutput, error) { stateConf := &retry.StateChangeConf{ - Pending: []string{string(awstypes.ProjectStatusDeleting), string(awstypes.ProjectStatusActive)}, + Pending: enum.Slice[awstypes.ProjectStatus](awstypes.ProjectStatusDeleting, awstypes.ProjectStatusActive), // Not too sure about this. Target: []string{}, Refresh: statusProject(ctx, conn, domain, identifier), Timeout: timeout, } outputRaw, err := stateConf.WaitForStateContext(ctx) - if out, ok := outputRaw.(*datazone.GetProjectOutput); ok { // not too sure if this is correct + if out, ok := outputRaw.(*datazone.GetProjectOutput); ok { return out, err } @@ -675,14 +430,13 @@ func statusProject(ctx context.Context, conn *datazone.Client, domain string, id } } -// TIP: ==== FINDERS ==== func findProjectByID(ctx context.Context, conn *datazone.Client, domain string, identifier string) (*datazone.GetProjectOutput, error) { in := &datazone.GetProjectInput{ DomainIdentifier: aws.String(domain), Identifier: aws.String(identifier), } - out, err := conn.GetProject(ctx, in) // add finder here later + out, err := conn.GetProject(ctx, in) if err != nil { if errs.IsA[*awstypes.ResourceNotFoundException](err) { return nil, &retry.NotFoundError{ @@ -701,91 +455,10 @@ func findProjectByID(ctx context.Context, conn *datazone.Client, domain string, return out, nil } -// TIP: ==== FLEX ==== -// Flatteners and expanders ("flex" functions) help handle complex data -// types. Flatteners take an API data type and return the equivalent Plugin-Framework -// type. In other words, flatteners translate from AWS -> Terraform. -// -// On the other hand, expanders take a Terraform data structure and return -// something that you can send to the AWS API. In other words, expanders -// translate from Terraform -> AWS. -// -// See more: -// https://hashicorp.github.io/terraform-provider-aws/data-handling-and-conversion/ -/* -func flattenComplexArgument(ctx context.Context, apiObject *awstypes.ComplexArgument) (types.List, diag.Diagnostics) { - var diags diag.Diagnostics - elemType := types.ObjectType{AttrTypes: complexArgumentAttrTypes} - - if apiObject == nil { - return types.ListNull(elemType), diags - } - - obj := map[string]attr.Value{ - "nested_required": flex.StringValueToFramework(ctx, apiObject.NestedRequired), - "nested_optional": flex.StringValueToFramework(ctx, apiObject.NestedOptional), - } - objVal, d := types.ObjectValue(complexArgumentAttrTypes, obj) - diags.Append(d...) - - listVal, d := types.ListValue(elemType, []attr.Value{objVal}) - diags.Append(d...) - - return listVal, diags -} -*/ - -// TIP: Often the AWS API will return a slice of structures in response to a -// request for information. Sometimes you will have set criteria (e.g., the ID) -// that means you'll get back a one-length slice. This plural function works -// brilliantly for that situation too. -/*/ -func flattenComplexArguments(ctx context.Context, apiObjects []*awstypes.ComplexArgument) (types.List, diag.Diagnostics) { - var diags diag.Diagnostics - elemType := types.ObjectType{AttrTypes: complexArgumentAttrTypes} - - if len(apiObjects) == 0 { - return types.ListNull(elemType), diags - } - - elems := []attr.Value{} - for _, apiObject := range apiObjects { - if apiObject == nil { - continue - } - - obj := map[string]attr.Value{ - "nested_required": flex.StringValueToFramework(ctx, apiObject.NestedRequired), - "nested_optional": flex.StringValueToFramework(ctx, apiObject.NestedOptional), - } - objVal, d := types.ObjectValue(complexArgumentAttrTypes, obj) - diags.Append(d...) - - elems = append(elems, objVal) - } - - listVal, d := types.ListValue(elemType, elems) - diags.Append(d...) - - return listVal, diags -} -*/ -// TIP: ==== DATA STRUCTURES ==== -// With Terraform Plugin-Framework configurations are deserialized into -// Go types, providing type safety without the need for type assertions. -// These structs should match the schema definition exactly, and the `tfsdk` -// tag value should match the attribute name. -// -// Nested objects are represented in their own data struct. These will -// also have a corresponding attribute type mapping for use inside flex -// functions. -// -// See more: -// https://developer.hashicorp.com/terraform/plugin/framework/handling-data/accessing-values type resourceProjectData struct { Description types.String `tfsdk:"description"` - DomainId fwtypes.Regexp `tfsdk:"domain_id"` - Name fwtypes.Regexp `tfsdk:"name"` + DomainId types.String `tfsdk:"domain_id"` + Name types.String `tfsdk:"name"` CreatedBy types.String `tfsdk:"created_by"` ID types.String `tfsdk:"id"` CreatedAt timetypes.RFC3339 `tfsdk:"created_at"` @@ -797,17 +470,7 @@ type resourceProjectData struct { GlossaryTerms fwtypes.ListValueOf[types.String] `tfsdk:"glossary_terms"` } -// DomainId types.String `tfsdk:"domain_id"` // is this the same thing as domainIdent - type dsProjectDeletionError struct { Code types.String `tfsdk:"code"` Message types.String `tfsdk:"message"` } - -/* -type Metadata struct { - values map[interface{}]interface{} `tfsdk:"metadata"` -} - -ResultMetadata types.MapType `tfsdk:"result_metadata"` -*/ diff --git a/internal/service/datazone/project_test.go b/internal/service/datazone/project_test.go index 0dab5e198a5..6cc7c5c8e54 100644 --- a/internal/service/datazone/project_test.go +++ b/internal/service/datazone/project_test.go @@ -3,36 +3,7 @@ package datazone_test -// **PLEASE DELETE THIS AND ALL TIP COMMENTS BEFORE SUBMITTING A PR FOR REVIEW!** -// -// TIP: ==== INTRODUCTION ==== -// Thank you for trying the skaff tool! -// -// You have opted to include these helpful comments. They all include "TIP:" -// to help you find and remove them when you're done with them. -// -// While some aspects of this file are customized to your input, the -// scaffold tool does *not* look at the AWS API and ensure it has correct -// function, structure, and variable names. It makes guesses based on -// commonalities. You will need to make significant adjustments. -// -// In other words, as generated, this is a rough outline of the work you will -// need to do. If something doesn't make sense for your situation, get rid of -// it. - import ( - // TIP: ==== IMPORTS ==== - // This is a common set of imports but not customized to your code since - // your code hasn't been written yet. Make sure you, your IDE, or - // goimports -w fixes these imports. - // - // The provider linter wants your imports to be in two groups: first, - // standard library (i.e., "fmt" or "strings"), second, everything else. - // - // Also, AWS Go SDK v2 may handle nested structures differently than v1, - // using the services/datazone/types package. If so, you'll - // need to import types and reference the nested types, e.g., as - // types.. "context" "errors" "fmt" @@ -50,30 +21,24 @@ import ( "github.com/hashicorp/terraform-provider-aws/internal/errs" "github.com/hashicorp/terraform-provider-aws/names" - // TIP: You will often need to import the package that this test file lives - // in. Since it is in the "test" context, it must import the package to use - // any normal context constants, variables, or functions. tfdatazone "github.com/hashicorp/terraform-provider-aws/internal/service/datazone" ) func TestAccDataZoneProject_basic(t *testing.T) { ctx := acctest.Context(t) - // TIP: This is a long-running test guard for tests that run longer than - // 300s (5 min) generally. if testing.Short() { t.Skip("skipping long-running test in short mode") } var project datazone.GetProjectOutput - rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) // Name for project - dName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) // Name for domain + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + dName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_datazone_project.test" + domainName := "aws_datazone_domain.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t) - //acctest.PreCheckPartitionHasService(t, names.DataZoneEndpointID) - //testAccPreCheckProject(ctx, t) }, ErrorCheck: acctest.ErrorCheck(t, names.DataZoneServiceID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, @@ -83,29 +48,23 @@ func TestAccDataZoneProject_basic(t *testing.T) { Config: testAccProjectConfig_basic(rName, dName), Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists(ctx, resourceName, &project), - // inputed - resource.TestCheckResourceAttrSet(resourceName, "domain_id"), // find a domain identifier - //resource.TestCheckResourceAttrSet(resourceName, "glossary_terms.#"), - resource.TestCheckResourceAttrSet(resourceName, "description"), - resource.TestCheckResourceAttrSet(resourceName, "name"), - resource.TestCheckResourceAttrSet(resourceName, "skip_deletion_check"), - // computed + resource.TestCheckResourceAttrPair(resourceName, "domain_id", domainName, names.AttrID), + resource.TestCheckResourceAttrSet(resourceName, "glossary_terms.#"), + resource.TestCheckResourceAttr(resourceName, names.AttrDescription, "desc"), + resource.TestCheckResourceAttr(resourceName, names.AttrName, rName), resource.TestCheckResourceAttrSet(resourceName, "created_by"), - resource.TestCheckResourceAttrSet(resourceName, "id"), - resource.TestCheckResourceAttrSet(resourceName, "created_at"), - //resource.TestCheckResourceAttrSet(resourceName, "failure_reasons.#"), + resource.TestCheckResourceAttrSet(resourceName, names.AttrID), + resource.TestCheckResourceAttrSet(resourceName, names.AttrCreatedAt), resource.TestCheckResourceAttrSet(resourceName, "last_updated_at"), - // should this ever be empty?? resource.TestCheckResourceAttrSet(resourceName, "project_status"), - //resource.TestCheckResourceAttrSet(resourceName, "result_metadata"), + //resource.TestCheckResourceAttrSet(resourceName, "project_status"), ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ExpectNonEmptyPlan: true, - - ImportStateVerifyIgnore: []string{"apply_immediately", "user"}, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: testAccAuthorizerImportStateIdFunc(resourceName), + ImportStateVerifyIgnore: []string{"skip_deletion_check", "project_status"}, }, }, }) @@ -118,7 +77,7 @@ func TestAccDataZoneProject_disappears(t *testing.T) { var project datazone.GetProjectOutput rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) - dName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) // Name for domain + dName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_datazone_project.test" resource.ParallelTest(t, resource.TestCase{ @@ -135,12 +94,6 @@ func TestAccDataZoneProject_disappears(t *testing.T) { Config: testAccProjectConfig_basic(rName, dName), Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists(ctx, resourceName, &project), - // TIP: The Plugin-Framework disappears helper is similar to the Plugin-SDK version, - // but expects a new resource factory function as the third argument. To expose this - // private function to the testing package, you may need to add a line like the following - // to exports_test.go: - // - // var ResourceProject = newResourceProject acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tfdatazone.ResourceProject, resourceName), ), ExpectNonEmptyPlan: true, @@ -163,54 +116,29 @@ func testAccCheckProjectDestroy(ctx context.Context) resource.TestCheckFunc { Identifier: aws.String(rs.Primary.ID), } _, err := conn.GetProject(ctx, input) - if errs.IsA[*types.ResourceNotFoundException](err) { + if errs.IsA[*types.AccessDeniedException](err) || errs.IsA[*types.ResourceNotFoundException](err) { return nil } if err != nil { return create.Error(names.DataZone, create.ErrActionCheckingDestroyed, tfdatazone.ResNameProject, rs.Primary.ID, err) } - - return create.Error(names.DataZone, create.ErrActionCheckingDestroyed, tfdatazone.ResNameProject, rs.Primary.ID, errors.New("not destroyed")) - } - - return testAccCheckProjectDomainDestroy(ctx, s) - } -} -func testAccCheckProjectDomainDestroy(ctx context.Context, s *terraform.State) error { - conn := acctest.Provider.Meta().(*conns.AWSClient).DataZoneClient(ctx) - - for _, rs := range s.RootModule().Resources { - if rs.Type != "aws_datazone_domain" { - continue - } - in := &datazone.GetDomainInput{ - Identifier: aws.String(rs.Primary.ID), - } - del := &datazone.DeleteDomainInput{ - Identifier: aws.String(rs.Primary.ID), - } - _, err := conn.DeleteDomain(ctx, del) - - if err != nil { - return create.Error(names.DataZone, create.ErrActionCheckingDestroyed, tfdatazone.ResNameDomain, rs.Primary.ID, err) - - } - - _, err = conn.GetDomain(ctx, in) - - if tfdatazone.IsResourceMissing(err) { - return nil } + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_datazone_domain" { + continue + } + _, err := conn.DeleteDomain(ctx, &datazone.DeleteDomainInput{ + Identifier: aws.String(rs.Primary.Attributes["domain_id"]), + }) - if err != nil { - return create.Error(names.DataZone, create.ErrActionCheckingDestroyed, tfdatazone.ResNameDomain, rs.Primary.ID, err) + if err != nil { + return create.Error(names.DataZone, create.ErrActionCheckingDestroyed, tfdatazone.ResNameDomain, rs.Primary.ID, err) + } } - - return create.Error(names.DataZone, create.ErrActionCheckingDestroyed, tfdatazone.ResNameDomain, rs.Primary.ID, errors.New("not destroyed")) + return nil } - - return nil } + func testAccCheckProjectExists(ctx context.Context, name string, project *datazone.GetProjectOutput) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[name] @@ -222,11 +150,9 @@ func testAccCheckProjectExists(ctx context.Context, name string, project *datazo return create.Error(names.DataZone, create.ErrActionCheckingExistence, tfdatazone.ResNameProject, name, errors.New("not set")) } if rs.Primary.Attributes["domain_id"] == "" { - return create.Error(names.DataZone, create.ErrActionCheckingExistence, tfdatazone.ResNameProject, name, errors.New("domain identifer not set")) - + return create.Error(names.DataZone, create.ErrActionCheckingExistence, tfdatazone.ResNameProject, name, errors.New("domain identifier not set")) } t := rs.Primary.Attributes["domain_id"] - conn := acctest.Provider.Meta().(*conns.AWSClient).DataZoneClient(ctx) resp, err := conn.GetProject(ctx, &datazone.GetProjectInput{ DomainIdentifier: &t, @@ -264,24 +190,21 @@ func testAccCheckProjectNotRecreated(before, after *datazone.GetProjectOutput) r return nil } } -func TestAccCheckProjectUpdate(t *testing.T) { +func TestAccDataZoneCheckProjectUpdate(t *testing.T) { ctx := acctest.Context(t) - // TIP: This is a long-running test guard for tests that run longer than - // 300s (5 min) generally. if testing.Short() { t.Skip("skipping long-running test in short mode") } - var project datazone.GetProjectOutput - pName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) // Name for datazone project - dName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) // Name for datazone domain + var v1, v2 datazone.GetProjectOutput + pName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + dName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_datazone_project.test" + domainName := "aws_datazone_domain.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t) - //acctest.PreCheckPartitionHasService(t, names.DataZoneEndpointID) - //testAccPreCheckProject(ctx, t) }, ErrorCheck: acctest.ErrorCheck(t, names.DataZoneServiceID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, @@ -290,52 +213,63 @@ func TestAccCheckProjectUpdate(t *testing.T) { { Config: testAccProjectConfig_basic(pName, dName), Check: resource.ComposeTestCheckFunc( - testAccCheckProjectExists(ctx, resourceName, &project), - // inputed - resource.TestCheckResourceAttrSet(resourceName, "domain_id"), // find a domain identifier - //resource.TestCheckResourceAttrSet(resourceName, "glossary_terms.#"), - resource.TestCheckResourceAttrSet(resourceName, "description"), - resource.TestCheckResourceAttrSet(resourceName, "name"), - resource.TestCheckResourceAttrSet(resourceName, "skip_deletion_check"), - // computed + testAccCheckProjectExists(ctx, resourceName, &v1), + resource.TestCheckResourceAttrPair(resourceName, "domain_id", domainName, names.AttrID), + resource.TestCheckResourceAttrSet(resourceName, "glossary_terms.#"), + resource.TestCheckResourceAttr(resourceName, names.AttrDescription, "desc"), + resource.TestCheckResourceAttr(resourceName, names.AttrName, pName), resource.TestCheckResourceAttrSet(resourceName, "created_by"), - resource.TestCheckResourceAttrSet(resourceName, "id"), - resource.TestCheckResourceAttrSet(resourceName, "created_at"), - //resource.TestCheckResourceAttrSet(resourceName, "failure_reasons.#"), + resource.TestCheckResourceAttrSet(resourceName, names.AttrID), + resource.TestCheckResourceAttrSet(resourceName, names.AttrCreatedAt), resource.TestCheckResourceAttrSet(resourceName, "last_updated_at"), - // should this ever be empty?? resource.TestCheckResourceAttrSet(resourceName, "project_status"), + //resource.TestCheckResourceAttrSet(resourceName, "project_status"), //resource.TestCheckResourceAttrSet(resourceName, "result_metadata"), ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - //ImportStateVerifyIgnore: []string{"apply_immediately", "user"}, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: testAccAuthorizerImportStateIdFunc(resourceName), + ImportStateVerifyIgnore: []string{"skip_deletion_check", "project_status"}, }, { - Config: testAccProjectConfig_basic(pName, dName), + Config: testAccProjectConfigBasicUpdate(pName, dName), Check: resource.ComposeTestCheckFunc( - testAccCheckProjectExists(ctx, resourceName, &project), - // inputed - resource.TestCheckResourceAttrSet(resourceName, "domain_id"), // find a domain identifier - //resource.TestCheckResourceAttrSet(resourceName, "glossary_terms.#"), - resource.TestCheckResourceAttrSet(resourceName, "description"), - resource.TestCheckResourceAttrSet(resourceName, "name"), - resource.TestCheckResourceAttrSet(resourceName, "skip_deletion_check"), - // computed + testAccCheckProjectExists(ctx, resourceName, &v2), + testAccCheckProjectNotRecreated(&v1, &v2), + resource.TestCheckResourceAttrPair(resourceName, "domain_id", domainName, names.AttrID), + resource.TestCheckResourceAttrSet(resourceName, "glossary_terms.#"), + resource.TestCheckResourceAttr(resourceName, names.AttrDescription, names.AttrDescription), + resource.TestCheckResourceAttr(resourceName, names.AttrName, pName), resource.TestCheckResourceAttrSet(resourceName, "created_by"), - resource.TestCheckResourceAttrSet(resourceName, "id"), - resource.TestCheckResourceAttrSet(resourceName, "created_at"), - //resource.TestCheckResourceAttrSet(resourceName, "failure_reasons.#"), + resource.TestCheckResourceAttrSet(resourceName, names.AttrID), + resource.TestCheckResourceAttrSet(resourceName, names.AttrCreatedAt), resource.TestCheckResourceAttrSet(resourceName, "last_updated_at"), - // should this ever be empty?? resource.TestCheckResourceAttrSet(resourceName, "project_status"), + //resource.TestCheckResourceAttrSet(resourceName, "project_status"), //resource.TestCheckResourceAttrSet(resourceName, "result_metadata"), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: testAccAuthorizerImportStateIdFunc(resourceName), + ImportStateVerifyIgnore: []string{"project_status", "skip_deletion_check"}, + }, }, }) +} +func testAccAuthorizerImportStateIdFunc(resourceName string) resource.ImportStateIdFunc { + return func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return "", fmt.Errorf("Not found: %s", resourceName) + } + + return fmt.Sprintf("%s:%s", rs.Primary.Attributes["domain_id"], rs.Primary.ID), nil + } } func testAccProjectConfig_basic(pName, dName string) string { @@ -344,10 +278,11 @@ resource "aws_security_group" "test" { name = %[1]q } -resource "aws_datazone_project" "test" { - domain_id = aws_datazone_domain.test.id - name = %[1]q - description = "desc" +resource "aws_datazone_project" "test" { + domain_id = aws_datazone_domain.test.id + glossary_terms = ["2N8w6XJCwZf"] + name = %[1]q + description = "desc" skip_deletion_check = true } `, pName)) @@ -358,13 +293,12 @@ resource "aws_security_group" "test" { name = %[1]q } -resource "aws_datazone_project" "test" { - domain_id = aws_datazone_domain.test.id - name = %[1]q - description = "description" +resource "aws_datazone_project" "test" { + domain_id = aws_datazone_domain.test.id + glossary_terms = ["2N8w6XJCwZf"] + name = %[1]q + description = "description" skip_deletion_check = true } `, pName)) } - -// glossary_terms = ["2N8w6XJCwZf"] diff --git a/website/docs/r/datazone_project.html.markdown b/website/docs/r/datazone_project.html.markdown index eee1fc3f870..c63e23bde62 100644 --- a/website/docs/r/datazone_project.html.markdown +++ b/website/docs/r/datazone_project.html.markdown @@ -1,69 +1,77 @@ --- subcategory: "DataZone" layout: "aws" -page_title: "AWS: aws_datazone_project" +page_title: "Amazon: aws_datazone_project" description: |- - Terraform resource for managing an AWS DataZone Project. + Terraform resource for managing an Amazon DataZone Project. --- -` # Resource: aws_datazone_project -Terraform resource for managing an AWS DataZone Project. - -## Example Usage +Terraform resource for managing an Amazon DataZone Project. ### Basic Usage ```terraform -resource "aws_datazone_project" "example" { + +resource "aws_datazone_project" "test" { + domain_id = aws_datazone_domain.test.id + glossary_terms = ["2N8w6XJCwZf"] + name = "name" + description = "desc" + skip_deletion_check = true } + ``` ## Argument Reference The following arguments are required: -* `example_arg` - (Required) Concise argument description. Do not begin the description with "An", "The", "Defines", "Indicates", or "Specifies," as these are verbose. In other words, "Indicates the amount of storage," can be rewritten as "Amount of storage," without losing any information. +* `domain_id` - (Required) Identifier of domain which the project is part of. Must follow the regex of ^dzd[-_][a-zA-Z0-9_-]{1,36}$. +* `name` - (Required) Name of the project. Must follow the regex of ^[\w -]+$. and have a length of at most 64. The following arguments are optional: -* `optional_arg` - (Optional) Concise argument description. Do not begin the description with "An", "The", "Defines", "Indicates", or "Specifies," as these are verbose. In other words, "Indicates the amount of storage," can be rewritten as "Amount of storage," without losing any information. +* `skip_deletion_check` - (Optional) Optional flag to delete all child entities within the project. +* `description` - (Optional) Description of project. +* `glossary_terms` - (Optional) List of glossary terms that can be used in the project. The list cannot be empty or include over 20 values. Each value must follow the regex of [a-zA-Z0-9_-]{1,36}$. ## Attribute Reference This resource exports the following attributes in addition to the arguments above: -* `arn` - ARN of the Project. Do not begin the description with "An", "The", "Defines", "Indicates", or "Specifies," as these are verbose. In other words, "Indicates the amount of storage," can be rewritten as "Amount of storage," without losing any information. -* `example_attribute` - Concise description. Do not begin the description with "An", "The", "Defines", "Indicates", or "Specifies," as these are verbose. In other words, "Indicates the amount of storage," can be rewritten as "Amount of storage," without losing any information. +* `created_by` - Creator of the project. +* `domain_id` - Id of the project's DataZone domain. +* `id` - Id of the project. +* `name` - Name of the project. +* `created_at` - Timestamp of when the project was made. +* `description` - Description of the project. +* `failure_reasons` - List of error messages if operation cannot be completed. +* `glossary_terms` - Business glossary terms that can be used in the project. +* `last_updated_at` - Timestamp of when the project was last updated. +* `project_status` - Enum that conveys state of project. Can be ACTIVE, DELETING, or DELETE_FAILED. ## Timeouts [Configuration options](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts): -* `create` - (Default `60m`) -* `update` - (Default `180m`) -* `delete` - (Default `90m`) +* `create` - (Default `3m`) +* `update` - (Default `30m`) +* `delete` - (Default `3m`) ## Import -In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import DataZone Project using the `example_id_arg`. For example: +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import DataZone Project using the `id`. For example: ```terraform import { to = aws_datazone_project.example - id = "project-id-12345678" + id = "projectid123" } ``` -Using `terraform import`, import DataZone Project using the `example_id_arg`. For example: +Using `terraform import`, import DataZone Project using the `id`. For example: ```console -% terraform import aws_datazone_project.example project-id-12345678 +% terraform import aws_datazone_project.example projectid123 ``` From ae29d5695c46829d05511ba90c3d9fdc511378f6 Mon Sep 17 00:00:00 2001 From: ThomasZalewski Date: Thu, 11 Jul 2024 12:47:48 -0400 Subject: [PATCH 05/17] Fixed resource update. --- internal/service/datazone/project.go | 28 +--------------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/internal/service/datazone/project.go b/internal/service/datazone/project.go index 3eaf13e10e7..c51e83504aa 100644 --- a/internal/service/datazone/project.go +++ b/internal/service/datazone/project.go @@ -296,15 +296,7 @@ func (r *resourceProject) Update(ctx context.Context, req resource.UpdateRequest return } - _, err = waitProjectUpdated(ctx, conn, plan.DomainId.ValueString(), plan.ID.ValueString(), r.UpdateTimeout(ctx, plan.Timeouts)) - if err != nil { - resp.Diagnostics.AddError( - create.ProblemStandardMessage(names.DataZone, create.ErrActionWaitingForUpdate, ResNameProject, plan.ID.String(), err), - err.Error(), - ) - return - } - + out.ProjectStatus = "ACTIVE" resp.Diagnostics.Append(flex.Flatten(ctx, out, &state)...) if resp.Diagnostics.HasError() { return @@ -381,24 +373,6 @@ func waitProjectCreated(ctx context.Context, conn *datazone.Client, domain strin return nil, err } -func waitProjectUpdated(ctx context.Context, conn *datazone.Client, domain string, identifier string, timeout time.Duration) (*datazone.GetProjectOutput, error) { - stateConf := &retry.StateChangeConf{ - Pending: []string{}, - Target: enum.Slice[awstypes.ProjectStatus](awstypes.ProjectStatusActive), - Refresh: statusProject(ctx, conn, domain, identifier), - Timeout: timeout, - NotFoundChecks: 20, - ContinuousTargetOccurence: 2, - } - - outputRaw, err := stateConf.WaitForStateContext(ctx) - if out, ok := outputRaw.(*datazone.GetProjectOutput); ok { - return out, err - } - - return nil, err -} - func waitProjectDeleted(ctx context.Context, conn *datazone.Client, domain string, identifier string, timeout time.Duration) (*datazone.GetProjectOutput, error) { stateConf := &retry.StateChangeConf{ Pending: enum.Slice[awstypes.ProjectStatus](awstypes.ProjectStatusDeleting, awstypes.ProjectStatusActive), // Not too sure about this. From 868a1bdb586b8be2a7f51eabbd0874c992a453d8 Mon Sep 17 00:00:00 2001 From: ThomasZalewski Date: Thu, 11 Jul 2024 17:02:54 -0400 Subject: [PATCH 06/17] Added support for disappearance test. --- internal/service/datazone/project.go | 37 ++++++++++++----------- internal/service/datazone/project_test.go | 24 +++------------ names/names.go | 1 + 3 files changed, 25 insertions(+), 37 deletions(-) diff --git a/internal/service/datazone/project.go b/internal/service/datazone/project.go index c51e83504aa..c05af77fd84 100644 --- a/internal/service/datazone/project.go +++ b/internal/service/datazone/project.go @@ -143,12 +143,13 @@ func (r *resourceProject) Schema(ctx context.Context, req resource.SchemaRequest } func (r *resourceProject) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - conn := r.Meta().DataZoneClient(ctx) var plan resourceProjectData resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) if resp.Diagnostics.HasError() { return } + conn := r.Meta().DataZoneClient(ctx) + var validateDomain datazone.GetDomainInput validateDomain.Identifier = plan.DomainId.ValueStringPointer() _, err := conn.GetDomain(ctx, &validateDomain) @@ -161,14 +162,12 @@ func (r *resourceProject) Create(ctx context.Context, req resource.CreateRequest } in := &datazone.CreateProjectInput{ - Name: aws.String(plan.Name.ValueString()), + Name: aws.String(plan.Name.ValueString()), + DomainIdentifier: aws.String(plan.DomainId.ValueString()), } if !plan.Description.IsNull() { in.Description = aws.String(plan.Description.ValueString()) } - if !plan.DomainId.IsNull() { - in.DomainIdentifier = aws.String(plan.DomainId.ValueString()) - } if !plan.GlossaryTerms.IsNull() { in.GlossaryTerms = aws.ToStringSlice(flex.ExpandFrameworkStringList(ctx, plan.GlossaryTerms)) } @@ -181,13 +180,22 @@ func (r *resourceProject) Create(ctx context.Context, req resource.CreateRequest ) return } - if out == nil || !(out.FailureReasons == nil) { + if out == nil { resp.Diagnostics.AddError( create.ProblemStandardMessage(names.DataZone, create.ErrActionCreating, ResNameProject, plan.Name.String(), nil), - errors.New("failure reasons populated").Error(), + errors.New("failure when creating").Error(), ) return } + if !(out.FailureReasons == nil) && len(out.FailureReasons) > 0 { + for _, x := range out.FailureReasons { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.DataZone, create.ErrActionCreating, ResNameProject, plan.Name.String(), nil), + errors.New("error message: "+*x.Message+" error code: "+*x.Code).Error(), + ) + } + return + } resp.Diagnostics.Append(flex.Flatten(ctx, out, &plan)...) if resp.Diagnostics.HasError() { @@ -214,11 +222,8 @@ func (r *resourceProject) Read(ctx context.Context, req resource.ReadRequest, re return } - in := &datazone.GetProjectInput{ - DomainIdentifier: state.DomainId.ValueStringPointer(), - Identifier: state.ID.ValueStringPointer(), - } - out, err := conn.GetProject(ctx, in) + //out, err := conn.GetProject(ctx, in) + out, err := findProjectByID(ctx, conn, state.DomainId.ValueString(), state.ID.ValueString()) if tfresource.NotFound(err) { resp.State.RemoveResource(ctx) return @@ -361,8 +366,8 @@ func waitProjectCreated(ctx context.Context, conn *datazone.Client, domain strin Target: enum.Slice[awstypes.ProjectStatus](awstypes.ProjectStatusActive), Refresh: statusProject(ctx, conn, domain, identifier), Timeout: timeout, - NotFoundChecks: 20, - ContinuousTargetOccurence: 2, + NotFoundChecks: 40, + ContinuousTargetOccurence: 10, } outputRaw, err := stateConf.WaitForStateContext(ctx) @@ -409,16 +414,14 @@ func findProjectByID(ctx context.Context, conn *datazone.Client, domain string, DomainIdentifier: aws.String(domain), Identifier: aws.String(identifier), } - out, err := conn.GetProject(ctx, in) if err != nil { - if errs.IsA[*awstypes.ResourceNotFoundException](err) { + if errs.IsA[*awstypes.ResourceNotFoundException](err) || errs.IsA[*awstypes.AccessDeniedException](err) { return nil, &retry.NotFoundError{ LastError: err, LastRequest: in, } } - return nil, err } diff --git a/internal/service/datazone/project_test.go b/internal/service/datazone/project_test.go index 6cc7c5c8e54..2518eb9e4bd 100644 --- a/internal/service/datazone/project_test.go +++ b/internal/service/datazone/project_test.go @@ -81,11 +81,7 @@ func TestAccDataZoneProject_disappears(t *testing.T) { resourceName := "aws_datazone_project.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { - acctest.PreCheck(ctx, t) - acctest.PreCheckPartitionHasService(t, names.AttrDomainName) - testAccPreCheckProject(ctx, t) - }, + PreCheck: func() { acctest.PreCheck(ctx, t); acctest.PreCheckPartitionHasService(t, names.DataZoneEndpointID) }, ErrorCheck: acctest.ErrorCheck(t, names.DataZoneServiceID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, CheckDestroy: testAccCheckProjectDestroy(ctx), @@ -97,6 +93,7 @@ func TestAccDataZoneProject_disappears(t *testing.T) { acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tfdatazone.ResourceProject, resourceName), ), ExpectNonEmptyPlan: true, + //ExpectError: regexache.MustCompile(`(AccessDeniedException)`), }, }, }) @@ -104,7 +101,6 @@ func TestAccDataZoneProject_disappears(t *testing.T) { func testAccCheckProjectDestroy(ctx context.Context) resource.TestCheckFunc { return func(s *terraform.State) error { conn := acctest.Provider.Meta().(*conns.AWSClient).DataZoneClient(ctx) - for _, rs := range s.RootModule().Resources { if rs.Type != "aws_datazone_project" { continue @@ -159,7 +155,7 @@ func testAccCheckProjectExists(ctx context.Context, name string, project *datazo Identifier: &rs.Primary.ID, }) - if err != nil { + if err != nil && !errs.IsA[*types.ResourceNotFoundException](err) { return create.Error(names.DataZone, create.ErrActionCheckingExistence, tfdatazone.ResNameProject, rs.Primary.ID, err) } @@ -168,19 +164,7 @@ func testAccCheckProjectExists(ctx context.Context, name string, project *datazo return nil } } -func testAccPreCheckProject(ctx context.Context, t *testing.T) { - conn := acctest.Provider.Meta().(*conns.AWSClient).DataZoneClient(ctx) - input := &datazone.ListProjectsInput{} - _, err := conn.ListProjects(ctx, input) - - if acctest.PreCheckSkipError(err) { - t.Skipf("skipping acceptance testing: %s", err) - } - if err != nil { - t.Fatalf("unexpected PreCheck error: %s", err) - } -} func testAccCheckProjectNotRecreated(before, after *datazone.GetProjectOutput) resource.TestCheckFunc { return func(s *terraform.State) error { if before, after := aws.ToString(before.Id), aws.ToString(after.Id); before != after { @@ -190,7 +174,7 @@ func testAccCheckProjectNotRecreated(before, after *datazone.GetProjectOutput) r return nil } } -func TestAccDataZoneCheckProjectUpdate(t *testing.T) { +func TestAccDataZoneProject_update(t *testing.T) { ctx := acctest.Context(t) if testing.Short() { t.Skip("skipping long-running test in short mode") diff --git a/names/names.go b/names/names.go index c89fe9aa207..910c6af4c9c 100644 --- a/names/names.go +++ b/names/names.go @@ -112,6 +112,7 @@ const ( VerifiedPermissionsEndpointID = "verifiedpermissions" WAFEndpointID = "waf" WAFRegionalEndpointID = "waf-regional" + DataZoneEndpointID = "datazone" ) // These should move to aws-sdk-go-base. From 3c7866af51f2effaef158541ef7e8de3b71e59ce Mon Sep 17 00:00:00 2001 From: ThomasZalewski Date: Thu, 11 Jul 2024 17:30:55 -0400 Subject: [PATCH 07/17] CI linter changes. --- internal/service/datazone/project_test.go | 3 +-- internal/service/datazone/service_package_gen.go | 4 ---- website/docs/r/datazone_project.html.markdown | 11 +++++++++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/internal/service/datazone/project_test.go b/internal/service/datazone/project_test.go index 2518eb9e4bd..7035a20dad3 100644 --- a/internal/service/datazone/project_test.go +++ b/internal/service/datazone/project_test.go @@ -19,9 +19,8 @@ import ( "github.com/hashicorp/terraform-provider-aws/internal/conns" "github.com/hashicorp/terraform-provider-aws/internal/create" "github.com/hashicorp/terraform-provider-aws/internal/errs" - "github.com/hashicorp/terraform-provider-aws/names" - tfdatazone "github.com/hashicorp/terraform-provider-aws/internal/service/datazone" + "github.com/hashicorp/terraform-provider-aws/names" ) func TestAccDataZoneProject_basic(t *testing.T) { diff --git a/internal/service/datazone/service_package_gen.go b/internal/service/datazone/service_package_gen.go index a2745588aad..7f0e80c975a 100644 --- a/internal/service/datazone/service_package_gen.go +++ b/internal/service/datazone/service_package_gen.go @@ -36,10 +36,6 @@ func (p *servicePackage) FrameworkResources(ctx context.Context) []*types.Servic Factory: newResourceEnvironmentBlueprintConfiguration, Name: "Environment Blueprint Configuration", }, - { - Factory: newResourceProject, - Name: "Project", - }, } } diff --git a/website/docs/r/datazone_project.html.markdown b/website/docs/r/datazone_project.html.markdown index c63e23bde62..da54a96a1c6 100644 --- a/website/docs/r/datazone_project.html.markdown +++ b/website/docs/r/datazone_project.html.markdown @@ -12,7 +12,15 @@ Terraform resource for managing an Amazon DataZone Project. ### Basic Usage ```terraform +resource "aws_datazone_project" "test" { + domain_id = aws_datazone_domain.test.id + name = "name" +} +``` +### Basic Usage + +```terraform resource "aws_datazone_project" "test" { domain_id = aws_datazone_domain.test.id glossary_terms = ["2N8w6XJCwZf"] @@ -20,14 +28,13 @@ resource "aws_datazone_project" "test" { description = "desc" skip_deletion_check = true } - ``` ## Argument Reference The following arguments are required: -* `domain_id` - (Required) Identifier of domain which the project is part of. Must follow the regex of ^dzd[-_][a-zA-Z0-9_-]{1,36}$. +* `domain_id` - (Required) Identifier of domain which the project is part of. Must follow the regex of ^dzd[-_][a-zA-Z0-9_-]{1,36}$. * `name` - (Required) Name of the project. Must follow the regex of ^[\w -]+$. and have a length of at most 64. The following arguments are optional: From 41f5c09e1d2e32b0d4180457264af34959a6bcea Mon Sep 17 00:00:00 2001 From: ThomasZalewski Date: Thu, 11 Jul 2024 17:34:41 -0400 Subject: [PATCH 08/17] CI changes. --- website/docs/r/datazone_project.html.markdown | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/docs/r/datazone_project.html.markdown b/website/docs/r/datazone_project.html.markdown index da54a96a1c6..6ac64fb79cb 100644 --- a/website/docs/r/datazone_project.html.markdown +++ b/website/docs/r/datazone_project.html.markdown @@ -13,8 +13,8 @@ Terraform resource for managing an Amazon DataZone Project. ```terraform resource "aws_datazone_project" "test" { - domain_id = aws_datazone_domain.test.id - name = "name" + domain_id = aws_datazone_domain.test.id + name = "name" } ``` From c0bb04b5d3a52f96f7ed6ef49a2d681b4786980a Mon Sep 17 00:00:00 2001 From: ThomasZalewski Date: Thu, 11 Jul 2024 17:59:28 -0400 Subject: [PATCH 09/17] CI documentation changes. --- website/docs/r/datazone_project.html.markdown | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/website/docs/r/datazone_project.html.markdown b/website/docs/r/datazone_project.html.markdown index 6ac64fb79cb..e9d44aa214a 100644 --- a/website/docs/r/datazone_project.html.markdown +++ b/website/docs/r/datazone_project.html.markdown @@ -9,12 +9,15 @@ description: |- Terraform resource for managing an Amazon DataZone Project. -### Basic Usage +## Example Usage ```terraform resource "aws_datazone_project" "test" { - domain_id = aws_datazone_domain.test.id - name = "name" + domain_id = aws_datazone_domain.test.id + glossary_terms = ["2N8w6XJCwZf"] + name = "name" + description = "desc" + skip_deletion_check = true } ``` @@ -22,14 +25,12 @@ resource "aws_datazone_project" "test" { ```terraform resource "aws_datazone_project" "test" { - domain_id = aws_datazone_domain.test.id - glossary_terms = ["2N8w6XJCwZf"] - name = "name" - description = "desc" - skip_deletion_check = true + domain_id = aws_datazone_domain.test.id + name = "name" } ``` + ## Argument Reference The following arguments are required: From 9efe04f557789fc59abd8d18d562c0c63d8958fb Mon Sep 17 00:00:00 2001 From: ThomasZalewski Date: Fri, 12 Jul 2024 09:09:10 -0400 Subject: [PATCH 10/17] Added changelog + website CI test changes. --- .changelog/38345.txt | 3 +++ website/docs/r/datazone_project.html.markdown | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 .changelog/38345.txt diff --git a/.changelog/38345.txt b/.changelog/38345.txt new file mode 100644 index 00000000000..ff9672bd132 --- /dev/null +++ b/.changelog/38345.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_datazone_project +``` \ No newline at end of file diff --git a/website/docs/r/datazone_project.html.markdown b/website/docs/r/datazone_project.html.markdown index e9d44aa214a..1aa6639c57c 100644 --- a/website/docs/r/datazone_project.html.markdown +++ b/website/docs/r/datazone_project.html.markdown @@ -1,7 +1,7 @@ --- subcategory: "DataZone" layout: "aws" -page_title: "Amazon: aws_datazone_project" +page_title: "AWS: aws_datazone_project" description: |- Terraform resource for managing an Amazon DataZone Project. --- @@ -30,7 +30,6 @@ resource "aws_datazone_project" "test" { } ``` - ## Argument Reference The following arguments are required: From d469fe056eb9c6be6132e8a9cc07e9f95ea32d90 Mon Sep 17 00:00:00 2001 From: ThomasZalewski Date: Fri, 12 Jul 2024 09:43:01 -0400 Subject: [PATCH 11/17] CI formatting changes + removed references to update timer. --- internal/service/datazone/project.go | 6 ++---- internal/service/datazone/service_package_gen.go | 4 ++++ website/docs/r/datazone_project.html.markdown | 7 +++---- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/internal/service/datazone/project.go b/internal/service/datazone/project.go index c05af77fd84..690e5e2257e 100644 --- a/internal/service/datazone/project.go +++ b/internal/service/datazone/project.go @@ -40,9 +40,8 @@ import ( func newResourceProject(_ context.Context) (resource.ResourceWithConfigure, error) { r := &resourceProject{} - r.SetDefaultCreateTimeout(3 * time.Minute) - r.SetDefaultUpdateTimeout(30 * time.Minute) - r.SetDefaultDeleteTimeout(3 * time.Minute) + r.SetDefaultCreateTimeout(10 * time.Minute) + r.SetDefaultDeleteTimeout(10 * time.Minute) return r, nil } @@ -135,7 +134,6 @@ func (r *resourceProject) Schema(ctx context.Context, req resource.SchemaRequest Blocks: map[string]schema.Block{ names.AttrTimeouts: timeouts.Block(ctx, timeouts.Opts{ Create: true, - Update: true, Delete: true, }), }, diff --git a/internal/service/datazone/service_package_gen.go b/internal/service/datazone/service_package_gen.go index 7f0e80c975a..a2745588aad 100644 --- a/internal/service/datazone/service_package_gen.go +++ b/internal/service/datazone/service_package_gen.go @@ -36,6 +36,10 @@ func (p *servicePackage) FrameworkResources(ctx context.Context) []*types.Servic Factory: newResourceEnvironmentBlueprintConfiguration, Name: "Environment Blueprint Configuration", }, + { + Factory: newResourceProject, + Name: "Project", + }, } } diff --git a/website/docs/r/datazone_project.html.markdown b/website/docs/r/datazone_project.html.markdown index 1aa6639c57c..d7809dfed38 100644 --- a/website/docs/r/datazone_project.html.markdown +++ b/website/docs/r/datazone_project.html.markdown @@ -7,7 +7,7 @@ description: |- --- # Resource: aws_datazone_project -Terraform resource for managing an Amazon DataZone Project. +Terraform resource for managing an AWS DataZone Project. ## Example Usage @@ -62,9 +62,8 @@ This resource exports the following attributes in addition to the arguments abov [Configuration options](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts): -* `create` - (Default `3m`) -* `update` - (Default `30m`) -* `delete` - (Default `3m`) +* `create` - (Default `10m`) +* `delete` - (Default `10m`) ## Import From a6fdad04407e3f50979f32fe39538af8af74c2d5 Mon Sep 17 00:00:00 2001 From: ThomasZalewski Date: Fri, 12 Jul 2024 10:23:18 -0400 Subject: [PATCH 12/17] Added framwork resource identifier to project.go --- internal/service/datazone/project.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/service/datazone/project.go b/internal/service/datazone/project.go index 690e5e2257e..04bb187a1f2 100644 --- a/internal/service/datazone/project.go +++ b/internal/service/datazone/project.go @@ -38,6 +38,7 @@ import ( "github.com/hashicorp/terraform-provider-aws/names" ) +// @FrameworkResource("aws_datazone_project", name="Project") func newResourceProject(_ context.Context) (resource.ResourceWithConfigure, error) { r := &resourceProject{} r.SetDefaultCreateTimeout(10 * time.Minute) From 405751355b32198f725f3b5a87cef7d839d6a7c2 Mon Sep 17 00:00:00 2001 From: ThomasZalewski Date: Mon, 15 Jul 2024 15:04:26 -0400 Subject: [PATCH 13/17] Update internal/service/datazone/project.go Removed aws.String Co-authored-by: Adrian Johnson --- internal/service/datazone/project.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/service/datazone/project.go b/internal/service/datazone/project.go index 04bb187a1f2..0e7555b4c01 100644 --- a/internal/service/datazone/project.go +++ b/internal/service/datazone/project.go @@ -318,7 +318,7 @@ func (r *resourceProject) Delete(ctx context.Context, req resource.DeleteRequest } in := &datazone.DeleteProjectInput{ - DomainIdentifier: aws.String((*state.DomainId.ValueStringPointer())), + DomainIdentifier: state.DomainId.ValueStringPointer(), Identifier: aws.String((*state.ID.ValueStringPointer())), } if !state.SkipDeletionCheck.IsNull() { From 865c310e6551b5ba017a4d37dae62091c8a9913f Mon Sep 17 00:00:00 2001 From: ThomasZalewski Date: Mon, 15 Jul 2024 16:35:55 -0400 Subject: [PATCH 14/17] Converted manual assignment of inputs to autoflex + documentation checks. --- internal/service/datazone/project.go | 114 ++++++------------ internal/service/datazone/project_test.go | 26 ++-- website/docs/r/datazone_project.html.markdown | 6 +- 3 files changed, 50 insertions(+), 96 deletions(-) diff --git a/internal/service/datazone/project.go b/internal/service/datazone/project.go index 0e7555b4c01..c706fb419a6 100644 --- a/internal/service/datazone/project.go +++ b/internal/service/datazone/project.go @@ -7,7 +7,6 @@ import ( "context" "errors" "fmt" - "reflect" "strings" "time" @@ -68,7 +67,7 @@ func (r *resourceProject) Schema(ctx context.Context, req resource.SchemaRequest stringvalidator.LengthAtMost(2048), }, }, - "domain_id": schema.StringAttribute{ + "domain_identifier": schema.StringAttribute{ Required: true, Validators: []validator.String{ stringvalidator.RegexMatches(regexache.MustCompile(`^dzd[-_][a-zA-Z0-9_-]{1,36}$`), "must conform to: ^dzd[-_][a-zA-Z0-9_-]{1,36}$ "), @@ -149,29 +148,14 @@ func (r *resourceProject) Create(ctx context.Context, req resource.CreateRequest } conn := r.Meta().DataZoneClient(ctx) - var validateDomain datazone.GetDomainInput - validateDomain.Identifier = plan.DomainId.ValueStringPointer() - _, err := conn.GetDomain(ctx, &validateDomain) - if err != nil { - resp.Diagnostics.AddError( - create.ProblemStandardMessage(names.DataZone, create.ErrActionCreating, ResNameProject, plan.Name.String(), err), - err.Error(), - ) - return - } + in := &datazone.CreateProjectInput{} + resp.Diagnostics.Append(flex.Expand(ctx, plan, in)...) - in := &datazone.CreateProjectInput{ - Name: aws.String(plan.Name.ValueString()), - DomainIdentifier: aws.String(plan.DomainId.ValueString()), - } - if !plan.Description.IsNull() { - in.Description = aws.String(plan.Description.ValueString()) - } - if !plan.GlossaryTerms.IsNull() { - in.GlossaryTerms = aws.ToStringSlice(flex.ExpandFrameworkStringList(ctx, plan.GlossaryTerms)) + out, err := conn.CreateProject(ctx, in) + if resp.Diagnostics.HasError() { + return } - out, err := conn.CreateProject(ctx, in) if err != nil { resp.Diagnostics.AddError( create.ProblemStandardMessage(names.DataZone, create.ErrActionCreating, ResNameProject, plan.Name.String(), err), @@ -201,7 +185,7 @@ func (r *resourceProject) Create(ctx context.Context, req resource.CreateRequest return } createTimeout := r.CreateTimeout(ctx, plan.Timeouts) - _, err = waitProjectCreated(ctx, conn, plan.DomainId.ValueString(), plan.ID.ValueString(), createTimeout) + _, err = waitProjectCreated(ctx, conn, plan.DomainIdentifier.ValueString(), plan.ID.ValueString(), createTimeout) if err != nil { resp.Diagnostics.AddError( create.ProblemStandardMessage(names.DataZone, create.ErrActionWaitingForCreation, ResNameProject, plan.Name.String(), err), @@ -221,8 +205,7 @@ func (r *resourceProject) Read(ctx context.Context, req resource.ReadRequest, re return } - //out, err := conn.GetProject(ctx, in) - out, err := findProjectByID(ctx, conn, state.DomainId.ValueString(), state.ID.ValueString()) + out, err := findProjectByID(ctx, conn, state.DomainIdentifier.ValueString(), state.ID.ValueString()) if tfresource.NotFound(err) { resp.State.RemoveResource(ctx) return @@ -252,59 +235,36 @@ func (r *resourceProject) Update(ctx context.Context, req resource.UpdateRequest if resp.Diagnostics.HasError() { return } + if plan.DomainIdentifier.Equal(state.DomainIdentifier) { + in := &datazone.UpdateProjectInput{} + resp.Diagnostics.Append(flex.Expand(ctx, plan, in)...) - if plan.DomainId != state.DomainId { - resp.Diagnostics.AddError( - create.ProblemStandardMessage(names.DataZone, create.ErrActionUpdating, ResNameProject, plan.ID.String(), nil), - errors.New("domain_id should not change with updates").Error(), - ) - return - } - - in := &datazone.UpdateProjectInput{ - DomainIdentifier: aws.String(plan.DomainId.ValueString()), - Identifier: aws.String(plan.ID.ValueString()), - } - - if plan.GlossaryTerms.IsNull() { - if !reflect.DeepEqual(plan.GlossaryTerms, state.GlossaryTerms) { - in.GlossaryTerms = aws.ToStringSlice(flex.ExpandFrameworkStringList(ctx, plan.GlossaryTerms)) + if resp.Diagnostics.HasError() { + return } - } - - if !plan.Description.IsNull() { - if plan.Description.ValueString() != state.Description.ValueString() { - in.Description = aws.String(plan.Description.ValueString()) + in.Identifier = plan.ID.ValueStringPointer() + out, err := conn.UpdateProject(ctx, in) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.DataZone, create.ErrActionUpdating, ResNameProject, plan.ID.String(), err), + err.Error(), + ) + return } - } - - if !plan.Name.IsNull() { - if plan.Name != state.Name { - in.Name = aws.String(plan.Name.ValueString()) + if out == nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.DataZone, create.ErrActionUpdating, ResNameProject, plan.ID.String(), nil), + errors.New("empty output from project update").Error(), + ) + return + } + out.ProjectStatus = "ACTIVE" + resp.Diagnostics.Append(flex.Flatten(ctx, out, &state)...) + if resp.Diagnostics.HasError() { + return } } - out, err := conn.UpdateProject(ctx, in) - if err != nil { - resp.Diagnostics.AddError( - create.ProblemStandardMessage(names.DataZone, create.ErrActionUpdating, ResNameProject, plan.ID.String(), err), - err.Error(), - ) - return - } - if out == nil { - resp.Diagnostics.AddError( - create.ProblemStandardMessage(names.DataZone, create.ErrActionUpdating, ResNameProject, plan.ID.String(), nil), - errors.New("empty output from project update").Error(), - ) - return - } - - out.ProjectStatus = "ACTIVE" - resp.Diagnostics.Append(flex.Flatten(ctx, out, &state)...) - if resp.Diagnostics.HasError() { - return - } resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) } @@ -318,7 +278,7 @@ func (r *resourceProject) Delete(ctx context.Context, req resource.DeleteRequest } in := &datazone.DeleteProjectInput{ - DomainIdentifier: state.DomainId.ValueStringPointer(), + DomainIdentifier: state.DomainIdentifier.ValueStringPointer(), Identifier: aws.String((*state.ID.ValueStringPointer())), } if !state.SkipDeletionCheck.IsNull() { @@ -338,7 +298,7 @@ func (r *resourceProject) Delete(ctx context.Context, req resource.DeleteRequest } deleteTimeout := r.DeleteTimeout(ctx, state.Timeouts) - _, err = waitProjectDeleted(ctx, conn, state.DomainId.ValueString(), state.ID.ValueString(), deleteTimeout) + _, err = waitProjectDeleted(ctx, conn, state.DomainIdentifier.ValueString(), state.ID.ValueString(), deleteTimeout) if err != nil && !errs.IsA[*awstypes.AccessDeniedException](err) { resp.Diagnostics.AddError( @@ -355,7 +315,7 @@ func (r *resourceProject) ImportState(ctx context.Context, req resource.ImportSt if len(parts) != 2 { resp.Diagnostics.AddError("Resource Import Invalid ID", fmt.Sprintf(`Unexpected format for import ID (%s), use: "DomainIdentifier:Id"`, req.ID)) } - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("domain_id"), parts[0])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("domain_identifier"), parts[0])...) resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root(names.AttrID), parts[1])...) } @@ -379,7 +339,7 @@ func waitProjectCreated(ctx context.Context, conn *datazone.Client, domain strin func waitProjectDeleted(ctx context.Context, conn *datazone.Client, domain string, identifier string, timeout time.Duration) (*datazone.GetProjectOutput, error) { stateConf := &retry.StateChangeConf{ - Pending: enum.Slice[awstypes.ProjectStatus](awstypes.ProjectStatusDeleting, awstypes.ProjectStatusActive), // Not too sure about this. + Pending: enum.Slice[awstypes.ProjectStatus](awstypes.ProjectStatusDeleting, awstypes.ProjectStatusActive), Target: []string{}, Refresh: statusProject(ctx, conn, domain, identifier), Timeout: timeout, @@ -433,7 +393,7 @@ func findProjectByID(ctx context.Context, conn *datazone.Client, domain string, type resourceProjectData struct { Description types.String `tfsdk:"description"` - DomainId types.String `tfsdk:"domain_id"` + DomainIdentifier types.String `tfsdk:"domain_identifier"` Name types.String `tfsdk:"name"` CreatedBy types.String `tfsdk:"created_by"` ID types.String `tfsdk:"id"` diff --git a/internal/service/datazone/project_test.go b/internal/service/datazone/project_test.go index 7035a20dad3..88374aaa12f 100644 --- a/internal/service/datazone/project_test.go +++ b/internal/service/datazone/project_test.go @@ -47,7 +47,7 @@ func TestAccDataZoneProject_basic(t *testing.T) { Config: testAccProjectConfig_basic(rName, dName), Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists(ctx, resourceName, &project), - resource.TestCheckResourceAttrPair(resourceName, "domain_id", domainName, names.AttrID), + resource.TestCheckResourceAttrPair(resourceName, "domain_identifier", domainName, names.AttrID), resource.TestCheckResourceAttrSet(resourceName, "glossary_terms.#"), resource.TestCheckResourceAttr(resourceName, names.AttrDescription, "desc"), resource.TestCheckResourceAttr(resourceName, names.AttrName, rName), @@ -55,7 +55,6 @@ func TestAccDataZoneProject_basic(t *testing.T) { resource.TestCheckResourceAttrSet(resourceName, names.AttrID), resource.TestCheckResourceAttrSet(resourceName, names.AttrCreatedAt), resource.TestCheckResourceAttrSet(resourceName, "last_updated_at"), - //resource.TestCheckResourceAttrSet(resourceName, "project_status"), ), }, { @@ -92,7 +91,6 @@ func TestAccDataZoneProject_disappears(t *testing.T) { acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tfdatazone.ResourceProject, resourceName), ), ExpectNonEmptyPlan: true, - //ExpectError: regexache.MustCompile(`(AccessDeniedException)`), }, }, }) @@ -104,7 +102,7 @@ func testAccCheckProjectDestroy(ctx context.Context) resource.TestCheckFunc { if rs.Type != "aws_datazone_project" { continue } - t := rs.Primary.Attributes["domain_id"] + t := rs.Primary.Attributes["domain_identifier"] input := &datazone.GetProjectInput{ DomainIdentifier: &t, @@ -123,7 +121,7 @@ func testAccCheckProjectDestroy(ctx context.Context) resource.TestCheckFunc { continue } _, err := conn.DeleteDomain(ctx, &datazone.DeleteDomainInput{ - Identifier: aws.String(rs.Primary.Attributes["domain_id"]), + Identifier: aws.String(rs.Primary.Attributes["domain_identifier"]), }) if err != nil { @@ -144,10 +142,10 @@ func testAccCheckProjectExists(ctx context.Context, name string, project *datazo if rs.Primary.ID == "" { return create.Error(names.DataZone, create.ErrActionCheckingExistence, tfdatazone.ResNameProject, name, errors.New("not set")) } - if rs.Primary.Attributes["domain_id"] == "" { + if rs.Primary.Attributes["domain_identifier"] == "" { return create.Error(names.DataZone, create.ErrActionCheckingExistence, tfdatazone.ResNameProject, name, errors.New("domain identifier not set")) } - t := rs.Primary.Attributes["domain_id"] + t := rs.Primary.Attributes["domain_identifier"] conn := acctest.Provider.Meta().(*conns.AWSClient).DataZoneClient(ctx) resp, err := conn.GetProject(ctx, &datazone.GetProjectInput{ DomainIdentifier: &t, @@ -197,7 +195,7 @@ func TestAccDataZoneProject_update(t *testing.T) { Config: testAccProjectConfig_basic(pName, dName), Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists(ctx, resourceName, &v1), - resource.TestCheckResourceAttrPair(resourceName, "domain_id", domainName, names.AttrID), + resource.TestCheckResourceAttrPair(resourceName, "domain_identifier", domainName, names.AttrID), resource.TestCheckResourceAttrSet(resourceName, "glossary_terms.#"), resource.TestCheckResourceAttr(resourceName, names.AttrDescription, "desc"), resource.TestCheckResourceAttr(resourceName, names.AttrName, pName), @@ -205,8 +203,6 @@ func TestAccDataZoneProject_update(t *testing.T) { resource.TestCheckResourceAttrSet(resourceName, names.AttrID), resource.TestCheckResourceAttrSet(resourceName, names.AttrCreatedAt), resource.TestCheckResourceAttrSet(resourceName, "last_updated_at"), - //resource.TestCheckResourceAttrSet(resourceName, "project_status"), - //resource.TestCheckResourceAttrSet(resourceName, "result_metadata"), ), }, { @@ -221,7 +217,7 @@ func TestAccDataZoneProject_update(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists(ctx, resourceName, &v2), testAccCheckProjectNotRecreated(&v1, &v2), - resource.TestCheckResourceAttrPair(resourceName, "domain_id", domainName, names.AttrID), + resource.TestCheckResourceAttrPair(resourceName, "domain_identifier", domainName, names.AttrID), resource.TestCheckResourceAttrSet(resourceName, "glossary_terms.#"), resource.TestCheckResourceAttr(resourceName, names.AttrDescription, names.AttrDescription), resource.TestCheckResourceAttr(resourceName, names.AttrName, pName), @@ -229,8 +225,6 @@ func TestAccDataZoneProject_update(t *testing.T) { resource.TestCheckResourceAttrSet(resourceName, names.AttrID), resource.TestCheckResourceAttrSet(resourceName, names.AttrCreatedAt), resource.TestCheckResourceAttrSet(resourceName, "last_updated_at"), - //resource.TestCheckResourceAttrSet(resourceName, "project_status"), - //resource.TestCheckResourceAttrSet(resourceName, "result_metadata"), ), }, { @@ -251,7 +245,7 @@ func testAccAuthorizerImportStateIdFunc(resourceName string) resource.ImportStat return "", fmt.Errorf("Not found: %s", resourceName) } - return fmt.Sprintf("%s:%s", rs.Primary.Attributes["domain_id"], rs.Primary.ID), nil + return fmt.Sprintf("%s:%s", rs.Primary.Attributes["domain_identifier"], rs.Primary.ID), nil } } @@ -262,7 +256,7 @@ resource "aws_security_group" "test" { } resource "aws_datazone_project" "test" { - domain_id = aws_datazone_domain.test.id + domain_identifier = aws_datazone_domain.test.id glossary_terms = ["2N8w6XJCwZf"] name = %[1]q description = "desc" @@ -277,7 +271,7 @@ resource "aws_security_group" "test" { } resource "aws_datazone_project" "test" { - domain_id = aws_datazone_domain.test.id + domain_identifier = aws_datazone_domain.test.id glossary_terms = ["2N8w6XJCwZf"] name = %[1]q description = "description" diff --git a/website/docs/r/datazone_project.html.markdown b/website/docs/r/datazone_project.html.markdown index d7809dfed38..0a2880090df 100644 --- a/website/docs/r/datazone_project.html.markdown +++ b/website/docs/r/datazone_project.html.markdown @@ -25,8 +25,8 @@ resource "aws_datazone_project" "test" { ```terraform resource "aws_datazone_project" "test" { - domain_id = aws_datazone_domain.test.id - name = "name" + domain_identifier = aws_datazone_domain.test.id + name = "name" } ``` @@ -34,7 +34,7 @@ resource "aws_datazone_project" "test" { The following arguments are required: -* `domain_id` - (Required) Identifier of domain which the project is part of. Must follow the regex of ^dzd[-_][a-zA-Z0-9_-]{1,36}$. +* `domain_identifier` - (Required) Identifier of domain which the project is part of. Must follow the regex of ^dzd[-_][a-zA-Z0-9_-]{1,36}$. * `name` - (Required) Name of the project. Must follow the regex of ^[\w -]+$. and have a length of at most 64. The following arguments are optional: From ed31441c577fdfee5ef23dbf3347c277d67e7c72 Mon Sep 17 00:00:00 2001 From: ThomasZalewski Date: Tue, 16 Jul 2024 10:52:39 -0400 Subject: [PATCH 15/17] Added error checking for flex.Expand. Co-authored-by: Adrian Johnson --- internal/service/datazone/project.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/service/datazone/project.go b/internal/service/datazone/project.go index c706fb419a6..4333938b563 100644 --- a/internal/service/datazone/project.go +++ b/internal/service/datazone/project.go @@ -150,6 +150,10 @@ func (r *resourceProject) Create(ctx context.Context, req resource.CreateRequest in := &datazone.CreateProjectInput{} resp.Diagnostics.Append(flex.Expand(ctx, plan, in)...) + + if resp.Diagnostics.HasError() { + return + } out, err := conn.CreateProject(ctx, in) if resp.Diagnostics.HasError() { From 61fdc6789e56775cbf36ac25dc8aae0e2f075ba6 Mon Sep 17 00:00:00 2001 From: ThomasZalewski Date: Tue, 16 Jul 2024 10:59:44 -0400 Subject: [PATCH 16/17] Changed updating restriction to check for config changes Co-authored-by: Adrian Johnson --- internal/service/datazone/project.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/service/datazone/project.go b/internal/service/datazone/project.go index 4333938b563..cb318b2719e 100644 --- a/internal/service/datazone/project.go +++ b/internal/service/datazone/project.go @@ -239,7 +239,7 @@ func (r *resourceProject) Update(ctx context.Context, req resource.UpdateRequest if resp.Diagnostics.HasError() { return } - if plan.DomainIdentifier.Equal(state.DomainIdentifier) { + if !plan.Description.Equal(state.Description) || !plan.GlossaryTerms.Equal(state.GlossaryTerms) || !plan.Name.Equal(state.Name) { in := &datazone.UpdateProjectInput{} resp.Diagnostics.Append(flex.Expand(ctx, plan, in)...) From 141fe2b408516ca1d929eb27bce8cd36dabbb7d7 Mon Sep 17 00:00:00 2001 From: ThomasZalewski Date: Tue, 16 Jul 2024 11:14:48 -0400 Subject: [PATCH 17/17] CI formatting changes --- internal/service/datazone/project.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/service/datazone/project.go b/internal/service/datazone/project.go index cb318b2719e..5f627d453d7 100644 --- a/internal/service/datazone/project.go +++ b/internal/service/datazone/project.go @@ -150,7 +150,7 @@ func (r *resourceProject) Create(ctx context.Context, req resource.CreateRequest in := &datazone.CreateProjectInput{} resp.Diagnostics.Append(flex.Expand(ctx, plan, in)...) - + if resp.Diagnostics.HasError() { return }