-
Notifications
You must be signed in to change notification settings - Fork 361
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add oci pull capabilities #5075
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
b5e2955
feat: add oras pull capabilities
ricardomaraschini b0fb0e8
Update internal/oras/oras.go
ricardomaraschini efe0794
chore: rename package from oras to oci
ricardomaraschini 2b0b34a
chore: rename option structs
ricardomaraschini 5ecc030
chore: address some pr comments
ricardomaraschini f87788d
chore: remove dangling references to oras
ricardomaraschini efc3f72
feat: stop downloading artifact into a temporary location
ricardomaraschini 99991a5
feat: add support for oci download using plain http
ricardomaraschini File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,210 @@ | ||
/* | ||
Copyright 2024 k0s authors | ||
|
||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
|
||
http://www.apache.org/licenses/LICENSE-2.0 | ||
|
||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package oci | ||
|
||
import ( | ||
"context" | ||
"crypto/tls" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"os" | ||
|
||
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" | ||
"oras.land/oras-go/v2/registry/remote/auth" | ||
"oras.land/oras-go/v2/registry/remote/credentials" | ||
) | ||
|
||
// Download downloads the OCI artifact present at the given registry URL. | ||
// Usage example: | ||
// | ||
// artifact := "docker.io/company/k0s:latest" | ||
// fp, _ := os.CreateTemp("", "k0s-oci-artifact-*") | ||
// err := oci.Download(ctx, artifact, fp) | ||
// | ||
// 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 { | ||
opt(&opts) | ||
} | ||
|
||
creds, err := opts.auth.CredentialStore(ctx) | ||
if err != nil { | ||
return fmt.Errorf("failed to create credential store: %w", err) | ||
} | ||
|
||
imgref, err := registry.ParseReference(url) | ||
if err != nil { | ||
return fmt.Errorf("failed to parse artifact reference: %w", err) | ||
} | ||
|
||
tmpdir, err := os.MkdirTemp("", "k0s-oci-artifact-*") | ||
if err != nil { | ||
return fmt.Errorf("failed to create temp dir: %w", err) | ||
} | ||
defer func() { _ = os.RemoveAll(tmpdir) }() | ||
|
||
repo, err := remote.NewRepository(url) | ||
if err != nil { | ||
return fmt.Errorf("failed to create repository: %w", err) | ||
} | ||
|
||
if opts.plainHTTP { | ||
repo.PlainHTTP = true | ||
} | ||
|
||
fs, err := file.New(tmpdir) | ||
if err != nil { | ||
return fmt.Errorf("failed to create file store: %w", err) | ||
} | ||
defer fs.Close() | ||
|
||
transp := http.DefaultTransport.(*http.Transport).Clone() | ||
if opts.insecureSkipTLSVerify { | ||
transp.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} | ||
} | ||
|
||
repo.Client = &auth.Client{ | ||
Client: &http.Client{Transport: transp}, | ||
Credential: creds.Get, | ||
} | ||
|
||
tag := imgref.Reference | ||
desc, data, err := repo.Manifests().FetchReference(ctx, tag) | ||
if err != nil { | ||
return fmt.Errorf("failed to fetch manifest: %w", err) | ||
} | ||
defer data.Close() | ||
|
||
successors, err := content.Successors(ctx, repo, desc) | ||
if err != nil { | ||
return fmt.Errorf("failed to fetch successors: %w", err) | ||
} | ||
|
||
source, err := findArtifactDescriptor(successors, opts) | ||
if err != nil { | ||
return fmt.Errorf("failed to find artifact: %w", err) | ||
} | ||
|
||
// 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 fetch blob: %w", err) | ||
} | ||
defer reader.Close() | ||
|
||
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 | ||
plainHTTP bool | ||
} | ||
|
||
// DownloadOption is a function that sets an option for the OCI download. | ||
type DownloadOption func(*downloadOptions) | ||
|
||
// WithInsecureSkipTLSVerify sets the insecureSkipTLSVerify option to true. | ||
func WithInsecureSkipTLSVerify() DownloadOption { | ||
return func(opts *downloadOptions) { | ||
opts.insecureSkipTLSVerify = true | ||
} | ||
} | ||
|
||
// WithPlainHTTP sets the client to reach the remote registry using plain HTTP | ||
// instead of HTTPS. | ||
func WithPlainHTTP() DownloadOption { | ||
return func(opts *downloadOptions) { | ||
opts.plainHTTP = true | ||
} | ||
} | ||
|
||
// WithDockerAuth sets the Docker config to be used when authenticating to | ||
// the registry. | ||
func WithDockerAuth(auth DockerConfig) DownloadOption { | ||
return func(opts *downloadOptions) { | ||
opts.auth = auth | ||
} | ||
} | ||
|
||
// 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"` | ||
Password string `json:"password"` | ||
} | ||
|
||
// DockerConfig represents the content of the '.dockerconfigjson' file. | ||
type DockerConfig struct { | ||
Auths map[string]DockerConfigEntry `json:"auths"` | ||
} | ||
|
||
// CredentialStore turns the Docker configuration into a credential store and | ||
// returns it. | ||
func (d DockerConfig) CredentialStore(ctx context.Context) (credentials.Store, error) { | ||
creds := credentials.NewMemoryStore() | ||
for addr, entry := range d.Auths { | ||
if err := creds.Put(ctx, addr, auth.Credential{ | ||
Username: entry.Username, | ||
Password: entry.Password, | ||
}); err != nil { | ||
return nil, fmt.Errorf("failed to add credential: %w", err) | ||
} | ||
} | ||
return creds, nil | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When having a look at our implementation, I realize that the ADR just says: "We'll download via OCI", but not at all what's meant by this. As far as I understand, there's no common understanding of what it means to download an artifact via OCI, i.e. it is very much implementation dependent.
I think we should augment the ADR with a spec what k0s means/expects when it tries to download files from OCI registries. WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yes, this is what we mention in the ADR:
So we at least know what we expect to find in the registry. We should at least write down what we expect when it comes to file names and formats.