Skip to content

Commit

Permalink
Merge pull request juju#18386 from Aflynn50/oci-image-resource-store
Browse files Browse the repository at this point in the history
juju#18386

Add a container image metadata store and state. This holds metadata for oci image resources. This metadata is the content of the resource file that is sent to units.

The PR is based on and contains juju#18372.
<!-- 
The PR title should match: <type>(optional <scope>): <description>.

Please also ensure all commits in this PR comply with our conventional commits specification:
https://docs.google.com/document/d/1SYUo9G7qZ_jdoVXpUVamS5VCgHmtZ0QA-wZxKoMS-C0 
-->

## Checklist

<!-- If an item is not applicable, use `~strikethrough~`. -->

- [x] Code style: imports ordered, good names, simple structure, etc
- [x] Comments saying why design decisions were made
- [x] Go unit tests, with comments saying what you're testing


## QA steps

Unit tests (for now)

## Links

<!-- Link to all relevant specification, documentation, bug, issue or JIRA card. -->

**Jira card:** [JUJU-7167](https://warthogs.atlassian.net/browse/JUJU-7167)



[JUJU-7167]: https://warthogs.atlassian.net/browse/JUJU-7167?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
  • Loading branch information
jujubot authored Nov 29, 2024
2 parents 5b84a9a + d5999a7 commit d2f2609
Show file tree
Hide file tree
Showing 26 changed files with 1,099 additions and 60 deletions.
14 changes: 14 additions & 0 deletions core/resources/containermetadataresource/package_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright 2024 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package containermetadataresource

import (
"testing"

gc "gopkg.in/check.v1"
)

func TestPackage(t *testing.T) {
gc.TestingT(t)
}
20 changes: 20 additions & 0 deletions core/resources/containermetadataresource/testing/testing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright 2024 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package testing

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

"github.com/juju/juju/core/resources/containermetadataresource"
)

// GenContainerMetadataResourceUUID can be used in testing for generating a objectstore UUID
// that is checked for subsequent errors using the test suit's go check
// instance.
func GenContainerMetadataResourceUUID(c *gc.C) containermetadataresource.UUID {
id, err := containermetadataresource.NewUUID()
c.Assert(err, jc.ErrorIsNil)
return id
}
50 changes: 50 additions & 0 deletions core/resources/containermetadataresource/uuid.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright 2024 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package containermetadataresource

import (
"fmt"

"github.com/juju/errors"

"github.com/juju/juju/internal/uuid"
)

// UUID represents a container metadata resource unique identifier.
type UUID string

// NewUUID is a convince function for generating a new container metadata resource uuid.
func NewUUID() (UUID, error) {
uuid, err := uuid.NewUUID()
if err != nil {
return UUID(""), err
}
return UUID(uuid.String()), nil
}

// ParseUUID returns a new UUID from the given string. If the string is not a
// valid uuid an error satisfying [errors.NotValid] will be returned.
func ParseUUID(value string) (UUID, error) {
if !uuid.IsValidUUIDString(value) {
return "", fmt.Errorf("id %q %w", value, errors.NotValid)
}
return UUID(value), nil
}

// String implements the stringer interface for UUID.
func (u UUID) String() string {
return string(u)
}

// Validate ensures the consistency of the UUID. If the uuid is invalid an error
// satisfying [errors.NotValid] will be returned.
func (u UUID) Validate() error {
if u == "" {
return fmt.Errorf("%wuuid cannot be empty", errors.Hide(errors.NotValid))
}
if !uuid.IsValidUUIDString(string(u)) {
return fmt.Errorf("uuid %q %w", u, errors.NotValid)
}
return nil
}
50 changes: 50 additions & 0 deletions core/resources/containermetadataresource/uuid_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright 2024 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package containermetadataresource

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

"github.com/juju/juju/internal/uuid"
)

type ContainerMetadataResourceUUIDSuite struct {
testing.IsolationSuite
}

var _ = gc.Suite(&ContainerMetadataResourceUUIDSuite{})

func (*ContainerMetadataResourceUUIDSuite) TestIDValidate(c *gc.C) {
tests := []struct {
uuid string
err error
}{
{
uuid: "",
err: errors.NotValid,
},
{
uuid: "invalid",
err: errors.NotValid,
},
{
uuid: uuid.MustNewUUID().String(),
},
}

for i, test := range tests {
c.Logf("test %d: %q", i, test.uuid)
err := UUID(test.uuid).Validate()

if test.err == nil {
c.Check(err, gc.IsNil)
continue
}

c.Check(err, jc.ErrorIs, test.err)
}
}
4 changes: 4 additions & 0 deletions domain/application/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ const (
// not found.
ResourceNotFound = errors.ConstError("resource not found")

// ContainerImageMetadataNotFound describes an error that occurs when
// container image metadata is not found.
ContainerImageMetadataNotFound = errors.ConstError("container image metadata not found")

// UnknownResourceType describes an error where the resource type is
// not oci-image or file.
UnknownResourceType = errors.ConstError("unknown resource type")
Expand Down
37 changes: 20 additions & 17 deletions domain/application/resource/fileresourcestore.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"io"

"github.com/juju/juju/core/objectstore"
coreresource "github.com/juju/juju/core/resource"
"github.com/juju/juju/internal/charm/resource"
"github.com/juju/juju/internal/errors"
)
Expand All @@ -21,45 +20,49 @@ type fileResourceStore struct {
// Get the specified resource from the object store.
func (f fileResourceStore) Get(
ctx context.Context,
resourceUUID coreresource.UUID,
storageKey string,
) (io.ReadCloser, int64, error) {
if err := resourceUUID.Validate(); err != nil {
return nil, 0, errors.Errorf("validating resource UUID: %w", err)
if storageKey == "" {
return nil, 0, errors.Errorf("storage key empty")
}
return f.objectStore.Get(ctx, resourceUUID.String())
return f.objectStore.Get(ctx, storageKey)
}

// Put the given resource in the object store using the resource UUID as the
// Put the given resource in the object store using the storage key as the
// storage path. It returns the UUID of the object store metadata.
func (f fileResourceStore) Put(
ctx context.Context,
resourceUUID coreresource.UUID,
storageKey string,
r io.Reader,
size int64,
fingerprint resource.Fingerprint,
) (ResourceStorageUUID, error) {
if err := resourceUUID.Validate(); err != nil {
return nil, errors.Errorf("validating resource UUID: %w", err)
if storageKey == "" {
return "", errors.Errorf("storage key empty")
}
if r == nil {
return nil, errors.Errorf("validating resource: reader is nil")
return "", errors.Errorf("validating resource: reader is nil")
}
if size == 0 {
return nil, errors.Errorf("validating resource: size is 0")
return "", errors.Errorf("validating resource size: size is 0")
}
if err := fingerprint.Validate(); err != nil {
return nil, errors.Errorf("validating resource fingerprint: %w", err)
return "", errors.Errorf("validating resource fingerprint: %w", err)
}
return f.objectStore.PutAndCheckHash(ctx, resourceUUID.String(), r, size, fingerprint.String())
uuid, err := f.objectStore.PutAndCheckHash(ctx, storageKey, r, size, fingerprint.String())
if err != nil {
return "", err
}
return ResourceStorageUUID(uuid.String()), nil
}

// Remove the specified resource from the object store.
func (f fileResourceStore) Remove(
ctx context.Context,
resourceUUID coreresource.UUID,
storageKey string,
) error {
if err := resourceUUID.Validate(); err != nil {
return errors.Errorf("validating resource UUID: %w", err)
if storageKey == "" {
return errors.Errorf("storage key empty")
}
return f.objectStore.Remove(ctx, resourceUUID.String())
return f.objectStore.Remove(ctx, storageKey)
}
28 changes: 14 additions & 14 deletions domain/application/resource/fileresourcestore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,16 +75,16 @@ func (s *fileResourceStoreSuite) TestFileResourceStorePut(c *gc.C) {

storageUUID, err := store.Put(
context.Background(),
s.resource.UUID,
s.resource.UUID.String(),
s.file,
s.resource.Size,
s.resource.Fingerprint,
)
c.Assert(err, jc.ErrorIsNil)
c.Assert(storageUUID, gc.Equals, expectedStorageUUID)
c.Assert(storageUUID, gc.Equals, ResourceStorageUUID(expectedStorageUUID))
}

func (s *fileResourceStoreSuite) TestFileResourceStorePutBadUUID(c *gc.C) {
func (s *fileResourceStoreSuite) TestFileResourceStorePutBadStorageKey(c *gc.C) {
defer s.setupMocks(c).Finish()
store := fileResourceStore{s.objectStore}
_, err := store.Put(
Expand All @@ -94,15 +94,15 @@ func (s *fileResourceStoreSuite) TestFileResourceStorePutBadUUID(c *gc.C) {
s.resource.Size,
s.resource.Fingerprint,
)
c.Assert(err, gc.ErrorMatches, "validating resource UUID.*")
c.Assert(err, gc.ErrorMatches, "storage key empty")
}

func (s *fileResourceStoreSuite) TestFileResourceStorePutNilReader(c *gc.C) {
defer s.setupMocks(c).Finish()
store := fileResourceStore{s.objectStore}
_, err := store.Put(
context.Background(),
s.resource.UUID,
s.resource.UUID.String(),
nil,
s.resource.Size,
s.resource.Fingerprint,
Expand All @@ -115,7 +115,7 @@ func (s *fileResourceStoreSuite) TestFileResourceStorePutBadFingerprint(c *gc.C)
store := fileResourceStore{s.objectStore}
_, err := store.Put(
context.Background(),
s.resource.UUID,
s.resource.UUID.String(),
s.file,
s.resource.Size,
charmresource.Fingerprint{},
Expand All @@ -128,12 +128,12 @@ func (s *fileResourceStoreSuite) TestFileResourceStorePutZeroSize(c *gc.C) {
store := fileResourceStore{s.objectStore}
_, err := store.Put(
context.Background(),
s.resource.UUID,
s.resource.UUID.String(),
s.file,
0,
charmresource.Fingerprint{},
)
c.Assert(err, gc.ErrorMatches, "validating resource: size is 0")
c.Assert(err, gc.ErrorMatches, "validating resource size: size is 0")
}

func (s *fileResourceStoreSuite) TestFileResourceStoreGet(c *gc.C) {
Expand All @@ -142,20 +142,20 @@ func (s *fileResourceStoreSuite) TestFileResourceStoreGet(c *gc.C) {

s.objectStore.EXPECT().Get(context.Background(), s.resource.UUID.String()).Return(s.file, s.resource.Size, nil)

reader, size, err := store.Get(context.Background(), s.resource.UUID)
reader, size, err := store.Get(context.Background(), s.resource.UUID.String())
c.Assert(err, jc.ErrorIsNil)
c.Assert(reader, gc.Equals, s.file)
c.Assert(size, gc.Equals, s.resource.Size)
}

func (s *fileResourceStoreSuite) TestFileResourceStoreGetBadUUID(c *gc.C) {
func (s *fileResourceStoreSuite) TestFileResourceStoreGetBadStorageKey(c *gc.C) {
defer s.setupMocks(c).Finish()
store := fileResourceStore{s.objectStore}
_, _, err := store.Get(
context.Background(),
"",
)
c.Assert(err, gc.ErrorMatches, "validating resource UUID.*")
c.Assert(err, gc.ErrorMatches, "storage key empty")
}

func (s *fileResourceStoreSuite) TestFileResourceStoreRemove(c *gc.C) {
Expand All @@ -164,16 +164,16 @@ func (s *fileResourceStoreSuite) TestFileResourceStoreRemove(c *gc.C) {

s.objectStore.EXPECT().Remove(context.Background(), s.resource.UUID.String()).Return(nil)

err := store.Remove(context.Background(), s.resource.UUID)
err := store.Remove(context.Background(), s.resource.UUID.String())
c.Assert(err, jc.ErrorIsNil)
}

func (s *fileResourceStoreSuite) TestFileResourceStoreRemoveBadUUID(c *gc.C) {
func (s *fileResourceStoreSuite) TestFileResourceStoreRemoveBadStorageKey(c *gc.C) {
defer s.setupMocks(c).Finish()
store := fileResourceStore{s.objectStore}
err := store.Remove(
context.Background(),
"",
)
c.Assert(err, gc.ErrorMatches, "validating resource UUID.*")
c.Assert(err, gc.ErrorMatches, "storage key empty")
}
11 changes: 5 additions & 6 deletions domain/application/resource/resourcestore.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"sync"

"github.com/juju/juju/core/objectstore"
coreresource "github.com/juju/juju/core/resource"
applicationerrors "github.com/juju/juju/domain/application/errors"
"github.com/juju/juju/internal/charm/resource"
"github.com/juju/juju/internal/errors"
Expand All @@ -21,14 +20,14 @@ type ResourceStore interface {
// Get returns an io.ReadCloser for a resource in the resource store.
Get(
ctx context.Context,
resourceUUID coreresource.UUID,
storageKey string,
) (r io.ReadCloser, size int64, err error)

// Put stores data from io.Reader in the resource store at the
// using the resourceUUID as the key.
// Put stores data from io.Reader in the resource store using the storage
// key.
Put(
ctx context.Context,
resourceUUID coreresource.UUID,
storageKey string,
r io.Reader,
size int64,
fingerprint resource.Fingerprint,
Expand All @@ -37,7 +36,7 @@ type ResourceStore interface {
// Remove removes a resource from storage.
Remove(
ctx context.Context,
resourceUUID coreresource.UUID,
storageKey string,
) error
}

Expand Down
10 changes: 5 additions & 5 deletions domain/application/resource/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,8 @@ type SetRepositoryResourcesArgs struct {
LastPolled time.Time
}

// ResourceStorageUUID is a UUID used to reference the resource in storage.
type ResourceStorageUUID interface {
String() string
Validate() error
}
// ResourceStorageUUID is the UUID of the stored blob in the database, this can
// be used for adding referential integrity from the resource to the stored
// blob. This can be an object store metadata UUID or a container image metadata
// storage key.
type ResourceStorageUUID string
Loading

0 comments on commit d2f2609

Please sign in to comment.