diff --git a/domain/application/charm/store/store.go b/domain/application/charm/store/store.go index 41fd8330418..98f6de61365 100644 --- a/domain/application/charm/store/store.go +++ b/domain/application/charm/store/store.go @@ -7,6 +7,7 @@ import ( "context" "encoding/base32" "fmt" + "io" "os" "github.com/juju/juju/core/objectstore" @@ -19,7 +20,7 @@ const ( ErrNotFound = errors.ConstError("file not found") ) -// CharmStore provides an API for storing charms. +// CharmStore provides an API for storing and retrieving charm blobs. type CharmStore struct { objectStoreGetter objectstore.ModelObjectStoreGetter encoder *base32.Encoding @@ -66,3 +67,19 @@ func (s *CharmStore) Store(ctx context.Context, name string, path string, size i // Store the file in the object store. return objectStore.PutAndCheckHash(ctx, uniqueName, file, size, hash) } + +// Get retrieves a ReadCloser for the charm archive at the give path from +// the underlying storage. +// NOTE: It is up to the caller to verify the integrity of the data from the charm +// hash stored in DQLite. +func (s *CharmStore) Get(ctx context.Context, archivePath string) (io.ReadCloser, error) { + store, err := s.objectStoreGetter.GetObjectStore(ctx) + if err != nil { + return nil, errors.Errorf("getting object store: %w", err) + } + reader, _, err := store.Get(ctx, archivePath) + if err != nil { + return nil, errors.Errorf("getting charm: %w", err) + } + return reader, nil +} diff --git a/domain/application/charm/store/store_test.go b/domain/application/charm/store/store_test.go index 0bc945fd5e7..0a5186965ea 100644 --- a/domain/application/charm/store/store_test.go +++ b/domain/application/charm/store/store_test.go @@ -10,6 +10,7 @@ import ( "io" "os" "path/filepath" + "strings" "github.com/juju/testing" jc "github.com/juju/testing/checkers" @@ -111,6 +112,30 @@ func (s *storeSuite) TestStoreFailed(c *gc.C) { c.Assert(err, gc.ErrorMatches, ".*boom") } +func (s *storeSuite) TestGet(c *gc.C) { + defer s.setupMocks(c).Finish() + + archive := io.NopCloser(strings.NewReader("archive-content")) + s.objectStore.EXPECT().Get(gomock.Any(), "foo").Return(archive, 0, nil) + + storage := NewCharmStore(s.objectStoreGetter) + reader, err := storage.Get(context.Background(), "foo") + c.Assert(err, jc.ErrorIsNil) + content, err := io.ReadAll(reader) + c.Assert(err, jc.ErrorIsNil) + c.Check(string(content), gc.Equals, "archive-content") +} + +func (s *storeSuite) TestGetFailed(c *gc.C) { + defer s.setupMocks(c).Finish() + + s.objectStore.EXPECT().Get(gomock.Any(), "foo").Return(nil, 0, errors.Errorf("boom")) + + storage := NewCharmStore(s.objectStoreGetter) + _, err := storage.Get(context.Background(), "foo") + c.Assert(err, gc.ErrorMatches, ".*boom") +} + func (s *storeSuite) setupMocks(c *gc.C) *gomock.Controller { ctrl := gomock.NewController(c) diff --git a/domain/application/errors/errors.go b/domain/application/errors/errors.go index 244a043e352..54e1f7a8c4a 100644 --- a/domain/application/errors/errors.go +++ b/domain/application/errors/errors.go @@ -127,6 +127,11 @@ const ( // wrong value. CharmRelationRoleNotValid = errors.ConstError("charm relation role not valid") + // MultipleCharmHashes describes and error that occurs when a charm has multiple + // hash values. At the moment, we only support sha256 hash format, so if another + // is found, an error is returned. + MultipleCharmHashes = errors.ConstError("multiple charm hashes found") + // ResourceNotFound describes an error that occurs when a resource is // not found. ResourceNotFound = errors.ConstError("resource not found") diff --git a/domain/application/service/application_test.go b/domain/application/service/application_test.go index d43f48f092b..1ab533c9347 100644 --- a/domain/application/service/application_test.go +++ b/domain/application/service/application_test.go @@ -1236,6 +1236,7 @@ func (s *applicationWatcherServiceSuite) setupMocks(c *gc.C) *gomock.Controller s.watcherFactory, nil, nil, + nil, s.clock, loggertesting.WrapCheckLog(c), ) diff --git a/domain/application/service/charm.go b/domain/application/service/charm.go index 28ed02b4a3f..8e80cd4710d 100644 --- a/domain/application/service/charm.go +++ b/domain/application/service/charm.go @@ -6,6 +6,7 @@ package service import ( "context" "fmt" + "io" "regexp" "github.com/juju/errors" @@ -17,6 +18,7 @@ import ( "github.com/juju/juju/domain/application/charm" applicationerrors "github.com/juju/juju/domain/application/errors" internalcharm "github.com/juju/juju/internal/charm" + internalerrors "github.com/juju/juju/internal/errors" ) var ( @@ -99,7 +101,12 @@ type CharmState interface { // GetCharmArchivePath returns the archive storage path for the charm using // the charm ID. If the charm does not exist, a // [applicationerrors.CharmNotFound] error is returned. - GetCharmArchivePath(ctx context.Context, charmID corecharm.ID) (string, error) + GetCharmArchivePath(context.Context, corecharm.ID) (string, error) + + // GetCharmArchiveMetadata returns the archive storage path and hash for the + // charm using the charm ID. + // If the charm does not exist, a [errors.CharmNotFound] error is returned. + GetCharmArchiveMetadata(context.Context, corecharm.ID) (archivePath string, hash string, err error) // IsCharmAvailable returns whether the charm is available for use. If the // charm does not exist, a [applicationerrors.CharmNotFound] error is @@ -137,6 +144,14 @@ type CharmState interface { ListCharmsWithOriginByNames(ctx context.Context, names []string) ([]charm.CharmWithOrigin, error) } +// CharmStore defines the interface for storing and retrieving charms archive blobs +// from the underlying storage. +type CharmStore interface { + // GetCharm retrieves a ReadCloser for the charm archive at the give path from + // the underlying storage. + Get(ctx context.Context, archivePath string) (io.ReadCloser, error) +} + // GetCharmID returns a charm ID by name. It returns an error if the charm // can not be found by the name. // This can also be used as a cheap way to see if a charm exists without @@ -411,16 +426,40 @@ func (s *Service) GetCharmLXDProfile(ctx context.Context, id corecharm.ID) (inte // returned. func (s *Service) GetCharmArchivePath(ctx context.Context, id corecharm.ID) (string, error) { if err := id.Validate(); err != nil { - return "", fmt.Errorf("charm id: %w", err) + return "", internalerrors.Errorf("charm id: %w", err) } path, err := s.st.GetCharmArchivePath(ctx, id) if err != nil { - return "", errors.Trace(err) + return "", internalerrors.Errorf("getting charm archive path: %w", err) } return path, nil } +// GetCharmArchive returns a ReadCloser stream for the charm archive for a given +// charm id, along with the hash of the charm archive. Clients can use the hash +// to verify the integrity of the charm archive. +// +// If the charm does not exist, a [applicationerrors.CharmNotFound] error is +// returned. +func (s *Service) GetCharmArchive(ctx context.Context, id corecharm.ID) (io.ReadCloser, string, error) { + if err := id.Validate(); err != nil { + return nil, "", internalerrors.Errorf("charm id: %w", err) + } + + archivePath, hash, err := s.st.GetCharmArchiveMetadata(ctx, id) + if err != nil { + return nil, "", internalerrors.Errorf("getting charm archive metadata: %w", err) + } + + reader, err := s.charmStore.Get(ctx, archivePath) + if err != nil { + return nil, "", internalerrors.Errorf("getting charm archive: %w", err) + } + + return reader, hash, nil +} + // IsCharmAvailable returns whether the charm is available for use. This // indicates if the charm has been uploaded to the controller. // This will return true if the charm is available, and false otherwise. diff --git a/domain/application/service/charm_test.go b/domain/application/service/charm_test.go index fec0d9c150c..07c78be38af 100644 --- a/domain/application/service/charm_test.go +++ b/domain/application/service/charm_test.go @@ -5,6 +5,8 @@ package service import ( "context" + "io" + "strings" "github.com/juju/errors" jc "github.com/juju/testing/checkers" @@ -398,6 +400,24 @@ func (s *charmServiceSuite) TestGetCharmArchivePathInvalidUUID(c *gc.C) { c.Assert(err, jc.ErrorIs, errors.NotValid) } +func (s *charmServiceSuite) TestGetCharmArchive(c *gc.C) { + defer s.setupMocks(c).Finish() + + id := charmtesting.GenCharmID(c) + archive := io.NopCloser(strings.NewReader("archive-content")) + + s.state.EXPECT().GetCharmArchiveMetadata(gomock.Any(), id).Return("archive-path", "hash", nil) + s.charmStore.EXPECT().Get(gomock.Any(), "archive-path").Return(archive, nil) + + reader, hash, err := s.service.GetCharmArchive(context.Background(), id) + c.Assert(err, jc.ErrorIsNil) + c.Check(hash, gc.Equals, "hash") + + content, err := io.ReadAll(reader) + c.Assert(err, jc.ErrorIsNil) + c.Check(string(content), gc.Equals, "archive-content") +} + func (s *charmServiceSuite) TestSetCharmAvailable(c *gc.C) { defer s.setupMocks(c).Finish() diff --git a/domain/application/service/package_mock_test.go b/domain/application/service/package_mock_test.go index 5d523db69ec..58efe54c268 100644 --- a/domain/application/service/package_mock_test.go +++ b/domain/application/service/package_mock_test.go @@ -1,9 +1,9 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/juju/juju/domain/application/service (interfaces: State,DeleteSecretState,ResourceStoreGetter,WatcherFactory,AgentVersionGetter,Provider) +// Source: github.com/juju/juju/domain/application/service (interfaces: State,DeleteSecretState,ResourceStoreGetter,WatcherFactory,AgentVersionGetter,Provider,CharmStore) // // Generated by this command: // -// mockgen -typed -package service -destination package_mock_test.go github.com/juju/juju/domain/application/service State,DeleteSecretState,ResourceStoreGetter,WatcherFactory,AgentVersionGetter,Provider +// mockgen -typed -package service -destination package_mock_test.go github.com/juju/juju/domain/application/service State,DeleteSecretState,ResourceStoreGetter,WatcherFactory,AgentVersionGetter,Provider,CharmStore // // Package service is a generated GoMock package. @@ -11,6 +11,7 @@ package service import ( context "context" + io "io" reflect "reflect" application "github.com/juju/juju/core/application" @@ -613,6 +614,46 @@ func (c *MockStateGetCharmActionsCall) DoAndReturn(f func(context.Context, charm return c } +// GetCharmArchiveMetadata mocks base method. +func (m *MockState) GetCharmArchiveMetadata(arg0 context.Context, arg1 charm.ID) (string, string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCharmArchiveMetadata", arg0, arg1) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(string) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetCharmArchiveMetadata indicates an expected call of GetCharmArchiveMetadata. +func (mr *MockStateMockRecorder) GetCharmArchiveMetadata(arg0, arg1 any) *MockStateGetCharmArchiveMetadataCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCharmArchiveMetadata", reflect.TypeOf((*MockState)(nil).GetCharmArchiveMetadata), arg0, arg1) + return &MockStateGetCharmArchiveMetadataCall{Call: call} +} + +// MockStateGetCharmArchiveMetadataCall wrap *gomock.Call +type MockStateGetCharmArchiveMetadataCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockStateGetCharmArchiveMetadataCall) Return(arg0, arg1 string, arg2 error) *MockStateGetCharmArchiveMetadataCall { + c.Call = c.Call.Return(arg0, arg1, arg2) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockStateGetCharmArchiveMetadataCall) Do(f func(context.Context, charm.ID) (string, string, error)) *MockStateGetCharmArchiveMetadataCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockStateGetCharmArchiveMetadataCall) DoAndReturn(f func(context.Context, charm.ID) (string, string, error)) *MockStateGetCharmArchiveMetadataCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // GetCharmArchivePath mocks base method. func (m *MockState) GetCharmArchivePath(arg0 context.Context, arg1 charm.ID) (string, error) { m.ctrl.T.Helper() @@ -2974,3 +3015,65 @@ func (c *MockProviderSupportedFeaturesCall) DoAndReturn(f func() (assumes.Featur c.Call = c.Call.DoAndReturn(f) return c } + +// MockCharmStore is a mock of CharmStore interface. +type MockCharmStore struct { + ctrl *gomock.Controller + recorder *MockCharmStoreMockRecorder +} + +// MockCharmStoreMockRecorder is the mock recorder for MockCharmStore. +type MockCharmStoreMockRecorder struct { + mock *MockCharmStore +} + +// NewMockCharmStore creates a new mock instance. +func NewMockCharmStore(ctrl *gomock.Controller) *MockCharmStore { + mock := &MockCharmStore{ctrl: ctrl} + mock.recorder = &MockCharmStoreMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCharmStore) EXPECT() *MockCharmStoreMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockCharmStore) Get(arg0 context.Context, arg1 string) (io.ReadCloser, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", arg0, arg1) + ret0, _ := ret[0].(io.ReadCloser) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockCharmStoreMockRecorder) Get(arg0, arg1 any) *MockCharmStoreGetCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockCharmStore)(nil).Get), arg0, arg1) + return &MockCharmStoreGetCall{Call: call} +} + +// MockCharmStoreGetCall wrap *gomock.Call +type MockCharmStoreGetCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockCharmStoreGetCall) Return(arg0 io.ReadCloser, arg1 error) *MockCharmStoreGetCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockCharmStoreGetCall) Do(f func(context.Context, string) (io.ReadCloser, error)) *MockCharmStoreGetCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockCharmStoreGetCall) DoAndReturn(f func(context.Context, string) (io.ReadCloser, error)) *MockCharmStoreGetCall { + c.Call = c.Call.DoAndReturn(f) + return c +} diff --git a/domain/application/service/package_test.go b/domain/application/service/package_test.go index 07af2781c72..d9a4f00fcbf 100644 --- a/domain/application/service/package_test.go +++ b/domain/application/service/package_test.go @@ -26,7 +26,7 @@ import ( dummystorage "github.com/juju/juju/internal/storage/provider/dummy" ) -//go:generate go run go.uber.org/mock/mockgen -typed -package service -destination package_mock_test.go github.com/juju/juju/domain/application/service State,DeleteSecretState,ResourceStoreGetter,WatcherFactory,AgentVersionGetter,Provider +//go:generate go run go.uber.org/mock/mockgen -typed -package service -destination package_mock_test.go github.com/juju/juju/domain/application/service State,DeleteSecretState,ResourceStoreGetter,WatcherFactory,AgentVersionGetter,Provider,CharmStore //go:generate go run go.uber.org/mock/mockgen -typed -package service -destination charm_mock_test.go github.com/juju/juju/internal/charm Charm func TestPackage(t *testing.T) { @@ -41,6 +41,7 @@ type baseSuite struct { state *MockState charm *MockCharm secret *MockDeleteSecretState + charmStore *MockCharmStore agentVersionGetter *MockAgentVersionGetter provider *MockProvider @@ -67,6 +68,7 @@ func (s *baseSuite) setupMocksWithProvider(c *gc.C, fn func(ctx context.Context) s.state = NewMockState(ctrl) s.charm = NewMockCharm(ctrl) s.secret = NewMockDeleteSecretState(ctrl) + s.charmStore = NewMockCharmStore(ctrl) s.storageRegistryGetter = corestorage.ConstModelStorageRegistry(func() storage.ProviderRegistry { return storage.ChainedProviderRegistry{ @@ -83,6 +85,7 @@ func (s *baseSuite) setupMocksWithProvider(c *gc.C, fn func(ctx context.Context) s.modelID, s.agentVersionGetter, fn, + s.charmStore, s.clock, loggertesting.WrapCheckLog(c), ) @@ -103,6 +106,7 @@ func (s *baseSuite) setupMocksWithAtomic(c *gc.C, fn func(domain.AtomicContext) s.state = NewMockState(ctrl) s.charm = NewMockCharm(ctrl) s.secret = NewMockDeleteSecretState(ctrl) + s.charmStore = NewMockCharmStore(ctrl) s.storageRegistryGetter = corestorage.ConstModelStorageRegistry(func() storage.ProviderRegistry { return storage.ChainedProviderRegistry{ @@ -121,6 +125,7 @@ func (s *baseSuite) setupMocksWithAtomic(c *gc.C, fn func(domain.AtomicContext) func(ctx context.Context) (Provider, error) { return s.provider, nil }, + s.charmStore, s.clock, loggertesting.WrapCheckLog(c), ) diff --git a/domain/application/service/service.go b/domain/application/service/service.go index 1ac9b6ec505..159b7ad36fe 100644 --- a/domain/application/service/service.go +++ b/domain/application/service/service.go @@ -62,6 +62,7 @@ type Service struct { storageRegistryGetter corestorage.ModelStorageRegistryGetter secretDeleter DeleteSecretState + charmStore CharmStore } // NewService returns a new service reference wrapping the input state. @@ -69,6 +70,7 @@ func NewService( st State, deleteSecretState DeleteSecretState, storageRegistryGetter corestorage.ModelStorageRegistryGetter, + charmStore CharmStore, clock clock.Clock, logger logger.Logger, ) *Service { @@ -78,6 +80,7 @@ func NewService( clock: clock, storageRegistryGetter: storageRegistryGetter, secretDeleter: deleteSecretState, + charmStore: charmStore, } } @@ -113,6 +116,7 @@ func NewProviderService( modelID coremodel.UUID, agentVersionGetter AgentVersionGetter, provider providertracker.ProviderGetter[Provider], + charmStore CharmStore, clock clock.Clock, logger logger.Logger, ) *ProviderService { @@ -121,6 +125,7 @@ func NewProviderService( st, deleteSecretState, storageRegistryGetter, + charmStore, clock, logger, ), @@ -180,6 +185,7 @@ func NewWatchableService( watcherFactory WatcherFactory, agentVersionGetter AgentVersionGetter, provider providertracker.ProviderGetter[Provider], + charmStore CharmStore, clock clock.Clock, logger logger.Logger, ) *WatchableService { @@ -191,6 +197,7 @@ func NewWatchableService( modelID, agentVersionGetter, provider, + charmStore, clock, logger, ), diff --git a/domain/application/service_test.go b/domain/application/service_test.go index cf0d0319b8e..51f38488f7c 100644 --- a/domain/application/service_test.go +++ b/domain/application/service_test.go @@ -64,6 +64,7 @@ func (s *serviceSuite) SetUpTest(c *gc.C) { corestorage.ConstModelStorageRegistry(func() storage.ProviderRegistry { return provider.CommonStorageProviders() }), + nil, clock.WallClock, loggertesting.WrapCheckLog(c), ) diff --git a/domain/application/state/charm.go b/domain/application/state/charm.go index fb62548dc96..702edadbe3c 100644 --- a/domain/application/state/charm.go +++ b/domain/application/state/charm.go @@ -11,7 +11,6 @@ import ( "github.com/juju/errors" corecharm "github.com/juju/juju/core/charm" - "github.com/juju/juju/domain" "github.com/juju/juju/domain/application/charm" applicationerrors "github.com/juju/juju/domain/application/errors" internalerrors "github.com/juju/juju/internal/errors" @@ -374,7 +373,7 @@ WHERE uuid = $charmID.uuid; stmt, err := s.Prepare(query, archivePath, ident) if err != nil { - return "", internalerrors.Errorf("failed to prepare query: %w", err) + return "", internalerrors.Errorf("preparing query: %w", err) } if err := db.Txn(ctx, func(ctx context.Context, tx *sqlair.TX) error { @@ -382,16 +381,58 @@ WHERE uuid = $charmID.uuid; if errors.Is(err, sqlair.ErrNoRows) { return applicationerrors.CharmNotFound } - return internalerrors.Errorf("failed to get charm archive path: %w", err) + return err } return nil }); err != nil { - return "", internalerrors.Errorf("failed to get charm archive path: %w", domain.CoerceError(err)) + return "", internalerrors.Errorf("getting charm archive path: %w", err) } return archivePath.ArchivePath, nil } +// GetCharmArchiveMetadata returns the archive storage path and the sha256 hash +// for the charm using the charm ID. +// If the charm does not exist, a [errors.CharmNotFound] error is returned. +func (s *State) GetCharmArchiveMetadata(ctx context.Context, id corecharm.ID) (archivePath string, hash string, err error) { + db, err := s.DB() + if err != nil { + return "", "", internalerrors.Capture(err) + } + + var archivePathAndHashes []charmArchivePathAndHash + ident := charmID{UUID: id.String()} + + query := ` +SELECT &charmArchivePathAndHash.* +FROM charm +JOIN charm_hash ON charm.uuid = charm_hash.charm_uuid +WHERE charm.uuid = $charmID.uuid; +` + + stmt, err := s.Prepare(query, charmArchivePathAndHash{}, ident) + if err != nil { + return "", "", internalerrors.Errorf("preparing query: %w", err) + } + + if err := db.Txn(ctx, func(ctx context.Context, tx *sqlair.TX) error { + if err := tx.Query(ctx, stmt, ident).GetAll(&archivePathAndHashes); err != nil { + if errors.Is(err, sqlair.ErrNoRows) { + return applicationerrors.CharmNotFound + } + return err + } + return nil + }); err != nil { + return "", "", internalerrors.Errorf("getting charm archive metadata: %w", err) + } + if len(archivePathAndHashes) > 1 { + return "", "", internalerrors.Errorf("getting charm archive metadata: %w", applicationerrors.MultipleCharmHashes) + } + + return archivePathAndHashes[0].ArchivePath, archivePathAndHashes[0].Hash, nil +} + // GetCharmMetadata returns the metadata for the charm using the charm ID. // If the charm does not exist, a [errors.CharmNotFound] error is returned. func (s *State) GetCharmMetadata(ctx context.Context, id corecharm.ID) (charm.Metadata, error) { diff --git a/domain/application/state/charm_test.go b/domain/application/state/charm_test.go index 166ad1da48f..077eb56d5b8 100644 --- a/domain/application/state/charm_test.go +++ b/domain/application/state/charm_test.go @@ -13,6 +13,7 @@ import ( "github.com/juju/version/v2" gc "gopkg.in/check.v1" + corecharm "github.com/juju/juju/core/charm" charmtesting "github.com/juju/juju/core/charm/testing" coredatabase "github.com/juju/juju/core/database" "github.com/juju/juju/domain/application/charm" @@ -2489,6 +2490,50 @@ func (s *charmStateSuite) TestGetCharmArchivePathCharmNotFound(c *gc.C) { c.Assert(err, jc.ErrorIs, applicationerrors.CharmNotFound) } +func (s *charmStateSuite) TestGetCharmArchiveMetadata(c *gc.C) { + st := NewState(s.TxnRunnerFactory(), loggertesting.WrapCheckLog(c)) + + id, err := st.SetCharm(context.Background(), charm.Charm{ + Metadata: charm.Metadata{ + Name: "ubuntu", + }, + }, setStateArgs("ubuntu")) + c.Assert(err, jc.ErrorIsNil) + + got, hash, err := st.GetCharmArchiveMetadata(context.Background(), id) + c.Assert(err, jc.ErrorIsNil) + c.Check(got, gc.DeepEquals, "archive") + c.Check(hash, gc.DeepEquals, "hash") +} + +func (s *charmStateSuite) TestGetCharmArchiveMetadataInsertAdditionalHashKind(c *gc.C) { + st := NewState(s.TxnRunnerFactory(), loggertesting.WrapCheckLog(c)) + + id, err := st.SetCharm(context.Background(), charm.Charm{ + Metadata: charm.Metadata{ + Name: "ubuntu", + }, + }, setStateArgs("ubuntu")) + c.Assert(err, jc.ErrorIsNil) + + err = s.TxnRunner().StdTxn(context.Background(), func(ctx context.Context, tx *sql.Tx) error { + return insertAdditonalHashKindForCharm(ctx, c, tx, id, "sha386", "hash386") + }) + c.Assert(err, jc.ErrorIsNil) + + _, _, err = st.GetCharmArchiveMetadata(context.Background(), id) + c.Assert(err, jc.ErrorIs, applicationerrors.MultipleCharmHashes) +} + +func (s *charmStateSuite) TestGetCharmArchiveMetadataCharmNotFound(c *gc.C) { + st := NewState(s.TxnRunnerFactory(), loggertesting.WrapCheckLog(c)) + + id := charmtesting.GenCharmID(c) + + _, _, err := st.GetCharmArchiveMetadata(context.Background(), id) + c.Assert(err, jc.ErrorIs, applicationerrors.CharmNotFound) +} + func (s *charmStateSuite) TestCharmsWithOriginWithNoEntries(c *gc.C) { st := NewState(s.TxnRunnerFactory(), loggertesting.WrapCheckLog(c)) @@ -2665,6 +2710,28 @@ func insertCharmManifest(ctx context.Context, c *gc.C, tx *sql.Tx, uuid string) return charm.Manifest{}, nil } +func insertAdditonalHashKindForCharm(ctx context.Context, c *gc.C, tx *sql.Tx, charmId corecharm.ID, kind, hash string) error { + var kindId int + rows, err := tx.QueryContext(ctx, `SELECT id FROM hash_kind`) + c.Assert(err, jc.ErrorIsNil) + for rows.Next() { + var id int + err := rows.Scan(&id) + c.Assert(err, jc.ErrorIsNil) + kindId = max(kindId, id) + } + kindId++ + defer func() { _ = rows.Close() }() + + _, err = tx.ExecContext(ctx, `INSERT INTO hash_kind (id, name) VALUES (?, ?)`, kindId, kind) + c.Assert(err, jc.ErrorIsNil) + + _, err = tx.ExecContext(ctx, `INSERT INTO charm_hash (charm_uuid, hash_kind_id, hash) VALUES (?, ?, ?)`, charmId, kindId, hash) + c.Assert(err, jc.ErrorIsNil) + + return nil +} + func assertTableEmpty(c *gc.C, runner coredatabase.TxnRunner, table string) { // Ensure that we don't use zero values for the count, as that would // pass if the table is empty. diff --git a/domain/application/state/types.go b/domain/application/state/types.go index 962f1d1d898..adddd003e39 100644 --- a/domain/application/state/types.go +++ b/domain/application/state/types.go @@ -561,6 +561,11 @@ type charmArchivePath struct { ArchivePath string `db:"archive_path"` } +type charmArchivePathAndHash struct { + ArchivePath string `db:"archive_path"` + Hash string `db:"hash"` +} + // charmOrigin is used to get the origin of a charm. type charmOrigin struct { CharmUUID string `db:"charm_uuid"` diff --git a/domain/application/watcher_test.go b/domain/application/watcher_test.go index 1083616d119..1032e669a0f 100644 --- a/domain/application/watcher_test.go +++ b/domain/application/watcher_test.go @@ -462,7 +462,7 @@ func (s *watcherSuite) setupService(c *gc.C, factory domain.WatchableDBFactory) }), "", domain.NewWatcherFactory(factory, loggertesting.WrapCheckLog(c)), - nil, nil, + nil, nil, nil, clock.WallClock, loggertesting.WrapCheckLog(c), ) diff --git a/domain/schema/model/sql/0015-charm.sql b/domain/schema/model/sql/0015-charm.sql index 220da97a77d..ceedb882cdc 100644 --- a/domain/schema/model/sql/0015-charm.sql +++ b/domain/schema/model/sql/0015-charm.sql @@ -159,6 +159,7 @@ CREATE TABLE hash_kind ( CREATE UNIQUE INDEX idx_hash_kind_name ON hash_kind (name); +-- We only support sha256 hashes for now. INSERT INTO hash_kind VALUES (0, 'sha256'); diff --git a/domain/secret/service_test.go b/domain/secret/service_test.go index 80107ac2267..13ec15f5970 100644 --- a/domain/secret/service_test.go +++ b/domain/secret/service_test.go @@ -97,6 +97,7 @@ func (s *serviceSuite) createSecret(c *gc.C, data map[string]string, valueRef *c corestorage.ConstModelStorageRegistry(func() storage.ProviderRegistry { return storage.NotImplementedProviderRegistry{} }), + nil, clock.WallClock, loggertesting.WrapCheckLog(c), ) diff --git a/domain/secret/watcher_test.go b/domain/secret/watcher_test.go index 3ee64e98df3..ab40354ac85 100644 --- a/domain/secret/watcher_test.go +++ b/domain/secret/watcher_test.go @@ -62,6 +62,7 @@ func (s *watcherSuite) setupUnits(c *gc.C, appName string) { corestorage.ConstModelStorageRegistry(func() storage.ProviderRegistry { return storage.NotImplementedProviderRegistry{} }), + nil, clock.WallClock, logger, ) diff --git a/domain/services/model.go b/domain/services/model.go index 14700b5547b..d799d1b5556 100644 --- a/domain/services/model.go +++ b/domain/services/model.go @@ -20,6 +20,7 @@ import ( agentprovisionerstate "github.com/juju/juju/domain/agentprovisioner/state" annotationService "github.com/juju/juju/domain/annotation/service" annotationState "github.com/juju/juju/domain/annotation/state" + charmstore "github.com/juju/juju/domain/application/charm/store" applicationservice "github.com/juju/juju/domain/application/service" applicationstate "github.com/juju/juju/domain/application/state" blockcommandservice "github.com/juju/juju/domain/blockcommand/service" @@ -176,6 +177,7 @@ func (s *ModelServices) Application() *applicationservice.WatchableService { s.modelWatcherFactory("application"), modelagentstate.NewState(changestream.NewTxnRunnerFactory(s.controllerDB)), providertracker.ProviderRunner[applicationservice.Provider](s.providerFactory, s.modelUUID.String()), + charmstore.NewCharmStore(s.objectstore), s.clock, log, )