-
Notifications
You must be signed in to change notification settings - Fork 183
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Xiaoxuan Wang <xiaoxuanwang@microsoft.com>
- Loading branch information
Xiaoxuan Wang
committed
Aug 20, 2024
1 parent
dbfef49
commit ef20bdf
Showing
6 changed files
with
604 additions
and
0 deletions.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
/* | ||
Copyright The ORAS 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 index | ||
|
||
import ( | ||
"github.com/spf13/cobra" | ||
) | ||
|
||
func Cmd() *cobra.Command { | ||
cmd := &cobra.Command{ | ||
Use: "index [command]", | ||
Short: "Index operations", | ||
} | ||
|
||
cmd.AddCommand( | ||
createCmd(), | ||
updateCmd(), | ||
) | ||
return cmd | ||
} |
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,204 @@ | ||
/* | ||
Copyright The ORAS 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 index | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"strings" | ||
|
||
"github.com/opencontainers/image-spec/specs-go" | ||
ocispec "github.com/opencontainers/image-spec/specs-go/v1" | ||
"github.com/spf13/cobra" | ||
"oras.land/oras-go/v2" | ||
"oras.land/oras-go/v2/content" | ||
"oras.land/oras-go/v2/errdef" | ||
"oras.land/oras/cmd/oras/internal/command" | ||
"oras.land/oras/cmd/oras/internal/display" | ||
oerrors "oras.land/oras/cmd/oras/internal/errors" | ||
"oras.land/oras/cmd/oras/internal/option" | ||
"oras.land/oras/cmd/oras/internal/output" | ||
"oras.land/oras/internal/contentutil" | ||
"oras.land/oras/internal/descriptor" | ||
"oras.land/oras/internal/listener" | ||
) | ||
|
||
type createOptions struct { | ||
option.Common | ||
option.Target | ||
|
||
sources []string | ||
extraRefs []string | ||
} | ||
|
||
func createCmd() *cobra.Command { | ||
var opts createOptions | ||
cmd := &cobra.Command{ | ||
Use: "create [flags] <name>[:<tag[,<tag>][...]] [{<tag>|<digest>}...]", | ||
Short: "Create and push an index from provided manifests", | ||
Long: `Create and push an index to a repository or an OCI image layout | ||
Example - create an index from source manifests tagged amd64, arm64, darwin in the repository | ||
localhost:5000/hello, and push the index without tagging it: | ||
oras manifest index create localhost:5000/hello amd64 arm64 darwin | ||
Example - create an index from source manifests tagged amd64, arm64, darwin in the repository | ||
localhost:5000/hello, and push the index with tag 'latest': | ||
oras manifest index create localhost:5000/hello:latest amd64 arm64 darwin | ||
Example - create an index from source manifests using both tags and digests, | ||
and push the index with tag 'latest': | ||
oras manifest index create localhost:5000/hello latest amd64 sha256:xxx darwin | ||
Example - create an index and push it with multiple tags: | ||
oras manifest index create localhost:5000/tag1, tag2, tag3 amd64 arm64 sha256:xxx | ||
`, | ||
Args: cobra.MinimumNArgs(1), | ||
PreRunE: func(cmd *cobra.Command, args []string) error { | ||
refs := strings.Split(args[0], ",") | ||
opts.RawReference = refs[0] | ||
opts.extraRefs = refs[1:] | ||
opts.sources = args[1:] | ||
return option.Parse(cmd, &opts) | ||
}, | ||
RunE: func(cmd *cobra.Command, args []string) error { | ||
return createIndex(cmd, opts) | ||
}, | ||
} | ||
option.ApplyFlags(&opts, cmd.Flags()) | ||
return oerrors.Command(cmd, &opts.Target) | ||
} | ||
|
||
func createIndex(cmd *cobra.Command, opts createOptions) error { | ||
ctx, logger := command.GetLogger(cmd, &opts.Common) | ||
target, err := opts.NewTarget(opts.Common, logger) | ||
if err != nil { | ||
return err | ||
} | ||
// we assume that the sources and the to be created index are all in the same | ||
// repository, so no copy is needed | ||
manifests, err := resolveSourceManifests(ctx, target, opts) | ||
if err != nil { | ||
return err | ||
} | ||
desc, content, err := packIndex(&ocispec.Index{}, manifests) | ||
if err != nil { | ||
return err | ||
} | ||
opts.Println("Created the index") | ||
return pushIndex(ctx, target, desc, content, opts.Reference, opts.extraRefs, opts.AnnotatedReference(), opts.Printer) | ||
} | ||
|
||
func resolveSourceManifests(ctx context.Context, target oras.ReadOnlyTarget, opts createOptions) ([]ocispec.Descriptor, error) { | ||
var resolved []ocispec.Descriptor | ||
for _, source := range opts.sources { | ||
desc, content, err := oras.FetchBytes(ctx, target, source, oras.DefaultFetchBytesOptions) | ||
if err != nil { | ||
if errors.Is(err, errdef.ErrNotFound) { | ||
return nil, fmt.Errorf("could not resolve %s. Make sure the manifest exists in %s", source, opts.Path) | ||
} | ||
return nil, err | ||
} | ||
opts.Println("Resolved manifest", source) | ||
if descriptor.IsImageManifest(desc) { | ||
desc.Platform, err = getPlatform(ctx, target, content) | ||
if err != nil { | ||
return nil, err | ||
} | ||
} | ||
resolved = append(resolved, desc) | ||
} | ||
return resolved, nil | ||
} | ||
|
||
func getPlatform(ctx context.Context, target oras.ReadOnlyTarget, manifestBytes []byte) (*ocispec.Platform, error) { | ||
// extract config descriptor | ||
var manifest ocispec.Manifest | ||
if err := json.Unmarshal(manifestBytes, &manifest); err != nil { | ||
return nil, err | ||
} | ||
// fetch config content | ||
contentBytes, err := content.FetchAll(ctx, target, manifest.Config) | ||
if err != nil { | ||
return nil, err | ||
} | ||
var platform ocispec.Platform | ||
if err := json.Unmarshal(contentBytes, &platform); err != nil { | ||
return nil, err | ||
} | ||
return &platform, nil | ||
} | ||
|
||
func packIndex(oldIndex *ocispec.Index, manifests []ocispec.Descriptor) (ocispec.Descriptor, []byte, error) { | ||
index := ocispec.Index{ | ||
Versioned: specs.Versioned{ | ||
SchemaVersion: 2, | ||
}, | ||
MediaType: ocispec.MediaTypeImageIndex, | ||
ArtifactType: oldIndex.ArtifactType, | ||
Manifests: manifests, | ||
Subject: oldIndex.Subject, | ||
Annotations: oldIndex.Annotations, | ||
} | ||
indexBytes, err := json.Marshal(index) | ||
if err != nil { | ||
return ocispec.Descriptor{}, nil, err | ||
} | ||
desc := content.NewDescriptorFromBytes(ocispec.MediaTypeImageIndex, indexBytes) | ||
return desc, indexBytes, nil | ||
} | ||
|
||
func pushIndex(ctx context.Context, target oras.Target, desc ocispec.Descriptor, content []byte, ref string, extraRefs []string, path string, printer *output.Printer) error { | ||
// push the index | ||
var err error | ||
if ref == "" || contentutil.IsDigest(ref) { | ||
err = target.Push(ctx, desc, bytes.NewReader(content)) | ||
} else { | ||
_, err = oras.TagBytes(ctx, target, desc.MediaType, content, ref) | ||
} | ||
if err != nil { | ||
return err | ||
} | ||
printer.Println("Pushed", path) | ||
|
||
// tag the index if extra tags are provided | ||
extraRefs = removeDigests(extraRefs) | ||
if len(extraRefs) != 0 { | ||
handler := display.NewManifestPushHandler(printer) | ||
tagListener := listener.NewTaggedListener(target, handler.OnTagged) | ||
if _, err = oras.TagBytesN(ctx, tagListener, desc.MediaType, content, extraRefs, oras.DefaultTagBytesNOptions); err != nil { | ||
return err | ||
} | ||
} | ||
return printer.Println("Digest:", desc.Digest) | ||
} | ||
|
||
func removeDigests(refs []string) []string { | ||
pointer := len(refs) - 1 | ||
for i, m := range refs { | ||
if contentutil.IsDigest(m) { | ||
// swap the digest to the end of slice for removal | ||
refs[i] = refs[pointer] | ||
pointer = pointer - 1 | ||
} | ||
} | ||
// shrink the slice to remove the digest | ||
refs = refs[:pointer+1] | ||
return refs | ||
} |
Oops, something went wrong.