Skip to content

Commit

Permalink
feat: stop downloading artifact into a temporary location
Browse files Browse the repository at this point in the history
this commit changes the oci downloader in some aspects:

- stops downloading the artifact into a temporary location.
- does not fail if multiple artifacts are present.
- allow users to specify the artifact they desire to download.
- if multiple artifacts are present and no artifact name is provided
  downloads the first one.

this commit also adds a test to verify the new behavior.

Signed-off-by: Ricardo Maraschini <[email protected]>
  • Loading branch information
ricardomaraschini committed Oct 9, 2024
1 parent f87788d commit 77470f5
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 25 deletions.
69 changes: 49 additions & 20 deletions internal/oci/oci.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ import (
"io"
"net/http"
"os"
"path/filepath"

"oras.land/oras-go/v2"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/content/file"
"oras.land/oras-go/v2/registry"
"oras.land/oras-go/v2/registry/remote"
Expand All @@ -40,9 +40,9 @@ import (
// fp, _ := os.CreateTemp("", "k0s-oci-artifact-*")
// err := oci.Download(ctx, artifact, fp)
//
// This function expects only one artifact to be present, if none is found this
// returns an error. The artifact is downloaded in a temporary location before
// being copied to the target.
// This function expects at least one artifact to be present, if none is found
// this returns an error. The artifact name can be specified using the
// WithArtifactName option.
func Download(ctx context.Context, url string, target io.Writer, options ...DownloadOption) (err error) {
opts := downloadOptions{}
for _, opt := range options {
Expand Down Expand Up @@ -87,40 +87,61 @@ func Download(ctx context.Context, url string, target io.Writer, options ...Down
}

tag := imgref.Reference
if _, err := oras.Copy(ctx, repo, tag, fs, tag, oras.DefaultCopyOptions); err != nil {
return fmt.Errorf("failed to fetch artifact: %w", err)
desc, data, err := repo.Manifests().FetchReference(ctx, tag)
if err != nil {
return fmt.Errorf("failed to fetch manifest: %w", err)
}
defer data.Close()

files, err := os.ReadDir(tmpdir)
successors, err := content.Successors(ctx, repo, desc)
if err != nil {
return fmt.Errorf("failed to read temp dir: %w", err)
return fmt.Errorf("failed to fetch successors: %w", err)
}

// we always expect only one single file to be downloaded.
if len(files) == 0 {
return fmt.Errorf("no artifacts found")
} else if len(files) > 1 {
return fmt.Errorf("multiple artifacts found")
source, err := findArtifactDescriptor(successors, opts)
if err != nil {
return fmt.Errorf("failed to find artifact: %w", err)
}

fpath := filepath.Join(tmpdir, files[0].Name())
fp, err := os.Open(fpath)
// get a reader to the blob and copies it to the target.
reader, err := repo.Blobs().Fetch(ctx, source)
if err != nil {
return fmt.Errorf("failed to open file: %w", err)
return fmt.Errorf("failed to fetch blob: %w", err)
}
defer fp.Close()
defer reader.Close()

if _, err := io.Copy(target, fp); err != nil {
return fmt.Errorf("failed to copy file: %w", err)
if _, err := io.Copy(target, reader); err != nil {
return fmt.Errorf("failed to copy blob: %w", err)
}

return nil
}

// findArtifactDescriptor filters, out of the provided list of descriptors, the
// one that matches the given options. If no artifact name is provided, it
// returns the first descriptor.
func findArtifactDescriptor(all []ocispec.Descriptor, opts downloadOptions) (ocispec.Descriptor, error) {
for _, desc := range all {
if desc.MediaType == ocispec.MediaTypeEmptyJSON {
continue
}
// if no artifact name is specified, we use the first one.
fname := opts.artifactName
if fname == "" || fname == desc.Annotations[ocispec.AnnotationTitle] {
return desc, nil
}
}
if opts.artifactName == "" {
return ocispec.Descriptor{}, fmt.Errorf("no artifacts found")
}
return ocispec.Descriptor{}, fmt.Errorf("artifact %q not found", opts.artifactName)
}

// downloadOptions holds the options used when downloading OCI artifacts.
type downloadOptions struct {
insecureSkipTLSVerify bool
auth DockerConfig
artifactName string
}

// DownloadOption is a function that sets an option for the OCI download.
Expand All @@ -141,6 +162,14 @@ func WithDockerAuth(auth DockerConfig) DownloadOption {
}
}

// WithArtifactName sets the name of the artifact to be downloaded. This is
// used to filter out the artifacts present in the manifest.
func WithArtifactName(name string) DownloadOption {
return func(opts *downloadOptions) {
opts.artifactName = name
}
}

// DockerConfigEntry holds an entry in the '.dockerconfigjson' file.
type DockerConfigEntry struct {
Username string `json:"username"`
Expand Down
5 changes: 5 additions & 0 deletions internal/oci/oci_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ type testFile struct {
AuthUser string `json:"authUser"`
AuthPass string `json:"authPass"`
Artifacts map[string]string `json:"artifacts"`
ArtifactName string `json:"artifactName"`
}

func TestDownload(t *testing.T) {
Expand All @@ -87,6 +88,10 @@ func TestDownload(t *testing.T) {
))
}

if tt.ArtifactName != "" {
opts = append(opts, oci.WithArtifactName(tt.ArtifactName))
}

buf := bytes.NewBuffer(nil)
url := fmt.Sprintf("%s/repository/artifact:latest", addr)
err := oci.Download(context.TODO(), url, buf, opts...)
Expand Down
46 changes: 46 additions & 0 deletions internal/oci/testdata/artifact-by-name.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
manifest: |
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"artifactType": "application/vnd.unknown.artifact.v1",
"config": {
"mediaType": "application/vnd.oci.empty.v1+json",
"digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
"size": 2
},
"layers": [
{
"mediaType": "application/vnd.oci.image.layer.v1.tar",
"digest": "sha256:c147efcfc2d7ea666a9e4f5187b115c90903f0fc896a56df9a6ef5d8f3fc9f31",
"size": 5,
"annotations": {
"org.opencontainers.image.title": "file1"
}
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar",
"digest": "sha256:3377870dfeaaa7adf79a374d2702a3fdb13e5e5ea0dd8aa95a802ad39044a92f",
"size": 5,
"annotations": {
"org.opencontainers.image.title": "file2"
}
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar",
"digest": "sha256:6f3fef6dc51c7996a74992b70d0c35f328ed909a5e07646cf0bab3383c95bb02",
"size": 5,
"annotations": {
"org.opencontainers.image.title": "file3"
}
}
],
"annotations": {
"org.opencontainers.image.created": "2024-10-03T14:32:57Z"
}
}
artifacts:
44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a: "{}"
c147efcfc2d7ea666a9e4f5187b115c90903f0fc896a56df9a6ef5d8f3fc9f31: "file1"
6f3fef6dc51c7996a74992b70d0c35f328ed909a5e07646cf0bab3383c95bb02: "file3"
artifactName: file3
expected: file3
2 changes: 1 addition & 1 deletion internal/oci/testdata/multiple-artifacts.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,4 @@ artifacts:
44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a: "{}"
c147efcfc2d7ea666a9e4f5187b115c90903f0fc896a56df9a6ef5d8f3fc9f31: "file1"
3377870dfeaaa7adf79a374d2702a3fdb13e5e5ea0dd8aa95a802ad39044a92f: "file2"
error: multiple artifacts found
expected: file1
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,19 @@ manifest: |
"layers": [
{
"mediaType": "application/vnd.oci.image.layer.v1.tar",
"digest": "sha256:2d6c9a90dd38f6852515274cde41a8cd8e7e1a7a053835334ec7e29f61b918dd",
"size": 2,
"digest": "sha256:c147efcfc2d7ea666a9e4f5187b115c90903f0fc896a56df9a6ef5d8f3fc9f31",
"size": 5,
"annotations": {
"org.opencontainers.image.title": "file1"
}
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar",
"digest": "sha256:3377870dfeaaa7adf79a374d2702a3fdb13e5e5ea0dd8aa95a802ad39044a92f",
"size": 5,
"annotations": {
"org.opencontainers.image.title": "file2"
}
}
],
"annotations": {
Expand All @@ -24,5 +32,6 @@ manifest: |
}
artifacts:
44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a: "{}"
2d6c9a90dd38f6852515274cde41a8cd8e7e1a7a053835334ec7e29f61b918dd: "xy"
error: mismatched digest
c147efcfc2d7ea666a9e4f5187b115c90903f0fc896a56df9a6ef5d8f3fc9f31: "file1"
artifactName: unknown
error: 'artifact "unknown" not found'

0 comments on commit 77470f5

Please sign in to comment.