diff --git a/internal/juju/interfaces.go b/internal/juju/interfaces.go index 85af3c64..689336d5 100644 --- a/internal/juju/interfaces.go +++ b/internal/juju/interfaces.go @@ -9,10 +9,12 @@ import ( apiapplication "github.com/juju/juju/api/client/application" apiclient "github.com/juju/juju/api/client/client" apiresources "github.com/juju/juju/api/client/resources" + apisecrets "github.com/juju/juju/api/client/secrets" apicommoncharm "github.com/juju/juju/api/common/charm" "github.com/juju/juju/core/constraints" "github.com/juju/juju/core/model" "github.com/juju/juju/core/resources" + "github.com/juju/juju/core/secrets" "github.com/juju/juju/rpc/params" "github.com/juju/names/v4" ) @@ -62,3 +64,13 @@ type ResourceAPIClient interface { AddPendingResources(args apiresources.AddPendingResourcesArgs) ([]string, error) ListResources(applications []string) ([]resources.ApplicationResources, error) } + +type SecretAPIClient interface { + CreateSecret(name, description string, data map[string]string) (string, error) + ListSecrets(reveal bool, filter secrets.Filter) ([]apisecrets.SecretDetails, error) + UpdateSecret( + uri *secrets.URI, name string, autoPrune *bool, + newName string, description string, data map[string]string, + ) error + RemoveSecret(uri *secrets.URI, name string, revision *int) error +} diff --git a/internal/juju/mock_test.go b/internal/juju/mock_test.go index d12e790c..c169cc85 100644 --- a/internal/juju/mock_test.go +++ b/internal/juju/mock_test.go @@ -1,9 +1,9 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/juju/terraform-provider-juju/internal/juju (interfaces: SharedClient,ClientAPIClient,ApplicationAPIClient,ModelConfigAPIClient,ResourceAPIClient) +// Source: github.com/juju/terraform-provider-juju/internal/juju (interfaces: SharedClient,ClientAPIClient,ApplicationAPIClient,ModelConfigAPIClient,ResourceAPIClient,SecretAPIClient) // // Generated by this command: // -// mockgen -package juju -destination mock_test.go github.com/juju/terraform-provider-juju/internal/juju SharedClient,ClientAPIClient,ApplicationAPIClient,ModelConfigAPIClient,ResourceAPIClient +// mockgen -package juju -destination mock_test.go github.com/juju/terraform-provider-juju/internal/juju SharedClient,ClientAPIClient,ApplicationAPIClient,ModelConfigAPIClient,ResourceAPIClient,SecretAPIClient // // Package juju is a generated GoMock package. @@ -17,10 +17,12 @@ import ( application "github.com/juju/juju/api/client/application" client "github.com/juju/juju/api/client/client" resources "github.com/juju/juju/api/client/resources" + secrets "github.com/juju/juju/api/client/secrets" charm0 "github.com/juju/juju/api/common/charm" constraints "github.com/juju/juju/core/constraints" model "github.com/juju/juju/core/model" resources0 "github.com/juju/juju/core/resources" + secrets0 "github.com/juju/juju/core/secrets" params "github.com/juju/juju/rpc/params" names "github.com/juju/names/v4" gomock "go.uber.org/mock/gomock" @@ -569,3 +571,84 @@ func (mr *MockResourceAPIClientMockRecorder) ListResources(arg0 any) *gomock.Cal mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListResources", reflect.TypeOf((*MockResourceAPIClient)(nil).ListResources), arg0) } + +// MockSecretAPIClient is a mock of SecretAPIClient interface. +type MockSecretAPIClient struct { + ctrl *gomock.Controller + recorder *MockSecretAPIClientMockRecorder +} + +// MockSecretAPIClientMockRecorder is the mock recorder for MockSecretAPIClient. +type MockSecretAPIClientMockRecorder struct { + mock *MockSecretAPIClient +} + +// NewMockSecretAPIClient creates a new mock instance. +func NewMockSecretAPIClient(ctrl *gomock.Controller) *MockSecretAPIClient { + mock := &MockSecretAPIClient{ctrl: ctrl} + mock.recorder = &MockSecretAPIClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSecretAPIClient) EXPECT() *MockSecretAPIClientMockRecorder { + return m.recorder +} + +// CreateSecret mocks base method. +func (m *MockSecretAPIClient) CreateSecret(arg0, arg1 string, arg2 map[string]string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateSecret", arg0, arg1, arg2) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateSecret indicates an expected call of CreateSecret. +func (mr *MockSecretAPIClientMockRecorder) CreateSecret(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSecret", reflect.TypeOf((*MockSecretAPIClient)(nil).CreateSecret), arg0, arg1, arg2) +} + +// ListSecrets mocks base method. +func (m *MockSecretAPIClient) ListSecrets(arg0 bool, arg1 secrets0.Filter) ([]secrets.SecretDetails, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListSecrets", arg0, arg1) + ret0, _ := ret[0].([]secrets.SecretDetails) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListSecrets indicates an expected call of ListSecrets. +func (mr *MockSecretAPIClientMockRecorder) ListSecrets(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListSecrets", reflect.TypeOf((*MockSecretAPIClient)(nil).ListSecrets), arg0, arg1) +} + +// RemoveSecret mocks base method. +func (m *MockSecretAPIClient) RemoveSecret(arg0 *secrets0.URI, arg1 string, arg2 *int) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RemoveSecret", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// RemoveSecret indicates an expected call of RemoveSecret. +func (mr *MockSecretAPIClientMockRecorder) RemoveSecret(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveSecret", reflect.TypeOf((*MockSecretAPIClient)(nil).RemoveSecret), arg0, arg1, arg2) +} + +// UpdateSecret mocks base method. +func (m *MockSecretAPIClient) UpdateSecret(arg0 *secrets0.URI, arg1 string, arg2 *bool, arg3, arg4 string, arg5 map[string]string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateSecret", arg0, arg1, arg2, arg3, arg4, arg5) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateSecret indicates an expected call of UpdateSecret. +func (mr *MockSecretAPIClientMockRecorder) UpdateSecret(arg0, arg1, arg2, arg3, arg4, arg5 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateSecret", reflect.TypeOf((*MockSecretAPIClient)(nil).UpdateSecret), arg0, arg1, arg2, arg3, arg4, arg5) +} diff --git a/internal/juju/package_test.go b/internal/juju/package_test.go index ebcbff12..8a3f724f 100644 --- a/internal/juju/package_test.go +++ b/internal/juju/package_test.go @@ -3,5 +3,5 @@ package juju_test -//go:generate go run go.uber.org/mock/mockgen -package juju -destination mock_test.go github.com/juju/terraform-provider-juju/internal/juju SharedClient,ClientAPIClient,ApplicationAPIClient,ModelConfigAPIClient,ResourceAPIClient +//go:generate go run go.uber.org/mock/mockgen -package juju -destination mock_test.go github.com/juju/terraform-provider-juju/internal/juju SharedClient,ClientAPIClient,ApplicationAPIClient,ModelConfigAPIClient,ResourceAPIClient,SecretAPIClient //go:generate go run go.uber.org/mock/mockgen -package juju -destination jujuapi_mock_test.go github.com/juju/juju/api Connection diff --git a/internal/juju/secrets.go b/internal/juju/secrets.go new file mode 100644 index 00000000..25dc6250 --- /dev/null +++ b/internal/juju/secrets.go @@ -0,0 +1,139 @@ +// Copyright 2024 Canonical Ltd. +// Licensed under the Apache License, Version 2.0, see LICENCE file for details. + +package juju + +import ( + "errors" + + "github.com/juju/juju/api" + apisecrets "github.com/juju/juju/api/client/secrets" + coresecrets "github.com/juju/juju/core/secrets" +) + +type secretsClient struct { + SharedClient + + getSecretAPIClient func(connection api.Connection) SecretAPIClient +} + +type CreateSecretInput struct { + ModelName string + Name string + Value map[string]string + Info string +} + +type CreateSecretOutput struct { + SecretId string +} + +type ReadSecretInput struct { + ModelName string + Name string +} + +type ReadSecretOutput struct { + Value map[string]string +} + +type UpdateSecretInput struct { + ModelName string + Name string + Value map[string]string + Info string +} + +type DeleteSecretInput struct { + ModelName string + Name string +} + +func newSecretsClient(sc SharedClient) *secretsClient { + return &secretsClient{ + SharedClient: sc, + getSecretAPIClient: func(connection api.Connection) SecretAPIClient { + return apisecrets.NewClient(connection) + }, + } +} + +// CreateSecret creates a new secret. +func (c *secretsClient) CreateSecret(input *CreateSecretInput) (*CreateSecretOutput, error) { + conn, err := c.GetConnection(&input.ModelName) + if err != nil { + return nil, err + } + defer func() { _ = conn.Close() }() + + secretAPIClient := c.getSecretAPIClient(conn) + secretId, err := secretAPIClient.CreateSecret(input.Name, input.Info, input.Value) + if err != nil { + return nil, err + } + return &CreateSecretOutput{ + SecretId: secretId, + }, nil +} + +// ReadSecret reads a secret. +func (c *secretsClient) ReadSecret(input *ReadSecretInput) (*ReadSecretOutput, error) { + conn, err := c.GetConnection(&input.ModelName) + if err != nil { + return nil, err + } + defer func() { _ = conn.Close() }() + + secretAPIClient := c.getSecretAPIClient(conn) + results, err := secretAPIClient.ListSecrets(true, coresecrets.Filter{Label: &input.Name}) + if err != nil { + return nil, err + } + if results[0].Error != "" { + return nil, errors.New(results[0].Error) + } + + value, err := results[0].Value.Values() + if err != nil { + return nil, err + } + return &ReadSecretOutput{ + Value: value, + }, nil +} + +// UpdateSecret updates a secret. +func (c *secretsClient) UpdateSecret(input *UpdateSecretInput) error { + conn, err := c.GetConnection(&input.ModelName) + if err != nil { + return err + } + defer func() { _ = conn.Close() }() + + secretAPIClient := c.getSecretAPIClient(conn) + // TODO: find a way to model auto-prune option + // TODO: looks like we can change the secret name, but not sure if that's a good idea + err = secretAPIClient.UpdateSecret(nil, input.Name, nil, input.Name, input.Info, input.Value) + if err != nil { + return err + } + + return nil +} + +// DeleteSecret deletes a secret. +func (c *secretsClient) DeleteSecret(input *DeleteSecretInput) error { + conn, err := c.GetConnection(&input.ModelName) + if err != nil { + return err + } + + secretAPIClient := c.getSecretAPIClient(conn) + // TODO: think about removing concrete revision. + err = secretAPIClient.RemoveSecret(nil, input.Name, nil) + if err != nil { + return err + } + + return nil +} diff --git a/internal/juju/secrets_test.go b/internal/juju/secrets_test.go new file mode 100644 index 00000000..c019ac5c --- /dev/null +++ b/internal/juju/secrets_test.go @@ -0,0 +1,87 @@ +// Copyright 2024 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package juju + +import ( + "github.com/juju/juju/api" + "testing" + + "github.com/stretchr/testify/suite" + "go.uber.org/mock/gomock" +) + +// Basic imports + +type SecretSuite struct { + suite.Suite + + testModelName string + + mockSecretClient *MockSecretAPIClient + mockClient *MockClientAPIClient + mockResourceAPIClient *MockResourceAPIClient + mockConnection *MockConnection + mockModelConfigClient *MockModelConfigAPIClient + mockSharedClient *MockSharedClient +} + +func (s *SecretSuite) SetupTest() {} + +func (s *SecretSuite) setupMocks(t *testing.T) *gomock.Controller { + s.testModelName = "test-secret-model" + + ctlr := gomock.NewController(t) + s.mockSecretClient = NewMockSecretAPIClient(ctlr) + + s.mockConnection = NewMockConnection(ctlr) + s.mockConnection.EXPECT().Close().Return(nil).AnyTimes() + + log := func(msg string, additionalFields ...map[string]interface{}) { + s.T().Logf("logging from shared client %q, %+v", msg, additionalFields) + } + s.mockSharedClient = NewMockSharedClient(ctlr) + s.mockSharedClient.EXPECT().Debugf(gomock.Any(), gomock.Any()).Do(log).AnyTimes() + s.mockSharedClient.EXPECT().Errorf(gomock.Any(), gomock.Any()).Do(log).AnyTimes() + s.mockSharedClient.EXPECT().Tracef(gomock.Any(), gomock.Any()).Do(log).AnyTimes() + s.mockSharedClient.EXPECT().JujuLogger().Return(&jujuLoggerShim{}).AnyTimes() + s.mockSharedClient.EXPECT().GetConnection(&s.testModelName).Return(s.mockConnection, nil).AnyTimes() + + return ctlr +} + +func (s *SecretSuite) getSecretsClient() secretsClient { + return secretsClient{ + SharedClient: s.mockSharedClient, + getSecretAPIClient: func(connection api.Connection) SecretAPIClient { + return s.mockSecretClient + }, + } +} + +func (s *SecretSuite) TestCreateSecret() { + ctlr := s.setupMocks(s.T()) + defer ctlr.Finish() + + s.mockSecretClient.EXPECT().CreateSecret( + "test-secret", "test info", map[string]string{"key": "value"}, + ).Return("secret-id", nil).AnyTimes() + + client := s.getSecretsClient() + output, err := client.CreateSecret(&CreateSecretInput{ + ModelName: s.testModelName, + Name: "test-secret", + Value: map[string]string{"key": "value"}, + Info: "test info", + }) + s.Require().NoError(err) + s.Require().NotNil(output) + s.Require().Equal("secret-id", output.SecretId) + +} + +// In order for 'go test' to run this suite, we need to create +// a normal test function and pass our suite to suite.Run +func TestUserSecretSuite(t *testing.T) { + suite.Run(t, new(SecretSuite)) +} diff --git a/internal/juju/userSecret.go b/internal/juju/userSecret.go deleted file mode 100644 index 0c2dbac3..00000000 --- a/internal/juju/userSecret.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2024 Canonical Ltd. -// Licensed under the Apache License, Version 2.0, see LICENCE file for details. - -package juju - -import ( - "strings" - - "github.com/juju/juju/api/client/secrets" -) - -type userSecretClient struct { - SharedClient -} - -type AddUserSecretInput struct { - ModelName string - Name string - Value string - Description string -} - -type AddUserSecretOutput struct { - SecretURI string -} - -type ReadUserSecretInput struct { - ModelName string - Name string -} - -type UpdateUserSecretInput struct { - ModelName string - Name string - Value string -} - -type RemoveUserSecretInput struct { - ModelName string - Name string -} - -func newUserSecretClient(sc SharedClient) *userSecretClient { - return &userSecretClient{ - SharedClient: sc, - } -} - -func (c *userSecretClient) AddUserSecret(input *AddUserSecretInput) (*AddUserSecretOutput, error) { - conn, err := c.GetConnection(&input.ModelName) - if err != nil { - return nil, err - } - defer func() { _ = conn.Close() }() - - client := secrets.NewClient(conn) - secretURI, err := client.CreateSecret(input.Name, input.Description, parseSecretValueStringToMap(input.Value)) - if err != nil { - return nil, err - } - return &AddUserSecretOutput{ - SecretURI: secretURI, - }, nil -} - -func parseSecretValueStringToMap(input string) map[string]string { - result := make(map[string]string) - pairs := strings.Split(input, " ") - - for _, pair := range pairs { - kv := strings.Split(pair, "=") - if len(kv) == 2 { - result[kv[0]] = kv[1] - } - } - - return result -}