diff --git a/internal/provider/resource_value.go b/internal/provider/resource_value.go index ecd1e91..eab9b9b 100644 --- a/internal/provider/resource_value.go +++ b/internal/provider/resource_value.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "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" @@ -12,6 +13,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/humanitec/humanitec-go-autogen" "github.com/humanitec/humanitec-go-autogen/client" @@ -41,7 +44,7 @@ type ValueModel struct { Description types.String `tfsdk:"description"` IsSecret types.Bool `tfsdk:"is_secret"` Value types.String `tfsdk:"value"` - SecretRef *SecretRef `tfsdk:"secret_ref"` + SecretRef types.Object `tfsdk:"secret_ref"` } // SecretRef describes a secret reference that might contain a secret value or a reference to an already stored secret. @@ -52,6 +55,15 @@ type SecretRef struct { Value types.String `tfsdk:"value"` } +func SecretRefAttributeTypes() map[string]attr.Type { + return map[string]attr.Type{ + "ref": types.StringType, + "store": types.StringType, + "version": types.StringType, + "value": types.StringType, + } +} + func (r *ResourceValue) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_value" } @@ -107,18 +119,22 @@ func (r *ResourceValue) Schema(ctx context.Context, req resource.SchemaRequest, "secret_ref": schema.SingleNestedAttribute{ MarkdownDescription: "The sensitive value that will be stored in the primary organization store or a reference to a sensitive value already stored in one of the registered stores. It can't be defined if is_secret is false or value is defined.", Optional: true, + Computed: true, Attributes: map[string]schema.Attribute{ "ref": schema.StringAttribute{ MarkdownDescription: "Secret reference in the format of the target store. It can't be defined if value is defined.", Optional: true, + Computed: true, }, "store": schema.StringAttribute{ MarkdownDescription: "Secret Store id. This can't be humanitec (our internal Secret Store). It's mandatory if ref is defined and can't be used if value is defined.", Optional: true, + Computed: true, }, "version": schema.StringAttribute{ MarkdownDescription: "Only valid if ref is defined. It's the version of the secret as defined in the target store.", Optional: true, + Computed: true, }, "value": schema.StringAttribute{ MarkdownDescription: "Value to store in the secret store. It can't be defined if ref is defined.", @@ -156,21 +172,40 @@ func envValueIdPrefix(appID, envID string) string { return strings.Join([]string{appID, envID}, "/") } -func parseValueResponse(res *client.ValueResponse, data *ValueModel, idPrefix string) { +func parseValueResponse(ctx context.Context, res *client.ValueResponse, data *ValueModel, idPrefix string) { data.ID = types.StringValue(strings.Join([]string{idPrefix, res.Key}, "/")) data.Key = types.StringValue(res.Key) data.Description = types.StringValue(res.Description) data.IsSecret = types.BoolValue(res.IsSecret) if !res.IsSecret { data.Value = types.StringValue(res.Value) + data.SecretRef = basetypes.NewObjectNull(SecretRefAttributeTypes()) } else { - data.SecretRef = &SecretRef{ - Ref: types.StringValue(*res.SecretKey), - Store: types.StringValue(*res.SecretStoreId), + var secretRef SecretRef + if data.SecretRef.IsUnknown() { + secretRef = SecretRef{} + } else { + diags := data.SecretRef.As(ctx, &secretRef, basetypes.ObjectAsOptions{}) + if diags.HasError() { + tflog.Debug(ctx, "can't populate secretRef from model", map[string]interface{}{"err": diags.Errors()}) + return + } } + + secretRef.Ref = types.StringValue(*res.SecretKey) + secretRef.Store = types.StringValue(*res.SecretStoreId) if res.SecretVersion != nil { - data.SecretRef.Version = types.StringValue(*res.SecretVersion) + secretRef.Version = types.StringValue(*res.SecretVersion) + } + if secretRef.Value.IsNull() { + secretRef.Value = types.StringNull() + } + objectValue, diags := types.ObjectValueFrom(ctx, SecretRefAttributeTypes(), secretRef) + if diags.HasError() { + tflog.Debug(ctx, "can't decode object from secret ref", map[string]interface{}{"err": diags}) + return } + data.SecretRef = objectValue } } @@ -194,14 +229,25 @@ func (r *ResourceValue) Create(ctx context.Context, req resource.CreateRequest, Description: data.Description.ValueStringPointer(), IsSecret: data.IsSecret.ValueBoolPointer(), } - if data.SecretRef == nil { + if !data.Value.IsNull() { createPayload.Value = data.Value.ValueStringPointer() } else { - createPayload.SecretRef = &client.SecretReference{ - Ref: data.SecretRef.Ref.ValueStringPointer(), - Store: data.SecretRef.Store.ValueStringPointer(), - Version: data.SecretRef.Version.ValueStringPointer(), - Value: data.SecretRef.Value.ValueStringPointer(), + var secretRef SecretRef + diags := data.SecretRef.As(ctx, &secretRef, basetypes.ObjectAsOptions{}) + if diags.HasError() { + tflog.Debug(ctx, "can't populate secretRef from model", map[string]interface{}{"err": diags.Errors()}) + return + } + if !secretRef.Value.IsNull() { + createPayload.SecretRef = &client.SecretReference{ + Value: secretRef.Value.ValueStringPointer(), + } + } else { + createPayload.SecretRef = &client.SecretReference{ + Ref: secretRef.Ref.ValueStringPointer(), + Store: secretRef.Store.ValueStringPointer(), + Version: secretRef.Version.ValueStringPointer(), + } } } @@ -236,7 +282,7 @@ func (r *ResourceValue) Create(ctx context.Context, req resource.CreateRequest, idPrefix = envValueIdPrefix(appID, envID) } - parseValueResponse(res, data, idPrefix) + parseValueResponse(ctx, res, data, idPrefix) // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) @@ -299,7 +345,7 @@ func (r *ResourceValue) Read(ctx context.Context, req resource.ReadRequest, resp return } - parseValueResponse(&value, data, idPrefix) + parseValueResponse(ctx, &value, data, idPrefix) // Save updated data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) @@ -321,17 +367,27 @@ func (r *ResourceValue) Update(ctx context.Context, req resource.UpdateRequest, Description: data.Description.ValueStringPointer(), IsSecret: data.IsSecret.ValueBoolPointer(), } - if data.SecretRef == nil { + if !data.Value.IsNull() { editPayload.Value = data.Value.ValueStringPointer() } else { - editPayload.SecretRef = &client.SecretReference{ - Ref: data.SecretRef.Ref.ValueStringPointer(), - Store: data.SecretRef.Store.ValueStringPointer(), - Version: data.SecretRef.Version.ValueStringPointer(), - Value: data.SecretRef.Value.ValueStringPointer(), + var secretRef SecretRef + diags := data.SecretRef.As(ctx, &secretRef, basetypes.ObjectAsOptions{}) + if diags.HasError() { + tflog.Debug(ctx, "can't populate secretRef from model", map[string]interface{}{"err": diags.Errors()}) + return + } + if !secretRef.Value.IsNull() { + editPayload.SecretRef = &client.SecretReference{ + Value: secretRef.Value.ValueStringPointer(), + } + } else { + editPayload.SecretRef = &client.SecretReference{ + Ref: secretRef.Ref.ValueStringPointer(), + Store: secretRef.Store.ValueStringPointer(), + Version: secretRef.Version.ValueStringPointer(), + } } } - if data.EnvID.IsNull() { httpResp, err := r.client.PutOrgsOrgIdAppsAppIdValuesKeyWithResponse(ctx, r.orgId, appID, data.Key.ValueString(), editPayload) if err != nil { @@ -363,7 +419,7 @@ func (r *ResourceValue) Update(ctx context.Context, req resource.UpdateRequest, idPrefix = envValueIdPrefix(appID, envID) } - parseValueResponse(res, data, idPrefix) + parseValueResponse(ctx, res, data, idPrefix) // Save updated data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) diff --git a/internal/provider/resource_value_test.go b/internal/provider/resource_value_test.go index 758b012..7b62f95 100644 --- a/internal/provider/resource_value_test.go +++ b/internal/provider/resource_value_test.go @@ -50,9 +50,76 @@ func TestAccResourceValue(t *testing.T) { }) } +func TestAccResourceValueWithSecretValue(t *testing.T) { + appID := fmt.Sprintf("val-test-app-%d", time.Now().UnixNano()) + key := "VAL_SECRET_1" + orgID := os.Getenv("HUMANITEC_ORG_ID") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: testAccResourceVALUETestAccResourceValueSecret(appID, key, "Example value with secret"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("humanitec_value.app_val_with_secret", "key", key), + resource.TestCheckResourceAttr("humanitec_value.app_val_with_secret", "description", "Example value with secret"), + resource.TestCheckResourceAttr("humanitec_value.app_val_with_secret", "secret_ref.ref", fmt.Sprintf("orgs/%s/apps/%s/secret_values/%s/.value", orgID, appID, key)), + ), + }, + // ImportState testing + { + ResourceName: "humanitec_value.app_val_with_secret", + ImportState: true, + ImportStateIdFunc: func(s *terraform.State) (string, error) { + return fmt.Sprintf("%s/%s", appID, key), nil + }, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"secret_ref", "value"}, + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func TestAccResourceValueWithSecretValueSecretRefValue(t *testing.T) { + appID := fmt.Sprintf("val-test-app-%d", time.Now().UnixNano()) + key := "VAL_SECRET_1" + orgID := os.Getenv("HUMANITEC_ORG_ID") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: testAccResourceVALUETestAccResourceValueSecretRefValue(appID, key, "Example value with secret set via secret reference value"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("humanitec_value.app_val_with_secret", "key", key), + resource.TestCheckResourceAttr("humanitec_value.app_val_with_secret", "description", "Example value with secret set via secret reference value"), + resource.TestCheckResourceAttr("humanitec_value.app_val_with_secret", "secret_ref.ref", fmt.Sprintf("orgs/%s/apps/%s/secret_values/%s/.value", orgID, appID, key)), + ), + }, + // ImportState testing + { + ResourceName: "humanitec_value.app_val_with_secret", + ImportState: true, + ImportStateIdFunc: func(s *terraform.State) (string, error) { + return fmt.Sprintf("%s/%s", appID, key), nil + }, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"secret_ref"}, + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + func TestAccResourceValueWithSecretRef(t *testing.T) { appID := fmt.Sprintf("val-test-app-%d", time.Now().UnixNano()) key := "VAL_SECRET_REF_1" + orgID := os.Getenv("HUMANITEC_ORG_ID") resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -62,26 +129,35 @@ func TestAccResourceValueWithSecretRef(t *testing.T) { { Config: testAccResourceVALUETestAccResourceValueSecretRef(appID, key, "path/to/secret", "Example value with secret reference"), Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("humanitec_value.app_val_with_secret_ref1", "key", key), - resource.TestCheckResourceAttr("humanitec_value.app_val_with_secret_ref1", "description", "Example value with secret reference"), - resource.TestCheckResourceAttr("humanitec_value.app_val_with_secret_ref1", "secret_ref.ref", "path/to/secret"), + resource.TestCheckResourceAttr("humanitec_value.app_val_with_secret", "key", key), + resource.TestCheckResourceAttr("humanitec_value.app_val_with_secret", "description", "Example value with secret reference"), + resource.TestCheckResourceAttr("humanitec_value.app_val_with_secret", "secret_ref.ref", "path/to/secret"), ), }, // ImportState testing { - ResourceName: "humanitec_value.app_val_with_secret_ref1", + ResourceName: "humanitec_value.app_val_with_secret", ImportState: true, ImportStateIdFunc: func(s *terraform.State) (string, error) { return fmt.Sprintf("%s/%s", appID, key), nil }, - ImportStateVerify: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"secret_ref"}, }, // Update and Read testing { Config: testAccResourceVALUETestAccResourceValueSecretRef(appID, key, "path/to/secret/changed", "Example value with secret reference changed"), Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("humanitec_value.app_val_with_secret_ref1", "description", "Example value with secret reference changed"), - resource.TestCheckResourceAttr("humanitec_value.app_val_with_secret_ref1", "secret_ref.ref", "path/to/secret/changed"), + resource.TestCheckResourceAttr("humanitec_value.app_val_with_secret", "description", "Example value with secret reference changed"), + resource.TestCheckResourceAttr("humanitec_value.app_val_with_secret", "secret_ref.ref", "path/to/secret/changed"), + ), + }, + // Update and Read testing + { + Config: testAccResourceVALUETestAccResourceValueSecret(appID, key, "Example value with secret reference updated with plain value"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("humanitec_value.app_val_with_secret", "description", "Example value with secret reference updated with plain value"), + resource.TestCheckResourceAttr("humanitec_value.app_val_with_secret", "secret_ref.ref", fmt.Sprintf("orgs/%s/apps/%s/secret_values/%s/.value", orgID, appID, key)), ), }, // Delete testing automatically occurs in TestCase @@ -93,6 +169,7 @@ func TestAccResourceValueDeletedOutManually(t *testing.T) { assert := assert.New(t) ctx := context.Background() appID := fmt.Sprintf("val-test-app-%d", time.Now().UnixNano()) + key := "VAL_1" orgID := os.Getenv("HUMANITEC_ORG_ID") @@ -139,6 +216,7 @@ func TestAccResourceValueDeletedOutManually(t *testing.T) { func TestAccResourceValueWithEnv(t *testing.T) { appID := fmt.Sprintf("val-test-app-env-%d", time.Now().UnixNano()) + envID := "dev" key := "VAL_1" @@ -239,17 +317,55 @@ resource "humanitec_application" "val_test" { name = "val-test" } -resource "humanitec_value" "app_val_with_secret_ref1" { +resource "humanitec_value" "app_val_with_secret" { app_id = humanitec_application.val_test.id key = "%s" description = "%s" is_secret = true - secret_ref = { - ref = "%s" - store = "external-store-id" + secret_ref = { + ref = "%s" + store = "external-store-id" version = "1" } } `, appID, key, description, secretPath) } + +func testAccResourceVALUETestAccResourceValueSecret(appID, key, description string) string { + return fmt.Sprintf(` +resource "humanitec_application" "val_test" { + id = "%s" + name = "val-test" +} + +resource "humanitec_value" "app_val_with_secret" { + app_id = humanitec_application.val_test.id + + key = "%s" + description = "%s" + is_secret = true + value = "secret" +} +`, appID, key, description) +} + +func testAccResourceVALUETestAccResourceValueSecretRefValue(appID, key, description string) string { + return fmt.Sprintf(` +resource "humanitec_application" "val_test" { + id = "%s" + name = "val-test" +} + +resource "humanitec_value" "app_val_with_secret" { + app_id = humanitec_application.val_test.id + + key = "%s" + description = "%s" + is_secret = true + secret_ref = { + value = "secret" + } +} +`, appID, key, description) +}