internal/api/orassdk.go (110 lines of code) (raw):
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package api
import (
"context"
"io"
orasauth "github.com/Azure/acr-cli/auth/oras"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2"
"oras.land/oras-go/v2/content/file"
"oras.land/oras-go/v2/registry"
"oras.land/oras-go/v2/registry/remote"
)
const lifecycleAnnotation = "application/vnd.microsoft.artifact.lifecycle"
// The ORASClient wraps the oras-go sdk and is used for interacting with artifacts in a registry.
// it implements the ORASClientInterface.
type ORASClient struct {
client remote.Client
}
// GraphTarget is a tracked oras.GraphTarget.
type GraphTarget interface {
oras.GraphTarget
io.Closer
Prompt(desc ocispec.Descriptor, prompt string) error
Inner() oras.GraphTarget
}
func (o *ORASClient) DiscoverLifecycleAnnotation(ctx context.Context, reference string, artifactType string) (bool, error) {
ref, err := o.getTarget(reference)
if err != nil {
return false, err
}
subject, err := ref.Resolve(ctx, reference)
if err != nil {
return false, err
}
descriptors, err := registry.Referrers(ctx, ref, subject, artifactType)
if err != nil {
return false, err
}
for _, desc := range descriptors {
if desc.ArtifactType == lifecycleAnnotation {
return true, nil
}
}
return false, nil
}
// type packFunc func() (ocispec.Descriptor, error)
// type copyFunc func(desc ocispec.Descriptor) error
func (o *ORASClient) Annotate(ctx context.Context, reference string, artifactType string, annotationsArg map[string]string) error {
dst, err := o.getTarget(reference)
if err != nil {
return err
}
// do the equivalent of
// prepare manifest
store, err := file.New("")
if err != nil {
return err
}
defer store.Close()
subject, err := dst.Resolve(ctx, reference)
if err != nil {
return err
}
graphCopyOptions := oras.DefaultCopyGraphOptions
packOpts := oras.PackManifestOptions{
Subject: &subject,
ManifestAnnotations: annotationsArg,
}
pack := func() (ocispec.Descriptor, error) {
return oras.PackManifest(ctx, store, oras.PackManifestVersion1_1, artifactType, packOpts)
}
copyFunc := func(root ocispec.Descriptor) error {
return oras.CopyGraph(ctx, store, dst, root, graphCopyOptions)
}
// Attach
targetDst := oras.Target(dst)
if tracked, ok := targetDst.(GraphTarget); ok {
defer tracked.Close()
}
// Push
root, err := pack()
if err != nil {
return err
}
if err = copyFunc(root); err != nil {
return err
}
return nil
}
// getTarget gets an oras remote.Repository object that refers to the target of our annotation request
func (o *ORASClient) getTarget(reference string) (repo *remote.Repository, err error) {
repo, err = remote.NewRepository(reference)
if err != nil {
return nil, err
}
repo.SkipReferrersGC = true
repo.Client = o.client
repo.SetReferrersCapability(true)
return repo, nil
}
func GetORASClientWithAuth(username string, password string, configs []string) (*ORASClient, error) {
clientOpts := orasauth.ClientOptions{}
if username != "" && password != "" {
clientOpts.Credential = orasauth.Credential(username, password)
} else {
store, err := orasauth.NewStore(configs...)
if err != nil {
return nil, err
}
clientOpts.CredentialStore = store
}
c := orasauth.NewClient(clientOpts)
orasClient := ORASClient{
client: c,
}
return &orasClient, nil
}
// ORASClientInterface defines the required methods that the acr-cli will need to use with ORAS.
type ORASClientInterface interface {
Annotate(ctx context.Context, reference string, artifactType string, annotations map[string]string) error
DiscoverLifecycleAnnotation(ctx context.Context, reference string, artifactType string) (bool, error)
}