diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index 171059f9..80cf388b 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -75,6 +75,7 @@ jobs: run: | CONTROLLER=$(juju whoami --format yaml | yq .controller) + echo "JUJU_AGENT_VERSION=$(juju show-controller | yq .$CONTROLLER.details.agent-version |tr -d '"')" >> $GITHUB_ENV echo "JUJU_CONTROLLER_ADDRESSES=$(juju show-controller | yq .$CONTROLLER.details.api-endpoints | yq -r '. | join(",")')" >> $GITHUB_ENV echo "JUJU_USERNAME=$(juju show-controller | yq .$CONTROLLER.account.user)" >> $GITHUB_ENV echo "JUJU_PASSWORD=$(cat ~/.local/share/juju/accounts.yaml | yq .controllers.$CONTROLLER.password)" >> $GITHUB_ENV diff --git a/.github/workflows/k8s_tunnel.yml b/.github/workflows/k8s_tunnel.yml index 48d7e5bf..9e0f9a43 100644 --- a/.github/workflows/k8s_tunnel.yml +++ b/.github/workflows/k8s_tunnel.yml @@ -68,6 +68,7 @@ jobs: run: | echo "Determine Juju details" CONTROLLER=$(juju whoami --format yaml | yq .controller) + JUJU_AGENT_VERSION=$(juju show-controller | yq .$CONTROLLER.details.agent-version |tr -d '"') JUJU_USERNAME=$(juju show-controller | yq .$CONTROLLER.account.user) JUJU_PASSWORD=$(cat ~/.local/share/juju/accounts.yaml | yq .controllers.$CONTROLLER.password) JUJU_CA_CERT=$(juju show-controller | yq .$CONTROLLER.details.ca-cert | sed ':a;N;$!ba;s/\n/\\n/g') diff --git a/.github/workflows/test_add_machine.yml b/.github/workflows/test_add_machine.yml index ea68416d..f8aaa5a5 100644 --- a/.github/workflows/test_add_machine.yml +++ b/.github/workflows/test_add_machine.yml @@ -76,6 +76,7 @@ jobs: run: | CONTROLLER=$(juju whoami --format yaml | yq .controller) + echo "JUJU_AGENT_VERSION=$(juju show-controller | yq .$CONTROLLER.details.agent-version |tr -d '"')" >> $GITHUB_ENV echo "JUJU_CONTROLLER_ADDRESSES=$(juju show-controller | yq .$CONTROLLER.details.api-endpoints | yq -r '. | join(",")')" >> $GITHUB_ENV echo "JUJU_USERNAME=$(juju show-controller | yq .$CONTROLLER.account.user)" >> $GITHUB_ENV echo "JUJU_PASSWORD=$(cat ~/.local/share/juju/accounts.yaml | yq .controllers.$CONTROLLER.password)" >> $GITHUB_ENV diff --git a/.github/workflows/test_integration.yml b/.github/workflows/test_integration.yml index 9c964d33..bf8b73c6 100644 --- a/.github/workflows/test_integration.yml +++ b/.github/workflows/test_integration.yml @@ -79,6 +79,7 @@ jobs: run: | CONTROLLER=$(juju whoami --format yaml | yq .controller) + echo "JUJU_AGENT_VERSION=$(juju show-controller | yq .$CONTROLLER.details.agent-version |tr -d '"')" >> $GITHUB_ENV echo "JUJU_CONTROLLER_ADDRESSES=$(juju show-controller | yq .$CONTROLLER.details.api-endpoints | yq -r '. | join(",")')" >> $GITHUB_ENV echo "JUJU_USERNAME=$(juju show-controller | yq .$CONTROLLER.account.user)" >> $GITHUB_ENV echo "JUJU_PASSWORD=$(cat ~/.local/share/juju/accounts.yaml | yq .controllers.$CONTROLLER.password)" >> $GITHUB_ENV diff --git a/docs/data-sources/secret.md b/docs/data-sources/secret.md new file mode 100644 index 00000000..273b9c66 --- /dev/null +++ b/docs/data-sources/secret.md @@ -0,0 +1,57 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "juju_secret Data Source - terraform-provider-juju" +subcategory: "" +description: |- + A data source representing a Juju Secret. +--- + +# juju_secret (Data Source) + +A data source representing a Juju Secret. + +## Example Usage + +```terraform +data "juju_model" "my_model" { + name = "default" +} + +data "juju_secret" "my_secret_data_source" { + name = "my_secret" + model = data.juju_model.my_model.name +} + +resource "juju_application" "ubuntu" { + model = data.juju_model.my_model.name + name = "ubuntu" + + charm { + name = "ubuntu" + } + + config = { + secret = data.juju_secret.my_secret_data_source.secret_id + } +} + +resource "juju_access_secret" "my_secret_access" { + model = data.juju_model.my_model.name + applications = [ + juju_application.ubuntu.name + ] + secret_id = data.juju_secret.my_secret_data_source.secret_id +} +``` + + +## Schema + +### Required + +- `model` (String) The name of the model containing the secret. +- `name` (String) The name of the secret. + +### Read-Only + +- `secret_id` (String) The ID of the secret. diff --git a/docs/resources/secret.md b/docs/resources/secret.md new file mode 100644 index 00000000..b1d2fe83 --- /dev/null +++ b/docs/resources/secret.md @@ -0,0 +1,30 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "juju_secret Resource - terraform-provider-juju" +subcategory: "" +description: |- + A resource that represents a Juju secret. +--- + +# juju_secret (Resource) + +A resource that represents a Juju secret. + + + + +## Schema + +### Required + +- `model` (String) The model in which the secret belongs. +- `value` (Map of String, Sensitive) The value map of the secret. There can be more than one key-value pair. + +### Optional + +- `info` (String) The description of the secret. +- `name` (String) The name of the secret. + +### Read-Only + +- `secret_id` (String) The ID of the secret. diff --git a/examples/data-sources/juju_secret/data-source.tf b/examples/data-sources/juju_secret/data-source.tf new file mode 100644 index 00000000..cb39fc7d --- /dev/null +++ b/examples/data-sources/juju_secret/data-source.tf @@ -0,0 +1,30 @@ +data "juju_model" "my_model" { + name = "default" +} + +data "juju_secret" "my_secret_data_source" { + name = "my_secret" + model = data.juju_model.my_model.name +} + +resource "juju_application" "ubuntu" { + model = data.juju_model.my_model.name + name = "ubuntu" + + charm { + name = "ubuntu" + } + + config = { + secret = data.juju_secret.my_secret_data_source.secret_id + } +} + +resource "juju_access_secret" "my_secret_access" { + model = data.juju_model.my_model.name + applications = [ + juju_application.ubuntu.name + ] + secret_id = data.juju_secret.my_secret_data_source.secret_id +} + diff --git a/internal/provider/data_source_secrets.go b/internal/provider/data_source_secrets.go new file mode 100644 index 00000000..3dd61c01 --- /dev/null +++ b/internal/provider/data_source_secrets.go @@ -0,0 +1,140 @@ +// Copyright 2024 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package provider + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "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 _ datasource.DataSourceWithConfigure = &secretDataSource{} + +func NewSecretDataSource() datasource.DataSource { + return &secretDataSource{} +} + +type secretDataSource struct { + client *juju.Client + + // context for the logging subsystem. + subCtx context.Context +} + +// secretDataSourceModel is the juju data stored by terraform. +// tfsdk must match secret data source schema attribute names. +type secretDataSourceModel struct { + // Model to which the secret belongs. + Model types.String `tfsdk:"model"` + // Name of the secret in the model. + Name types.String `tfsdk:"name"` + // SecretId is the ID of the secret. + SecretId types.String `tfsdk:"secret_id"` +} + +// Metadata returns the full data source name as used in terraform plans. +func (d *secretDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_secret" +} + +// Schema returns the schema for the model data source. +func (d *secretDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "A data source representing a Juju Secret.", + Attributes: map[string]schema.Attribute{ + "model": schema.StringAttribute{ + Description: "The name of the model containing the secret.", + Required: true, + }, + "name": schema.StringAttribute{ + Description: "The name of the secret.", + Required: true, + }, + "secret_id": schema.StringAttribute{ + Description: "The ID of the secret.", + Computed: true, + }, + }, + } +} + +// Configure enables provider-level data or clients to be set in the +// provider-defined DataSource type. It is separately executed for each +// ReadDataSource RPC. +func (d *secretDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.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 Data Source Configure Type", + fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + d.client = client + d.subCtx = tflog.NewSubsystem(ctx, LogDataSourceSecret) +} + +// Read is called when the provider must read data source values in +// order to update state. Config values should be read from the +// ReadRequest and new state values set on the ReadResponse. +func (d *secretDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + // Prevent panic if the provider has not been configured. + if d.client == nil { + addDSClientNotConfiguredError(&resp.Diagnostics, "secret") + return + } + + var data secretDataSourceModel + + // Read Terraform configuration state into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + readSecretInput := juju.ReadSecretInput{ + ModelName: data.Model.ValueString(), + } + if data.SecretId.ValueString() == "" { + readSecretInput.Name = data.Name.ValueStringPointer() + } else { + readSecretInput.SecretId = data.SecretId.ValueString() + } + + readSecretOutput, err := d.client.Secrets.ReadSecret(&readSecretInput) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read secret, got error: %s", err)) + return + } + d.trace(fmt.Sprintf("read secret data source %q", data.SecretId)) + + data.SecretId = types.StringValue(readSecretOutput.SecretId) + + // Save state into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (d *secretDataSource) trace(msg string, additionalFields ...map[string]interface{}) { + if d.subCtx == nil { + return + } + + //SubsystemTrace(subCtx, "datasource-secret", "hello, world", map[string]interface{}{"foo": 123}) + // Output: + // {"@level":"trace","@message":"hello, world","@module":"juju.datasource-secret","foo":123} + tflog.SubsystemTrace(d.subCtx, LogDataSourceSecret, msg, additionalFields...) +} diff --git a/internal/provider/data_source_secrets_test.go b/internal/provider/data_source_secrets_test.go new file mode 100644 index 00000000..eab26bef --- /dev/null +++ b/internal/provider/data_source_secrets_test.go @@ -0,0 +1,76 @@ +// Copyright 2024 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package provider + +import ( + "fmt" + "os" + "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" +) + +// TODO(aflynn): Add add actual usage of the data source to the test. This is +// blocked on the lack of schema for secret access. + +func TestAcc_DataSourceSecret(t *testing.T) { + version := os.Getenv("JUJU_AGENT_VERSION") + if version == "" || internaltesting.CompareVersions(version, "3.3.0") < 0 { + t.Skip("JUJU_AGENT_VERSION is not set or is below 3.3.0") + } + modelName := acctest.RandomWithPrefix("tf-datasource-secret-test-model") + // ...-test-[0-9]+ is not a valid secret name, need to remove the dash before numbers + secretName := fmt.Sprintf("tf-datasource-secret-test%d", acctest.RandInt()) + secretValue := map[string]string{ + "key1": "value1", + "key2": "value2", + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: frameworkProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceSecret(modelName, secretName, secretValue), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.juju_secret.secret_data_source", "model", modelName), + resource.TestCheckResourceAttr("data.juju_secret.secret_data_source", "name", secretName), + resource.TestCheckResourceAttrPair("data.juju_secret.secret_data_source", "secretID", "juju_secret.secret_resource", "secretID"), + ), + }, + }, + }) +} + +func testAccDataSourceSecret(modelName, secretName string, secretValue map[string]string) string { + return internaltesting.GetStringFromTemplateWithData( + "testAccResourceSecret", + ` +resource "juju_model" "{{.ModelName}}" { + name = "{{.ModelName}}" +} + +resource "juju_secret" "secret_resource" { + model = juju_model.{{.ModelName}}.name + name = "{{.SecretName}}" + value = { + {{- range $key, $value := .SecretValue }} + "{{$key}}" = "{{$value}}" + {{- end }} + } +} + +data "juju_secret" "secret_data_source" { + name = juju_secret.secret_resource.name + model = juju_model.{{.ModelName}}.name +} +`, internaltesting.TemplateData{ + "ModelName": modelName, + "SecretName": secretName, + "SecretValue": secretValue, + }) +} diff --git a/internal/provider/helpers.go b/internal/provider/helpers.go index 71a1facb..0d2e5d7a 100644 --- a/internal/provider/helpers.go +++ b/internal/provider/helpers.go @@ -19,6 +19,7 @@ const ( LogDataSourceMachine = "datasource-machine" LogDataSourceModel = "datasource-model" LogDataSourceOffer = "datasource-offer" + LogDataSourceSecret = "datasource-secret" LogResourceApplication = "resource-application" LogResourceAccessModel = "resource-assess-model" diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 0fb3091c..b261c825 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -306,6 +306,7 @@ func (p *jujuProvider) DataSources(_ context.Context) []func() datasource.DataSo func() datasource.DataSource { return NewMachineDataSource() }, func() datasource.DataSource { return NewModelDataSource() }, func() datasource.DataSource { return NewOfferDataSource() }, + func() datasource.DataSource { return NewSecretDataSource() }, } }