Skip to content

Commit

Permalink
Implement methods for user secrets management
Browse files Browse the repository at this point in the history
This commit introduces internal/juju/userSecret and adds method to add user secrets.

Implement internal Juju secrets add, update, and remove functionality

This commit introduces several changes to the Juju client in the `internal/juju/client.go` file.
It includes the implementation of methods for adding, updating, and removing secrets. Additionally,

Furthermore, the commit includes changes to the `secret.go` file, introducing new types for managinng secrets.
It also includes changes to the `interfaces.go` file, defining new interfaces for the Juju client API.

Add secretURI to UpdateSecret

Add secretURI to DeleteSecret

Add AutoPrunt to UpdateSecret schema

Add SecretId to ReadSecret func instead of name.

Add lost Asserts.

Add secretNotFoundError

Extract mocks creation into separate suite.

Introduce typedError(err) usage in ClientAPI funcs.

Add renaming to UpdateSecret

Use struct raather than pointer for Output structures.

Introcue NewName in Update input struct.

Use pointers in all places in structs where the parameter is not
neccessary.

Implement schema for user secrets management

This commit introduces the ability to add, update, and remove user secrets in the schema.
This is done through the `userSecretResource` struct, which has methods for each of these actions.
The `Add`, `Update`, and `Remove` methods are currently stubbed out and will need to be implemented in future commits.

Add ReadSecret function implementation.

Implement Delete secret function

Add base64 encoding co Create and change Read fucntion to get value with
decode.

Add base64 encoding for values in Update
  • Loading branch information
anvial committed Apr 17, 2024
1 parent 7951fea commit 967b436
Show file tree
Hide file tree
Showing 8 changed files with 557 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ For example, here they are set using the currently active controller:
```shell
export CONTROLLER=$(juju whoami | yq .Controller)
export JUJU_AGENT_VERSION="$(juju show-controller | yq .[$CONTROLLER].details.\"agent-version\"|tr -d '"')"
export JUJU_CONTROLLER_ADDRESSES="$(juju show-controller | yq '.[$CONTROLLER]'.details.\"api-endpoints\" | tr -d "[]' "|tr -d '"'|tr -d '\n')"
export JUJU_USERNAME="$(cat ~/.local/share/juju/accounts.yaml | yq .controllers.$CONTROLLER.user|tr -d '"')"
export JUJU_PASSWORD="$(cat ~/.local/share/juju/accounts.yaml | yq .controllers.$CONTROLLER.password|tr -d '"')"
Expand Down
2 changes: 1 addition & 1 deletion internal/juju/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ func (c *modelsClient) CreateModel(input CreateModelInput) (CreateModelResponse,
resp.Type = modelInfo.Type.String()
resp.UUID = modelInfo.UUID

// Add the model to the client cache of jujuModel
// Add a model object on the client internal to the provider
c.AddModel(modelInfo.Name, modelInfo.UUID, modelInfo.Type)

// set constraints when required
Expand Down
1 change: 1 addition & 0 deletions internal/juju/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ func populateControllerConfig() {
}

localProviderConfig = map[string]string{}
localProviderConfig["JUJU_AGENT_VERSION"] = controllerConfig.ProviderDetails.AgentVersion
localProviderConfig["JUJU_CONTROLLER_ADDRESSES"] = strings.Join(controllerConfig.ProviderDetails.ApiEndpoints, ",")
localProviderConfig["JUJU_CA_CERT"] = controllerConfig.ProviderDetails.CACert
localProviderConfig["JUJU_USERNAME"] = controllerConfig.Account.User
Expand Down
1 change: 1 addition & 0 deletions internal/provider/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const (
LogResourceOffer = "resource-offer"
LogResourceSSHKey = "resource-sshkey"
LogResourceUser = "resource-user"
LogResourceSecret = "resource-secret"
)

const LogResourceIntegration = "resource-integration"
Expand Down
1 change: 1 addition & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ func (p *jujuProvider) Resources(_ context.Context) []func() resource.Resource {
func() resource.Resource { return NewOfferResource() },
func() resource.Resource { return NewSSHKeyResource() },
func() resource.Resource { return NewUserResource() },
func() resource.Resource { return NewSecretResource() },
}
}

Expand Down
308 changes: 308 additions & 0 deletions internal/provider/resource_secret.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
// Copyright 2024 Canonical Ltd.
// Licensed under the Apache License, Version 2.0, see LICENCE file for details.

package provider

import (
"context"
"fmt"

"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/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/juju/terraform-provider-juju/internal/juju"
)

// Ensure provider defined types fully satisfy framework interfaces.
var _ resource.Resource = &secretResource{}
var _ resource.ResourceWithConfigure = &secretResource{}
var _ resource.ResourceWithImportState = &secretResource{}

func NewSecretResource() resource.Resource {
return &secretResource{}
}

type secretResource struct {
client *juju.Client

// subCtx is the context created with the new tflog subsystem for applications.
subCtx context.Context
}

type secretResourceModel struct {
// Model to which the secret belongs. This attribute is required for all actions.
Model types.String `tfsdk:"model"`
// Name of the secret to be updated or removed. This attribute is required for 'update' and 'remove' actions.
Name types.String `tfsdk:"name"`
// Value of the secret to be added or updated. This attribute is required for 'add' and 'update' actions.
// Template: [<key>[#base64]]=<value>[ ...]
Value types.Map `tfsdk:"value"`
// SecretId is the ID of the secret to be updated or removed. This attribute is required for 'update' and 'remove' actions.
SecretId types.String `tfsdk:"secret_id"`
// Info is the description of the secret. This attribute is optional for all actions.
Info types.String `tfsdk:"info"`
}

func (s *secretResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}

func (s *secretResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_secret"
}

func (s *secretResource) Schema(_ context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "A resource that represents a Juju secret.",
Attributes: map[string]schema.Attribute{
"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.",
Optional: true,
},
"value": schema.MapAttribute{
Description: "The value map of the secret. There can be more than one key-value pair.",
ElementType: types.StringType,
Required: true,
Sensitive: true,
},
"secret_id": schema.StringAttribute{
Description: "The ID of the secret.",
Computed: true,
},
"info": schema.StringAttribute{
Description: "The description of the secret.",
Optional: true,
},
},
}
}

// Configure sets up the Juju client for the secret resource.
func (s *secretResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
// Prevent panic if the provider has not been configured.
if req.ProviderData == nil {
return
}

client, ok := req.ProviderData.(*juju.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *juju.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
s.client = client
// Create the local logging subsystem here, using the TF context when creating it.
s.subCtx = tflog.NewSubsystem(ctx, LogResourceSecret)
}

// Create creates a new secret in the Juju model.
func (s *secretResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
// Prevent panic if the provider has not been configured.
if s.client == nil {
addClientNotConfiguredError(&resp.Diagnostics, "secret", "create")
return
}

var plan secretResourceModel

// Read Terraform plan into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}

s.trace(fmt.Sprintf("creating secret resource %q", plan.Name.ValueString()))

secretValue := make(map[string]string)
resp.Diagnostics.Append(plan.Value.ElementsAs(ctx, &secretValue, false)...)

createSecretOutput, err := s.client.Secrets.CreateSecret(&juju.CreateSecretInput{
ModelName: plan.Model.ValueString(),
Name: plan.Name.ValueString(),
Value: secretValue,
Info: plan.Info.ValueString(),
})
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to add secret, got error: %s", err))
return
}

plan.SecretId = types.StringValue(createSecretOutput.SecretId)

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

s.trace(fmt.Sprintf("created secret resource %q", plan.SecretId))
}

// Read reads the details of a secret in the Juju model.
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 {
addClientNotConfiguredError(&resp.Diagnostics, "secret", "read")
return
}

var state secretResourceModel

// Read Terraform configuration state into the model
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}

s.trace(fmt.Sprintf("reading secret resource %q", state.SecretId))

readSecretOutput, err := s.client.Secrets.ReadSecret(&juju.ReadSecretInput{
SecretId: state.SecretId.ValueString(),
ModelName: state.Model.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))
return
}

// 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)...)

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

// Update updates the details of a secret in the Juju model.
func (s *secretResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
// Prevent panic if the provider has not been configured.
if s.client == nil {
addClientNotConfiguredError(&resp.Diagnostics, "secret", "update")
return
}

var plan, state secretResourceModel

// Read Terraform plan and state into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}

s.trace(fmt.Sprintf("updating secret resource %q", state.SecretId))
s.trace(fmt.Sprintf("Update - current state: %v", state))
s.trace(fmt.Sprintf("Update - proposed plan: %v", plan))

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
if !plan.Name.Equal(state.Name) {
noChange = false
state.Name = plan.Name
updatedSecretInput.Name = plan.Name.ValueStringPointer()
}

// Check if the secret value has changed
if !plan.Value.Equal(state.Value) {
noChange = 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
if !plan.Info.Equal(state.Info) {
noChange = false
state.Info = plan.Info
updatedSecretInput.Info = plan.Info.ValueStringPointer()
}

if noChange {
return
}

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("updated secret resource %q", state.SecretId))
}

// Delete removes a secret from the Juju model.
func (s *secretResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
// Prevent panic if the provider has not been configured.
if s.client == nil {
addClientNotConfiguredError(&resp.Diagnostics, "secret", "delete")
return
}

var state secretResourceModel

// Read Terraform configuration state into the model
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}

s.trace(fmt.Sprintf("deleting secret resource %q", state.SecretId))

err := s.client.Secrets.DeleteSecret(&juju.DeleteSecretInput{
ModelName: state.Model.ValueString(),
SecretId: state.SecretId.ValueString(),
})
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete secret, got error: %s", err))
return
}

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

func (s *secretResource) trace(msg string, additionalFields ...map[string]interface{}) {
if s.subCtx == nil {
return
}
tflog.SubsystemTrace(s.subCtx, LogResourceSecret, msg, additionalFields...)
}
Loading

0 comments on commit 967b436

Please sign in to comment.