From ee1e760ec54e01363fd4b8f005b7b2106bee9405 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Dexemple?= Date: Sat, 20 Jan 2024 18:37:37 +0100 Subject: [PATCH 01/15] feat: add aws_verifiedpermissions_policy --- .../service/verifiedpermissions/policy.go | 853 ++++++++++++++++++ .../verifiedpermissions/policy_test.go | 333 +++++++ .../verifiedpermissions_policy.html.markdown | 69 ++ 3 files changed, 1255 insertions(+) create mode 100644 internal/service/verifiedpermissions/policy.go create mode 100644 internal/service/verifiedpermissions/policy_test.go create mode 100644 website/docs/r/verifiedpermissions_policy.html.markdown diff --git a/internal/service/verifiedpermissions/policy.go b/internal/service/verifiedpermissions/policy.go new file mode 100644 index 000000000000..4af88d1e0a98 --- /dev/null +++ b/internal/service/verifiedpermissions/policy.go @@ -0,0 +1,853 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package verifiedpermissions + +import ( + "context" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/verifiedpermissions" + awstypes "github.com/aws/aws-sdk-go-v2/service/verifiedpermissions/types" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework-validators/objectvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "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/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-provider-aws/internal/create" + "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" + "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(name="Policy") +func newResourcePolicy(_ context.Context) (resource.ResourceWithConfigure, error) { + r := &resourcePolicy{} + + return r, nil +} + +const ( + ResNamePolicy = "Policy" +) + +type resourcePolicy struct { + framework.ResourceWithConfigure + framework.WithTimeouts +} + +func (r *resourcePolicy) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "aws_verifiedpermissions_policy" +} + +// 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 *resourcePolicy) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "created_date": schema.StringAttribute{ + Computed: true, + }, + "last_updated_date": schema.StringAttribute{ + Computed: true, + }, + "policy_id": framework.IDAttribute(), + "policy_store_id": schema.StringAttribute{ + Required: true, + }, + "policy_type": schema.StringAttribute{ + Required: true, // TODO + }, + "principal": schema.StringAttribute{ + Optional: true, + }, + "resource": schema.StringAttribute{ + Optional: true, + }, + }, + Blocks: map[string]schema.Block{ + //"complex_argument": schema.ListNestedBlock{ + // // TIP: ==== LIST VALIDATORS ==== + // // List and set validators take the place of MaxItems and MinItems in + // // Plugin-Framework based resources. Use listvalidator.SizeAtLeast(1) to + // // make a nested object required. Similar to Plugin-SDK, complex objects + // // can be represented as lists or sets with listvalidator.SizeAtMost(1). + // // + // // For a complete mapping of Plugin-SDK to Plugin-Framework schema fields, + // // see: + // // https://developer.hashicorp.com/terraform/plugin/framework/migrating/attributes-blocks/blocks + // Validators: []validator.List{ + // listvalidator.SizeAtMost(1), + // }, + // NestedObject: schema.NestedBlockObject{ + // Attributes: map[string]schema.Attribute{ + // "nested_required": schema.StringAttribute{ + // Required: true, + // }, + // "nested_computed": schema.StringAttribute{ + // Computed: true, + // PlanModifiers: []planmodifier.String{ + // stringplanmodifier.UseStateForUnknown(), + // }, + // }, + // }, + // }, + //}, + "definition": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "static": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "description": schema.StringAttribute{ + Required: true, + }, + "statement": schema.StringAttribute{ + Required: true, + }, + }, + Validators: []validator.Object{ + // Validate only this attribute or other_attr is configured. + objectvalidator.ExactlyOneOf(path.Expressions{ + path.MatchRoot("template_linked"), + }...), + }, + }, + }, + "template_linked": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "policy_template_id": schema.StringAttribute{ + Required: true, + }, + }, + Blocks: map[string]schema.Block{ + "principal": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "entity_id": schema.StringAttribute{ + Required: true, + }, + "entity_type": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + "resource": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "entity_id": schema.StringAttribute{ + Required: true, + }, + "entity_type": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + }, + Validators: []validator.Object{ + // Validate only this attribute or other_attr is configured. + objectvalidator.ExactlyOneOf(path.Expressions{ + path.MatchRoot("static"), + }...), + }, + }, + }, + }, + }, + }, + "timeouts": timeouts.Block(ctx, timeouts.Opts{ + Create: true, + Update: true, + Delete: true, + }), + }, + } +} + +func (r *resourcePolicy) 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().VerifiedPermissionsClient(ctx) + + // TIP: -- 2. Fetch the plan + var plan resourcePolicyData + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + // TIP: -- 3. Populate a create input structure + in := &verifiedpermissions.CreatePolicyInput{ + // TIP: Mandatory or fields that will always be present can be set when + // you create the Input structure. (Replace these with real fields.) + //TODO: F PolicyName: aws.String(plan.Name.ValueString()), + //TODO: F/PolicyType: aws.String(plan.Type.ValueString()), + } + + resp.Diagnostics.Append(flex.Expand(ctx, plan, in)...) + + if resp.Diagnostics.HasError() { + return + } + + clientToken := id.UniqueId() + in.ClientToken = aws.String(clientToken) + + //if !plan.Description.IsNull() { + // // TIP: Optional fields should be set based on whether or not they are + // // used. + // //in.Description = aws.String(plan.Description.ValueString()) + //} + //if !plan.ComplexArgument.IsNull() { + // // TIP: Use an expander to assign a complex argument. The elements must be + // // deserialized into the appropriate struct before being passed to the expander. + // var tfList []complexArgumentData + // resp.Diagnostics.Append(plan.ComplexArgument.ElementsAs(ctx, &tfList, false)...) + // if resp.Diagnostics.HasError() { + // return + // } + // + // //TODO: F in.ComplexArgument = expandComplexArgument(tfList) + //} + + // TIP: -- 4. Call the AWS create function + out, err := conn.CreatePolicy(ctx, in) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.VerifiedPermissions, create.ErrActionCreating, ResNamePolicy, clientToken, err), + err.Error(), + ) + return + } + //if out == nil || out.Policy == nil { + // resp.Diagnostics.AddError( + // create.ProblemStandardMessage(names.VerifiedPermissions, create.ErrActionCreating, ResNamePolicy, plan.Name.String(), nil), + // errors.New("empty output").Error(), + // ) + // return + //} + + // TIP: -- 5. Using the output from the create function, set the minimum attributes + plan.ID = flex.StringToFramework(ctx, out.PolicyId) + + // TODO F : Think something like that is needed + //response.Diagnostics.Append(flex.Flatten(ctx, output, &state)...) + + // TIP: -- 7. Save the request plan to response state + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} + +func (r *resourcePolicy) 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().VerifiedPermissionsClient(ctx) + + // TIP: -- 2. Fetch the state + var state resourcePolicyData + 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. + out, err := findPolicyByID(ctx, conn, state.ID.ValueString()) + // 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.VerifiedPermissions, create.ErrActionSetting, ResNamePolicy, state.ID.String(), err), + err.Error(), + ) + return + } + + // TIP: -- 5. Set the arguments and attributes + // + // For simple data types (i.e., schema.StringAttribute, schema.BoolAttribute, + // schema.Int64Attribute, and schema.Float64Attribue), simply setting the + // appropriate data struct field is sufficient. The flex package implements + // helpers for converting between Go and Plugin-Framework types seamlessly. No + // error or nil checking is necessary. + // + // However, there are some situations where more handling is needed such as + // complex data types (e.g., schema.ListAttribute, schema.SetAttribute). In + // these cases the flatten function may have a diagnostics return value, which + // should be appended to resp.Diagnostics. + //state.ARN = flex.StringToFramework(ctx, out.Arn) + //state.ID = flex.StringToFramework(ctx, out.PolicyId) + //state.Name = flex.StringToFramework(ctx, out.PolicyName) + //state.Type = flex.StringToFramework(ctx, out.PolicyType) + + // TIP: Setting a complex type. + //complexArgument, d := flattenComplexArgument(ctx, out.ComplexArgument) + //resp.Diagnostics.Append(d...) + //state.ComplexArgument = complexArgument + + // TIP: -- 6. Set the state + resp.Diagnostics.Append(flex.Flatten(ctx, out, &state)...) + + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *resourcePolicy) 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().VerifiedPermissionsClient(ctx) + + // TIP: -- 2. Fetch the plan + var plan, state resourcePolicyData + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + // 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 := &verifiedpermissions.UpdatePolicyInput{ + // TIP: Mandatory or fields that will always be present can be set when + // you create the Input structure. (Replace these with real fields.) + //PolicyId: aws.String(plan.ID.ValueString()), + //PolicyName: aws.String(plan.Name.ValueString()), + //PolicyType: aws.String(plan.Type.ValueString()), + } + resp.Diagnostics.Append(flex.Expand(ctx, plan, in)...) + if resp.Diagnostics.HasError() { + return + } + //if !plan.Description.IsNull() { + // // TIP: Optional fields should be set based on whether or not they are + // // used. + // in.Description = aws.String(plan.Description.ValueString()) + //} + //if !plan.ComplexArgument.IsNull() { + // // TIP: Use an expander to assign a complex argument. The elements must be + // // deserialized into the appropriate struct before being passed to the expander. + // var tfList []complexArgumentData + // resp.Diagnostics.Append(plan.ComplexArgument.ElementsAs(ctx, &tfList, false)...) + // if resp.Diagnostics.HasError() { + // return + // } + // + // in.ComplexArgument = expandComplexArgument(tfList) + //} + + // TIP: -- 4. Call the AWS modify/update function + out, err := conn.UpdatePolicy(ctx, in) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.VerifiedPermissions, create.ErrActionUpdating, ResNamePolicy, plan.ID.String(), err), + err.Error(), + ) + return + } + //if out == nil || out.Policy == nil { + // resp.Diagnostics.AddError( + // create.ProblemStandardMessage(names.VerifiedPermissions, create.ErrActionUpdating, ResNamePolicy, plan.ID.String(), nil), + // errors.New("empty output").Error(), + // ) + // return + //} + + // TIP: Using the output from the update function, re-set any computed attributes + //plan.ARN = flex.StringToFramework(ctx, out.Policy.Arn) + //plan.ID = flex.StringToFramework(ctx, out.Policy.PolicyId) + resp.Diagnostics.Append(flex.Flatten(ctx, out, &plan)...) + } + + // TIP: -- 5. Use a waiter to wait for update to complete + //updateTimeout := r.UpdateTimeout(ctx, plan.Timeouts) + //_, err := waitPolicyUpdated(ctx, conn, plan.ID.ValueString(), updateTimeout) + //if err != nil { + // resp.Diagnostics.AddError( + // create.ProblemStandardMessage(names.VerifiedPermissions, create.ErrActionWaitingForUpdate, ResNamePolicy, 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 *resourcePolicy) 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().VerifiedPermissionsClient(ctx) + + // TIP: -- 2. Fetch the state + var state resourcePolicyData + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + // TIP: -- 3. Populate a delete input structure + in := &verifiedpermissions.DeletePolicyInput{ + PolicyId: aws.String(state.ID.ValueString()), + } + + // TIP: -- 4. Call the AWS delete function + _, err := conn.DeletePolicy(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.VerifiedPermissions, create.ErrActionDeleting, ResNamePolicy, state.ID.String(), err), + err.Error(), + ) + return + } + + // TIP: -- 5. Use a waiter to wait for delete to complete + //deleteTimeout := r.DeleteTimeout(ctx, state.Timeouts) + //_, err = waitPolicyDeleted(ctx, conn, state.ID.ValueString(), deleteTimeout) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.VerifiedPermissions, create.ErrActionWaitingForDeletion, ResNamePolicy, 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 *resourcePolicy) 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" +// statusDeleting = "Deleting" +// statusNormal = "Normal" +// 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 waitPolicyCreated(ctx context.Context, conn *verifiedpermissions.Client, id string, timeout time.Duration) (*awstypes.PolicyDefinitionItem, error) { +// stateConf := &retry.StateChangeConf{ +// Pending: []string{}, +// Target: []string{statusNormal}, +// Refresh: statusPolicy(ctx, conn, id), +// Timeout: timeout, +// NotFoundChecks: 20, +// ContinuousTargetOccurence: 2, +// } +// +// outputRaw, err := stateConf.WaitForStateContext(ctx) +// if out, ok := outputRaw.(*verifiedpermissions.CreatePolicyOutput); 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 waitPolicyUpdated(ctx context.Context, conn *verifiedpermissions.Client, id string, timeout time.Duration) (*awstypes.Policy, error) { +// stateConf := &retry.StateChangeConf{ +// Pending: []string{statusChangePending}, +// Target: []string{statusUpdated}, +// Refresh: statusPolicy(ctx, conn, id), +// Timeout: timeout, +// NotFoundChecks: 20, +// ContinuousTargetOccurence: 2, +// } +// +// outputRaw, err := stateConf.WaitForStateContext(ctx) +// if out, ok := outputRaw.(*verifiedpermissions.Policy); 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 waitPolicyDeleted(ctx context.Context, conn *verifiedpermissions.Client, id string, timeout time.Duration) (*awstypes.Policy, error) { +// stateConf := &retry.StateChangeConf{ +// Pending: []string{statusDeleting, statusNormal}, +// Target: []string{}, +// Refresh: statusPolicy(ctx, conn, id), +// Timeout: timeout, +// } +// +// outputRaw, err := stateConf.WaitForStateContext(ctx) +// if out, ok := outputRaw.(*verifiedpermissions.Policy); ok { +// return out, err +// } +// +// return nil, err +// } +// +// // TIP: ==== STATUS ==== +// // The status function can return an actual status when that field is +// // available from the API (e.g., out.Status). Otherwise, you can use custom +// // statuses to communicate the states of the resource. +// // +// // Waiters consume the values returned by status functions. Design status so +// // that it can be reused by a create, update, and delete waiter, if possible. +// +// func statusPolicy(ctx context.Context, conn *verifiedpermissions.Client, id string) retry.StateRefreshFunc { +// return func() (interface{}, string, error) { +// out, err := findPolicyByID(ctx, conn, id) +// if tfresource.NotFound(err) { +// return nil, "", nil +// } +// +// if err != nil { +// return nil, "", err +// } +// +// return out, aws.ToString(out.Status), nil +// } +// } +// +// TIP: ==== FINDERS ==== +// The find function is not strictly necessary. You could do the API +// request from the status function. However, we have found that find often +// comes in handy in other places besides the status function. As a result, it +// is good practice to define it separately. +func findPolicyByID(ctx context.Context, conn *verifiedpermissions.Client, id string) (*verifiedpermissions.GetPolicyOutput, error) { + in := &verifiedpermissions.GetPolicyInput{ + PolicyId: aws.String(id), + } + + out, err := conn.GetPolicy(ctx, in) + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: in, + } + } + if err != nil { + return nil, err + } + + if out == nil || out.PolicyId == 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: Remember, as mentioned above, 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 expandComplexArgument(tfList []complexArgumentData) *awstypes.ComplexArgument { +// if len(tfList) == 0 { +// return nil +// } +// +// tfObj := tfList[0] +// apiObject := &awstypes.ComplexArgument{ +// NestedRequired: aws.String(tfObj.NestedRequired.ValueString()), +// } +// if !tfObj.NestedOptional.IsNull() { +// apiObject.NestedOptional = aws.String(tfObj.NestedOptional.ValueString()) +// } +// +// return apiObject +//} +// +//// TIP: Even when you have a list with max length of 1, this plural function +//// works brilliantly. However, if the AWS API takes a structure rather than a +//// slice of structures, you will not need it. +//func expandComplexArguments(tfList []complexArgumentData) []*verifiedpermissions.ComplexArgument { +// // TIP: The AWS API can be picky about whether you send a nil or zero- +// // length for an argument that should be cleared. For example, in some +// // cases, if you send a nil value, the AWS API interprets that as "make no +// // changes" when what you want to say is "remove everything." Sometimes +// // using a zero-length list will cause an error. +// // +// // As a result, here are two options. Usually, option 1, nil, will work as +// // expected, clearing the field. But, test going from something to nothing +// // to make sure it works. If not, try the second option. +// // TIP: Option 1: Returning nil for zero-length list +// if len(tfList) == 0 { +// return nil +// } +// var apiObject []*awstypes.ComplexArgument +// // TIP: Option 2: Return zero-length list for zero-length list. If option 1 does +// // not work, after testing going from something to nothing (if that is +// // possible), uncomment out the next line and remove option 1. +// // +// // apiObject := make([]*verifiedpermissions.ComplexArgument, 0) +// +// for _, tfObj := range tfList { +// item := &verifiedpermissions.ComplexArgument{ +// NestedRequired: aws.String(tfObj.NestedRequired.ValueString()), +// } +// if !tfObj.NestedOptional.IsNull() { +// item.NestedOptional = aws.String(tfObj.NestedOptional.ValueString()) +// } +// +// apiObject = append(apiObject, item) +// } +// +// return apiObject +//} + +// 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 resourcePolicyData struct { + ARN types.String `tfsdk:"arn"` + ComplexArgument types.List `tfsdk:"complex_argument"` + Description types.String `tfsdk:"description"` + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Timeouts timeouts.Value `tfsdk:"timeouts"` + Type types.String `tfsdk:"type"` +} + +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/verifiedpermissions/policy_test.go b/internal/service/verifiedpermissions/policy_test.go new file mode 100644 index 000000000000..e5b36eb98ece --- /dev/null +++ b/internal/service/verifiedpermissions/policy_test.go @@ -0,0 +1,333 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package verifiedpermissions_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/verifiedpermissions/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/verifiedpermissions" + "github.com/aws/aws-sdk-go-v2/service/verifiedpermissions/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. + tfverifiedpermissions "github.com/hashicorp/terraform-provider-aws/internal/service/verifiedpermissions" +) + +// TIP: File Structure. The basic outline for all test files should be as +// follows. Improve this resource's maintainability by following this +// outline. +// +// 1. Package declaration (add "_test" since this is a test file) +// 2. Imports +// 3. Unit tests +// 4. Basic test +// 5. Disappears test +// 6. All the other tests +// 7. Helper functions (exists, destroy, check, etc.) +// 8. Functions that return Terraform configurations + +// TIP: ==== UNIT TESTS ==== +// This is an example of a unit test. Its name is not prefixed with +// "TestAcc" like an acceptance test. +// +// Unlike acceptance tests, unit tests do not access AWS and are focused on a +// function (or method). Because of this, they are quick and cheap to run. +// +// In designing a resource's implementation, isolate complex bits from AWS bits +// so that they can be tested through a unit test. We encourage more unit tests +// in the provider. +// +// Cut and dry functions using well-used patterns, like typical flatteners and +// expanders, don't need unit testing. However, if they are complex or +// intricate, they should be unit tested. +func TestPolicyExampleUnitTest(t *testing.T) { + t.Parallel() + + testCases := []struct { + TestName string + Input string + Expected string + Error bool + }{ + { + TestName: "empty", + Input: "", + Expected: "", + Error: true, + }, + { + TestName: "descriptive name", + Input: "some input", + Expected: "some output", + Error: false, + }, + { + TestName: "another descriptive name", + Input: "more input", + Expected: "more output", + Error: false, + }, + } + + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.TestName, func(t *testing.T) { + t.Parallel() + got, err := tfverifiedpermissions.FunctionFromResource(testCase.Input) + + if err != nil && !testCase.Error { + t.Errorf("got error (%s), expected no error", err) + } + + if err == nil && testCase.Error { + t.Errorf("got (%s) and no error, expected error", got) + } + + if got != testCase.Expected { + t.Errorf("got %s, expected %s", got, testCase.Expected) + } + }) + } +} + +// TIP: ==== ACCEPTANCE TESTS ==== +// This is an example of a basic acceptance test. This should test as much of +// standard functionality of the resource as possible, and test importing, if +// applicable. We prefix its name with "TestAcc", the service, and the +// resource name. +// +// Acceptance test access AWS and cost money to run. +func TestAccVerifiedPermissionsPolicy_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 policy verifiedpermissions.DescribePolicyResponse + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_verifiedpermissions_policy.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.VerifiedPermissionsEndpointID) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.VerifiedPermissionsEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckPolicyDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccPolicyConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckPolicyExists(ctx, resourceName, &policy), + 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", "verifiedpermissions", regexache.MustCompile(`policy:+.`)), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"apply_immediately", "user"}, + }, + }, + }) +} + +func TestAccVerifiedPermissionsPolicy_disappears(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var policy verifiedpermissions.DescribePolicyResponse + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_verifiedpermissions_policy.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.VerifiedPermissionsEndpointID) + testAccPreCheck(t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.VerifiedPermissionsEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckPolicyDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccPolicyConfig_basic(rName, testAccPolicyVersionNewer), + Check: resource.ComposeTestCheckFunc( + testAccCheckPolicyExists(ctx, resourceName, &policy), + // 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 ResourcePolicy = newResourcePolicy + acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tfverifiedpermissions.ResourcePolicy, resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckPolicyDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).VerifiedPermissionsClient(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_verifiedpermissions_policy" { + continue + } + + input := &verifiedpermissions.DescribePolicyInput{ + PolicyId: aws.String(rs.Primary.ID), + } + _, err := conn.DescribePolicy(ctx, &verifiedpermissions.DescribePolicyInput{ + PolicyId: aws.String(rs.Primary.ID), + }) + if errs.IsA[*types.ResourceNotFoundException](err) { + return nil + } + if err != nil { + return create.Error(names.VerifiedPermissions, create.ErrActionCheckingDestroyed, tfverifiedpermissions.ResNamePolicy, rs.Primary.ID, err) + } + + return create.Error(names.VerifiedPermissions, create.ErrActionCheckingDestroyed, tfverifiedpermissions.ResNamePolicy, rs.Primary.ID, errors.New("not destroyed")) + } + + return nil + } +} + +func testAccCheckPolicyExists(ctx context.Context, name string, policy *verifiedpermissions.DescribePolicyResponse) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return create.Error(names.VerifiedPermissions, create.ErrActionCheckingExistence, tfverifiedpermissions.ResNamePolicy, name, errors.New("not found")) + } + + if rs.Primary.ID == "" { + return create.Error(names.VerifiedPermissions, create.ErrActionCheckingExistence, tfverifiedpermissions.ResNamePolicy, name, errors.New("not set")) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).VerifiedPermissionsClient(ctx) + resp, err := conn.DescribePolicy(ctx, &verifiedpermissions.DescribePolicyInput{ + PolicyId: aws.String(rs.Primary.ID), + }) + + if err != nil { + return create.Error(names.VerifiedPermissions, create.ErrActionCheckingExistence, tfverifiedpermissions.ResNamePolicy, rs.Primary.ID, err) + } + + *policy = *resp + + return nil + } +} + +func testAccPreCheck(ctx context.Context, t *testing.T) { + conn := acctest.Provider.Meta().(*conns.AWSClient).VerifiedPermissionsClient(ctx) + + input := &verifiedpermissions.ListPolicysInput{} + _, err := conn.ListPolicys(ctx, input) + + if acctest.PreCheckSkipError(err) { + t.Skipf("skipping acceptance testing: %s", err) + } + if err != nil { + t.Fatalf("unexpected PreCheck error: %s", err) + } +} + +func testAccCheckPolicyNotRecreated(before, after *verifiedpermissions.DescribePolicyResponse) resource.TestCheckFunc { + return func(s *terraform.State) error { + if before, after := aws.ToString(before.PolicyId), aws.ToString(after.PolicyId); before != after { + return create.Error(names.VerifiedPermissions, create.ErrActionCheckingNotRecreated, tfverifiedpermissions.ResNamePolicy, aws.ToString(before.PolicyId), errors.New("recreated")) + } + + return nil + } +} + +func testAccPolicyConfig_basic(rName, version string) string { + return fmt.Sprintf(` +resource "aws_security_group" "test" { + name = %[1]q +} + +resource "aws_verifiedpermissions_policy" "test" { + policy_name = %[1]q + engine_type = "ActiveVerifiedPermissions" + engine_version = %[2]q + host_instance_type = "verifiedpermissions.t2.micro" + security_groups = [aws_security_group.test.id] + authentication_strategy = "simple" + storage_type = "efs" + + logs { + general = true + } + + user { + username = "Test" + password = "TestTest1234" + } +} +`, rName, version) +} diff --git a/website/docs/r/verifiedpermissions_policy.html.markdown b/website/docs/r/verifiedpermissions_policy.html.markdown new file mode 100644 index 000000000000..4e80b3efdc87 --- /dev/null +++ b/website/docs/r/verifiedpermissions_policy.html.markdown @@ -0,0 +1,69 @@ +--- +subcategory: "Verified Permissions" +layout: "aws" +page_title: "AWS: aws_verifiedpermissions_policy" +description: |- + Terraform resource for managing an AWS Verified Permissions Policy. +--- +` +# Resource: aws_verifiedpermissions_policy + +Terraform resource for managing an AWS Verified Permissions Policy. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_verifiedpermissions_policy" "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 Policy. 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 Verified Permissions Policy using the `example_id_arg`. For example: + +```terraform +import { + to = aws_verifiedpermissions_policy.example + id = "policy-id-12345678" +} +``` + +Using `terraform import`, import Verified Permissions Policy using the `example_id_arg`. For example: + +```console +% terraform import aws_verifiedpermissions_policy.example policy-id-12345678 +``` From 8889d121a4ab3cb4b58ccd6e93f2009518c0588c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Dexemple?= Date: Sat, 20 Jan 2024 18:46:12 +0100 Subject: [PATCH 02/15] feat: add aws_verifiedpermissions_policy --- .../service/verifiedpermissions/policy.go | 538 +----------------- 1 file changed, 5 insertions(+), 533 deletions(-) diff --git a/internal/service/verifiedpermissions/policy.go b/internal/service/verifiedpermissions/policy.go index 4af88d1e0a98..1d89b188494a 100644 --- a/internal/service/verifiedpermissions/policy.go +++ b/internal/service/verifiedpermissions/policy.go @@ -26,17 +26,6 @@ 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(name="Policy") func newResourcePolicy(_ context.Context) (resource.ResourceWithConfigure, error) { r := &resourcePolicy{} @@ -57,50 +46,6 @@ func (r *resourcePolicy) Metadata(_ context.Context, req resource.MetadataReques resp.TypeName = "aws_verifiedpermissions_policy" } -// 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 *resourcePolicy) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ Attributes: map[string]schema.Attribute{ @@ -125,33 +70,6 @@ func (r *resourcePolicy) Schema(ctx context.Context, req resource.SchemaRequest, }, }, Blocks: map[string]schema.Block{ - //"complex_argument": schema.ListNestedBlock{ - // // TIP: ==== LIST VALIDATORS ==== - // // List and set validators take the place of MaxItems and MinItems in - // // Plugin-Framework based resources. Use listvalidator.SizeAtLeast(1) to - // // make a nested object required. Similar to Plugin-SDK, complex objects - // // can be represented as lists or sets with listvalidator.SizeAtMost(1). - // // - // // For a complete mapping of Plugin-SDK to Plugin-Framework schema fields, - // // see: - // // https://developer.hashicorp.com/terraform/plugin/framework/migrating/attributes-blocks/blocks - // Validators: []validator.List{ - // listvalidator.SizeAtMost(1), - // }, - // NestedObject: schema.NestedBlockObject{ - // Attributes: map[string]schema.Attribute{ - // "nested_required": schema.StringAttribute{ - // Required: true, - // }, - // "nested_computed": schema.StringAttribute{ - // Computed: true, - // PlanModifiers: []planmodifier.String{ - // stringplanmodifier.UseStateForUnknown(), - // }, - // }, - // }, - // }, - //}, "definition": schema.SetNestedBlock{ NestedObject: schema.NestedBlockObject{ Blocks: map[string]schema.Block{ @@ -166,7 +84,6 @@ func (r *resourcePolicy) Schema(ctx context.Context, req resource.SchemaRequest, }, }, Validators: []validator.Object{ - // Validate only this attribute or other_attr is configured. objectvalidator.ExactlyOneOf(path.Expressions{ path.MatchRoot("template_linked"), }...), @@ -207,7 +124,6 @@ func (r *resourcePolicy) Schema(ctx context.Context, req resource.SchemaRequest, }, }, Validators: []validator.Object{ - // Validate only this attribute or other_attr is configured. objectvalidator.ExactlyOneOf(path.Expressions{ path.MatchRoot("static"), }...), @@ -227,37 +143,15 @@ func (r *resourcePolicy) Schema(ctx context.Context, req resource.SchemaRequest, } func (r *resourcePolicy) 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().VerifiedPermissionsClient(ctx) - // TIP: -- 2. Fetch the plan var plan resourcePolicyData resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) if resp.Diagnostics.HasError() { return } - // TIP: -- 3. Populate a create input structure - in := &verifiedpermissions.CreatePolicyInput{ - // TIP: Mandatory or fields that will always be present can be set when - // you create the Input structure. (Replace these with real fields.) - //TODO: F PolicyName: aws.String(plan.Name.ValueString()), - //TODO: F/PolicyType: aws.String(plan.Type.ValueString()), - } + in := &verifiedpermissions.CreatePolicyInput{} resp.Diagnostics.Append(flex.Expand(ctx, plan, in)...) @@ -268,24 +162,6 @@ func (r *resourcePolicy) Create(ctx context.Context, req resource.CreateRequest, clientToken := id.UniqueId() in.ClientToken = aws.String(clientToken) - //if !plan.Description.IsNull() { - // // TIP: Optional fields should be set based on whether or not they are - // // used. - // //in.Description = aws.String(plan.Description.ValueString()) - //} - //if !plan.ComplexArgument.IsNull() { - // // TIP: Use an expander to assign a complex argument. The elements must be - // // deserialized into the appropriate struct before being passed to the expander. - // var tfList []complexArgumentData - // resp.Diagnostics.Append(plan.ComplexArgument.ElementsAs(ctx, &tfList, false)...) - // if resp.Diagnostics.HasError() { - // return - // } - // - // //TODO: F in.ComplexArgument = expandComplexArgument(tfList) - //} - - // TIP: -- 4. Call the AWS create function out, err := conn.CreatePolicy(ctx, in) if err != nil { resp.Diagnostics.AddError( @@ -294,50 +170,22 @@ func (r *resourcePolicy) Create(ctx context.Context, req resource.CreateRequest, ) return } - //if out == nil || out.Policy == nil { - // resp.Diagnostics.AddError( - // create.ProblemStandardMessage(names.VerifiedPermissions, create.ErrActionCreating, ResNamePolicy, plan.Name.String(), nil), - // errors.New("empty output").Error(), - // ) - // return - //} - - // TIP: -- 5. Using the output from the create function, set the minimum attributes - plan.ID = flex.StringToFramework(ctx, out.PolicyId) - // TODO F : Think something like that is needed - //response.Diagnostics.Append(flex.Flatten(ctx, output, &state)...) + plan.ID = flex.StringToFramework(ctx, out.PolicyId) - // TIP: -- 7. Save the request plan to response state resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) } func (r *resourcePolicy) 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().VerifiedPermissionsClient(ctx) - // TIP: -- 2. Fetch the state var state resourcePolicyData 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. out, err := findPolicyByID(ctx, conn, state.ID.ValueString()) - // TIP: -- 4. Remove resource from state if it is not found if tfresource.NotFound(err) { resp.State.RemoveResource(ctx) return @@ -350,29 +198,6 @@ func (r *resourcePolicy) Read(ctx context.Context, req resource.ReadRequest, res return } - // TIP: -- 5. Set the arguments and attributes - // - // For simple data types (i.e., schema.StringAttribute, schema.BoolAttribute, - // schema.Int64Attribute, and schema.Float64Attribue), simply setting the - // appropriate data struct field is sufficient. The flex package implements - // helpers for converting between Go and Plugin-Framework types seamlessly. No - // error or nil checking is necessary. - // - // However, there are some situations where more handling is needed such as - // complex data types (e.g., schema.ListAttribute, schema.SetAttribute). In - // these cases the flatten function may have a diagnostics return value, which - // should be appended to resp.Diagnostics. - //state.ARN = flex.StringToFramework(ctx, out.Arn) - //state.ID = flex.StringToFramework(ctx, out.PolicyId) - //state.Name = flex.StringToFramework(ctx, out.PolicyName) - //state.Type = flex.StringToFramework(ctx, out.PolicyType) - - // TIP: Setting a complex type. - //complexArgument, d := flattenComplexArgument(ctx, out.ComplexArgument) - //resp.Diagnostics.Append(d...) - //state.ComplexArgument = complexArgument - - // TIP: -- 6. Set the state resp.Diagnostics.Append(flex.Flatten(ctx, out, &state)...) if resp.Diagnostics.HasError() { @@ -383,30 +208,8 @@ func (r *resourcePolicy) Read(ctx context.Context, req resource.ReadRequest, res } func (r *resourcePolicy) 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().VerifiedPermissionsClient(ctx) - // TIP: -- 2. Fetch the plan var plan, state resourcePolicyData resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) resp.Diagnostics.Append(req.State.Get(ctx, &state)...) @@ -414,41 +217,17 @@ func (r *resourcePolicy) Update(ctx context.Context, req resource.UpdateRequest, return } - // 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 := &verifiedpermissions.UpdatePolicyInput{ - // TIP: Mandatory or fields that will always be present can be set when - // you create the Input structure. (Replace these with real fields.) - //PolicyId: aws.String(plan.ID.ValueString()), - //PolicyName: aws.String(plan.Name.ValueString()), - //PolicyType: aws.String(plan.Type.ValueString()), - } + in := &verifiedpermissions.UpdatePolicyInput{} resp.Diagnostics.Append(flex.Expand(ctx, plan, in)...) if resp.Diagnostics.HasError() { return } - //if !plan.Description.IsNull() { - // // TIP: Optional fields should be set based on whether or not they are - // // used. - // in.Description = aws.String(plan.Description.ValueString()) - //} - //if !plan.ComplexArgument.IsNull() { - // // TIP: Use an expander to assign a complex argument. The elements must be - // // deserialized into the appropriate struct before being passed to the expander. - // var tfList []complexArgumentData - // resp.Diagnostics.Append(plan.ComplexArgument.ElementsAs(ctx, &tfList, false)...) - // if resp.Diagnostics.HasError() { - // return - // } - // - // in.ComplexArgument = expandComplexArgument(tfList) - //} - - // TIP: -- 4. Call the AWS modify/update function + out, err := conn.UpdatePolicy(ctx, in) if err != nil { resp.Diagnostics.AddError( @@ -457,70 +236,27 @@ func (r *resourcePolicy) Update(ctx context.Context, req resource.UpdateRequest, ) return } - //if out == nil || out.Policy == nil { - // resp.Diagnostics.AddError( - // create.ProblemStandardMessage(names.VerifiedPermissions, create.ErrActionUpdating, ResNamePolicy, plan.ID.String(), nil), - // errors.New("empty output").Error(), - // ) - // return - //} - - // TIP: Using the output from the update function, re-set any computed attributes - //plan.ARN = flex.StringToFramework(ctx, out.Policy.Arn) - //plan.ID = flex.StringToFramework(ctx, out.Policy.PolicyId) + resp.Diagnostics.Append(flex.Flatten(ctx, out, &plan)...) } - // TIP: -- 5. Use a waiter to wait for update to complete - //updateTimeout := r.UpdateTimeout(ctx, plan.Timeouts) - //_, err := waitPolicyUpdated(ctx, conn, plan.ID.ValueString(), updateTimeout) - //if err != nil { - // resp.Diagnostics.AddError( - // create.ProblemStandardMessage(names.VerifiedPermissions, create.ErrActionWaitingForUpdate, ResNamePolicy, 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 *resourcePolicy) 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().VerifiedPermissionsClient(ctx) - // TIP: -- 2. Fetch the state var state resourcePolicyData resp.Diagnostics.Append(req.State.Get(ctx, &state)...) if resp.Diagnostics.HasError() { return } - // TIP: -- 3. Populate a delete input structure in := &verifiedpermissions.DeletePolicyInput{ PolicyId: aws.String(state.ID.ValueString()), } - // TIP: -- 4. Call the AWS delete function _, err := conn.DeletePolicy(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 @@ -532,9 +268,6 @@ func (r *resourcePolicy) Delete(ctx context.Context, req resource.DeleteRequest, return } - // TIP: -- 5. Use a waiter to wait for delete to complete - //deleteTimeout := r.DeleteTimeout(ctx, state.Timeouts) - //_, err = waitPolicyDeleted(ctx, conn, state.ID.ValueString(), deleteTimeout) if err != nil { resp.Diagnostics.AddError( create.ProblemStandardMessage(names.VerifiedPermissions, create.ErrActionWaitingForDeletion, ResNamePolicy, state.ID.String(), err), @@ -544,132 +277,10 @@ func (r *resourcePolicy) 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 *resourcePolicy) 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" -// statusDeleting = "Deleting" -// statusNormal = "Normal" -// 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 waitPolicyCreated(ctx context.Context, conn *verifiedpermissions.Client, id string, timeout time.Duration) (*awstypes.PolicyDefinitionItem, error) { -// stateConf := &retry.StateChangeConf{ -// Pending: []string{}, -// Target: []string{statusNormal}, -// Refresh: statusPolicy(ctx, conn, id), -// Timeout: timeout, -// NotFoundChecks: 20, -// ContinuousTargetOccurence: 2, -// } -// -// outputRaw, err := stateConf.WaitForStateContext(ctx) -// if out, ok := outputRaw.(*verifiedpermissions.CreatePolicyOutput); 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 waitPolicyUpdated(ctx context.Context, conn *verifiedpermissions.Client, id string, timeout time.Duration) (*awstypes.Policy, error) { -// stateConf := &retry.StateChangeConf{ -// Pending: []string{statusChangePending}, -// Target: []string{statusUpdated}, -// Refresh: statusPolicy(ctx, conn, id), -// Timeout: timeout, -// NotFoundChecks: 20, -// ContinuousTargetOccurence: 2, -// } -// -// outputRaw, err := stateConf.WaitForStateContext(ctx) -// if out, ok := outputRaw.(*verifiedpermissions.Policy); 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 waitPolicyDeleted(ctx context.Context, conn *verifiedpermissions.Client, id string, timeout time.Duration) (*awstypes.Policy, error) { -// stateConf := &retry.StateChangeConf{ -// Pending: []string{statusDeleting, statusNormal}, -// Target: []string{}, -// Refresh: statusPolicy(ctx, conn, id), -// Timeout: timeout, -// } -// -// outputRaw, err := stateConf.WaitForStateContext(ctx) -// if out, ok := outputRaw.(*verifiedpermissions.Policy); ok { -// return out, err -// } -// -// return nil, err -// } -// -// // TIP: ==== STATUS ==== -// // The status function can return an actual status when that field is -// // available from the API (e.g., out.Status). Otherwise, you can use custom -// // statuses to communicate the states of the resource. -// // -// // Waiters consume the values returned by status functions. Design status so -// // that it can be reused by a create, update, and delete waiter, if possible. -// -// func statusPolicy(ctx context.Context, conn *verifiedpermissions.Client, id string) retry.StateRefreshFunc { -// return func() (interface{}, string, error) { -// out, err := findPolicyByID(ctx, conn, id) -// if tfresource.NotFound(err) { -// return nil, "", nil -// } -// -// if err != nil { -// return nil, "", err -// } -// -// return out, aws.ToString(out.Status), nil -// } -// } -// -// TIP: ==== FINDERS ==== -// The find function is not strictly necessary. You could do the API -// request from the status function. However, we have found that find often -// comes in handy in other places besides the status function. As a result, it -// is good practice to define it separately. func findPolicyByID(ctx context.Context, conn *verifiedpermissions.Client, id string) (*verifiedpermissions.GetPolicyOutput, error) { in := &verifiedpermissions.GetPolicyInput{ PolicyId: aws.String(id), @@ -693,145 +304,6 @@ func findPolicyByID(ctx context.Context, conn *verifiedpermissions.Client, id st 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: Remember, as mentioned above, 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 expandComplexArgument(tfList []complexArgumentData) *awstypes.ComplexArgument { -// if len(tfList) == 0 { -// return nil -// } -// -// tfObj := tfList[0] -// apiObject := &awstypes.ComplexArgument{ -// NestedRequired: aws.String(tfObj.NestedRequired.ValueString()), -// } -// if !tfObj.NestedOptional.IsNull() { -// apiObject.NestedOptional = aws.String(tfObj.NestedOptional.ValueString()) -// } -// -// return apiObject -//} -// -//// TIP: Even when you have a list with max length of 1, this plural function -//// works brilliantly. However, if the AWS API takes a structure rather than a -//// slice of structures, you will not need it. -//func expandComplexArguments(tfList []complexArgumentData) []*verifiedpermissions.ComplexArgument { -// // TIP: The AWS API can be picky about whether you send a nil or zero- -// // length for an argument that should be cleared. For example, in some -// // cases, if you send a nil value, the AWS API interprets that as "make no -// // changes" when what you want to say is "remove everything." Sometimes -// // using a zero-length list will cause an error. -// // -// // As a result, here are two options. Usually, option 1, nil, will work as -// // expected, clearing the field. But, test going from something to nothing -// // to make sure it works. If not, try the second option. -// // TIP: Option 1: Returning nil for zero-length list -// if len(tfList) == 0 { -// return nil -// } -// var apiObject []*awstypes.ComplexArgument -// // TIP: Option 2: Return zero-length list for zero-length list. If option 1 does -// // not work, after testing going from something to nothing (if that is -// // possible), uncomment out the next line and remove option 1. -// // -// // apiObject := make([]*verifiedpermissions.ComplexArgument, 0) -// -// for _, tfObj := range tfList { -// item := &verifiedpermissions.ComplexArgument{ -// NestedRequired: aws.String(tfObj.NestedRequired.ValueString()), -// } -// if !tfObj.NestedOptional.IsNull() { -// item.NestedOptional = aws.String(tfObj.NestedOptional.ValueString()) -// } -// -// apiObject = append(apiObject, item) -// } -// -// return apiObject -//} - -// 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 resourcePolicyData struct { ARN types.String `tfsdk:"arn"` ComplexArgument types.List `tfsdk:"complex_argument"` From 78f4894de1476363253dd81caa7738dde9c72f02 Mon Sep 17 00:00:00 2001 From: Adrian Johnson Date: Mon, 15 Apr 2024 17:52:00 -0500 Subject: [PATCH 03/15] aws_verifiedpermissions_policy: udpate schema and handlers --- .../verifiedpermissions/exports_test.go | 1 + .../service/verifiedpermissions/policy.go | 260 ++++++++++++------ .../service_package_gen.go | 4 + 3 files changed, 187 insertions(+), 78 deletions(-) diff --git a/internal/service/verifiedpermissions/exports_test.go b/internal/service/verifiedpermissions/exports_test.go index 728be6893c10..a64025abfca7 100644 --- a/internal/service/verifiedpermissions/exports_test.go +++ b/internal/service/verifiedpermissions/exports_test.go @@ -5,6 +5,7 @@ package verifiedpermissions // Exports for use in tests only. var ( + ResourcePolicy = newResourcePolicy ResourcePolicyStore = newResourcePolicyStore ResourcePolicyTemplate = newResourcePolicyTemplate ResourceSchema = newResourceSchema diff --git a/internal/service/verifiedpermissions/policy.go b/internal/service/verifiedpermissions/policy.go index 1d89b188494a..fe957b94b9ff 100644 --- a/internal/service/verifiedpermissions/policy.go +++ b/internal/service/verifiedpermissions/policy.go @@ -5,12 +5,12 @@ package verifiedpermissions import ( "context" + "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/verifiedpermissions" awstypes "github.com/aws/aws-sdk-go-v2/service/verifiedpermissions/types" - "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" - "github.com/hashicorp/terraform-plugin-framework-validators/objectvalidator" - "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -20,13 +20,16 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "github.com/hashicorp/terraform-provider-aws/internal/create" "github.com/hashicorp/terraform-provider-aws/internal/errs" + interflex "github.com/hashicorp/terraform-provider-aws/internal/flex" "github.com/hashicorp/terraform-provider-aws/internal/framework" "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + fwflex "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" ) -// @FrameworkResource(name="Policy") +// @FrameworkResource(aws_verifiedpermissions_policy, name="Policy") func newResourcePolicy(_ context.Context) (resource.ResourceWithConfigure, error) { r := &resourcePolicy{} @@ -39,7 +42,6 @@ const ( type resourcePolicy struct { framework.ResourceWithConfigure - framework.WithTimeouts } func (r *resourcePolicy) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { @@ -50,47 +52,51 @@ func (r *resourcePolicy) Schema(ctx context.Context, req resource.SchemaRequest, resp.Schema = schema.Schema{ Attributes: map[string]schema.Attribute{ "created_date": schema.StringAttribute{ - Computed: true, - }, - "last_updated_date": schema.StringAttribute{ - Computed: true, + CustomType: timetypes.RFC3339Type{}, + Computed: true, }, + "id": framework.IDAttribute(), "policy_id": framework.IDAttribute(), "policy_store_id": schema.StringAttribute{ Required: true, }, - "policy_type": schema.StringAttribute{ - Required: true, // TODO - }, - "principal": schema.StringAttribute{ - Optional: true, - }, - "resource": schema.StringAttribute{ - Optional: true, - }, }, Blocks: map[string]schema.Block{ - "definition": schema.SetNestedBlock{ + "definition": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[policyDefinition](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + listvalidator.IsRequired(), + }, NestedObject: schema.NestedBlockObject{ Blocks: map[string]schema.Block{ - "static": schema.SetNestedBlock{ + "static": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[staticPolicyDefinition](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + listvalidator.ExactlyOneOf( + path.MatchRelative().AtParent().AtName("template_linked"), + ), + }, NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ "description": schema.StringAttribute{ - Required: true, + Optional: true, }, "statement": schema.StringAttribute{ Required: true, }, }, - Validators: []validator.Object{ - objectvalidator.ExactlyOneOf(path.Expressions{ - path.MatchRoot("template_linked"), - }...), - }, }, }, - "template_linked": schema.SetNestedBlock{ + "template_linked": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[templateLinkedPolicyDefinition](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + listvalidator.ExactlyOneOf( + path.MatchRelative().AtParent().AtName("static"), + ), + }, NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ "policy_template_id": schema.StringAttribute{ @@ -98,7 +104,11 @@ func (r *resourcePolicy) Schema(ctx context.Context, req resource.SchemaRequest, }, }, Blocks: map[string]schema.Block{ - "principal": schema.SetNestedBlock{ + "principal": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[templateLinkedPrincipal](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ "entity_id": schema.StringAttribute{ @@ -110,7 +120,11 @@ func (r *resourcePolicy) Schema(ctx context.Context, req resource.SchemaRequest, }, }, }, - "resource": schema.SetNestedBlock{ + "resource": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[templateLinkedResource](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ "entity_id": schema.StringAttribute{ @@ -123,25 +137,19 @@ func (r *resourcePolicy) Schema(ctx context.Context, req resource.SchemaRequest, }, }, }, - Validators: []validator.Object{ - objectvalidator.ExactlyOneOf(path.Expressions{ - path.MatchRoot("static"), - }...), - }, }, }, }, }, }, - "timeouts": timeouts.Block(ctx, timeouts.Opts{ - Create: true, - Update: true, - Delete: true, - }), }, } } +const ( + resourcePolicyIDPartsCount = 2 +) + func (r *resourcePolicy) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { conn := r.Meta().VerifiedPermissionsClient(ctx) @@ -153,25 +161,98 @@ func (r *resourcePolicy) Create(ctx context.Context, req resource.CreateRequest, in := &verifiedpermissions.CreatePolicyInput{} - resp.Diagnostics.Append(flex.Expand(ctx, plan, in)...) + in.ClientToken = aws.String(id.UniqueId()) + in.PolicyStoreId = aws.String(plan.PolicyStoreID.ValueString()) - if resp.Diagnostics.HasError() { + def, diags := plan.Definition.ToPtr(ctx) + resp.Diagnostics.Append(diags...) + if diags.HasError() { return } - clientToken := id.UniqueId() - in.ClientToken = aws.String(clientToken) + if !def.Static.IsNull() { + static, diags := def.Static.ToPtr(ctx) + resp.Diagnostics.Append(diags...) + if diags.HasError() { + return + } + + in.Definition = &awstypes.PolicyDefinitionMemberStatic{ + Value: awstypes.StaticPolicyDefinition{ + Statement: fwflex.StringFromFramework(ctx, static.Statement), + Description: fwflex.StringFromFramework(ctx, static.Description), + }, + } + } + + if !def.TemplateLinked.IsNull() { + templateLinked, diags := def.TemplateLinked.ToPtr(ctx) + resp.Diagnostics.Append(diags...) + if diags.HasError() { + return + } + + value := awstypes.TemplateLinkedPolicyDefinition{ + PolicyTemplateId: aws.String(templateLinked.PolicyTemplateID.ValueString()), + } + + if !templateLinked.Principal.IsNull() { + principal, diags := templateLinked.Principal.ToPtr(ctx) + resp.Diagnostics.Append(diags...) + if diags.HasError() { + return + } + + value.Principal = &awstypes.EntityIdentifier{ + EntityId: fwflex.StringFromFramework(ctx, principal.EntityID), + EntityType: fwflex.StringFromFramework(ctx, principal.EntityType), + } + } + + if !templateLinked.Resource.IsNull() { + res, diags := templateLinked.Resource.ToPtr(ctx) + resp.Diagnostics.Append(diags...) + if diags.HasError() { + return + } + + value.Principal = &awstypes.EntityIdentifier{ + EntityId: fwflex.StringFromFramework(ctx, res.EntityID), + EntityType: fwflex.StringFromFramework(ctx, res.EntityType), + } + } + + in.Definition = &awstypes.PolicyDefinitionMemberTemplateLinked{ + Value: value, + } + } out, err := conn.CreatePolicy(ctx, in) if err != nil { resp.Diagnostics.AddError( - create.ProblemStandardMessage(names.VerifiedPermissions, create.ErrActionCreating, ResNamePolicy, clientToken, err), + create.ProblemStandardMessage(names.VerifiedPermissions, create.ErrActionCreating, ResNamePolicy, plan.PolicyStoreID.String(), err), err.Error(), ) return } - plan.ID = flex.StringToFramework(ctx, out.PolicyId) + idParts := []string{ + aws.ToString(out.PolicyId), + aws.ToString(out.PolicyStoreId), + } + + rID, err := interflex.FlattenResourceId(idParts, resourcePolicyIDPartsCount, false) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.VerifiedPermissions, create.ErrActionCreating, ResNamePolicy, plan.PolicyStoreID.String(), err), + err.Error(), + ) + return + } + + plan.ID = flex.StringValueToFramework(ctx, rID) + plan.CreatedDate = timetypes.NewRFC3339TimePointerValue(out.CreatedDate) + plan.PolicyID = flex.StringToFramework(ctx, out.PolicyId) resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) } @@ -185,11 +266,21 @@ func (r *resourcePolicy) Read(ctx context.Context, req resource.ReadRequest, res return } - out, err := findPolicyByID(ctx, conn, state.ID.ValueString()) + rID, err := interflex.ExpandResourceId(state.ID.ValueString(), resourcePolicyIDPartsCount, false) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.VerifiedPermissions, create.ErrActionSetting, ResNamePolicy, state.ID.String(), err), + err.Error(), + ) + return + } + + out, err := findPolicyByID(ctx, conn, rID[0], rID[1]) if tfresource.NotFound(err) { resp.State.RemoveResource(ctx) return } + if err != nil { resp.Diagnostics.AddError( create.ProblemStandardMessage(names.VerifiedPermissions, create.ErrActionSetting, ResNamePolicy, state.ID.String(), err), @@ -198,10 +289,16 @@ func (r *resourcePolicy) Read(ctx context.Context, req resource.ReadRequest, res return } - resp.Diagnostics.Append(flex.Flatten(ctx, out, &state)...) + if val, ok := out.Definition.(*awstypes.PolicyDefinitionDetailMemberStatic); ok && val != nil { + static := fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &staticPolicyDefinition{ + Statement: flex.StringToFramework(ctx, val.Value.Statement), + Description: flex.StringToFramework(ctx, val.Value.Description), + }) - if resp.Diagnostics.HasError() { - return + state.Definition = fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &policyDefinition{ + Static: static, + TemplateLinked: fwtypes.NewListNestedObjectValueOfNull[templateLinkedPolicyDefinition](ctx), + }) } resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) @@ -217,10 +314,7 @@ func (r *resourcePolicy) Update(ctx context.Context, req resource.UpdateRequest, return } - if !plan.Name.Equal(state.Name) || - !plan.Description.Equal(state.Description) || - !plan.ComplexArgument.Equal(state.ComplexArgument) || - !plan.Type.Equal(state.Type) { + if !plan.Definition.Equal(state.Definition) { in := &verifiedpermissions.UpdatePolicyInput{} resp.Diagnostics.Append(flex.Expand(ctx, plan, in)...) @@ -253,24 +347,19 @@ func (r *resourcePolicy) Delete(ctx context.Context, req resource.DeleteRequest, } in := &verifiedpermissions.DeletePolicyInput{ - PolicyId: aws.String(state.ID.ValueString()), + PolicyId: aws.String(state.PolicyID.ValueString()), + PolicyStoreId: aws.String(state.PolicyStoreID.ValueString()), } _, err := conn.DeletePolicy(ctx, in) - if err != nil { - if errs.IsA[*awstypes.ResourceNotFoundException](err) { - return - } - resp.Diagnostics.AddError( - create.ProblemStandardMessage(names.VerifiedPermissions, create.ErrActionDeleting, ResNamePolicy, state.ID.String(), err), - err.Error(), - ) + + if errs.IsA[*awstypes.ResourceNotFoundException](err) { return } if err != nil { resp.Diagnostics.AddError( - create.ProblemStandardMessage(names.VerifiedPermissions, create.ErrActionWaitingForDeletion, ResNamePolicy, state.ID.String(), err), + create.ProblemStandardMessage(names.VerifiedPermissions, create.ErrActionDeleting, ResNamePolicy, state.ID.String(), err), err.Error(), ) return @@ -281,9 +370,10 @@ func (r *resourcePolicy) ImportState(ctx context.Context, req resource.ImportSta resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) } -func findPolicyByID(ctx context.Context, conn *verifiedpermissions.Client, id string) (*verifiedpermissions.GetPolicyOutput, error) { +func findPolicyByID(ctx context.Context, conn *verifiedpermissions.Client, id, policyStoreId string) (*verifiedpermissions.GetPolicyOutput, error) { in := &verifiedpermissions.GetPolicyInput{ - PolicyId: aws.String(id), + PolicyId: aws.String(id), + PolicyStoreId: aws.String(policyStoreId), } out, err := conn.GetPolicy(ctx, in) @@ -305,21 +395,35 @@ func findPolicyByID(ctx context.Context, conn *verifiedpermissions.Client, id st } type resourcePolicyData struct { - ARN types.String `tfsdk:"arn"` - ComplexArgument types.List `tfsdk:"complex_argument"` - Description types.String `tfsdk:"description"` - ID types.String `tfsdk:"id"` - Name types.String `tfsdk:"name"` - Timeouts timeouts.Value `tfsdk:"timeouts"` - Type types.String `tfsdk:"type"` + CreatedDate timetypes.RFC3339 `tfsdk:"created_date"` + Definition fwtypes.ListNestedObjectValueOf[policyDefinition] `tfsdk:"definition"` + ID types.String `tfsdk:"id"` + PolicyID types.String `tfsdk:"policy_id"` + PolicyStoreID types.String `tfsdk:"policy_store_id"` +} + +type policyDefinition struct { + Static fwtypes.ListNestedObjectValueOf[staticPolicyDefinition] `tfsdk:"static"` + TemplateLinked fwtypes.ListNestedObjectValueOf[templateLinkedPolicyDefinition] `tfsdk:"template_linked"` +} + +type staticPolicyDefinition struct { + Statement types.String `tfsdk:"statement"` + Description types.String `tfsdk:"description"` +} + +type templateLinkedPolicyDefinition struct { + PolicyTemplateID types.String `tfsdk:"policy_template_id"` + Principal fwtypes.ListNestedObjectValueOf[templateLinkedPrincipal] `tfsdk:"principal"` + Resource fwtypes.SetNestedObjectValueOf[templateLinkedResource] `tfsdk:"resource"` } -type complexArgumentData struct { - NestedRequired types.String `tfsdk:"nested_required"` - NestedOptional types.String `tfsdk:"nested_optional"` +type templateLinkedPrincipal struct { + EntityID types.String `tfsdk:"entity_id"` + EntityType types.String `tfsdk:"entity_type"` } -var complexArgumentAttrTypes = map[string]attr.Type{ - "nested_required": types.StringType, - "nested_optional": types.StringType, +type templateLinkedResource struct { + EntityID types.String `tfsdk:"entity_id"` + EntityType types.String `tfsdk:"entity_type"` } diff --git a/internal/service/verifiedpermissions/service_package_gen.go b/internal/service/verifiedpermissions/service_package_gen.go index 2c61c23e6158..6213f171433b 100644 --- a/internal/service/verifiedpermissions/service_package_gen.go +++ b/internal/service/verifiedpermissions/service_package_gen.go @@ -25,6 +25,10 @@ func (p *servicePackage) FrameworkDataSources(ctx context.Context) []*types.Serv func (p *servicePackage) FrameworkResources(ctx context.Context) []*types.ServicePackageFrameworkResource { return []*types.ServicePackageFrameworkResource{ + { + Factory: newResourcePolicy, + Name: "Policy", + }, { Factory: newResourcePolicyStore, Name: "Policy Store", From aaf0c52dd6a02ced1435c1446e8bea40df19360b Mon Sep 17 00:00:00 2001 From: Adrian Johnson Date: Mon, 15 Apr 2024 18:20:12 -0500 Subject: [PATCH 04/15] aws_verifiedpermissions_policy: set template linked definition --- .../service/verifiedpermissions/policy.go | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/internal/service/verifiedpermissions/policy.go b/internal/service/verifiedpermissions/policy.go index fe957b94b9ff..d8c0f9bbfbc8 100644 --- a/internal/service/verifiedpermissions/policy.go +++ b/internal/service/verifiedpermissions/policy.go @@ -216,7 +216,7 @@ func (r *resourcePolicy) Create(ctx context.Context, req resource.CreateRequest, return } - value.Principal = &awstypes.EntityIdentifier{ + value.Resource = &awstypes.EntityIdentifier{ EntityId: fwflex.StringFromFramework(ctx, res.EntityID), EntityType: fwflex.StringFromFramework(ctx, res.EntityType), } @@ -301,6 +301,37 @@ func (r *resourcePolicy) Read(ctx context.Context, req resource.ReadRequest, res }) } + if val, ok := out.Definition.(*awstypes.PolicyDefinitionDetailMemberTemplateLinked); ok && val != nil { + tpl := templateLinkedPolicyDefinition{ + PolicyTemplateID: fwflex.StringToFramework(ctx, val.Value.PolicyTemplateId), + } + + if val.Value.Principal != nil { + tpl.Principal = fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &templateLinkedPrincipal{ + EntityID: flex.StringToFramework(ctx, val.Value.Principal.EntityId), + EntityType: flex.StringToFramework(ctx, val.Value.Principal.EntityType), + }) + } else { + tpl.Principal = fwtypes.NewListNestedObjectValueOfNull[templateLinkedPrincipal](ctx) + } + + if val.Value.Resource != nil { + tpl.Resource = fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &templateLinkedResource{ + EntityID: flex.StringToFramework(ctx, val.Value.Principal.EntityId), + EntityType: flex.StringToFramework(ctx, val.Value.Principal.EntityType), + }) + } else { + tpl.Resource = fwtypes.NewListNestedObjectValueOfNull[templateLinkedResource](ctx) + } + + templateLinked := fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &tpl) + + state.Definition = fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &policyDefinition{ + Static: fwtypes.NewListNestedObjectValueOfNull[staticPolicyDefinition](ctx), + TemplateLinked: templateLinked, + }) + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) } @@ -415,7 +446,7 @@ type staticPolicyDefinition struct { type templateLinkedPolicyDefinition struct { PolicyTemplateID types.String `tfsdk:"policy_template_id"` Principal fwtypes.ListNestedObjectValueOf[templateLinkedPrincipal] `tfsdk:"principal"` - Resource fwtypes.SetNestedObjectValueOf[templateLinkedResource] `tfsdk:"resource"` + Resource fwtypes.ListNestedObjectValueOf[templateLinkedResource] `tfsdk:"resource"` } type templateLinkedPrincipal struct { From c46514bc5f848f85f2f27cebd01babf3f316f1aa Mon Sep 17 00:00:00 2001 From: Adrian Johnson Date: Fri, 19 Apr 2024 14:53:44 -0500 Subject: [PATCH 05/15] resolve merge conflicts --- go.mod | 1 + go.sum | 2 + .../service/verifiedpermissions/policy.go | 138 +++++++++++++++++- 3 files changed, 137 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 00a92573a866..bf2251733161 100644 --- a/go.mod +++ b/go.mod @@ -169,6 +169,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/xray v1.25.4 github.com/aws/smithy-go v1.20.2 github.com/beevik/etree v1.3.0 + github.com/cedar-policy/cedar-go v0.0.0-20240318205125-470d1fe984bb github.com/davecgh/go-spew v1.1.1 github.com/gertd/go-pluralize v0.2.1 github.com/google/go-cmp v0.6.0 diff --git a/go.sum b/go.sum index cb061d544898..76ab48f31e2f 100644 --- a/go.sum +++ b/go.sum @@ -377,6 +377,8 @@ github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyX github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bufbuild/protocompile v0.6.0 h1:Uu7WiSQ6Yj9DbkdnOe7U4mNKp58y9WDMKDn28/ZlunY= github.com/bufbuild/protocompile v0.6.0/go.mod h1:YNP35qEYoYGme7QMtz5SBCoN4kL4g12jTtjuzRNdjpE= +github.com/cedar-policy/cedar-go v0.0.0-20240318205125-470d1fe984bb h1:WaOlZeLno47GR/TvgUNCqB6itqhT7kMLsUwlIjxWW4Y= +github.com/cedar-policy/cedar-go v0.0.0-20240318205125-470d1fe984bb/go.mod h1:qZuNWmkhx7pxkYvgmNPcBE4NtfGBF6nmI+bjecaQp14= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= diff --git a/internal/service/verifiedpermissions/policy.go b/internal/service/verifiedpermissions/policy.go index d8c0f9bbfbc8..daab567d56bf 100644 --- a/internal/service/verifiedpermissions/policy.go +++ b/internal/service/verifiedpermissions/policy.go @@ -9,11 +9,14 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/verifiedpermissions" awstypes "github.com/aws/aws-sdk-go-v2/service/verifiedpermissions/types" + cedar "github.com/cedar-policy/cedar-go/x/exp/parser" "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes" "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "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/id" @@ -42,6 +45,7 @@ const ( type resourcePolicy struct { framework.ResourceWithConfigure + framework.WithImportByID } func (r *resourcePolicy) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { @@ -54,11 +58,17 @@ func (r *resourcePolicy) Schema(ctx context.Context, req resource.SchemaRequest, "created_date": schema.StringAttribute{ CustomType: timetypes.RFC3339Type{}, Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "id": framework.IDAttribute(), "policy_id": framework.IDAttribute(), "policy_store_id": schema.StringAttribute{ Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, }, Blocks: map[string]schema.Block{ @@ -78,6 +88,9 @@ func (r *resourcePolicy) Schema(ctx context.Context, req resource.SchemaRequest, path.MatchRelative().AtParent().AtName("template_linked"), ), }, + //PlanModifiers: []planmodifier.List{ + // listplanmodifier.RequiresReplace(), + //}, NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ "description": schema.StringAttribute{ @@ -85,6 +98,11 @@ func (r *resourcePolicy) Schema(ctx context.Context, req resource.SchemaRequest, }, "statement": schema.StringAttribute{ Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplaceIf( + statementReplaceIf, "Replace cedar statement diff", "Replace cedar statement diff", + ), + }, }, }, }, @@ -97,6 +115,9 @@ func (r *resourcePolicy) Schema(ctx context.Context, req resource.SchemaRequest, path.MatchRelative().AtParent().AtName("static"), ), }, + //PlanModifiers: []planmodifier.List{ + // listplanmodifier.RequiresReplace(), + //}, NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ "policy_template_id": schema.StringAttribute{ @@ -146,6 +167,53 @@ func (r *resourcePolicy) Schema(ctx context.Context, req resource.SchemaRequest, } } +func statementReplaceIf(ctx context.Context, req planmodifier.StringRequest, resp *stringplanmodifier.RequiresReplaceIfFuncResponse) { + if req.State.Raw.IsNull() { + return + } + + if req.Plan.Raw.IsNull() { + return + } + cedarPlan, err := cedar.Tokenize([]byte(req.PlanValue.ValueString())) + if err != nil { + resp.Diagnostics.AddError(err.Error(), err.Error()) + return + } + + cedarState, err := cedar.Tokenize([]byte(req.StateValue.ValueString())) + if err != nil { + resp.Diagnostics.AddError(err.Error(), err.Error()) + return + } + + policyPlan, err := cedar.Parse(cedarPlan) + if err != nil { + resp.Diagnostics.AddError(err.Error(), err.Error()) + return + } + + policyState, err := cedar.Parse(cedarState) + if err != nil { + resp.Diagnostics.AddError(err.Error(), err.Error()) + return + } + + var policyPrincipal bool + if len(policyPlan) > 0 && len(policyState) > 0 && (len(policyPlan[0].Principal.Entity.Path) > 0 && (len(policyState[0].Principal.Entity.Path)) > 0) { + policyPrincipal = (policyPlan[0].Principal.Entity.String() != policyState[0].Principal.Entity.String()) || (policyPlan[0].Principal.Type != policyState[0].Principal.Type) + } + + var policyResource bool + if len(policyPlan) > 0 && len(policyState) > 0 && (len(policyPlan[0].Resource.Entity.Path) > 0 && (len(policyState[0].Resource.Entity.Path)) > 0) { + policyResource = (policyPlan[0].Resource.Entity.String() != policyState[0].Resource.Entity.String()) || (policyPlan[0].Resource.Type != policyState[0].Resource.Type) + } + + policyEffect := policyPlan[0].Effect != policyState[0].Effect + + resp.RequiresReplace = policyEffect || policyResource || policyPrincipal +} + const ( resourcePolicyIDPartsCount = 2 ) @@ -346,7 +414,6 @@ func (r *resourcePolicy) Update(ctx context.Context, req resource.UpdateRequest, } if !plan.Definition.Equal(state.Definition) { - in := &verifiedpermissions.UpdatePolicyInput{} resp.Diagnostics.Append(flex.Expand(ctx, plan, in)...) if resp.Diagnostics.HasError() { @@ -397,9 +464,72 @@ func (r *resourcePolicy) Delete(ctx context.Context, req resource.DeleteRequest, } } -func (r *resourcePolicy) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) -} +//func (r *resourcePolicy) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { +// if !req.State.Raw.IsNull() && !req.Plan.Raw.IsNull() { +// var plan, state resourcePolicyData +// resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) +// resp.Diagnostics.Append(req.State.Get(ctx, &state)...) +// if resp.Diagnostics.HasError() { +// return +// } +// +// defPlan, diags := plan.Definition.ToPtr(ctx) +// defState, diags := state.Definition.ToPtr(ctx) +// resp.Diagnostics.Append(diags...) +// if diags.HasError() { +// return +// } +// +// staticPlan, diags := defPlan.Static.ToPtr(ctx) +// staticState, diags := defState.Static.ToPtr(ctx) +// resp.Diagnostics.Append(diags...) +// if diags.HasError() { +// return +// } +// +// if !staticPlan.Statement.Equal(staticState.Statement) { +// cedarPlan, err := cedar.Tokenize([]byte(staticPlan.Statement.ValueString())) +// if err != nil { +// resp.Diagnostics.AddError( +// create.ProblemStandardMessage(names.VerifiedPermissions, create.ErrActionUpdating, ResNamePolicy, plan.ID.String(), err), +// err.Error(), +// ) +// return +// } +// +// cedarState, err := cedar.Tokenize([]byte(staticState.Statement.ValueString())) +// if err != nil { +// resp.Diagnostics.AddError( +// create.ProblemStandardMessage(names.VerifiedPermissions, create.ErrActionUpdating, ResNamePolicy, state.ID.String(), err), +// err.Error(), +// ) +// return +// } +// +// policyPlan, err := cedar.Parse(cedarPlan) +// if err != nil { +// resp.Diagnostics.AddError( +// create.ProblemStandardMessage(names.VerifiedPermissions, create.ErrActionUpdating, ResNamePolicy, plan.ID.String(), err), +// err.Error(), +// ) +// return +// } +// +// policyState, err := cedar.Parse(cedarState) +// if err != nil { +// resp.Diagnostics.AddError( +// create.ProblemStandardMessage(names.VerifiedPermissions, create.ErrActionUpdating, ResNamePolicy, state.ID.String(), err), +// err.Error(), +// ) +// return +// } +// +// if policyPlan[0].Effect != policyState[0].Effect { +// resp.RequiresReplace = []path.Path{path.Root("definition").AtListIndex(0).AtName("static").AtListIndex(0).AtName("statement")} +// } +// } +// } +//} func findPolicyByID(ctx context.Context, conn *verifiedpermissions.Client, id, policyStoreId string) (*verifiedpermissions.GetPolicyOutput, error) { in := &verifiedpermissions.GetPolicyInput{ From 61be88284127dfca8eb3d0acd4d27edfba52f902 Mon Sep 17 00:00:00 2001 From: Adrian Johnson Date: Tue, 16 Apr 2024 12:16:06 -0500 Subject: [PATCH 06/15] aws_verifiedpermissions_policy: fix update for static statement --- .../verifiedpermissions/exports_test.go | 1 + .../service/verifiedpermissions/policy.go | 156 +++++++++--------- .../verifiedpermissions/policy_test.go | 148 +---------------- 3 files changed, 83 insertions(+), 222 deletions(-) diff --git a/internal/service/verifiedpermissions/exports_test.go b/internal/service/verifiedpermissions/exports_test.go index a64025abfca7..55ade545b2ce 100644 --- a/internal/service/verifiedpermissions/exports_test.go +++ b/internal/service/verifiedpermissions/exports_test.go @@ -10,6 +10,7 @@ var ( ResourcePolicyTemplate = newResourcePolicyTemplate ResourceSchema = newResourceSchema + FindPolicyByID = findPolicyByID FindPolicyStoreByID = findPolicyStoreByID FindPolicyTemplateByID = findPolicyTemplateByID FindSchemaByPolicyStoreID = findSchemaByPolicyStoreID diff --git a/internal/service/verifiedpermissions/policy.go b/internal/service/verifiedpermissions/policy.go index daab567d56bf..c14fd6a9b3a6 100644 --- a/internal/service/verifiedpermissions/policy.go +++ b/internal/service/verifiedpermissions/policy.go @@ -15,6 +15,7 @@ import ( "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/listplanmodifier" "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" @@ -88,9 +89,6 @@ func (r *resourcePolicy) Schema(ctx context.Context, req resource.SchemaRequest, path.MatchRelative().AtParent().AtName("template_linked"), ), }, - //PlanModifiers: []planmodifier.List{ - // listplanmodifier.RequiresReplace(), - //}, NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ "description": schema.StringAttribute{ @@ -115,13 +113,13 @@ func (r *resourcePolicy) Schema(ctx context.Context, req resource.SchemaRequest, path.MatchRelative().AtParent().AtName("static"), ), }, - //PlanModifiers: []planmodifier.List{ - // listplanmodifier.RequiresReplace(), - //}, NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ "policy_template_id": schema.StringAttribute{ Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, }, Blocks: map[string]schema.Block{ @@ -130,13 +128,22 @@ func (r *resourcePolicy) Schema(ctx context.Context, req resource.SchemaRequest, Validators: []validator.List{ listvalidator.SizeAtMost(1), }, + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ "entity_id": schema.StringAttribute{ Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, "entity_type": schema.StringAttribute{ Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, }, }, @@ -150,9 +157,15 @@ func (r *resourcePolicy) Schema(ctx context.Context, req resource.SchemaRequest, Attributes: map[string]schema.Attribute{ "entity_id": schema.StringAttribute{ Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, "entity_type": schema.StringAttribute{ Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, }, }, @@ -175,6 +188,7 @@ func statementReplaceIf(ctx context.Context, req planmodifier.StringRequest, res if req.Plan.Raw.IsNull() { return } + cedarPlan, err := cedar.Tokenize([]byte(req.PlanValue.ValueString())) if err != nil { resp.Diagnostics.AddError(err.Error(), err.Error()) @@ -209,7 +223,10 @@ func statementReplaceIf(ctx context.Context, req planmodifier.StringRequest, res policyResource = (policyPlan[0].Resource.Entity.String() != policyState[0].Resource.Entity.String()) || (policyPlan[0].Resource.Type != policyState[0].Resource.Type) } - policyEffect := policyPlan[0].Effect != policyState[0].Effect + var policyEffect bool + if len(policyPlan) > 0 && len(policyState) > 0 { + policyEffect = policyPlan[0].Effect != policyState[0].Effect + } resp.RequiresReplace = policyEffect || policyResource || policyPrincipal } @@ -385,8 +402,8 @@ func (r *resourcePolicy) Read(ctx context.Context, req resource.ReadRequest, res if val.Value.Resource != nil { tpl.Resource = fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &templateLinkedResource{ - EntityID: flex.StringToFramework(ctx, val.Value.Principal.EntityId), - EntityType: flex.StringToFramework(ctx, val.Value.Principal.EntityType), + EntityID: flex.StringToFramework(ctx, val.Value.Resource.EntityId), + EntityType: flex.StringToFramework(ctx, val.Value.Resource.EntityType), }) } else { tpl.Resource = fwtypes.NewListNestedObjectValueOfNull[templateLinkedResource](ctx) @@ -415,12 +432,32 @@ func (r *resourcePolicy) Update(ctx context.Context, req resource.UpdateRequest, if !plan.Definition.Equal(state.Definition) { in := &verifiedpermissions.UpdatePolicyInput{} - resp.Diagnostics.Append(flex.Expand(ctx, plan, in)...) - if resp.Diagnostics.HasError() { + in.PolicyId = fwflex.StringFromFramework(ctx, state.PolicyID) + in.PolicyStoreId = fwflex.StringFromFramework(ctx, state.PolicyStoreID) + + defPlan, diags := plan.Definition.ToPtr(ctx) + defState, diags := state.Definition.ToPtr(ctx) + resp.Diagnostics.Append(diags...) + if diags.HasError() { return } - out, err := conn.UpdatePolicy(ctx, in) + if !defPlan.Static.Equal(defState.Static) { + static, diags := defPlan.Static.ToPtr(ctx) + resp.Diagnostics.Append(diags...) + if diags.HasError() { + return + } + + in.Definition = &awstypes.UpdatePolicyDefinitionMemberStatic{ + Value: awstypes.UpdateStaticPolicyDefinition{ + Statement: fwflex.StringFromFramework(ctx, static.Statement), + Description: fwflex.StringFromFramework(ctx, static.Description), + }, + } + } + + _, err := conn.UpdatePolicy(ctx, in) if err != nil { resp.Diagnostics.AddError( create.ProblemStandardMessage(names.VerifiedPermissions, create.ErrActionUpdating, ResNamePolicy, plan.ID.String(), err), @@ -428,8 +465,6 @@ func (r *resourcePolicy) Update(ctx context.Context, req resource.UpdateRequest, ) return } - - resp.Diagnostics.Append(flex.Flatten(ctx, out, &plan)...) } resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) @@ -464,72 +499,33 @@ func (r *resourcePolicy) Delete(ctx context.Context, req resource.DeleteRequest, } } -//func (r *resourcePolicy) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { -// if !req.State.Raw.IsNull() && !req.Plan.Raw.IsNull() { -// var plan, state resourcePolicyData -// resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) -// resp.Diagnostics.Append(req.State.Get(ctx, &state)...) -// if resp.Diagnostics.HasError() { -// return -// } -// -// defPlan, diags := plan.Definition.ToPtr(ctx) -// defState, diags := state.Definition.ToPtr(ctx) -// resp.Diagnostics.Append(diags...) -// if diags.HasError() { -// return -// } -// -// staticPlan, diags := defPlan.Static.ToPtr(ctx) -// staticState, diags := defState.Static.ToPtr(ctx) -// resp.Diagnostics.Append(diags...) -// if diags.HasError() { -// return -// } -// -// if !staticPlan.Statement.Equal(staticState.Statement) { -// cedarPlan, err := cedar.Tokenize([]byte(staticPlan.Statement.ValueString())) -// if err != nil { -// resp.Diagnostics.AddError( -// create.ProblemStandardMessage(names.VerifiedPermissions, create.ErrActionUpdating, ResNamePolicy, plan.ID.String(), err), -// err.Error(), -// ) -// return -// } -// -// cedarState, err := cedar.Tokenize([]byte(staticState.Statement.ValueString())) -// if err != nil { -// resp.Diagnostics.AddError( -// create.ProblemStandardMessage(names.VerifiedPermissions, create.ErrActionUpdating, ResNamePolicy, state.ID.String(), err), -// err.Error(), -// ) -// return -// } -// -// policyPlan, err := cedar.Parse(cedarPlan) -// if err != nil { -// resp.Diagnostics.AddError( -// create.ProblemStandardMessage(names.VerifiedPermissions, create.ErrActionUpdating, ResNamePolicy, plan.ID.String(), err), -// err.Error(), -// ) -// return -// } -// -// policyState, err := cedar.Parse(cedarState) -// if err != nil { -// resp.Diagnostics.AddError( -// create.ProblemStandardMessage(names.VerifiedPermissions, create.ErrActionUpdating, ResNamePolicy, state.ID.String(), err), -// err.Error(), -// ) -// return -// } -// -// if policyPlan[0].Effect != policyState[0].Effect { -// resp.RequiresReplace = []path.Path{path.Root("definition").AtListIndex(0).AtName("static").AtListIndex(0).AtName("statement")} -// } -// } -// } -//} +func (r *resourcePolicy) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if !req.State.Raw.IsNull() && !req.Plan.Raw.IsNull() { + var plan, state resourcePolicyData + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if !plan.Definition.Equal(state.Definition) { + defPlan, diags := plan.Definition.ToPtr(ctx) + defState, diags := state.Definition.ToPtr(ctx) + resp.Diagnostics.Append(diags...) + if diags.HasError() { + return + } + + if !defState.Static.IsNull() && defPlan.Static.IsNull() { + resp.RequiresReplace = []path.Path{path.Root("definition").AtListIndex(0).AtName("static")} + } + + if !defState.TemplateLinked.IsNull() && defPlan.TemplateLinked.IsNull() { + resp.RequiresReplace = []path.Path{path.Root("definition").AtListIndex(0).AtName("template_linked")} + } + } + } +} func findPolicyByID(ctx context.Context, conn *verifiedpermissions.Client, id, policyStoreId string) (*verifiedpermissions.GetPolicyOutput, error) { in := &verifiedpermissions.GetPolicyInput{ diff --git a/internal/service/verifiedpermissions/policy_test.go b/internal/service/verifiedpermissions/policy_test.go index e5b36eb98ece..a0fcc80eae05 100644 --- a/internal/service/verifiedpermissions/policy_test.go +++ b/internal/service/verifiedpermissions/policy_test.go @@ -3,36 +3,7 @@ package verifiedpermissions_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/verifiedpermissions/types package. If so, you'll - // need to import types and reference the nested types, e.g., as - // types.. "context" "errors" "fmt" @@ -41,7 +12,6 @@ import ( "github.com/YakDriver/regexache" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/verifiedpermissions" - "github.com/aws/aws-sdk-go-v2/service/verifiedpermissions/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" @@ -49,107 +19,17 @@ 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" - - // 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. tfverifiedpermissions "github.com/hashicorp/terraform-provider-aws/internal/service/verifiedpermissions" + "github.com/hashicorp/terraform-provider-aws/names" ) -// TIP: File Structure. The basic outline for all test files should be as -// follows. Improve this resource's maintainability by following this -// outline. -// -// 1. Package declaration (add "_test" since this is a test file) -// 2. Imports -// 3. Unit tests -// 4. Basic test -// 5. Disappears test -// 6. All the other tests -// 7. Helper functions (exists, destroy, check, etc.) -// 8. Functions that return Terraform configurations - -// TIP: ==== UNIT TESTS ==== -// This is an example of a unit test. Its name is not prefixed with -// "TestAcc" like an acceptance test. -// -// Unlike acceptance tests, unit tests do not access AWS and are focused on a -// function (or method). Because of this, they are quick and cheap to run. -// -// In designing a resource's implementation, isolate complex bits from AWS bits -// so that they can be tested through a unit test. We encourage more unit tests -// in the provider. -// -// Cut and dry functions using well-used patterns, like typical flatteners and -// expanders, don't need unit testing. However, if they are complex or -// intricate, they should be unit tested. -func TestPolicyExampleUnitTest(t *testing.T) { - t.Parallel() - - testCases := []struct { - TestName string - Input string - Expected string - Error bool - }{ - { - TestName: "empty", - Input: "", - Expected: "", - Error: true, - }, - { - TestName: "descriptive name", - Input: "some input", - Expected: "some output", - Error: false, - }, - { - TestName: "another descriptive name", - Input: "more input", - Expected: "more output", - Error: false, - }, - } - - for _, testCase := range testCases { - testCase := testCase - t.Run(testCase.TestName, func(t *testing.T) { - t.Parallel() - got, err := tfverifiedpermissions.FunctionFromResource(testCase.Input) - - if err != nil && !testCase.Error { - t.Errorf("got error (%s), expected no error", err) - } - - if err == nil && testCase.Error { - t.Errorf("got (%s) and no error, expected error", got) - } - - if got != testCase.Expected { - t.Errorf("got %s, expected %s", got, testCase.Expected) - } - }) - } -} - -// TIP: ==== ACCEPTANCE TESTS ==== -// This is an example of a basic acceptance test. This should test as much of -// standard functionality of the resource as possible, and test importing, if -// applicable. We prefix its name with "TestAcc", the service, and the -// resource name. -// -// Acceptance test access AWS and cost money to run. func TestAccVerifiedPermissionsPolicy_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 policy verifiedpermissions.DescribePolicyResponse + var policy verifiedpermissions.GetPolicyOutput rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_verifiedpermissions_policy.test" @@ -157,9 +37,8 @@ func TestAccVerifiedPermissionsPolicy_basic(t *testing.T) { PreCheck: func() { acctest.PreCheck(ctx, t) acctest.PreCheckPartitionHasService(t, names.VerifiedPermissionsEndpointID) - testAccPreCheck(ctx, t) }, - ErrorCheck: acctest.ErrorCheck(t, names.VerifiedPermissionsEndpointID), + ErrorCheck: acctest.ErrorCheck(t, names.VerifiedPermissionsServiceID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, CheckDestroy: testAccCheckPolicyDestroy(ctx), Steps: []resource.TestStep{ @@ -194,7 +73,7 @@ func TestAccVerifiedPermissionsPolicy_disappears(t *testing.T) { t.Skip("skipping long-running test in short mode") } - var policy verifiedpermissions.DescribePolicyResponse + var policy verifiedpermissions.GetPolicyOutput rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_verifiedpermissions_policy.test" @@ -202,9 +81,8 @@ func TestAccVerifiedPermissionsPolicy_disappears(t *testing.T) { PreCheck: func() { acctest.PreCheck(ctx, t) acctest.PreCheckPartitionHasService(t, names.VerifiedPermissionsEndpointID) - testAccPreCheck(t) }, - ErrorCheck: acctest.ErrorCheck(t, names.VerifiedPermissionsEndpointID), + ErrorCheck: acctest.ErrorCheck(t, names.VerifiedPermissionsServiceID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, CheckDestroy: testAccCheckPolicyDestroy(ctx), Steps: []resource.TestStep{ @@ -255,7 +133,7 @@ func testAccCheckPolicyDestroy(ctx context.Context) resource.TestCheckFunc { } } -func testAccCheckPolicyExists(ctx context.Context, name string, policy *verifiedpermissions.DescribePolicyResponse) resource.TestCheckFunc { +func testAccCheckPolicyExists(ctx context.Context, name string, policy *verifiedpermissions.GetPolicyOutput) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[name] if !ok { @@ -281,20 +159,6 @@ func testAccCheckPolicyExists(ctx context.Context, name string, policy *verified } } -func testAccPreCheck(ctx context.Context, t *testing.T) { - conn := acctest.Provider.Meta().(*conns.AWSClient).VerifiedPermissionsClient(ctx) - - input := &verifiedpermissions.ListPolicysInput{} - _, err := conn.ListPolicys(ctx, input) - - if acctest.PreCheckSkipError(err) { - t.Skipf("skipping acceptance testing: %s", err) - } - if err != nil { - t.Fatalf("unexpected PreCheck error: %s", err) - } -} - func testAccCheckPolicyNotRecreated(before, after *verifiedpermissions.DescribePolicyResponse) resource.TestCheckFunc { return func(s *terraform.State) error { if before, after := aws.ToString(before.PolicyId), aws.ToString(after.PolicyId); before != after { From 06f669e88b9b559e2b7c22c5932df8eb7f744abf Mon Sep 17 00:00:00 2001 From: Adrian Johnson Date: Tue, 16 Apr 2024 12:41:31 -0500 Subject: [PATCH 07/15] aws_verifiedpermissions_policy: add initial testing --- .../service/verifiedpermissions/policy.go | 10 +- .../verifiedpermissions/policy_test.go | 114 +++++++++--------- 2 files changed, 62 insertions(+), 62 deletions(-) diff --git a/internal/service/verifiedpermissions/policy.go b/internal/service/verifiedpermissions/policy.go index c14fd6a9b3a6..514fd2f818a4 100644 --- a/internal/service/verifiedpermissions/policy.go +++ b/internal/service/verifiedpermissions/policy.go @@ -232,7 +232,7 @@ func statementReplaceIf(ctx context.Context, req planmodifier.StringRequest, res } const ( - resourcePolicyIDPartsCount = 2 + ResourcePolicyIDPartsCount = 2 ) func (r *resourcePolicy) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { @@ -326,7 +326,7 @@ func (r *resourcePolicy) Create(ctx context.Context, req resource.CreateRequest, aws.ToString(out.PolicyStoreId), } - rID, err := interflex.FlattenResourceId(idParts, resourcePolicyIDPartsCount, false) + rID, err := interflex.FlattenResourceId(idParts, ResourcePolicyIDPartsCount, false) if err != nil { resp.Diagnostics.AddError( create.ProblemStandardMessage(names.VerifiedPermissions, create.ErrActionCreating, ResNamePolicy, plan.PolicyStoreID.String(), err), @@ -351,7 +351,7 @@ func (r *resourcePolicy) Read(ctx context.Context, req resource.ReadRequest, res return } - rID, err := interflex.ExpandResourceId(state.ID.ValueString(), resourcePolicyIDPartsCount, false) + rID, err := interflex.ExpandResourceId(state.ID.ValueString(), ResourcePolicyIDPartsCount, false) if err != nil { resp.Diagnostics.AddError( create.ProblemStandardMessage(names.VerifiedPermissions, create.ErrActionSetting, ResNamePolicy, state.ID.String(), err), @@ -374,6 +374,10 @@ func (r *resourcePolicy) Read(ctx context.Context, req resource.ReadRequest, res return } + state.PolicyID = fwflex.StringToFramework(ctx, out.PolicyId) + state.PolicyStoreID = fwflex.StringToFramework(ctx, out.PolicyStoreId) + state.CreatedDate = timetypes.NewRFC3339TimePointerValue(out.CreatedDate) + if val, ok := out.Definition.(*awstypes.PolicyDefinitionDetailMemberStatic); ok && val != nil { static := fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &staticPolicyDefinition{ Statement: flex.StringToFramework(ctx, val.Value.Statement), diff --git a/internal/service/verifiedpermissions/policy_test.go b/internal/service/verifiedpermissions/policy_test.go index a0fcc80eae05..aa144f0953f8 100644 --- a/internal/service/verifiedpermissions/policy_test.go +++ b/internal/service/verifiedpermissions/policy_test.go @@ -9,9 +9,8 @@ import ( "fmt" "testing" - "github.com/YakDriver/regexache" - "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/verifiedpermissions" + awstypes "github.com/aws/aws-sdk-go-v2/service/verifiedpermissions/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" @@ -19,6 +18,7 @@ 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" + interflex "github.com/hashicorp/terraform-provider-aws/internal/flex" tfverifiedpermissions "github.com/hashicorp/terraform-provider-aws/internal/service/verifiedpermissions" "github.com/hashicorp/terraform-provider-aws/names" ) @@ -33,6 +33,8 @@ func TestAccVerifiedPermissionsPolicy_basic(t *testing.T) { rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_verifiedpermissions_policy.test" + policyStatement := "permit (principal, action == Action::\"view\", resource in Album:: \"test_album\");" + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t) @@ -43,25 +45,18 @@ func TestAccVerifiedPermissionsPolicy_basic(t *testing.T) { CheckDestroy: testAccCheckPolicyDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccPolicyConfig_basic(rName), + Config: testAccPolicyConfig_basic(rName, policyStatement), Check: resource.ComposeTestCheckFunc( testAccCheckPolicyExists(ctx, resourceName, &policy), - 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", "verifiedpermissions", regexache.MustCompile(`policy:+.`)), + resource.TestCheckResourceAttr(resourceName, "definition.0.static.0.description", rName), + resource.TestCheckResourceAttr(resourceName, "definition.0.static.0.statement", policyStatement), + resource.TestCheckResourceAttrSet(resourceName, "policy_id"), ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"apply_immediately", "user"}, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, }, }, }) @@ -77,6 +72,8 @@ func TestAccVerifiedPermissionsPolicy_disappears(t *testing.T) { rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_verifiedpermissions_policy.test" + policyStatement := "permit (principal, action == Action::\"view\", resource in Album:: \"test_album\");" + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t) @@ -87,15 +84,9 @@ func TestAccVerifiedPermissionsPolicy_disappears(t *testing.T) { CheckDestroy: testAccCheckPolicyDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccPolicyConfig_basic(rName, testAccPolicyVersionNewer), + Config: testAccPolicyConfig_basic(rName, policyStatement), Check: resource.ComposeTestCheckFunc( testAccCheckPolicyExists(ctx, resourceName, &policy), - // 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 ResourcePolicy = newResourcePolicy acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tfverifiedpermissions.ResourcePolicy, resourceName), ), ExpectNonEmptyPlan: true, @@ -113,15 +104,17 @@ func testAccCheckPolicyDestroy(ctx context.Context) resource.TestCheckFunc { continue } - input := &verifiedpermissions.DescribePolicyInput{ - PolicyId: aws.String(rs.Primary.ID), + rID, err := interflex.ExpandResourceId(rs.Primary.ID, tfverifiedpermissions.ResourcePolicyIDPartsCount, false) + if err != nil { + return err } - _, err := conn.DescribePolicy(ctx, &verifiedpermissions.DescribePolicyInput{ - PolicyId: aws.String(rs.Primary.ID), - }) - if errs.IsA[*types.ResourceNotFoundException](err) { + + _, err = tfverifiedpermissions.FindPolicyByID(ctx, conn, rID[0], rID[1]) + + if errs.IsA[*awstypes.ResourceNotFoundException](err) { return nil } + if err != nil { return create.Error(names.VerifiedPermissions, create.ErrActionCheckingDestroyed, tfverifiedpermissions.ResNamePolicy, rs.Primary.ID, err) } @@ -145,9 +138,12 @@ func testAccCheckPolicyExists(ctx context.Context, name string, policy *verified } conn := acctest.Provider.Meta().(*conns.AWSClient).VerifiedPermissionsClient(ctx) - resp, err := conn.DescribePolicy(ctx, &verifiedpermissions.DescribePolicyInput{ - PolicyId: aws.String(rs.Primary.ID), - }) + rID, err := interflex.ExpandResourceId(rs.Primary.ID, tfverifiedpermissions.ResourcePolicyIDPartsCount, false) + if err != nil { + return err + } + + resp, err := tfverifiedpermissions.FindPolicyByID(ctx, conn, rID[0], rID[1]) if err != nil { return create.Error(names.VerifiedPermissions, create.ErrActionCheckingExistence, tfverifiedpermissions.ResNamePolicy, rs.Primary.ID, err) @@ -159,39 +155,39 @@ func testAccCheckPolicyExists(ctx context.Context, name string, policy *verified } } -func testAccCheckPolicyNotRecreated(before, after *verifiedpermissions.DescribePolicyResponse) resource.TestCheckFunc { - return func(s *terraform.State) error { - if before, after := aws.ToString(before.PolicyId), aws.ToString(after.PolicyId); before != after { - return create.Error(names.VerifiedPermissions, create.ErrActionCheckingNotRecreated, tfverifiedpermissions.ResNamePolicy, aws.ToString(before.PolicyId), errors.New("recreated")) - } +func testAccPolicyConfig_base(rName string) string { + return fmt.Sprintf(` +resource "aws_verifiedpermissions_policy_store" "test" { + description = %[1]q + validation_settings { + mode = "OFF" + } +} - return nil - } +resource "aws_verifiedpermissions_schema" "test" { + policy_store_id = aws_verifiedpermissions_policy_store.test.policy_store_id + + definition { + value = "{\"CHANGEDD\":{\"actions\":{},\"entityTypes\":{}}}" + } } -func testAccPolicyConfig_basic(rName, version string) string { - return fmt.Sprintf(` -resource "aws_security_group" "test" { - name = %[1]q +`, rName) } +func testAccPolicyConfig_basic(rName, policyStatement string) string { + return acctest.ConfigCompose( + testAccPolicyConfig_base(rName), + fmt.Sprintf(` resource "aws_verifiedpermissions_policy" "test" { - policy_name = %[1]q - engine_type = "ActiveVerifiedPermissions" - engine_version = %[2]q - host_instance_type = "verifiedpermissions.t2.micro" - security_groups = [aws_security_group.test.id] - authentication_strategy = "simple" - storage_type = "efs" - - logs { - general = true - } - - user { - username = "Test" - password = "TestTest1234" + policy_store_id = aws_verifiedpermissions_policy_store.test.id + + definition { + static { + description = %[1]q + statement = %[2]q + } } } -`, rName, version) +`, rName, policyStatement)) } From b502ae466b7e281fd4c6f1bd9024c7d46a08c53c Mon Sep 17 00:00:00 2001 From: Adrian Johnson Date: Tue, 16 Apr 2024 13:13:05 -0500 Subject: [PATCH 08/15] aws_verifiedpermissions_policy: update documentation --- .../service/verifiedpermissions/policy.go | 2 +- .../verifiedpermissions/policy_test.go | 73 +++++++++++++++++++ .../verifiedpermissions_policy.html.markdown | 58 +++++++++------ 3 files changed, 110 insertions(+), 23 deletions(-) diff --git a/internal/service/verifiedpermissions/policy.go b/internal/service/verifiedpermissions/policy.go index 514fd2f818a4..d4c834adde0a 100644 --- a/internal/service/verifiedpermissions/policy.go +++ b/internal/service/verifiedpermissions/policy.go @@ -180,7 +180,7 @@ func (r *resourcePolicy) Schema(ctx context.Context, req resource.SchemaRequest, } } -func statementReplaceIf(ctx context.Context, req planmodifier.StringRequest, resp *stringplanmodifier.RequiresReplaceIfFuncResponse) { +func statementReplaceIf(_ context.Context, req planmodifier.StringRequest, resp *stringplanmodifier.RequiresReplaceIfFuncResponse) { if req.State.Raw.IsNull() { return } diff --git a/internal/service/verifiedpermissions/policy_test.go b/internal/service/verifiedpermissions/policy_test.go index aa144f0953f8..83b2cf756275 100644 --- a/internal/service/verifiedpermissions/policy_test.go +++ b/internal/service/verifiedpermissions/policy_test.go @@ -62,6 +62,46 @@ func TestAccVerifiedPermissionsPolicy_basic(t *testing.T) { }) } +func TestAccVerifiedPermissionsPolicy_templateLinked(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var policy verifiedpermissions.GetPolicyOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_verifiedpermissions_policy.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.VerifiedPermissionsEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.VerifiedPermissionsServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckPolicyDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccPolicyConfig_templateLinked(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckPolicyExists(ctx, resourceName, &policy), + resource.TestCheckResourceAttrSet(resourceName, "definition.0.template_linked.0.policy_template_id"), + resource.TestCheckResourceAttr(resourceName, "definition.0.template_linked.0.principal.0.entity_id", "TestUsers"), + resource.TestCheckResourceAttr(resourceName, "definition.0.template_linked.0.principal.0.entity_type", "User"), + resource.TestCheckResourceAttr(resourceName, "definition.0.template_linked.0.resource.0.entity_id", "test_album"), + resource.TestCheckResourceAttr(resourceName, "definition.0.template_linked.0.resource.0.entity_type", "Album"), + resource.TestCheckResourceAttrSet(resourceName, "policy_id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func TestAccVerifiedPermissionsPolicy_disappears(t *testing.T) { ctx := acctest.Context(t) if testing.Short() { @@ -191,3 +231,36 @@ resource "aws_verifiedpermissions_policy" "test" { } `, rName, policyStatement)) } + +func testAccPolicyConfig_templateLinked(rName string) string { + return acctest.ConfigCompose( + testAccPolicyConfig_base(rName), + fmt.Sprintf(` +resource "aws_verifiedpermissions_policy_template" "test" { + policy_store_id = aws_verifiedpermissions_policy_store.test.id + + statement = "permit (principal in ?principal, action in PhotoFlash::Action::\"FullPhotoAccess\", resource == ?resource) unless { resource.IsPrivate };" + description = %[1]q +} + +resource "aws_verifiedpermissions_policy" "test" { + policy_store_id = aws_verifiedpermissions_policy_store.test.id + + definition { + template_linked { + policy_template_id = aws_verifiedpermissions_policy_template.test.policy_template_id + + principal { + entity_id = "TestUsers" + entity_type = "User" + } + + resource { + entity_id = "test_album" + entity_type = "Album" + } + } + } +} +`, rName)) +} diff --git a/website/docs/r/verifiedpermissions_policy.html.markdown b/website/docs/r/verifiedpermissions_policy.html.markdown index 4e80b3efdc87..bf922541bf54 100644 --- a/website/docs/r/verifiedpermissions_policy.html.markdown +++ b/website/docs/r/verifiedpermissions_policy.html.markdown @@ -5,14 +5,7 @@ page_title: "AWS: aws_verifiedpermissions_policy" description: |- Terraform resource for managing an AWS Verified Permissions Policy. --- -` + # Resource: aws_verifiedpermissions_policy Terraform resource for managing an AWS Verified Permissions Policy. @@ -22,7 +15,14 @@ Terraform resource for managing an AWS Verified Permissions Policy. ### Basic Usage ```terraform -resource "aws_verifiedpermissions_policy" "example" { +resource "aws_verifiedpermissions_policy" "test" { + policy_store_id = aws_verifiedpermissions_policy_store.test.id + + definition { + static { + statement = "permit (principal, action == Action::\"view\", resource in Album:: \"test_album\");" + } + } } ``` @@ -30,12 +30,34 @@ resource "aws_verifiedpermissions_policy" "example" { 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. +* `policy_store_id` - (Required) The Policy Store ID of the policy store. +* `definition`- (Required) The definition of the policy. See [Definition](#definition) below. 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. + +### Definition + +* `static` - (Optional) The static policy statement. See [Static](#static) below. +* `template_linked` - (Optional) The template linked policy. See [Template Linked](#template_linked) below. + +#### Static + +* `description` - (Optional) The description of the static policy. +* `statement` - (Required) The statement of the static policy. + +#### Template Linked + +* `policy_template_id` - (Required) The ID of the template. +* `principal` - (Optional) The principal of the template linked policy. + * `entity_id` - (Required) The entity ID of the principal. + * `entity_type` - (Required) The entity type of the principal. +* `resource` - (Optional) The resource of the template linked policy. + * `entity_id` - (Required) The entity ID of the resource. + * `entity_type` - (Required) The entity type of the resource. + ## Attribute Reference This resource exports the following attributes in addition to the arguments above: @@ -43,27 +65,19 @@ This resource exports the following attributes in addition to the arguments abov * `arn` - ARN of the Policy. 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 Verified Permissions Policy 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 Verified Permissions Policy using the `policy_id,policy_store_id`. For example: ```terraform import { to = aws_verifiedpermissions_policy.example - id = "policy-id-12345678" + id = "policy-id-12345678,policy-store-id-12345678" } ``` -Using `terraform import`, import Verified Permissions Policy using the `example_id_arg`. For example: +Using `terraform import`, import Verified Permissions Policy using the `policy_id,policy_store_id`. For example: ```console -% terraform import aws_verifiedpermissions_policy.example policy-id-12345678 +% terraform import aws_verifiedpermissions_policy.example policy-id-12345678,policy-store-id-12345678 ``` From ce7af7049dc68f147b8af06fca2878ae53da14bb Mon Sep 17 00:00:00 2001 From: Adrian Johnson Date: Tue, 16 Apr 2024 13:21:04 -0500 Subject: [PATCH 09/15] chore: linter --- internal/service/verifiedpermissions/policy_test.go | 8 ++++---- website/docs/r/verifiedpermissions_policy.html.markdown | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/service/verifiedpermissions/policy_test.go b/internal/service/verifiedpermissions/policy_test.go index 83b2cf756275..b84e72816dfe 100644 --- a/internal/service/verifiedpermissions/policy_test.go +++ b/internal/service/verifiedpermissions/policy_test.go @@ -221,7 +221,7 @@ func testAccPolicyConfig_basic(rName, policyStatement string) string { fmt.Sprintf(` resource "aws_verifiedpermissions_policy" "test" { policy_store_id = aws_verifiedpermissions_policy_store.test.id - + definition { static { description = %[1]q @@ -245,18 +245,18 @@ resource "aws_verifiedpermissions_policy_template" "test" { resource "aws_verifiedpermissions_policy" "test" { policy_store_id = aws_verifiedpermissions_policy_store.test.id - + definition { template_linked { policy_template_id = aws_verifiedpermissions_policy_template.test.policy_template_id principal { - entity_id = "TestUsers" + entity_id = "TestUsers" entity_type = "User" } resource { - entity_id = "test_album" + entity_id = "test_album" entity_type = "Album" } } diff --git a/website/docs/r/verifiedpermissions_policy.html.markdown b/website/docs/r/verifiedpermissions_policy.html.markdown index bf922541bf54..19fdfb81c0c4 100644 --- a/website/docs/r/verifiedpermissions_policy.html.markdown +++ b/website/docs/r/verifiedpermissions_policy.html.markdown @@ -52,11 +52,11 @@ The following arguments are optional: * `policy_template_id` - (Required) The ID of the template. * `principal` - (Optional) The principal of the template linked policy. - * `entity_id` - (Required) The entity ID of the principal. - * `entity_type` - (Required) The entity type of the principal. + * `entity_id` - (Required) The entity ID of the principal. + * `entity_type` - (Required) The entity type of the principal. * `resource` - (Optional) The resource of the template linked policy. - * `entity_id` - (Required) The entity ID of the resource. - * `entity_type` - (Required) The entity type of the resource. + * `entity_id` - (Required) The entity ID of the resource. + * `entity_type` - (Required) The entity type of the resource. ## Attribute Reference From b9622f074887ecedd63ec64ff6a96cad2ae02792 Mon Sep 17 00:00:00 2001 From: Adrian Johnson Date: Tue, 16 Apr 2024 13:32:24 -0500 Subject: [PATCH 10/15] chore: markdown lint --- website/docs/r/verifiedpermissions_policy.html.markdown | 1 - 1 file changed, 1 deletion(-) diff --git a/website/docs/r/verifiedpermissions_policy.html.markdown b/website/docs/r/verifiedpermissions_policy.html.markdown index 19fdfb81c0c4..0eee8667d048 100644 --- a/website/docs/r/verifiedpermissions_policy.html.markdown +++ b/website/docs/r/verifiedpermissions_policy.html.markdown @@ -37,7 +37,6 @@ 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. - ### Definition * `static` - (Optional) The static policy statement. See [Static](#static) below. From a67d509a163c43eeeb033f7bcb44dd87b8d82aeb Mon Sep 17 00:00:00 2001 From: Adrian Johnson Date: Tue, 16 Apr 2024 13:44:52 -0500 Subject: [PATCH 11/15] aws_verifiedpermissions_policy: test for statement updates --- .../verifiedpermissions/policy_test.go | 54 +++++++++++++++++++ .../verifiedpermissions_policy.html.markdown | 2 +- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/internal/service/verifiedpermissions/policy_test.go b/internal/service/verifiedpermissions/policy_test.go index b84e72816dfe..975260ab2a13 100644 --- a/internal/service/verifiedpermissions/policy_test.go +++ b/internal/service/verifiedpermissions/policy_test.go @@ -102,6 +102,60 @@ func TestAccVerifiedPermissionsPolicy_templateLinked(t *testing.T) { }) } +func TestAccVerifiedPermissionsPolicy_update(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var policy verifiedpermissions.GetPolicyOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_verifiedpermissions_policy.test" + + policyStatement := "permit (principal, action == Action::\"view\", resource in Album:: \"test_album\");" + policyStatementActionUpdated := "permit (principal, action == Action::\"write\", resource in Album:: \"test_album\");" + policyStatementEffectUpdated := "forbid (principal, action == Action::\"view\", resource in Album:: \"test_album\");" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.VerifiedPermissionsEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.VerifiedPermissionsServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckPolicyDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccPolicyConfig_basic(rName, policyStatement), + Check: resource.ComposeTestCheckFunc( + testAccCheckPolicyExists(ctx, resourceName, &policy), + resource.TestCheckResourceAttr(resourceName, "definition.0.static.0.description", rName), + resource.TestCheckResourceAttr(resourceName, "definition.0.static.0.statement", policyStatement), + resource.TestCheckResourceAttrSet(resourceName, "policy_id"), + ), + }, + { + Config: testAccPolicyConfig_basic(rName, policyStatementActionUpdated), + Check: resource.ComposeTestCheckFunc( + testAccCheckPolicyExists(ctx, resourceName, &policy), + resource.TestCheckResourceAttr(resourceName, "definition.0.static.0.description", rName), + resource.TestCheckResourceAttr(resourceName, "definition.0.static.0.statement", policyStatementActionUpdated), + resource.TestCheckResourceAttrSet(resourceName, "policy_id"), + ), + }, + { + Config: testAccPolicyConfig_basic(rName, policyStatementEffectUpdated), + Check: resource.ComposeTestCheckFunc( + testAccCheckPolicyExists(ctx, resourceName, &policy), + resource.TestCheckResourceAttr(resourceName, "definition.0.static.0.description", rName), + resource.TestCheckResourceAttr(resourceName, "definition.0.static.0.statement", policyStatementEffectUpdated), + resource.TestCheckResourceAttrSet(resourceName, "policy_id"), + ), + }, + }, + }) +} + func TestAccVerifiedPermissionsPolicy_disappears(t *testing.T) { ctx := acctest.Context(t) if testing.Short() { diff --git a/website/docs/r/verifiedpermissions_policy.html.markdown b/website/docs/r/verifiedpermissions_policy.html.markdown index 0eee8667d048..963e43714ed0 100644 --- a/website/docs/r/verifiedpermissions_policy.html.markdown +++ b/website/docs/r/verifiedpermissions_policy.html.markdown @@ -40,7 +40,7 @@ The following arguments are optional: ### Definition * `static` - (Optional) The static policy statement. See [Static](#static) below. -* `template_linked` - (Optional) The template linked policy. See [Template Linked](#template_linked) below. +* `template_linked` - (Optional) The template linked policy. See [Template Linked](#template-linked) below. #### Static From 987f0edac6b5a1026562bb5a20eace2c8970a36a Mon Sep 17 00:00:00 2001 From: Adrian Johnson Date: Tue, 16 Apr 2024 14:03:11 -0500 Subject: [PATCH 12/15] add sweepers --- internal/service/verifiedpermissions/sweep.go | 62 +++++++++++++++++++ internal/sweep/register_gen_test.go | 2 + 2 files changed, 64 insertions(+) create mode 100644 internal/service/verifiedpermissions/sweep.go diff --git a/internal/service/verifiedpermissions/sweep.go b/internal/service/verifiedpermissions/sweep.go new file mode 100644 index 000000000000..2bea9cc10ec4 --- /dev/null +++ b/internal/service/verifiedpermissions/sweep.go @@ -0,0 +1,62 @@ +package verifiedpermissions + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/verifiedpermissions" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-provider-aws/internal/sweep" + "github.com/hashicorp/terraform-provider-aws/internal/sweep/awsv2" + "github.com/hashicorp/terraform-provider-aws/internal/sweep/framework" +) + +func RegisterSweepers() { + resource.AddTestSweepers("aws_verifiedpermissions_policy_store", &resource.Sweeper{ + Name: "aws_verifiedpermissions_policy_store", + F: sweepPolicyStores, + }) +} + +func sweepPolicyStores(region string) error { + ctx := sweep.Context(region) + client, err := sweep.SharedRegionalSweepClient(ctx, region) + if err != nil { + return fmt.Errorf("error getting client: %s", err) + } + + conn := client.VerifiedPermissionsClient(ctx) + sweepResources := make([]sweep.Sweepable, 0) + in := &verifiedpermissions.ListPolicyStoresInput{} + + pages := verifiedpermissions.NewListPolicyStoresPaginator(conn, in) + + for pages.HasMorePages() { + page, err := pages.NextPage(ctx) + + if awsv2.SkipSweepError(err) { + log.Printf("[WARN] Skipping VerifiedPermissions Policy Stores sweep for %s: %s", region, err) + return nil + } + + if err != nil { + return fmt.Errorf("error retrieving VerifiedPermissions Policy Stores: %w", err) + } + + for _, store := range page.PolicyStores { + id := aws.ToString(store.PolicyStoreId) + log.Printf("[INFO] Deleting VerifiedPermissions Policy Store: %s", id) + + sweepResources = append(sweepResources, framework.NewSweepResource(newResourcePolicyStore, client, + framework.NewAttribute("id", id), + )) + } + } + + if err := sweep.SweepOrchestrator(ctx, sweepResources); err != nil { + return fmt.Errorf("error sweeping VerifiedPermissions Policy Stores for %s: %w", region, err) + } + + return nil +} diff --git a/internal/sweep/register_gen_test.go b/internal/sweep/register_gen_test.go index d6861528327c..86be1dfad8bc 100644 --- a/internal/sweep/register_gen_test.go +++ b/internal/sweep/register_gen_test.go @@ -147,6 +147,7 @@ import ( "github.com/hashicorp/terraform-provider-aws/internal/service/timestreamwrite" "github.com/hashicorp/terraform-provider-aws/internal/service/transcribe" "github.com/hashicorp/terraform-provider-aws/internal/service/transfer" + "github.com/hashicorp/terraform-provider-aws/internal/service/verifiedpermissions" "github.com/hashicorp/terraform-provider-aws/internal/service/vpclattice" "github.com/hashicorp/terraform-provider-aws/internal/service/waf" "github.com/hashicorp/terraform-provider-aws/internal/service/wafregional" @@ -299,6 +300,7 @@ func registerSweepers() { timestreamwrite.RegisterSweepers() transcribe.RegisterSweepers() transfer.RegisterSweepers() + verifiedpermissions.RegisterSweepers() vpclattice.RegisterSweepers() waf.RegisterSweepers() wafregional.RegisterSweepers() From 0a4e7a6c7c8177c21f5ab6d88bc4472ab538b6a2 Mon Sep 17 00:00:00 2001 From: Adrian Johnson Date: Tue, 16 Apr 2024 14:05:02 -0500 Subject: [PATCH 13/15] add copyright headers --- internal/service/verifiedpermissions/sweep.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/service/verifiedpermissions/sweep.go b/internal/service/verifiedpermissions/sweep.go index 2bea9cc10ec4..3c13712867d3 100644 --- a/internal/service/verifiedpermissions/sweep.go +++ b/internal/service/verifiedpermissions/sweep.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package verifiedpermissions import ( From f0ab7695f862745fe8e4b75112b00be04a5db762 Mon Sep 17 00:00:00 2001 From: Adrian Johnson Date: Tue, 16 Apr 2024 14:10:45 -0500 Subject: [PATCH 14/15] add CHANGELOG entry --- .changelog/35413.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/35413.txt diff --git a/.changelog/35413.txt b/.changelog/35413.txt new file mode 100644 index 000000000000..ccc131e9edbf --- /dev/null +++ b/.changelog/35413.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_verifiedpermissions_policy +``` From 92be836b04fd6ab3e0d2b0bd9590a7ec7aa3c0d3 Mon Sep 17 00:00:00 2001 From: Adrian Johnson Date: Tue, 16 Apr 2024 14:53:27 -0500 Subject: [PATCH 15/15] chore: golangcilint --- .../service/verifiedpermissions/policy.go | 42 +++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/internal/service/verifiedpermissions/policy.go b/internal/service/verifiedpermissions/policy.go index d4c834adde0a..2ef2cede94a8 100644 --- a/internal/service/verifiedpermissions/policy.go +++ b/internal/service/verifiedpermissions/policy.go @@ -26,7 +26,6 @@ import ( "github.com/hashicorp/terraform-provider-aws/internal/errs" interflex "github.com/hashicorp/terraform-provider-aws/internal/flex" "github.com/hashicorp/terraform-provider-aws/internal/framework" - "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" fwflex "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" @@ -335,9 +334,9 @@ func (r *resourcePolicy) Create(ctx context.Context, req resource.CreateRequest, return } - plan.ID = flex.StringValueToFramework(ctx, rID) + plan.ID = fwflex.StringValueToFramework(ctx, rID) plan.CreatedDate = timetypes.NewRFC3339TimePointerValue(out.CreatedDate) - plan.PolicyID = flex.StringToFramework(ctx, out.PolicyId) + plan.PolicyID = fwflex.StringToFramework(ctx, out.PolicyId) resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) } @@ -380,8 +379,8 @@ func (r *resourcePolicy) Read(ctx context.Context, req resource.ReadRequest, res if val, ok := out.Definition.(*awstypes.PolicyDefinitionDetailMemberStatic); ok && val != nil { static := fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &staticPolicyDefinition{ - Statement: flex.StringToFramework(ctx, val.Value.Statement), - Description: flex.StringToFramework(ctx, val.Value.Description), + Statement: fwflex.StringToFramework(ctx, val.Value.Statement), + Description: fwflex.StringToFramework(ctx, val.Value.Description), }) state.Definition = fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &policyDefinition{ @@ -397,8 +396,8 @@ func (r *resourcePolicy) Read(ctx context.Context, req resource.ReadRequest, res if val.Value.Principal != nil { tpl.Principal = fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &templateLinkedPrincipal{ - EntityID: flex.StringToFramework(ctx, val.Value.Principal.EntityId), - EntityType: flex.StringToFramework(ctx, val.Value.Principal.EntityType), + EntityID: fwflex.StringToFramework(ctx, val.Value.Principal.EntityId), + EntityType: fwflex.StringToFramework(ctx, val.Value.Principal.EntityType), }) } else { tpl.Principal = fwtypes.NewListNestedObjectValueOfNull[templateLinkedPrincipal](ctx) @@ -406,8 +405,8 @@ func (r *resourcePolicy) Read(ctx context.Context, req resource.ReadRequest, res if val.Value.Resource != nil { tpl.Resource = fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &templateLinkedResource{ - EntityID: flex.StringToFramework(ctx, val.Value.Resource.EntityId), - EntityType: flex.StringToFramework(ctx, val.Value.Resource.EntityType), + EntityID: fwflex.StringToFramework(ctx, val.Value.Resource.EntityId), + EntityType: fwflex.StringToFramework(ctx, val.Value.Resource.EntityType), }) } else { tpl.Resource = fwtypes.NewListNestedObjectValueOfNull[templateLinkedResource](ctx) @@ -439,10 +438,15 @@ func (r *resourcePolicy) Update(ctx context.Context, req resource.UpdateRequest, in.PolicyId = fwflex.StringFromFramework(ctx, state.PolicyID) in.PolicyStoreId = fwflex.StringFromFramework(ctx, state.PolicyStoreID) - defPlan, diags := plan.Definition.ToPtr(ctx) - defState, diags := state.Definition.ToPtr(ctx) - resp.Diagnostics.Append(diags...) - if diags.HasError() { + defPlan, diagsPlan := plan.Definition.ToPtr(ctx) + resp.Diagnostics.Append(diagsPlan...) + if resp.Diagnostics.HasError() { + return + } + + defState, diagsState := state.Definition.ToPtr(ctx) + resp.Diagnostics.Append(diagsState...) + if resp.Diagnostics.HasError() { return } @@ -513,10 +517,14 @@ func (r *resourcePolicy) ModifyPlan(ctx context.Context, req resource.ModifyPlan } if !plan.Definition.Equal(state.Definition) { - defPlan, diags := plan.Definition.ToPtr(ctx) - defState, diags := state.Definition.ToPtr(ctx) - resp.Diagnostics.Append(diags...) - if diags.HasError() { + defPlan, diagsPlan := plan.Definition.ToPtr(ctx) + resp.Diagnostics.Append(diagsPlan...) + if resp.Diagnostics.HasError() { + return + } + defState, diagsState := state.Definition.ToPtr(ctx) + resp.Diagnostics.Append(diagsState...) + if resp.Diagnostics.HasError() { return }