diff --git a/go.mod b/go.mod index e86fc8af..737dbbc9 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,6 @@ require ( github.com/juju/cmd/v3 v3.0.14 github.com/juju/collections v1.0.4 github.com/juju/errors v1.0.0 - github.com/juju/names/v4 v4.0.0 github.com/juju/names/v5 v5.0.0 github.com/juju/retry v1.0.0 github.com/juju/utils/v3 v3.1.1 @@ -129,6 +128,7 @@ require ( github.com/juju/lumberjack/v2 v2.0.2 // indirect github.com/juju/mgo/v3 v3.0.4 // indirect github.com/juju/mutex/v2 v2.0.0 // indirect + github.com/juju/names/v4 v4.0.0-20220207005702-9c6532a52823 // indirect github.com/juju/os/v2 v2.2.3 // indirect github.com/juju/packaging/v2 v2.0.1 // indirect github.com/juju/persistent-cookiejar v1.0.0 // indirect @@ -197,17 +197,17 @@ require ( github.com/yuin/goldmark-meta v1.1.0 // indirect github.com/zclconf/go-cty v1.14.4 // indirect go.abhg.dev/goldmark/frontmatter v0.2.0 // indirect - golang.org/x/crypto v0.22.0 // indirect + golang.org/x/crypto v0.21.0 // indirect golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect - golang.org/x/mod v0.17.0 // indirect - golang.org/x/net v0.24.0 // indirect + golang.org/x/mod v0.16.0 // indirect + golang.org/x/net v0.22.0 // indirect golang.org/x/oauth2 v0.16.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.19.0 // indirect - golang.org/x/term v0.19.0 // indirect + golang.org/x/sync v0.6.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/term v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.20.0 // indirect + golang.org/x/tools v0.16.1 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect google.golang.org/grpc v1.62.1 // indirect diff --git a/go.sum b/go.sum index 3a56337b..791ba5ea 100644 --- a/go.sum +++ b/go.sum @@ -368,8 +368,8 @@ github.com/juju/mgo/v3 v3.0.4 h1:ek6YDy71tqikpoFSpvLkpCZ7zvYNYH+xSk/MebMkCEE= github.com/juju/mgo/v3 v3.0.4/go.mod h1:fAvhDCRbUlEbRIae6UQT8RvPUoLwKnJsBgO6OzHKNxw= github.com/juju/mutex/v2 v2.0.0 h1:rVmJdOaXGWF8rjcFHBNd4x57/1tks5CgXHx55O55SB0= github.com/juju/mutex/v2 v2.0.0/go.mod h1:jwCfBs/smYDaeZLqeaCi8CB8M+tOes4yf827HoOEoqk= -github.com/juju/names/v4 v4.0.0 h1:XeQZbwT70i98TynM+2RJr9At6EGb9X/P6l8qF56hPns= -github.com/juju/names/v4 v4.0.0/go.mod h1:xpkrQpHbz1DGY+0Geo32ZnyognGA/2vSB++rpu/Z+Lc= +github.com/juju/names/v4 v4.0.0-20220207005702-9c6532a52823 h1:Sv0+v4107/GHA0S25ay/rgGVmLyc+5Fjp0NnTksW/IQ= +github.com/juju/names/v4 v4.0.0-20220207005702-9c6532a52823/go.mod h1:xpkrQpHbz1DGY+0Geo32ZnyognGA/2vSB++rpu/Z+Lc= github.com/juju/names/v5 v5.0.0 h1:3IkRTUaniNXsgjy4lNqbJx7dVdsONlzuH6YMYT7uXss= github.com/juju/names/v5 v5.0.0/go.mod h1:PkvHbErUTniKvLu1ejJ5m/AbXOW55MFn1jsGVEbVXk8= github.com/juju/naturalsort v1.0.0 h1:kGmUUy3h8mJ5/SJYaqKOBR3f3owEd5R52Lh+Tjg/dNM= @@ -671,8 +671,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No= golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= @@ -683,8 +683,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= +golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -698,8 +698,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= @@ -709,8 +709,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -732,13 +732,13 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= -golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -760,8 +760,8 @@ golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= -golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= +golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= +golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/juju/applications_test.go b/internal/juju/applications_test.go index 67b56d7c..3ff80d2f 100644 --- a/internal/juju/applications_test.go +++ b/internal/juju/applications_test.go @@ -15,7 +15,7 @@ import ( "github.com/juju/juju/core/resources" "github.com/juju/juju/environs/config" "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/juju/utils/v3" "github.com/juju/version/v2" "github.com/stretchr/testify/suite" diff --git a/internal/juju/secrets.go b/internal/juju/secrets.go index 5e0f4269..999fda69 100644 --- a/internal/juju/secrets.go +++ b/internal/juju/secrets.go @@ -9,7 +9,6 @@ import ( "fmt" "strings" - 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" @@ -61,10 +60,11 @@ type ReadSecretInput struct { } type ReadSecretOutput struct { - SecretId string - Name string - Value map[string]string - Info string + SecretId string + Name string + Value map[string]string + Applications []string + Info string } type UpdateSecretInput struct { @@ -176,11 +176,15 @@ func (c *secretsClient) ReadSecret(input *ReadSecretInput) (ReadSecretOutput, er return ReadSecretOutput{}, err } + // Get applications from Access info + applications := getApplicationsFromAccessInfo(results[0].Access) + return ReadSecretOutput{ - SecretId: results[0].Metadata.URI.String(), - Name: results[0].Metadata.Label, - Value: decodedValue, - Info: results[0].Metadata.Description, + SecretId: results[0].Metadata.URI.String(), + Name: results[0].Metadata.Label, + Value: decodedValue, + Applications: applications, + Info: results[0].Metadata.Description, }, nil } @@ -259,7 +263,7 @@ func (c *secretsClient) DeleteSecret(input *DeleteSecretInput) error { } // TODO: think about removing concrete revision. err = secretAPIClient.RemoveSecret(secretURI, "", nil) - if !errors.Is(err, jujuerrors.NotFound) { + if err != nil { return typedError(err) } @@ -301,3 +305,13 @@ func (c *secretsClient) UpdateSecretAccess(input *GrantRevokeSecretAccessInput, return nil } + +// getApplicationsFromAccessInfo returns a list of applications from the access info. +func getApplicationsFromAccessInfo(accessInfo []coresecrets.AccessInfo) []string { + applications := make([]string, 0, len(accessInfo)) + for _, info := range accessInfo { + // Trim the prefix "application-" from the application name (info.Target) + applications = append(applications, strings.TrimPrefix(info.Target, "application-")) + } + return applications +} diff --git a/internal/provider/helpers.go b/internal/provider/helpers.go index 71a1facb..5a464672 100644 --- a/internal/provider/helpers.go +++ b/internal/provider/helpers.go @@ -53,3 +53,21 @@ func intPtr(value types.Int64) *int { count := int(value.ValueInt64()) return &count } + +// getStringSliceDifference returns the getStringSliceDifference between two slices. +func getStringSliceDifference(slice1, slice2 []string) []string { + var diff []string + m := make(map[string]bool) + + for _, s1 := range slice1 { + m[s1] = true + } + + for _, s2 := range slice2 { + if _, ok := m[s2]; !ok { + diff = append(diff, s2) + } + } + + return diff +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 0fb3091c..af7f07f7 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -293,6 +293,7 @@ func (p *jujuProvider) Resources(_ context.Context) []func() resource.Resource { func() resource.Resource { return NewSSHKeyResource() }, func() resource.Resource { return NewUserResource() }, func() resource.Resource { return NewSecretResource() }, + func() resource.Resource { return NewSecretAccessResource() }, } } diff --git a/internal/provider/resource_model.go b/internal/provider/resource_model.go index 442f8710..8cac55e9 100644 --- a/internal/provider/resource_model.go +++ b/internal/provider/resource_model.go @@ -24,7 +24,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/juju/juju/core/constraints" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/juju/utils/v3" "github.com/juju/terraform-provider-juju/internal/juju" diff --git a/internal/provider/resource_secret.go b/internal/provider/resource_secret.go index 710cb6cf..5b428623 100644 --- a/internal/provider/resource_secret.go +++ b/internal/provider/resource_secret.go @@ -79,6 +79,9 @@ func (s *secretResource) Schema(_ context.Context, req resource.SchemaRequest, r "secret_id": schema.StringAttribute{ Description: "The ID of the secret.", Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "info": schema.StringAttribute{ Description: "The description of the secret.", diff --git a/internal/provider/resource_secret_access.go b/internal/provider/resource_secret_access.go new file mode 100644 index 00000000..49733a4f --- /dev/null +++ b/internal/provider/resource_secret_access.go @@ -0,0 +1,311 @@ +// 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/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" + "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 = &secretAccessResource{} +var _ resource.ResourceWithConfigure = &secretAccessResource{} +var _ resource.ResourceWithImportState = &secretAccessResource{} + +func NewSecretAccessResource() resource.Resource { + return &secretAccessResource{} +} + +type secretAccessResource struct { + client *juju.Client + + // subCtx is the context created with the new tflog subsystem for applications. + subCtx context.Context +} + +type secretAccessResourceModel struct { + // Model to which the secret belongs. + Model types.String `tfsdk:"model"` + // SecretId is the ID of the secret to be grant or revoked. + 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"` +} + +func (s *secretAccessResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (s *secretAccessResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_secret_access" +} + +// Schema is called when the resource schema is being initialized. +func (s *secretAccessResource) Schema(_ context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "A resource that represents a Juju secret access.", + Attributes: map[string]schema.Attribute{ + "model": schema.StringAttribute{ + Description: "The model in which the secret belongs.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplaceIfConfigured(), + stringplanmodifier.UseStateForUnknown(), + }, + }, + "secret_id": schema.StringAttribute{ + Description: "The ID of the secret.", + Required: true, + }, + "applications": schema.ListAttribute{ + Description: "The list of applications to which the secret is granted or revoked.", + Required: true, + ElementType: types.StringType, + }, + }, + } +} + +// Configure is called when the resource is being configured. +func (s *secretAccessResource) 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 is called when the resource is being created. +func (s *secretAccessResource) 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 secretAccessResourceModel + + // Read Terraform plan into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + applications := make([]string, len(plan.Applications.Elements())) + resp.Diagnostics.Append(plan.Applications.ElementsAs(ctx, &applications, false)...) + + err := s.client.Secrets.UpdateSecretAccess(&juju.GrantRevokeSecretAccessInput{ + ModelName: plan.Model.ValueString(), + SecretId: plan.SecretId.ValueString(), + Applications: applications, + }, juju.GrantAccess) + if err != nil { + resp.Diagnostics.AddError("Failed to grant secret access", err.Error()) + return + } + + // Save plan into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + + s.trace(fmt.Sprintf("grant secret access to %q", plan.SecretId)) +} + +// Read is called when the resource is being read. +func (s *secretAccessResource) 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_access", "read") + return + } + + var state secretAccessResourceModel + + // Read Terraform configuration state into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + readSecretOutput, err := s.client.Secrets.ReadSecret(&juju.ReadSecretInput{ + SecretId: state.SecretId.ValueString(), + ModelName: state.Model.ValueString(), + }) + 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 + 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("read secret access %q", state.SecretId)) +} + +// Update is called when the resource is being updated. +func (s *secretAccessResource) 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_access", "update") + return + } + + var plan, state secretAccessResourceModel + + // 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 + } + + var err error + noChange := true + + var updatedSecretAccessInput juju.GrantRevokeSecretAccessInput + + updatedSecretAccessInput.ModelName = state.Model.ValueString() + updatedSecretAccessInput.SecretId = state.SecretId.ValueString() + + var applicationsToRevoke, applicationsToGrant []string + if !plan.Applications.Equal(state.Applications) { + noChange = false + + planApplications := make([]string, len(plan.Applications.Elements())) + resp.Diagnostics.Append(plan.Applications.ElementsAs(ctx, &planApplications, false)...) + + stateApplications := make([]string, len(state.Applications.Elements())) + resp.Diagnostics.Append(state.Applications.ElementsAs(ctx, &stateApplications, false)...) + + applicationsToRevoke = getStringSliceDifference(planApplications, stateApplications) + applicationsToGrant = getStringSliceDifference(stateApplications, planApplications) + + s.trace(fmt.Sprintf("applications to revoke secret: %v", applicationsToRevoke)) + s.trace(fmt.Sprintf("applications to grant secret: %v", applicationsToGrant)) + + resp.Diagnostics.Append(plan.Applications.ElementsAs(ctx, &state.Applications, false)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(plan.Applications.ElementsAs(ctx, &updatedSecretAccessInput.Applications, false)...) + if resp.Diagnostics.HasError() { + return + } + } + + if noChange { + return + } + + // revoke access to applications that are in the state but not in the plan + if applicationsToGrant != nil { + err = s.client.Secrets.UpdateSecretAccess(&juju.GrantRevokeSecretAccessInput{ + ModelName: state.Model.ValueString(), + SecretId: state.SecretId.ValueString(), + Applications: applicationsToGrant, + }, juju.GrantAccess) + if err != nil { + resp.Diagnostics.AddError("Failed to revoke secret access", err.Error()) + return + } + } + + // grant access to applications that are in the plan but not in the state + if applicationsToRevoke != nil { + err = s.client.Secrets.UpdateSecretAccess(&juju.GrantRevokeSecretAccessInput{ + ModelName: state.Model.ValueString(), + SecretId: state.SecretId.ValueString(), + Applications: applicationsToRevoke, + }, juju.RevokeAccess) + if err != nil { + resp.Diagnostics.AddError("Failed to grant secret access", err.Error()) + return + } + } + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) + + s.trace(fmt.Sprintf("update secret access %q", state.SecretId)) +} + +// Delete is called when the resource is being deleted. +func (s *secretAccessResource) 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_access", "delete") + return + } + + var state secretAccessResourceModel + + // Read Terraform configuration state into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + applications := make([]string, len(state.Applications.Elements())) + resp.Diagnostics.Append(state.Applications.ElementsAs(ctx, &applications, false)...) + if resp.Diagnostics.HasError() { + return + } + + err := s.client.Secrets.UpdateSecretAccess(&juju.GrantRevokeSecretAccessInput{ + ModelName: state.Model.ValueString(), + SecretId: state.SecretId.ValueString(), + Applications: applications, + }, juju.RevokeAccess) + if err != nil { + resp.Diagnostics.AddError("Failed to revoke secret access", err.Error()) + return + } + + // Save empty list of applications into Terraform state + emptyApplicationList, errDiag := types.ListValue(types.StringType, []attr.Value{}) + resp.Diagnostics.Append(errDiag...) + if resp.Diagnostics.HasError() { + return + } + state.Applications = emptyApplicationList + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) + + s.trace(fmt.Sprintf("revoke secret access %q", state.SecretId)) +} + +func (s *secretAccessResource) trace(msg string, additionalFields ...map[string]interface{}) { + if s.subCtx == nil { + return + } + tflog.SubsystemTrace(s.subCtx, LogResourceSecret, msg, additionalFields...) +} diff --git a/internal/provider/resource_secret_access_test.go b/internal/provider/resource_secret_access_test.go new file mode 100644 index 00000000..4f504f66 --- /dev/null +++ b/internal/provider/resource_secret_access_test.go @@ -0,0 +1 @@ +package provider