forked from oras-project/oras
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add attach command (oras-project#433)
Signed-off-by: Billy Zha <[email protected]>
- Loading branch information
Showing
14 changed files
with
408 additions
and
148 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
/* | ||
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 main | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
|
||
ocispec "github.com/opencontainers/image-spec/specs-go/v1" | ||
artifactspec "github.com/oras-project/artifacts-spec/specs-go/v1" | ||
"github.com/spf13/cobra" | ||
"oras.land/oras-go/v2" | ||
"oras.land/oras-go/v2/content" | ||
"oras.land/oras-go/v2/content/file" | ||
"oras.land/oras/cmd/oras/internal/display" | ||
"oras.land/oras/cmd/oras/internal/option" | ||
) | ||
|
||
type attachOptions struct { | ||
option.Common | ||
option.Remote | ||
option.Pusher | ||
|
||
targetRef string | ||
artifactType string | ||
} | ||
|
||
func attachCmd() *cobra.Command { | ||
var opts attachOptions | ||
cmd := &cobra.Command{ | ||
Use: "attach name<:tag|@digest> file[:type] [file...]", | ||
Short: "[Preview] Attach files to an existing artifact", | ||
Long: `[Preview] Attach files to an existing artifact | ||
** This command is in preview and under development. ** | ||
Example - Attach file 'hi.txt' with type 'doc/example' to manifest 'hello:test' in registry 'localhost:5000' | ||
oras attach localhost:5000/hello:test hi.txt --artifact-type doc/example | ||
`, | ||
Args: cobra.MinimumNArgs(2), | ||
PreRunE: func(cmd *cobra.Command, args []string) error { | ||
return opts.ReadPassword() | ||
}, | ||
RunE: func(cmd *cobra.Command, args []string) error { | ||
opts.targetRef = args[0] | ||
opts.FileRefs = args[1:] | ||
return runAttach(opts) | ||
}, | ||
} | ||
|
||
cmd.Flags().StringVarP(&opts.artifactType, "artifact-type", "", "", "artifact type") | ||
option.ApplyFlags(&opts, cmd.Flags()) | ||
return cmd | ||
} | ||
|
||
func runAttach(opts attachOptions) error { | ||
ctx, _ := opts.SetLoggerLevel() | ||
annotations, err := opts.LoadManifestAnnotations() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
// Prepare manifest | ||
store := file.New("") | ||
defer store.Close() | ||
store.AllowPathTraversalOnWrite = opts.PathValidationDisabled | ||
|
||
dst, err := opts.NewRepository(opts.targetRef, opts.Common) | ||
if err != nil { | ||
return err | ||
} | ||
if dst.Reference.Reference == "" { | ||
return newErrInvalidReference(dst.Reference) | ||
} | ||
ociSubject, err := dst.Resolve(ctx, dst.Reference.Reference) | ||
if err != nil { | ||
return err | ||
} | ||
subject := ociToArtifact(ociSubject) | ||
ociDescs, err := loadFiles(ctx, store, annotations, opts.FileRefs, opts.Verbose) | ||
if err != nil { | ||
return err | ||
} | ||
orasDescs := make([]artifactspec.Descriptor, len(ociDescs)) | ||
for i := range ociDescs { | ||
orasDescs[i] = ociToArtifact(ociDescs[i]) | ||
} | ||
desc, err := oras.PackArtifact( | ||
ctx, store, opts.artifactType, orasDescs, | ||
oras.PackArtifactOptions{ | ||
Subject: &subject, | ||
}) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
// Prepare Push | ||
graphCopyOptions := oras.DefaultCopyGraphOptions | ||
graphCopyOptions.FindSuccessors = func(ctx context.Context, fetcher content.Fetcher, node ocispec.Descriptor) ([]ocispec.Descriptor, error) { | ||
if isEqualOCIDescriptor(node, desc) { | ||
// Skip subject | ||
return ociDescs, nil | ||
} | ||
return content.Successors(ctx, fetcher, node) | ||
} | ||
graphCopyOptions.PreCopy = display.StatusPrinter("Uploading", opts.Verbose) | ||
graphCopyOptions.OnCopySkipped = display.StatusPrinter("Exists ", opts.Verbose) | ||
graphCopyOptions.PostCopy = display.StatusPrinter("Uploaded ", opts.Verbose) | ||
|
||
// Push | ||
err = oras.CopyGraph(ctx, store, dst, desc, graphCopyOptions) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
fmt.Println("Attached to", opts.targetRef) | ||
fmt.Println("Digest:", desc.Digest) | ||
|
||
// Export manifest | ||
return opts.ExportManifest(ctx, store, desc) | ||
} | ||
|
||
func isEqualOCIDescriptor(a, b ocispec.Descriptor) bool { | ||
return a.Size == b.Size && a.Digest == b.Digest && a.MediaType == b.MediaType | ||
} | ||
|
||
// ociToArtifact converts OCI descriptor to artifact descriptor. | ||
func ociToArtifact(desc ocispec.Descriptor) artifactspec.Descriptor { | ||
return artifactspec.Descriptor{ | ||
MediaType: desc.MediaType, | ||
Digest: desc.Digest, | ||
Size: desc.Size, | ||
URLs: desc.URLs, | ||
Annotations: desc.Annotations, | ||
} | ||
} |
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,58 @@ | ||
/* | ||
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 main | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"path/filepath" | ||
|
||
ocispec "github.com/opencontainers/image-spec/specs-go/v1" | ||
"oras.land/oras-go/v2/content/file" | ||
) | ||
|
||
func loadFiles(ctx context.Context, store *file.Store, annotations map[string]map[string]string, fileRefs []string, verbose bool) ([]ocispec.Descriptor, error) { | ||
var files []ocispec.Descriptor | ||
for _, fileRef := range fileRefs { | ||
filename, mediaType := parseFileReference(fileRef, "") | ||
name := filepath.Clean(filename) | ||
if !filepath.IsAbs(name) { | ||
// convert to slash-separated path unless it is absolute path | ||
name = filepath.ToSlash(name) | ||
} | ||
if verbose { | ||
fmt.Println("Preparing", name) | ||
} | ||
file, err := store.Add(ctx, name, mediaType, filename) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if value, ok := annotations[filename]; ok { | ||
if file.Annotations == nil { | ||
file.Annotations = value | ||
} else { | ||
for k, v := range value { | ||
file.Annotations[k] = v | ||
} | ||
} | ||
} | ||
files = append(files, file) | ||
} | ||
if len(files) == 0 { | ||
fmt.Println("Uploading empty artifact") | ||
} | ||
return files, nil | ||
} |
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,30 @@ | ||
//go:build !windows | ||
|
||
/* | ||
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 main | ||
|
||
import "strings" | ||
|
||
// parseFileReference parses file reference on unix. | ||
func parseFileReference(reference string, mediaType string) (filePath, mediatype string) { | ||
i := strings.LastIndex(reference, ":") | ||
if i < 0 { | ||
return reference, mediaType | ||
} | ||
return reference[:i], reference[i+1:] | ||
|
||
} |
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,40 @@ | ||
/* | ||
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 main | ||
|
||
import ( | ||
"strings" | ||
"unicode" | ||
) | ||
|
||
// parseFileReference parses file reference on windows. | ||
// Windows systems does not allow ':' in the file path except for drive letter. | ||
func parseFileReference(reference string, mediaType string) (filePath, mediatype string) { | ||
i := strings.Index(reference, ":") | ||
if i < 0 { | ||
return reference, mediaType | ||
} | ||
|
||
// In case it is C:\ | ||
if i == 1 && len(reference) > 2 && reference[2] == '\\' && unicode.IsLetter(rune(reference[0])) { | ||
i = strings.Index(reference[3:], ":") | ||
if i < 0 { | ||
return reference, mediaType | ||
} | ||
i += 3 | ||
} | ||
return reference[:i], reference[i+1:] | ||
} |
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,75 @@ | ||
/* | ||
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 option | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"os" | ||
|
||
ocispec "github.com/opencontainers/image-spec/specs-go/v1" | ||
"github.com/spf13/pflag" | ||
"oras.land/oras-go/v2/content" | ||
) | ||
|
||
// Pusher option struct. | ||
type Pusher struct { | ||
ManifestExportPath string | ||
PathValidationDisabled bool | ||
ManifestAnnotations string | ||
|
||
FileRefs []string | ||
} | ||
|
||
// ApplyFlags applies flags to a command flag set. | ||
func (opts *Pusher) ApplyFlags(fs *pflag.FlagSet) { | ||
fs.StringVarP(&opts.ManifestExportPath, "export-manifest", "", "", "export the pushed manifest") | ||
fs.StringVarP(&opts.ManifestAnnotations, "manifest-annotations", "", "", "manifest annotation file") | ||
fs.BoolVarP(&opts.PathValidationDisabled, "disable-path-validation", "", false, "skip path validation") | ||
} | ||
|
||
// ExportManifest saves the pushed manifest to a local file. | ||
func (opts *Pusher) ExportManifest(ctx context.Context, fetcher content.Fetcher, desc ocispec.Descriptor) error { | ||
if opts.ManifestExportPath == "" { | ||
return nil | ||
} | ||
manifestBytes, err := content.FetchAll(ctx, fetcher, desc) | ||
if err != nil { | ||
return err | ||
} | ||
return os.WriteFile(opts.ManifestExportPath, manifestBytes, 0666) | ||
} | ||
|
||
// LoadManifestAnnotations loads the manifest annotation map. | ||
func (opts *Pusher) LoadManifestAnnotations() (map[string]map[string]string, error) { | ||
var annotations map[string]map[string]string | ||
if opts.ManifestAnnotations != "" { | ||
if err := decodeJSON(opts.ManifestAnnotations, &annotations); err != nil { | ||
return nil, err | ||
} | ||
} | ||
return annotations, nil | ||
} | ||
|
||
// decodeJSON decodes a json file v to filename. | ||
func decodeJSON(filename string, v interface{}) error { | ||
file, err := os.Open(filename) | ||
if err != nil { | ||
return err | ||
} | ||
defer file.Close() | ||
return json.NewDecoder(file).Decode(v) | ||
} |
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
Oops, something went wrong.