Skip to content

Commit

Permalink
Implement full ImportState for secret and access secret resources.
Browse files Browse the repository at this point in the history
Do not use ImportStatePassthroughID. This allows for importing these
resources by the secret name rather than requiring the secret URI.
It will be the only time we Read one of the resources by name rather
than ID.

Terraform errors if the ID attribute name provided in the ImportStateRequest
is not written to state at some point. In state, this will be
modelname:secretname.

Updating docs with new examples and updated schema information.
  • Loading branch information
hmlanigan committed Apr 22, 2024
1 parent d8a8f72 commit e47f065
Show file tree
Hide file tree
Showing 10 changed files with 238 additions and 58 deletions.
8 changes: 6 additions & 2 deletions docs/resources/access_secret.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ A resource that represents a Juju secret access.

### Required

- `applications` (List of String) The list of applications to which the secret is granted or revoked.
- `applications` (List of String) The list of applications to which the secret is granted.
- `model` (String) The model in which the secret belongs.
- `secret_id` (String) The ID of the secret.
- `secret_id` (String) The ID of the secret. E.g. coj8mulh8b41e8nv6p90

### Read-Only

- `id` (String) The ID of the secret. Used for terraform import.
28 changes: 15 additions & 13 deletions docs/resources/secret.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,24 @@ A resource that represents a Juju secret.
## Example Usage

```terraform
resource "juju_secret" "this" {
resource "juju_secret" "my-secret" {
model = juju_model.development.name
name = "this_secret_name"
name = "my_secret_name"
value = {
key1 = "value1"
key2 = "value2"
}
info = "This is the secret"
}
resource "juju_application" "my-application" {
#
config = {
# Reference my-secret within the plan by using the secret_id
secret = juju_secret.my-secret.secret_id
}
#
}
```

<!-- schema generated by tfplugindocs -->
Expand All @@ -39,21 +48,14 @@ resource "juju_secret" "this" {

### Read-Only

- `secret_id` (String) The ID of the secret.
- `id` (String) The ID of the secret. Used for terraform import.
- `secret_id` (String) The ID of the secret. E.g. coj8mulh8b41e8nv6p90

## Import

Import is supported using the following syntax:

```shell
# Secrets can be imported by using the URI as in the juju show-secrets output.
# Example:
# $juju show-secret secret-name
# coh2uo2ji6m0ue9a7tj0:
# revision: 1
# owner: <model>
# name: secret-name
# created: 2024-04-19T08:46:25Z
# updated: 2024-04-19T08:46:25Z
$ terraform import juju_secret.secret-name coh2uo2ji6m0ue9a7tj0
# Secrets can be imported by using the model and secret names.
$ terraform import juju_secret.secret-name testmodel:secret-name
```
12 changes: 2 additions & 10 deletions examples/resources/juju_secret/import.sh
Original file line number Diff line number Diff line change
@@ -1,10 +1,2 @@
# Secrets can be imported by using the URI as in the juju show-secrets output.
# Example:
# $juju show-secret secret-name
# coh2uo2ji6m0ue9a7tj0:
# revision: 1
# owner: <model>
# name: secret-name
# created: 2024-04-19T08:46:25Z
# updated: 2024-04-19T08:46:25Z
$ terraform import juju_secret.secret-name coh2uo2ji6m0ue9a7tj0
# Secrets can be imported by using the model and secret names.
$ terraform import juju_secret.secret-name testmodel:secret-name
13 changes: 11 additions & 2 deletions examples/resources/juju_secret/resource.tf
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
resource "juju_secret" "this" {
resource "juju_secret" "my-secret" {
model = juju_model.development.name
name = "this_secret_name"
name = "my_secret_name"
value = {
key1 = "value1"
key2 = "value2"
}
info = "This is the secret"
}

resource "juju_application" "my-application" {
#
config = {
# Reference my-secret within the plan by using the secret_id
secret = juju_secret.my-secret.secret_id
}
#
}
12 changes: 2 additions & 10 deletions examples/resources/juju_secret_access/import.sh
Original file line number Diff line number Diff line change
@@ -1,10 +1,2 @@
# Secret access can be imported by using the URI as in the juju show-secrets output.
# Example:
# $juju show-secret secret-name
# coh2uo2ji6m0ue9a7tj0:
# revision: 1
# owner: <model>
# name: secret-name
# created: 2024-04-19T08:46:25Z
# updated: 2024-04-19T08:46:25Z
$ terraform import juju_access_secret.access-secret-name coh2uo2ji6m0ue9a7tj0
# Secret access can be imported by using the model and secret names.
$ terraform import juju_access_secret.access-secret-name modelname:secret-name
15 changes: 13 additions & 2 deletions examples/resources/juju_secret_access/resource.tf
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
resource "juju_access_secret" "this" {
resource "juju_secret" "my-secret" {
model = juju_model.development.name
name = "my_secret_name"
value = {
key1 = "value1"
key2 = "value2"
}
info = "This is the secret"
}

resource "juju_access_secret" "my-secret-access" {
model = juju_model.development.name
applications = [
juju_application.app.name, juju_application.app2.name
]
secret_id = juju_secret.that.secret_id
# Use the secret_id from your secret resource or data source.
secret_id = juju_secret.my-secret.secret_id
}
76 changes: 67 additions & 9 deletions internal/provider/resource_access_secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ package provider
import (
"context"
"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"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
Expand Down Expand Up @@ -43,10 +43,58 @@ type accessSecretResourceModel struct {
SecretId types.String `tfsdk:"secret_id"`
// Applications is the list of applications to which the secret is granted or revoked.
Applications types.List `tfsdk:"applications"`
// ID is used during terraform import.
ID types.String `tfsdk:"id"`
}

// ImportState reads the secret based on the model name and secret name to be
// imported into terraform.
func (s *accessSecretResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
// Prevent panic if the provider has not been configured.
if s.client == nil {
addClientNotConfiguredError(&resp.Diagnostics, "access secret", "import")
return
}
// model:name
parts := strings.Split(req.ID, ":")
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
resp.Diagnostics.AddError(
"Unexpected Import Identifier",
fmt.Sprintf("Expected import identifier with format: <modelname>:<secretname>. Got: %q", req.ID),
)
return
}
modelName := parts[0]
secretName := parts[1]

readSecretOutput, err := s.client.Secrets.ReadSecret(&juju.ReadSecretInput{
ModelName: modelName,
Name: &secretName,
})
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read secret for import, got error: %s", err))
return
}

// Save the secret access details into the Terraform state
state := accessSecretResourceModel{
Model: types.StringValue(modelName),
SecretId: types.StringValue(readSecretOutput.SecretId),
ID: types.StringValue(newSecretID(modelName, readSecretOutput.SecretId)),
}

// Save the secret details into the Terraform state
secretApplications, errDiag := types.ListValueFrom(ctx, types.StringType, readSecretOutput.Applications)
resp.Diagnostics.Append(errDiag...)
if resp.Diagnostics.HasError() {
return
}
state.Applications = secretApplications

// Save state into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)

s.trace(fmt.Sprintf("import access secret resource %q", state.SecretId))
}

func (s *accessSecretResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
Expand All @@ -66,17 +114,24 @@ func (s *accessSecretResource) Schema(_ context.Context, req resource.SchemaRequ
},
},
"secret_id": schema.StringAttribute{
Description: "The ID of the secret.",
Description: "The ID of the secret. E.g. coj8mulh8b41e8nv6p90",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"applications": schema.ListAttribute{
Description: "The list of applications to which the secret is granted or revoked.",
Description: "The list of applications to which the secret is granted.",
Required: true,
ElementType: types.StringType,
},
"id": schema.StringAttribute{
Description: "The ID of the secret. Used for terraform import.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
},
}
}
Expand All @@ -98,7 +153,7 @@ func (s *accessSecretResource) Configure(ctx context.Context, req resource.Confi
}
s.client = client
// Create the local logging subsystem here, using the TF context when creating it.
s.subCtx = tflog.NewSubsystem(ctx, LogResourceSecret)
s.subCtx = tflog.NewSubsystem(ctx, LogResourceAccessSecret)
}

// Create is called when the resource is being created.
Expand Down Expand Up @@ -131,9 +186,10 @@ func (s *accessSecretResource) Create(ctx context.Context, req resource.CreateRe
}

// Save plan into Terraform state
plan.ID = types.StringValue(newSecretID(plan.Model.ValueString(), plan.SecretId.ValueString()))
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)

s.trace(fmt.Sprintf("grant secret access to %q", plan.SecretId))
s.trace(fmt.Sprintf("grant secret access to %s", plan.SecretId))
}

// Read is called when the resource is being read.
Expand Down Expand Up @@ -169,10 +225,12 @@ func (s *accessSecretResource) Read(ctx context.Context, req resource.ReadReques
}
state.Applications = secretApplications

state.ID = types.StringValue(newSecretID(state.Model.ValueString(), readSecretOutput.SecretId))

// Save state into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)

s.trace(fmt.Sprintf("read secret access %q", state.SecretId))
s.trace(fmt.Sprintf("read secret access %s", state.SecretId))
}

// Update is called when the resource is being updated.
Expand Down Expand Up @@ -264,7 +322,7 @@ func (s *accessSecretResource) Update(ctx context.Context, req resource.UpdateRe
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)

s.trace(fmt.Sprintf("update secret access %q", state.SecretId))
s.trace(fmt.Sprintf("update secret access %s", state.SecretId))
}

// Delete is called when the resource is being deleted.
Expand Down Expand Up @@ -310,7 +368,7 @@ func (s *accessSecretResource) Delete(ctx context.Context, req resource.DeleteRe
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)

s.trace(fmt.Sprintf("revoke secret access %q", state.SecretId))
s.trace(fmt.Sprintf("revoke secret access %s", state.SecretId))
}

func (s *accessSecretResource) trace(msg string, additionalFields ...map[string]interface{}) {
Expand Down
32 changes: 32 additions & 0 deletions internal/provider/resource_access_secret_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package provider

import (
"fmt"
"os"
"testing"

Expand Down Expand Up @@ -56,6 +57,37 @@ func TestAcc_ResourceAccessSecret_GrantRevoke(t *testing.T) {
})
}

func TestAcc_ResourceAccessSecret_Import(t *testing.T) {
agentVersion := os.Getenv(TestJujuAgentVersion)
if agentVersion == "" {
t.Errorf("%s is not set", TestJujuAgentVersion)
} else if internaltesting.CompareVersions(agentVersion, "3.3.0") < 0 {
t.Skipf("%s is not set or is below 3.3.0", TestJujuAgentVersion)
}

modelName := acctest.RandomWithPrefix("tf-test-model")

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: frameworkProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccResourceSecretWithAccess(modelName, true),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("juju_access_secret.test_access_secret", "model", modelName),
resource.TestCheckResourceAttr("juju_access_secret.test_access_secret", "applications.0", "jul"),
),
},
{
ImportStateVerify: true,
ImportState: true,
ImportStateId: fmt.Sprintf("%s:test_secret_name", modelName),
ResourceName: "juju_access_secret.test_access_secret",
},
},
})
}

func testAccResourceSecretWithAccess(modelName string, allApplicationAccess bool) string {
return internaltesting.GetStringFromTemplateWithData(
"testAccResourceSecret",
Expand Down
Loading

0 comments on commit e47f065

Please sign in to comment.