Skip to content

Commit

Permalink
feat: state resolve migration upload charm
Browse files Browse the repository at this point in the history
Adds the state calls for resolving a migrated uploaded charm.
  • Loading branch information
SimonRichardson committed Dec 17, 2024
1 parent f9ce437 commit e067796
Show file tree
Hide file tree
Showing 8 changed files with 220 additions and 40 deletions.
6 changes: 5 additions & 1 deletion apiserver/objects.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,11 @@ func (h *objectsCharmHTTPHandler) processPut(ctx context.Context, r *http.Reques
// correctly.
Importing: isImporting,
})
if err != nil {
if errors.Is(err, applicationerrors.CharmNotFound) {
return nil, jujuerrors.NotFoundf("charm")
} else if errors.Is(err, applicationerrors.CharmAlreadyAvailable) {
return nil, jujuerrors.AlreadyExistsf("charm")
} else if err != nil {
return nil, errors.Capture(err)
}

Expand Down
5 changes: 5 additions & 0 deletions domain/application/service/charm.go
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,9 @@ func (s *Service) resolveLocalUploadedCharm(ctx context.Context, args charm.Reso
return charm.CharmLocator{}, internalerrors.Errorf("reading charm archive %q: %w", result.ArchivePath, err)
}

// TODO (stickupkid): Sequence a charm revision, so each charm has a unique
// revision.

// This is a full blown upload, we need to set everything up.
resolved, warnings, err := s.setCharm(ctx, charm.SetCharmArgs{
Charm: ch,
Expand Down Expand Up @@ -688,6 +691,8 @@ func (s *Service) resolveMigratingUploadedCharm(ctx context.Context, args charm.
// 1. The charm metadata has already been verified.
// 2. The charm metadata has already been inserted into the database
// during the migration.
// 3. A local charm has already been sequenced, so we don't need to
// attempt to sequence the charm again.
//
// This means we need to locate the charm that's already stored in the
// database and set it as available.
Expand Down
21 changes: 0 additions & 21 deletions domain/application/state/application_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ import (
applicationtesting "github.com/juju/juju/core/application/testing"
coremodel "github.com/juju/juju/core/model"
"github.com/juju/juju/core/network"
"github.com/juju/juju/core/objectstore"
objectstoretesting "github.com/juju/juju/core/objectstore/testing"
"github.com/juju/juju/core/secrets"
coreunit "github.com/juju/juju/core/unit"
unittesting "github.com/juju/juju/core/unit/testing"
Expand Down Expand Up @@ -2464,25 +2462,6 @@ func (s *applicationStateSuite) createApplication(c *gc.C, name string, l life.L
return appID
}

func (s *applicationStateSuite) createObjectStoreBlob(c *gc.C, path string) objectstore.UUID {
uuid := objectstoretesting.GenObjectStoreUUID(c)
err := s.TxnRunner().StdTxn(context.Background(), func(ctx context.Context, tx *sql.Tx) error {
_, err := tx.ExecContext(ctx, `
INSERT INTO object_store_metadata (uuid, sha_256, sha_384, size) VALUES (?, 'foo', 'bar', 42)
`, uuid.String())
if err != nil {
return err
}

_, err = tx.ExecContext(ctx, `
INSERT INTO object_store_metadata_path (path, metadata_uuid) VALUES (?, ?)
`, path, uuid.String())
return err
})
c.Assert(err, jc.ErrorIsNil)
return uuid
}

func (s *applicationStateSuite) assertApplication(
c *gc.C,
name string,
Expand Down
113 changes: 96 additions & 17 deletions domain/application/state/charm.go
Original file line number Diff line number Diff line change
Expand Up @@ -751,29 +751,108 @@ WHERE charm.uuid = $charmID.uuid;
// ResolveMigratingUploadedCharm resolves the charm that is migrating from
// the uploaded state to the available state. If the charm is not found, a
// [applicationerrors.CharmNotFound] error is returned.
func (s *State) ResolveMigratingUploadedCharm(context.Context, corecharm.ID, charm.ResolvedMigratingUploadedCharm) (charm.CharmLocator, error) {
return charm.CharmLocator{}, nil
}
func (s *State) ResolveMigratingUploadedCharm(ctx context.Context, id corecharm.ID, info charm.ResolvedMigratingUploadedCharm) (charm.CharmLocator, error) {
db, err := s.DB()
if err != nil {
return charm.CharmLocator{}, internalerrors.Capture(err)
}

func decodeCharmLocators(results []charmLocator) ([]charm.CharmLocator, error) {
return transform.SliceOrErr(results, func(c charmLocator) (charm.CharmLocator, error) {
source, err := decodeCharmSource(c.SourceID)
if err != nil {
return charm.CharmLocator{}, err
charmUUID := charmID{UUID: id.String()}

resolvedQuery := `
SELECT &charmAvailable.*
FROM charm
WHERE uuid = $charmID.uuid
`
resolvedStmt, err := s.Prepare(resolvedQuery, charmUUID, charmAvailable{})
if err != nil {
return charm.CharmLocator{}, internalerrors.Errorf("preparing query for charm %q: %w", id, err)
}

chState := resolveCharmState{
ArchivePath: info.ArchivePath,
ObjectStoreUUID: info.ObjectStoreUUID.String(),
}

charmQuery := `
UPDATE charm
SET
archive_path = $resolveCharmState.archive_path,
object_store_uuid = $resolveCharmState.object_store_uuid,
available = TRUE
WHERE uuid = $charmID.uuid;`
charmStmt, err := s.Prepare(charmQuery, charmUUID, chState)
if err != nil {
return charm.CharmLocator{}, internalerrors.Errorf("preparing query: %w", err)
}

var locator charmLocator
locatorQuery := `
SELECT &charmLocator.*
FROM v_charm_locator
WHERE uuid = $charmID.uuid;
`
locatorStmt, err := s.Prepare(locatorQuery, charmUUID, locator)
if err != nil {
return charm.CharmLocator{}, internalerrors.Errorf("preparing query: %w", err)
}

err = db.Txn(ctx, func(ctx context.Context, tx *sqlair.TX) error {
var available charmAvailable
err := tx.Query(ctx, resolvedStmt, charmUUID).Get(&available)
if errors.Is(err, sqlair.ErrNoRows) {
return applicationerrors.CharmNotFound
} else if err != nil {
return internalerrors.Capture(err)
} else if available.Available {
// If the charm has already been processed, then we don't need to do
// anything. Handle the error on the other side.
return applicationerrors.CharmAlreadyAvailable
}

architecture, err := decodeArchitecture(c.ArchitectureID)
if err != nil {
return charm.CharmLocator{}, err
if err := tx.Query(ctx, charmStmt, charmUUID, chState).Run(); err != nil {
return internalerrors.Errorf("updating charm state: %w", err)
}

// Insert the charm download info.
if err := s.setCharmDownloadInfo(ctx, tx, id, info.DownloadInfo); err != nil {
return internalerrors.Errorf("setting charm download info: %w", err)
}

if err := tx.Query(ctx, locatorStmt, charmUUID).Get(&locator); err != nil {
return internalerrors.Errorf("getting charm locator: %w", err)
}

return charm.CharmLocator{
Name: c.Name,
Revision: c.Revision,
Source: source,
Architecture: architecture,
}, nil
return nil
})
if err != nil {
return charm.CharmLocator{}, internalerrors.Errorf("resolving migrating uploaded charm: %w", err)
}

return decodeCharmLocator(locator)
}

func decodeCharmLocators(results []charmLocator) ([]charm.CharmLocator, error) {
return transform.SliceOrErr(results, decodeCharmLocator)
}

func decodeCharmLocator(c charmLocator) (charm.CharmLocator, error) {
source, err := decodeCharmSource(c.SourceID)
if err != nil {
return charm.CharmLocator{}, internalerrors.Errorf("decoding charm source: %w", err)
}

architecture, err := decodeArchitecture(c.ArchitectureID)
if err != nil {
return charm.CharmLocator{}, internalerrors.Errorf("decoding architecture: %w", err)
}

return charm.CharmLocator{
Name: c.Name,
Revision: c.Revision,
Source: source,
Architecture: architecture,
}, nil
}

var tablesToDeleteFrom = []string{
Expand Down
83 changes: 83 additions & 0 deletions domain/application/state/charm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3208,6 +3208,89 @@ func (s *charmStateSuite) TestGetAvailableCharmArchiveSHA256NotFound(c *gc.C) {
c.Assert(err, jc.ErrorIs, applicationerrors.CharmNotFound)
}

func (s *charmStateSuite) TestResolveMigratingUploadedCharmNotFound(c *gc.C) {
st := NewState(s.TxnRunnerFactory(), clock.WallClock, loggertesting.WrapCheckLog(c))

objectStoreUUID := objectstoretesting.GenObjectStoreUUID(c)

_, err := st.ResolveMigratingUploadedCharm(context.Background(), charmtesting.GenCharmID(c), charm.ResolvedMigratingUploadedCharm{
ObjectStoreUUID: objectStoreUUID,
})
c.Assert(err, jc.ErrorIs, applicationerrors.CharmNotFound)
}

func (s *charmStateSuite) TestResolveMigratingUploadedCharmAlreadyAvailable(c *gc.C) {
st := NewState(s.TxnRunnerFactory(), clock.WallClock, loggertesting.WrapCheckLog(c))

objectStoreUUID := objectstoretesting.GenObjectStoreUUID(c)

info := &charm.DownloadInfo{
Provenance: charm.ProvenanceMigration,
}

id, err := st.SetCharm(context.Background(), charm.Charm{
Metadata: charm.Metadata{
Name: "foo",
},
Manifest: s.minimalManifest(c),
Source: charm.CharmHubSource,
Revision: 42,
ReferenceName: "foo",
Hash: "hash",
Version: "deadbeef",
}, info)
c.Assert(err, jc.ErrorIsNil)

err = st.SetCharmAvailable(context.Background(), id)
c.Assert(err, jc.ErrorIsNil)

_, err = st.ResolveMigratingUploadedCharm(context.Background(), id, charm.ResolvedMigratingUploadedCharm{
ObjectStoreUUID: objectStoreUUID,
})
c.Assert(err, jc.ErrorIs, applicationerrors.CharmAlreadyAvailable)
}

func (s *charmStateSuite) TestResolveMigratingUploaded(c *gc.C) {
st := NewState(s.TxnRunnerFactory(), clock.WallClock, loggertesting.WrapCheckLog(c))

objectStoreUUID := s.createObjectStoreBlob(c, "archive")

info := &charm.DownloadInfo{
Provenance: charm.ProvenanceMigration,
}

id, err := st.SetCharm(context.Background(), charm.Charm{
Metadata: charm.Metadata{
Name: "foo",
},
Manifest: s.minimalManifest(c),
Source: charm.CharmHubSource,
Revision: 42,
ReferenceName: "foo",
Hash: "hash",
Version: "deadbeef",
}, info)
c.Assert(err, jc.ErrorIsNil)

locator, err := st.ResolveMigratingUploadedCharm(context.Background(), id, charm.ResolvedMigratingUploadedCharm{
ObjectStoreUUID: objectStoreUUID,
ArchivePath: "archive",
Hash: "hash",
DownloadInfo: info,
})
c.Assert(err, jc.ErrorIsNil)
c.Check(locator, gc.DeepEquals, charm.CharmLocator{
Name: "foo",
Source: charm.CharmHubSource,
Revision: 42,
Architecture: architecture.AMD64,
})

available, err := st.IsCharmAvailable(context.Background(), id)
c.Assert(err, jc.ErrorIsNil)
c.Check(available, gc.Equals, true)
}

func insertCharmState(ctx context.Context, c *gc.C, tx *sql.Tx, uuid string) error {
_, err := tx.ExecContext(ctx, `
INSERT INTO charm (uuid, archive_path, available, reference_name, revision, version, architecture_id)
Expand Down
24 changes: 24 additions & 0 deletions domain/application/state/package_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@
package state

import (
"context"
"database/sql"
"testing"

jc "github.com/juju/testing/checkers"
gc "gopkg.in/check.v1"

"github.com/juju/juju/core/objectstore"
objectstoretesting "github.com/juju/juju/core/objectstore/testing"
"github.com/juju/juju/domain/application"
"github.com/juju/juju/domain/application/architecture"
"github.com/juju/juju/domain/application/charm"
Expand Down Expand Up @@ -80,3 +85,22 @@ func (s *baseSuite) addApplicationArgForResources(c *gc.C,
Resources: addResourcesArgs,
}
}

func (s *baseSuite) createObjectStoreBlob(c *gc.C, path string) objectstore.UUID {
uuid := objectstoretesting.GenObjectStoreUUID(c)
err := s.TxnRunner().StdTxn(context.Background(), func(ctx context.Context, tx *sql.Tx) error {
_, err := tx.ExecContext(ctx, `
INSERT INTO object_store_metadata (uuid, sha_256, sha_384, size) VALUES (?, 'foo', 'bar', 42)
`, uuid.String())
if err != nil {
return err
}

_, err = tx.ExecContext(ctx, `
INSERT INTO object_store_metadata_path (path, metadata_uuid) VALUES (?, ?)
`, path, uuid.String())
return err
})
c.Assert(err, jc.ErrorIsNil)
return uuid
}
7 changes: 6 additions & 1 deletion domain/application/state/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,12 @@ func (s *State) setCharmDownloadInfo(ctx context.Context, tx *sqlair.TX, id core
DownloadSize: downloadInfo.DownloadSize,
}

query := `INSERT INTO charm_download_info (*) VALUES ($setCharmDownloadInfo.*);`
// If the charmDownloadInfo has already been inserted, we don't need to do
// anything. We want to keep the original download information.
query := `
INSERT INTO charm_download_info (*)
VALUES ($setCharmDownloadInfo.*)
ON CONFLICT DO NOTHING;`
stmt, err := s.Prepare(query, downloadInfoState)
if err != nil {
return fmt.Errorf("preparing query: %w", err)
Expand Down
1 change: 1 addition & 0 deletions domain/schema/model/sql/0015-charm.sql
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,7 @@ LEFT JOIN charm_config_type AS cct ON cc.type_id = cct.id;

CREATE VIEW v_charm_locator AS
SELECT
c.uuid,
c.reference_name,
c.revision,
c.source_id,
Expand Down

0 comments on commit e067796

Please sign in to comment.