Skip to content

Commit

Permalink
Merge pull request juju#18431 from SimonRichardson/charm-downloader-a…
Browse files Browse the repository at this point in the history
…nd-store

juju#18431

Infrastructure work for downloading a charm asynchronously. This supersedes the internal/charm/downloader, which will be removed once the async downloading of a charm is done.

This correctly handles the downloading of a charm, checking the integrity of a file once downloaded against the source of the file, and the removal of files on errors. By doing less, we can achieve more.

----

This pull request introduces several new functionalities and improvements, including the addition of a `String` method to the `Origin` struct, new charm store functionalities, and enhancements to the charm downloader. The most important changes are summarized below:

### New functionalities and methods:

* Added a `String` method to the `Origin` struct in `core/charm/origin.go` to provide a formatted string representation of the `Origin` object.

### Charm store enhancements:

* Introduced the `CharmStore` struct and implemented methods for storing charms, including error handling for file not found and hash checking, in `domain/application/charm/store/store.go`.
* Added unit tests for the `CharmStore` functionalities in `domain/application/charm/store/store_test.go`.
* Generated mock implementations for `ObjectStore` and `ModelObjectStoreGetter` interfaces using `go:generate` in `domain/application/charm/store/store_mock_test.go`.

### Charm downloader improvements:

* Added the `CharmDownloader` struct and implemented methods for downloading charms, verifying their hashes, and handling temporary files in `internal/charm/charmdownloader/downloader.go`.
* Documented the purpose and functionality of the `charmdownloader` package in `internal/charm/charmdownloader/doc.go`.

These changes enhance the charm store and downloader functionalities, improve error handling, and add comprehensive unit tests and documentation.

----

## QA Steps

Regression testing should be employed, all of this is just infrastructure work, broken off the juju#18398 PR.

## Jira

https://warthogs.atlassian.net/browse/JUJU-7166
  • Loading branch information
jujubot authored Nov 26, 2024
2 parents 46b75e7 + 6b0006a commit 83fec01
Show file tree
Hide file tree
Showing 11 changed files with 962 additions and 0 deletions.
5 changes: 5 additions & 0 deletions core/charm/origin.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ func (o Origin) Validate() error {
return nil
}

func (o Origin) String() string {
return fmt.Sprintf("(source: %q, id: %s, hash: %s, revision: %v, channel: %v, platform: %s)",
o.Source, o.ID, o.Hash, o.Revision, o.Channel, o.Platform)
}

// Platform describes the platform used to install the charm with.
type Platform struct {
Architecture string
Expand Down
16 changes: 16 additions & 0 deletions domain/application/charm/store/package_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright 2024 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package store

import (
"testing"

gc "gopkg.in/check.v1"
)

//go:generate go run go.uber.org/mock/mockgen -typed -package store -destination store_mock_test.go github.com/juju/juju/core/objectstore ObjectStore,ModelObjectStoreGetter

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

package store

import (
"context"
"encoding/base32"
"fmt"
"os"

"github.com/juju/juju/core/objectstore"
"github.com/juju/juju/internal/errors"
"github.com/juju/juju/internal/uuid"
)

const (
// ErrNotFound is returned when the file is not found.
ErrNotFound = errors.ConstError("file not found")
)

// CharmStore provides an API for storing charms.
type CharmStore struct {
objectStoreGetter objectstore.ModelObjectStoreGetter
encoder *base32.Encoding
}

// NewCharmStore returns a new charm store instance.
func NewCharmStore(objectStoreGetter objectstore.ModelObjectStoreGetter) *CharmStore {
return &CharmStore{
objectStoreGetter: objectStoreGetter,
encoder: base32.StdEncoding.WithPadding(base32.NoPadding),
}
}

// Store the charm at the specified path into the object store. It is expected
// that the archive already exists at the specified path. If the file isn't
// found, a [ErrNotFound] is returned.
func (s *CharmStore) Store(ctx context.Context, name string, path string, size int64, hash string) (objectstore.UUID, error) {
objectStore, err := s.objectStoreGetter.GetObjectStore(ctx)
if err != nil {
return "", errors.Errorf("getting object store: %w", err)
}

file, err := os.Open(path)
if errors.Is(err, os.ErrNotExist) {
return "", errors.Errorf("%q: %w", path, ErrNotFound)
} else if err != nil {
return "", errors.Errorf("cannot open file %q: %w", path, err)
}

// Ensure that we close any open handles to the file.
defer file.Close()

// Generate a unique path for the file.
unique, err := uuid.NewUUID()
if err != nil {
return "", errors.Errorf("cannot generate unique path")
}

// The name won't be unique and that's ok. In that case, we'll slap a
// unique identifier at the end of the name. This can happen if you have
// a charm with the same name but different content.
uniqueName := fmt.Sprintf("%s-%s", name, s.encoder.EncodeToString(unique[:]))

// Store the file in the object store.
return objectStore.PutAndCheckHash(ctx, uniqueName, file, size, hash)
}
260 changes: 260 additions & 0 deletions domain/application/charm/store/store_mock_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 83fec01

Please sign in to comment.