diff --git a/domain/unitstate/service/package_test.go b/domain/unitstate/service/package_test.go new file mode 100644 index 00000000000..9f8a3119e82 --- /dev/null +++ b/domain/unitstate/service/package_test.go @@ -0,0 +1,19 @@ +// Copyright 2024 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package service + +import ( + "testing" + + gc "gopkg.in/check.v1" +) + +//go:generate go run go.uber.org/mock/mockgen -typed -package service -destination state_mock_test.go github.com/juju/juju/domain/unitstate/service State +func TestPackage(t *testing.T) { + gc.TestingT(t) +} + +func ptr[T any](v T) *T { + return &v +} diff --git a/domain/unitstate/service/service.go b/domain/unitstate/service/service.go new file mode 100644 index 00000000000..f4e060a2aca --- /dev/null +++ b/domain/unitstate/service/service.go @@ -0,0 +1,104 @@ +// Copyright 2024 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package service + +import ( + "context" + + "github.com/juju/juju/domain" + "github.com/juju/juju/domain/unitstate" + "github.com/juju/juju/internal/errors" +) + +// State defines an interface for interacting with the underlying state. +type State interface { + domain.AtomicStateBase + + // GetUnitUUIDForName returns the UUID for + // the unit identified by the input name. + GetUnitUUIDForName(domain.AtomicContext, string) (string, error) + + // EnsureUnitStateRecord ensures that there is a record + // for the agent state for the unit with the input UUID. + EnsureUnitStateRecord(domain.AtomicContext, string) error + + // UpdateUnitStateUniter updates the agent uniter + // state for the unit with the input UUID. + UpdateUnitStateUniter(domain.AtomicContext, string, string) error + + // UpdateUnitStateStorage updates the agent storage + // state for the unit with the input UUID. + UpdateUnitStateStorage(domain.AtomicContext, string, string) error + + // UpdateUnitStateSecret updates the agent secret + // state for the unit with the input UUID. + UpdateUnitStateSecret(domain.AtomicContext, string, string) error + + // UpdateUnitStateCharm updates the agent charm + // state for the unit with the input UUID. + UpdateUnitStateCharm(domain.AtomicContext, string, map[string]string) error + + // UpdateUnitStateRelation updates the agent relation + // state for the unit with the input UUID. + UpdateUnitStateRelation(domain.AtomicContext, string, map[int]string) error +} + +// Service defines a service for interacting with the underlying state. +type Service struct { + st State +} + +// NewService returns a new Service for interacting with the underlying state. +func NewService(st State) *Service { + return &Service{ + st: st, + } +} + +// SetState persists the input agent state selectively, +// based on its populated values. +func (s *Service) SetState(ctx context.Context, as unitstate.AgentState) error { + return s.st.RunAtomic(ctx, func(ctx domain.AtomicContext) error { + uuid, err := s.st.GetUnitUUIDForName(ctx, as.Name) + if err != nil { + return errors.Errorf("getting unit UUID for %s: %w", as.Name, err) + } + + if err = s.st.EnsureUnitStateRecord(ctx, uuid); err != nil { + return errors.Errorf("ensuring state record for %s: %w", as.Name, err) + } + + if as.UniterState != nil { + if err = s.st.UpdateUnitStateUniter(ctx, uuid, *as.UniterState); err != nil { + return errors.Errorf("setting uniter state for %s: %w", as.Name, err) + } + } + + if as.StorageState != nil { + if err = s.st.UpdateUnitStateStorage(ctx, uuid, *as.StorageState); err != nil { + return errors.Errorf("setting storage state for %s: %w", as.Name, err) + } + } + + if as.SecretState != nil { + if err = s.st.UpdateUnitStateSecret(ctx, uuid, *as.SecretState); err != nil { + return errors.Errorf("setting secret state for %s: %w", as.Name, err) + } + } + + if as.CharmState != nil { + if err = s.st.UpdateUnitStateCharm(ctx, uuid, *as.CharmState); err != nil { + return errors.Errorf("setting charm state for %s: %w", as.Name, err) + } + } + + if as.RelationState != nil { + if err = s.st.UpdateUnitStateRelation(ctx, uuid, *as.RelationState); err != nil { + return errors.Errorf("setting relation state for %s: %w", as.Name, err) + } + } + + return nil + }) +} diff --git a/domain/unitstate/service/service_test.go b/domain/unitstate/service/service_test.go new file mode 100644 index 00000000000..a1c6428e291 --- /dev/null +++ b/domain/unitstate/service/service_test.go @@ -0,0 +1,90 @@ +// Copyright 2024 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package service + +import ( + "context" + + jc "github.com/juju/testing/checkers" + "go.uber.org/mock/gomock" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/domain" + "github.com/juju/juju/domain/application/errors" + domaintesting "github.com/juju/juju/domain/testing" + "github.com/juju/juju/domain/unitstate" + unitstateerrors "github.com/juju/juju/domain/unitstate/errors" +) + +type serviceSuite struct { + st *MockState +} + +var _ = gc.Suite(&serviceSuite{}) + +func (s *serviceSuite) TestSetStateAllAttributes(c *gc.C) { + defer s.setupMocks(c).Finish() + + uuid := "some-unit-uuid" + + exp := s.st.EXPECT() + exp.GetUnitUUIDForName(gomock.Any(), "unit/0").Return(uuid, nil) + exp.EnsureUnitStateRecord(gomock.Any(), uuid).Return(nil) + exp.UpdateUnitStateUniter(gomock.Any(), uuid, "some-uniter-state-yaml").Return(nil) + exp.UpdateUnitStateStorage(gomock.Any(), uuid, "some-storage-state-yaml").Return(nil) + exp.UpdateUnitStateSecret(gomock.Any(), uuid, "some-secret-state-yaml").Return(nil) + exp.UpdateUnitStateCharm(gomock.Any(), uuid, map[string]string{"one-key": "one-value"}).Return(nil) + exp.UpdateUnitStateRelation(gomock.Any(), uuid, map[int]string{1: "one-value"}).Return(nil) + + err := NewService(s.st).SetState(context.Background(), unitstate.AgentState{ + Name: "unit/0", + CharmState: ptr(map[string]string{"one-key": "one-value"}), + UniterState: ptr("some-uniter-state-yaml"), + RelationState: ptr(map[int]string{1: "one-value"}), + StorageState: ptr("some-storage-state-yaml"), + SecretState: ptr("some-secret-state-yaml"), + }) + c.Assert(err, jc.ErrorIsNil) +} + +func (s *serviceSuite) TestSetStateSubsetAttributes(c *gc.C) { + defer s.setupMocks(c).Finish() + + uuid := "some-unit-uuid" + + exp := s.st.EXPECT() + exp.GetUnitUUIDForName(gomock.Any(), "unit/0").Return(uuid, nil) + exp.EnsureUnitStateRecord(gomock.Any(), uuid).Return(nil) + exp.UpdateUnitStateUniter(gomock.Any(), uuid, "some-uniter-state-yaml").Return(nil) + + err := NewService(s.st).SetState(context.Background(), unitstate.AgentState{ + Name: "unit/0", + UniterState: ptr("some-uniter-state-yaml"), + }) + c.Assert(err, jc.ErrorIsNil) +} + +func (s *serviceSuite) TestSetStateUnitNotFound(c *gc.C) { + defer s.setupMocks(c).Finish() + + exp := s.st.EXPECT() + exp.GetUnitUUIDForName(gomock.Any(), "unit/0").Return("", errors.UnitNotFound) + + err := NewService(s.st).SetState(context.Background(), unitstate.AgentState{ + Name: "unit/0", + UniterState: ptr("some-uniter-state-yaml"), + }) + c.Check(err, jc.ErrorIs, unitstateerrors.UnitNotFound) +} + +func (s *serviceSuite) setupMocks(c *gc.C) *gomock.Controller { + ctrl := gomock.NewController(c) + + s.st = NewMockState(ctrl) + s.st.EXPECT().RunAtomic(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, fn func(ctx domain.AtomicContext) error) error { + return fn(domaintesting.NewAtomicContext(ctx)) + }).AnyTimes() + + return ctrl +} diff --git a/domain/unitstate/service/state_mock_test.go b/domain/unitstate/service/state_mock_test.go new file mode 100644 index 00000000000..deb7cbb5e7f --- /dev/null +++ b/domain/unitstate/service/state_mock_test.go @@ -0,0 +1,346 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/juju/juju/domain/unitstate/service (interfaces: State) +// +// Generated by this command: +// +// mockgen -typed -package service -destination state_mock_test.go github.com/juju/juju/domain/unitstate/service State +// + +// Package service is a generated GoMock package. +package service + +import ( + context "context" + reflect "reflect" + + domain "github.com/juju/juju/domain" + gomock "go.uber.org/mock/gomock" +) + +// MockState is a mock of State interface. +type MockState struct { + ctrl *gomock.Controller + recorder *MockStateMockRecorder +} + +// MockStateMockRecorder is the mock recorder for MockState. +type MockStateMockRecorder struct { + mock *MockState +} + +// NewMockState creates a new mock instance. +func NewMockState(ctrl *gomock.Controller) *MockState { + mock := &MockState{ctrl: ctrl} + mock.recorder = &MockStateMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockState) EXPECT() *MockStateMockRecorder { + return m.recorder +} + +// EnsureUnitStateRecord mocks base method. +func (m *MockState) EnsureUnitStateRecord(arg0 domain.AtomicContext, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EnsureUnitStateRecord", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// EnsureUnitStateRecord indicates an expected call of EnsureUnitStateRecord. +func (mr *MockStateMockRecorder) EnsureUnitStateRecord(arg0, arg1 any) *MockStateEnsureUnitStateRecordCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnsureUnitStateRecord", reflect.TypeOf((*MockState)(nil).EnsureUnitStateRecord), arg0, arg1) + return &MockStateEnsureUnitStateRecordCall{Call: call} +} + +// MockStateEnsureUnitStateRecordCall wrap *gomock.Call +type MockStateEnsureUnitStateRecordCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockStateEnsureUnitStateRecordCall) Return(arg0 error) *MockStateEnsureUnitStateRecordCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockStateEnsureUnitStateRecordCall) Do(f func(domain.AtomicContext, string) error) *MockStateEnsureUnitStateRecordCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockStateEnsureUnitStateRecordCall) DoAndReturn(f func(domain.AtomicContext, string) error) *MockStateEnsureUnitStateRecordCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// GetUnitUUIDForName mocks base method. +func (m *MockState) GetUnitUUIDForName(arg0 domain.AtomicContext, arg1 string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUnitUUIDForName", arg0, arg1) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUnitUUIDForName indicates an expected call of GetUnitUUIDForName. +func (mr *MockStateMockRecorder) GetUnitUUIDForName(arg0, arg1 any) *MockStateGetUnitUUIDForNameCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUnitUUIDForName", reflect.TypeOf((*MockState)(nil).GetUnitUUIDForName), arg0, arg1) + return &MockStateGetUnitUUIDForNameCall{Call: call} +} + +// MockStateGetUnitUUIDForNameCall wrap *gomock.Call +type MockStateGetUnitUUIDForNameCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockStateGetUnitUUIDForNameCall) Return(arg0 string, arg1 error) *MockStateGetUnitUUIDForNameCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockStateGetUnitUUIDForNameCall) Do(f func(domain.AtomicContext, string) (string, error)) *MockStateGetUnitUUIDForNameCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockStateGetUnitUUIDForNameCall) DoAndReturn(f func(domain.AtomicContext, string) (string, error)) *MockStateGetUnitUUIDForNameCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// RunAtomic mocks base method. +func (m *MockState) RunAtomic(arg0 context.Context, arg1 func(domain.AtomicContext) error) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RunAtomic", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// RunAtomic indicates an expected call of RunAtomic. +func (mr *MockStateMockRecorder) RunAtomic(arg0, arg1 any) *MockStateRunAtomicCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunAtomic", reflect.TypeOf((*MockState)(nil).RunAtomic), arg0, arg1) + return &MockStateRunAtomicCall{Call: call} +} + +// MockStateRunAtomicCall wrap *gomock.Call +type MockStateRunAtomicCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockStateRunAtomicCall) Return(arg0 error) *MockStateRunAtomicCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockStateRunAtomicCall) Do(f func(context.Context, func(domain.AtomicContext) error) error) *MockStateRunAtomicCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockStateRunAtomicCall) DoAndReturn(f func(context.Context, func(domain.AtomicContext) error) error) *MockStateRunAtomicCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// UpdateUnitStateCharm mocks base method. +func (m *MockState) UpdateUnitStateCharm(arg0 domain.AtomicContext, arg1 string, arg2 map[string]string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateUnitStateCharm", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateUnitStateCharm indicates an expected call of UpdateUnitStateCharm. +func (mr *MockStateMockRecorder) UpdateUnitStateCharm(arg0, arg1, arg2 any) *MockStateUpdateUnitStateCharmCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUnitStateCharm", reflect.TypeOf((*MockState)(nil).UpdateUnitStateCharm), arg0, arg1, arg2) + return &MockStateUpdateUnitStateCharmCall{Call: call} +} + +// MockStateUpdateUnitStateCharmCall wrap *gomock.Call +type MockStateUpdateUnitStateCharmCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockStateUpdateUnitStateCharmCall) Return(arg0 error) *MockStateUpdateUnitStateCharmCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockStateUpdateUnitStateCharmCall) Do(f func(domain.AtomicContext, string, map[string]string) error) *MockStateUpdateUnitStateCharmCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockStateUpdateUnitStateCharmCall) DoAndReturn(f func(domain.AtomicContext, string, map[string]string) error) *MockStateUpdateUnitStateCharmCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// UpdateUnitStateRelation mocks base method. +func (m *MockState) UpdateUnitStateRelation(arg0 domain.AtomicContext, arg1 string, arg2 map[int]string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateUnitStateRelation", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateUnitStateRelation indicates an expected call of UpdateUnitStateRelation. +func (mr *MockStateMockRecorder) UpdateUnitStateRelation(arg0, arg1, arg2 any) *MockStateUpdateUnitStateRelationCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUnitStateRelation", reflect.TypeOf((*MockState)(nil).UpdateUnitStateRelation), arg0, arg1, arg2) + return &MockStateUpdateUnitStateRelationCall{Call: call} +} + +// MockStateUpdateUnitStateRelationCall wrap *gomock.Call +type MockStateUpdateUnitStateRelationCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockStateUpdateUnitStateRelationCall) Return(arg0 error) *MockStateUpdateUnitStateRelationCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockStateUpdateUnitStateRelationCall) Do(f func(domain.AtomicContext, string, map[int]string) error) *MockStateUpdateUnitStateRelationCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockStateUpdateUnitStateRelationCall) DoAndReturn(f func(domain.AtomicContext, string, map[int]string) error) *MockStateUpdateUnitStateRelationCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// UpdateUnitStateSecret mocks base method. +func (m *MockState) UpdateUnitStateSecret(arg0 domain.AtomicContext, arg1, arg2 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateUnitStateSecret", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateUnitStateSecret indicates an expected call of UpdateUnitStateSecret. +func (mr *MockStateMockRecorder) UpdateUnitStateSecret(arg0, arg1, arg2 any) *MockStateUpdateUnitStateSecretCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUnitStateSecret", reflect.TypeOf((*MockState)(nil).UpdateUnitStateSecret), arg0, arg1, arg2) + return &MockStateUpdateUnitStateSecretCall{Call: call} +} + +// MockStateUpdateUnitStateSecretCall wrap *gomock.Call +type MockStateUpdateUnitStateSecretCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockStateUpdateUnitStateSecretCall) Return(arg0 error) *MockStateUpdateUnitStateSecretCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockStateUpdateUnitStateSecretCall) Do(f func(domain.AtomicContext, string, string) error) *MockStateUpdateUnitStateSecretCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockStateUpdateUnitStateSecretCall) DoAndReturn(f func(domain.AtomicContext, string, string) error) *MockStateUpdateUnitStateSecretCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// UpdateUnitStateStorage mocks base method. +func (m *MockState) UpdateUnitStateStorage(arg0 domain.AtomicContext, arg1, arg2 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateUnitStateStorage", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateUnitStateStorage indicates an expected call of UpdateUnitStateStorage. +func (mr *MockStateMockRecorder) UpdateUnitStateStorage(arg0, arg1, arg2 any) *MockStateUpdateUnitStateStorageCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUnitStateStorage", reflect.TypeOf((*MockState)(nil).UpdateUnitStateStorage), arg0, arg1, arg2) + return &MockStateUpdateUnitStateStorageCall{Call: call} +} + +// MockStateUpdateUnitStateStorageCall wrap *gomock.Call +type MockStateUpdateUnitStateStorageCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockStateUpdateUnitStateStorageCall) Return(arg0 error) *MockStateUpdateUnitStateStorageCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockStateUpdateUnitStateStorageCall) Do(f func(domain.AtomicContext, string, string) error) *MockStateUpdateUnitStateStorageCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockStateUpdateUnitStateStorageCall) DoAndReturn(f func(domain.AtomicContext, string, string) error) *MockStateUpdateUnitStateStorageCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// UpdateUnitStateUniter mocks base method. +func (m *MockState) UpdateUnitStateUniter(arg0 domain.AtomicContext, arg1, arg2 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateUnitStateUniter", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateUnitStateUniter indicates an expected call of UpdateUnitStateUniter. +func (mr *MockStateMockRecorder) UpdateUnitStateUniter(arg0, arg1, arg2 any) *MockStateUpdateUnitStateUniterCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUnitStateUniter", reflect.TypeOf((*MockState)(nil).UpdateUnitStateUniter), arg0, arg1, arg2) + return &MockStateUpdateUnitStateUniterCall{Call: call} +} + +// MockStateUpdateUnitStateUniterCall wrap *gomock.Call +type MockStateUpdateUnitStateUniterCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockStateUpdateUnitStateUniterCall) Return(arg0 error) *MockStateUpdateUnitStateUniterCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockStateUpdateUnitStateUniterCall) Do(f func(domain.AtomicContext, string, string) error) *MockStateUpdateUnitStateUniterCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockStateUpdateUnitStateUniterCall) DoAndReturn(f func(domain.AtomicContext, string, string) error) *MockStateUpdateUnitStateUniterCall { + c.Call = c.Call.DoAndReturn(f) + return c +}