diff --git a/internal/juju/secrets.go b/internal/juju/secrets.go index 2839c62f..201b934f 100644 --- a/internal/juju/secrets.go +++ b/internal/juju/secrets.go @@ -8,7 +8,6 @@ import ( "errors" "fmt" - jujuerrors "github.com/juju/errors" "github.com/juju/juju/api" apisecrets "github.com/juju/juju/api/client/secrets" coresecrets "github.com/juju/juju/core/secrets" @@ -200,12 +199,14 @@ func (c *secretsClient) UpdateSecret(input *UpdateSecretInput) error { return err } if input.Name == nil { + fmt.Println("Updating secret by ID") // Update secret without changing the name err = secretAPIClient.UpdateSecret(secretURI, "", input.AutoPrune, "", info, value) if err != nil { return typedError(err) } } else { + fmt.Println("Updating secret by ID with a new name") // Update secret with a new name err = secretAPIClient.UpdateSecret(secretURI, "", input.AutoPrune, *input.Name, info, value) if err != nil { @@ -216,6 +217,8 @@ func (c *secretsClient) UpdateSecret(input *UpdateSecretInput) error { return errors.New("updating secrets by name is not supported") } + fmt.Println("Secret updated successfully") + return nil } @@ -226,16 +229,22 @@ func (c *secretsClient) DeleteSecret(input *DeleteSecretInput) error { return err } + // print delete secret input + fmt.Printf("deleteSecretInput = %+v\n", input) + secretAPIClient := c.getSecretAPIClient(conn) secretURI, err := coresecrets.ParseURI(input.SecretId) + + fmt.Printf("secretURI = %+v\n", secretURI) if err != nil { return err } // TODO: think about removing concrete revision. err = secretAPIClient.RemoveSecret(secretURI, "", nil) - if !errors.Is(err, jujuerrors.NotFound) { + if err != nil { return typedError(err) } + fmt.Println("Secret deleted successfully") return nil } diff --git a/internal/provider/resource_secret.go b/internal/provider/resource_secret.go index a3c688d5..729c99bf 100644 --- a/internal/provider/resource_secret.go +++ b/internal/provider/resource_secret.go @@ -6,12 +6,14 @@ package provider import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "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/types" "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/juju/terraform-provider-juju/internal/juju" ) @@ -60,13 +62,16 @@ func (s *secretResource) Schema(_ context.Context, req resource.SchemaRequest, r "model": schema.StringAttribute{ Description: "The model in which the secret belongs.", Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, "name": schema.StringAttribute{ - Description: "The name of the secret. Note that the name cannot be changed once set.", + Description: "The name of the secret.", Optional: true, }, "value": schema.MapAttribute{ - Description: "The value of the secret.", + Description: "The value map of the secret. There can be more than one key-value pair.", ElementType: types.StringType, Required: true, Sensitive: true, @@ -117,7 +122,7 @@ func (s *secretResource) Create(ctx context.Context, req resource.CreateRequest, return } - var secretValue map[string]string + secretValue := make(map[string]string) resp.Diagnostics.Append(plan.Value.ElementsAs(ctx, &secretValue, false)...) createSecretOutput, err := s.client.Secrets.CreateSecret(&juju.CreateSecretInput{ @@ -140,10 +145,6 @@ func (s *secretResource) Create(ctx context.Context, req resource.CreateRequest, s.trace(fmt.Sprintf("create secret resource %q", plan.SecretId)) } -func StringPtr(s string) *string { - return &s -} - func (s *secretResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // Prevent panic if the provider has not been configured. if s.client == nil { @@ -162,7 +163,8 @@ func (s *secretResource) Read(ctx context.Context, req resource.ReadRequest, res readSecretOutput, err := s.client.Secrets.ReadSecret(&juju.ReadSecretInput{ SecretId: state.SecretId.ValueString(), ModelName: state.Model.ValueString(), - Name: StringPtr(state.Name.ValueString()), + Name: state.Name.ValueStringPointer(), + Revision: nil, }) if err != nil { resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read secret, got error: %s", err)) @@ -170,11 +172,23 @@ func (s *secretResource) Read(ctx context.Context, req resource.ReadRequest, res } s.trace(fmt.Sprintf("read secret resource %q", state.SecretId)) - stateConfig := make(map[string]string, len(readSecretOutput.Value)) - resp.Diagnostics.Append(state.Value.ElementsAs(ctx, &stateConfig, false)...) + // Print the secret details + fmt.Printf("r = %#v\n", readSecretOutput) + + // Save the secret details into the Terraform state + if !state.Name.IsNull() { + state.Name = types.StringValue(readSecretOutput.Name) + } + if !state.Info.IsNull() { + state.Info = types.StringValue(readSecretOutput.Info) + } + + secretValue, errDiag := types.MapValueFrom(ctx, types.StringType, readSecretOutput.Value) + resp.Diagnostics.Append(errDiag...) if resp.Diagnostics.HasError() { return } + state.Value = secretValue // Save state into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) @@ -196,48 +210,63 @@ func (s *secretResource) Update(ctx context.Context, req resource.UpdateRequest, return } + // print the plan and state + fmt.Printf("plan = %+v\n", plan) + fmt.Printf("state = %+v\n", state) + var err error noChange := true + var updatedSecretInput juju.UpdateSecretInput + + updatedSecretInput.ModelName = state.Model.ValueString() + updatedSecretInput.SecretId = state.SecretId.ValueString() + // Check if the secret name has changed - secretName := "" if !plan.Name.Equal(state.Name) { noChange = false - secretName = plan.Name.ValueString() + state.Name = plan.Name + updatedSecretInput.Name = plan.Name.ValueStringPointer() + } else { + updatedSecretInput.Name = state.Name.ValueStringPointer() } // Check if the secret value has changed - secretValue := make(map[string]string) if !plan.Value.Equal(state.Value) { noChange = false - resp.Diagnostics.Append(plan.Value.ElementsAs(ctx, &secretValue, false)...) + resp.Diagnostics.Append(plan.Value.ElementsAs(ctx, &state.Value, false)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(plan.Value.ElementsAs(ctx, &updatedSecretInput.Value, false)...) if resp.Diagnostics.HasError() { return } } // Check if the secret info has changed - secretInfo := "" if !plan.Info.Equal(state.Info) { noChange = false - secretInfo = plan.Info.ValueString() + state.Info = plan.Info + updatedSecretInput.Info = plan.Info.ValueStringPointer() } if noChange { return } - err = s.client.Secrets.UpdateSecret(&juju.UpdateSecretInput{ - ModelName: state.Model.ValueString(), - Name: &secretName, - SecretId: state.SecretId.ValueString(), - Value: &secretValue, - Info: &secretInfo, - }) + // print the updated secret input + fmt.Printf("updatedSecretInput = %+v\n", updatedSecretInput) + + err = s.client.Secrets.UpdateSecret(&updatedSecretInput) if err != nil { resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update secret, got error: %s", err)) return } + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) + s.trace(fmt.Sprintf("update secret resource %q", state.SecretId)) } @@ -256,6 +285,10 @@ func (s *secretResource) Delete(ctx context.Context, req resource.DeleteRequest, return } + s.trace("Deleting", map[string]interface{}{ + "ID": state.SecretId.ValueString(), + }) + err := s.client.Secrets.DeleteSecret(&juju.DeleteSecretInput{ ModelName: state.Model.ValueString(), SecretId: state.SecretId.ValueString(), @@ -264,7 +297,6 @@ func (s *secretResource) Delete(ctx context.Context, req resource.DeleteRequest, resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete secret, got error: %s", err)) return } - s.trace(fmt.Sprintf("delete secret resource %q", state.SecretId)) } func (s *secretResource) trace(msg string, additionalFields ...map[string]interface{}) { diff --git a/internal/provider/resource_secret_test.go b/internal/provider/resource_secret_test.go index 7ac5bada..9a5deeec 100644 --- a/internal/provider/resource_secret_test.go +++ b/internal/provider/resource_secret_test.go @@ -3,4 +3,188 @@ package provider -// TODO: add tests for the resource_user_secret.go file +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + internaltesting "github.com/juju/terraform-provider-juju/internal/testing" +) + +func TestAcc_ResourceSecret_CreateWithInfo(t *testing.T) { + modelName := acctest.RandomWithPrefix("tf-test-model") + secretName := "tf-test-secret" + secretInfo := "test-info" + secretValue := map[string]string{ + "key1": "value1", + "key2": "value2", + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: frameworkProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccResourceSecret(modelName, secretName, secretValue, secretInfo), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("juju_secret."+secretName, "model", modelName), + resource.TestCheckResourceAttr("juju_secret."+secretName, "name", secretName), + resource.TestCheckResourceAttr("juju_secret."+secretName, "info", secretInfo), + resource.TestCheckResourceAttr("juju_secret."+secretName, "value.key1", "value1"), + resource.TestCheckResourceAttr("juju_secret."+secretName, "value.key2", "value2"), + ), + }, + }, + }) +} + +func TestAcc_ResourceSecret_CreateWithNoInfo(t *testing.T) { + modelName := acctest.RandomWithPrefix("tf-test-model") + secretName := "tf-test-secret" + secretValue := map[string]string{ + "key1": "value1", + "key2": "value2", + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: frameworkProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccResourceSecret(modelName, secretName, secretValue, ""), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("juju_secret."+secretName, "model", modelName), + resource.TestCheckResourceAttr("juju_secret."+secretName, "name", secretName), + resource.TestCheckResourceAttr("juju_secret."+secretName, "value.key1", "value1"), + resource.TestCheckResourceAttr("juju_secret."+secretName, "value.key2", "value2"), + ), + }, + }, + }) +} + +func TestAcc_ResourceSecret_Update(t *testing.T) { + modelName := acctest.RandomWithPrefix("tf-test-model") + secretName := "tf-test-secret" + secretInfo := "test-info" + + updatedSecretInfo := "updated-test-info" + + secretValue := map[string]string{ + "key1": "value1", + "key2": "value2", + } + + secretValueUpdated := map[string]string{ + "key1": "value1", + "key2": "newValue2", + "key3": "value3", + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: frameworkProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccResourceSecret(modelName, secretName, secretValue, secretInfo), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("juju_secret."+secretName, "model", modelName), + resource.TestCheckResourceAttr("juju_secret."+secretName, "name", secretName), + resource.TestCheckResourceAttr("juju_secret."+secretName, "info", secretInfo), + resource.TestCheckResourceAttr("juju_secret."+secretName, "value.key1", "value1"), + resource.TestCheckResourceAttr("juju_secret."+secretName, "value.key2", "value2"), + ), + }, + { + Config: testAccResourceSecret(modelName, secretName, secretValueUpdated, updatedSecretInfo), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("juju_secret."+secretName, "model", modelName), + resource.TestCheckResourceAttr("juju_secret."+secretName, "name", secretName), + resource.TestCheckResourceAttr("juju_secret."+secretName, "info", updatedSecretInfo), + resource.TestCheckResourceAttr("juju_secret."+secretName, "value.key1", "value1"), + resource.TestCheckResourceAttr("juju_secret."+secretName, "value.key2", "newValue2"), + resource.TestCheckResourceAttr("juju_secret."+secretName, "value.key3", "value3"), + ), + }, + }, + }) +} + +func TestAcc_ResourceSecret_Delete(t *testing.T) { + modelName := acctest.RandomWithPrefix("tf-test-model") + secretName := "tf-test-secret" + secretInfo := "test-info" + secretValue := map[string]string{ + "key1": "value1", + } + + // print plan + t.Logf(" Step 1 plan = %+v\n", testAccResourceSecret(modelName, secretName, secretValue, secretInfo)) + t.Logf(" Step 2 plan = %+v\n", testAccResourceOnlyModel(modelName)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: frameworkProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccResourceSecret(modelName, secretName, secretValue, secretInfo), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("juju_secret."+secretName, "model", modelName), + resource.TestCheckResourceAttr("juju_secret."+secretName, "name", secretName), + resource.TestCheckResourceAttr("juju_secret."+secretName, "info", secretInfo), + resource.TestCheckResourceAttr("juju_secret."+secretName, "value.key1", "value1"), + ), + }, + { + Destroy: true, + Config: testAccResourceOnlyModel(modelName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckNoResourceAttr("juju_secret."+secretName, "model"), + resource.TestCheckNoResourceAttr("juju_secret."+secretName, "name"), + resource.TestCheckNoResourceAttr("juju_secret."+secretName, "info"), + resource.TestCheckNoResourceAttr("juju_secret."+secretName, "value"), + ), + }, + }, + }) +} + +func testAccResourceOnlyModel(modelName string) string { + return internaltesting.GetStringFromTemplateWithData( + "testAccResourceOnlyModel", + ` +resource "juju_model" "{{.ModelName}}" { + name = "{{.ModelName}}" +} +`, internaltesting.TemplateData{ + "ModelName": modelName, + }) +} + +func testAccResourceSecret(modelName, secretName string, secretValue map[string]string, secretInfo string) string { + return internaltesting.GetStringFromTemplateWithData( + "testAccResourceSecret", + ` +resource "juju_model" "{{.ModelName}}" { + name = "{{.ModelName}}" +} + +resource "juju_secret" "{{.SecretName}}" { + model = juju_model.{{.ModelName}}.name + name = "{{.SecretName}}" + value = { + {{- range $key, $value := .SecretValue }} + "{{$key}}" = "{{$value}}" + {{- end }} + } + {{- if ne .SecretInfo "" }} + info = "{{.SecretInfo}}" + {{- end }} +} +`, internaltesting.TemplateData{ + "ModelName": modelName, + "SecretName": secretName, + "SecretValue": secretValue, + "SecretInfo": secretInfo, + }) +}