Skip to content

Commit

Permalink
Add type and annotations to artifact add
Browse files Browse the repository at this point in the history
podman artifact add now supports two new command line switches.

--type string that describes the type of artifact
--annotation string slice in the form of key=val

These new options allow users to "tag" information in on their artifacts
for any number of purposes down the line

RUN-2446

Signed-off-by: Brent Baude <[email protected]>
  • Loading branch information
baude committed Jan 30, 2025
1 parent 48f8742 commit 822d6be
Show file tree
Hide file tree
Showing 13 changed files with 127 additions and 29 deletions.
24 changes: 23 additions & 1 deletion cmd/podman/artifact/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package artifact
import (
"fmt"

"github.com/containers/common/pkg/completion"
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/pkg/domain/entities"
Expand All @@ -22,15 +23,36 @@ var (
}
)

type artifactAddOptions struct {
ArtifactType string
Annotations []string
}

var (
addOpts artifactAddOptions
addTypeFlagName = "type"
)

func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: addCmd,
Parent: artifactCmd,
})
flags := addCmd.Flags()

annotationFlagName := "annotation"
flags.StringArrayVar(&addOpts.Annotations, annotationFlagName, nil, "set an `annotation` for the specified artifact")
_ = addCmd.RegisterFlagCompletionFunc(annotationFlagName, completion.AutocompleteNone)

flags.StringVar(&addOpts.ArtifactType, addTypeFlagName, "", "Use type to describe an artifact")
_ = addCmd.RegisterFlagCompletionFunc(addTypeFlagName, completion.AutocompleteNone)
}

func add(cmd *cobra.Command, args []string) error {
report, err := registry.ImageEngine().ArtifactAdd(registry.Context(), args[0], args[1:], entities.ArtifactAddoptions{})
opts := new(entities.ArtifactAddOptions)
opts.Annotations = addOpts.Annotations
opts.ArtifactType = addOpts.ArtifactType
report, err := registry.ImageEngine().ArtifactAdd(registry.Context(), args[0], args[1:], opts)
if err != nil {
return err
}
Expand Down
1 change: 1 addition & 0 deletions docs/source/markdown/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
podman-artifact-add.1.md
podman-artifact-pull.1.md
podman-artifact-push.1.md
podman-attach.1.md
Expand Down
2 changes: 1 addition & 1 deletion docs/source/markdown/options/annotation.manifest.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
####> This option file is used in:
####> podman manifest add, manifest annotate
####> podman artifact add, manifest add, manifest annotate
####> If file is edited, make sure the changes
####> are applicable to all of those.
#### **--annotation**=*annotation=value*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,15 @@ added.

## OPTIONS

@@option annotation.manifest

#### **--help**

Print usage statement.

#### **--type**

Set a type for the artifact being added.

## EXAMPLES

Expand All @@ -39,6 +44,10 @@ $ podman artifact add quay.io/myartifact/myml:latest /tmp/foobar1.ml /tmp/foobar
1487acae11b5a30948c50762882036b41ac91a7b9514be8012d98015c95ddb78
```

Set an annotation for an artifact
```
$ podman artifact add --annotation date=2025-01-30 quay.io/myartifact/myml:latest /tmp/foobar1.ml
```


## SEE ALSO
Expand Down
27 changes: 13 additions & 14 deletions pkg/api/handlers/libpod/manifests.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/containers/podman/v5/pkg/channel"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/containers/podman/v5/pkg/domain/infra/abi"
utils2 "github.com/containers/podman/v5/pkg/domain/utils"
"github.com/containers/podman/v5/pkg/errorhandling"
"github.com/gorilla/mux"
"github.com/gorilla/schema"
Expand Down Expand Up @@ -520,32 +521,30 @@ func ManifestModify(w http.ResponseWriter, r *http.Request) {
return
}

annotationsFromAnnotationSlice := func(annotation []string) map[string]string {
annotations := make(map[string]string)
for _, annotationSpec := range annotation {
key, val, hasVal := strings.Cut(annotationSpec, "=")
if !hasVal {
utils.Error(w, http.StatusBadRequest, fmt.Errorf("no value given for annotation %q", key))
return nil
}
annotations[key] = val
}
return annotations
}
if len(body.ManifestAddOptions.Annotation) != 0 {
if len(body.ManifestAddOptions.Annotations) != 0 {
utils.Error(w, http.StatusBadRequest, fmt.Errorf("can not set both Annotation and Annotations"))
return
}
body.ManifestAddOptions.Annotations = annotationsFromAnnotationSlice(body.ManifestAddOptions.Annotation)
annots, err := utils2.ParseAnnotations(body.ManifestAddOptions.Annotation)
if err != nil {
utils.Error(w, http.StatusBadRequest, err)
return
}
body.ManifestAddOptions.Annotations = annots
body.ManifestAddOptions.Annotation = nil
}
if len(body.ManifestAddOptions.IndexAnnotation) != 0 {
if len(body.ManifestAddOptions.IndexAnnotations) != 0 {
utils.Error(w, http.StatusBadRequest, fmt.Errorf("can not set both IndexAnnotation and IndexAnnotations"))
return
}
body.ManifestAddOptions.IndexAnnotations = annotationsFromAnnotationSlice(body.ManifestAddOptions.IndexAnnotation)
annots, err := utils2.ParseAnnotations(body.ManifestAddOptions.IndexAnnotation)
if err != nil {
utils.Error(w, http.StatusBadRequest, err)
return
}
body.ManifestAddOptions.IndexAnnotations = annots
body.ManifestAddOptions.IndexAnnotation = nil
}

Expand Down
3 changes: 2 additions & 1 deletion pkg/domain/entities/artifact.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import (
"github.com/opencontainers/go-digest"
)

type ArtifactAddoptions struct {
type ArtifactAddOptions struct {
Annotations []string
ArtifactType string
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/domain/entities/engine_image.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
)

type ImageEngine interface { //nolint:interfacebloat
ArtifactAdd(ctx context.Context, name string, paths []string, opts ArtifactAddoptions) (*ArtifactAddReport, error)
ArtifactAdd(ctx context.Context, name string, paths []string, opts *ArtifactAddOptions) (*ArtifactAddReport, error)
ArtifactInspect(ctx context.Context, name string, opts ArtifactInspectOptions) (*ArtifactInspectReport, error)
ArtifactList(ctx context.Context, opts ArtifactListOptions) ([]*ArtifactListReport, error)
ArtifactPull(ctx context.Context, name string, opts ArtifactPullOptions) (*ArtifactPullReport, error)
Expand Down
16 changes: 14 additions & 2 deletions pkg/domain/infra/abi/artifact.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import (

"github.com/containers/common/libimage"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/containers/podman/v5/pkg/domain/utils"
"github.com/containers/podman/v5/pkg/libartifact/store"
"github.com/containers/podman/v5/pkg/libartifact/types"
)

func getDefaultArtifactStore(ir *ImageEngine) string {
Expand Down Expand Up @@ -152,12 +154,22 @@ func (ir *ImageEngine) ArtifactPush(ctx context.Context, name string, opts entit
err = artStore.Push(ctx, name, name, copyOpts)
return &entities.ArtifactPushReport{}, err
}
func (ir *ImageEngine) ArtifactAdd(ctx context.Context, name string, paths []string, opts entities.ArtifactAddoptions) (*entities.ArtifactAddReport, error) {
func (ir *ImageEngine) ArtifactAdd(ctx context.Context, name string, paths []string, opts *entities.ArtifactAddOptions) (*entities.ArtifactAddReport, error) {
annotations, err := utils.ParseAnnotations(opts.Annotations)
if err != nil {
return nil, err
}
artStore, err := store.NewArtifactStore(getDefaultArtifactStore(ir), ir.Libpod.SystemContext())
if err != nil {
return nil, err
}
artifactDigest, err := artStore.Add(ctx, name, paths, opts.ArtifactType)

addOptions := types.AddOptions{
Annotations: annotations,
ArtifactType: opts.ArtifactType,
}

artifactDigest, err := artStore.Add(ctx, name, paths, &addOptions)
if err != nil {
return nil, err
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/domain/infra/tunnel/artifact.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (

// TODO For now, no remote support has been added. We need the API to firm up first.

func ArtifactAdd(ctx context.Context, path, name string, opts entities.ArtifactAddoptions) error {
func ArtifactAdd(ctx context.Context, path, name string, opts entities.ArtifactAddOptions) error {
return fmt.Errorf("not implemented")
}

Expand All @@ -33,6 +33,6 @@ func (ir *ImageEngine) ArtifactPush(ctx context.Context, name string, opts entit
return nil, fmt.Errorf("not implemented")
}

func (ir *ImageEngine) ArtifactAdd(ctx context.Context, name string, paths []string, opts entities.ArtifactAddoptions) (*entities.ArtifactAddReport, error) {
func (ir *ImageEngine) ArtifactAdd(ctx context.Context, name string, paths []string, opts *entities.ArtifactAddOptions) (*entities.ArtifactAddReport, error) {
return nil, fmt.Errorf("not implemented")
}
15 changes: 15 additions & 0 deletions pkg/domain/utils/utils.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package utils

import (
"fmt"
"net/url"
"strings"

Expand Down Expand Up @@ -39,3 +40,17 @@ func ToURLValues(f []string) (filters url.Values) {
}
return
}

// ParseAnnotations takes a string slice of options, expected to be "key=val" and returns
// a string map where the map index is the key and points to the value
func ParseAnnotations(options []string) (map[string]string, error) {
annotations := make(map[string]string)
for _, annotationSpec := range options {
key, val, hasVal := strings.Cut(annotationSpec, "=")
if !hasVal {
return nil, fmt.Errorf("no value given for annotation %q", key)
}
annotations[key] = val
}
return annotations, nil
}
27 changes: 20 additions & 7 deletions pkg/libartifact/store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/containers/image/v5/oci/layout"
"github.com/containers/image/v5/transports/alltransports"
"github.com/containers/image/v5/types"
"github.com/containers/podman/v5/pkg/domain/utils"
"github.com/containers/podman/v5/pkg/libartifact"
libartTypes "github.com/containers/podman/v5/pkg/libartifact/types"
"github.com/containers/storage/pkg/fileutils"
Expand Down Expand Up @@ -160,7 +161,11 @@ func (as ArtifactStore) Push(ctx context.Context, src, dest string, opts libimag

// Add takes one or more local files and adds them to the local artifact store. The empty
// string input is for possible custom artifact types.
func (as ArtifactStore) Add(ctx context.Context, dest string, paths []string, _ string) (*digest.Digest, error) {
func (as ArtifactStore) Add(ctx context.Context, dest string, paths []string, options *libartTypes.AddOptions) (*digest.Digest, error) {
annots := make(map[string]string)
if err := utils.DeepCopy(&annots, options.Annotations); err != nil {
return nil, err
}
if len(dest) == 0 {
return nil, ErrEmptyArtifactName
}
Expand Down Expand Up @@ -200,24 +205,32 @@ func (as ArtifactStore) Add(ctx context.Context, dest string, paths []string, _
if err != nil {
return nil, err
}
newArtifactAnnotations := map[string]string{}
newArtifactAnnotations[specV1.AnnotationTitle] = filepath.Base(path)
// currently we don't allow override of the filename
if _, hasTitle := options.Annotations[specV1.AnnotationTitle]; hasTitle {
return nil, fmt.Errorf("cannot override filename in artifact %s", options.Annotations[specV1.AnnotationTitle])
}

annots[specV1.AnnotationTitle] = filepath.Base(path)

newLayer := specV1.Descriptor{
MediaType: detectedType,
Digest: newBlobDigest,
Size: newBlobSize,
Annotations: newArtifactAnnotations,
Annotations: annots,
}

artifactManifestLayers = append(artifactManifestLayers, newLayer)
}

artifactManifest := specV1.Manifest{
Versioned: specs.Versioned{SchemaVersion: 2},
MediaType: specV1.MediaTypeImageManifest,
// TODO This should probably be configurable once the CLI is capable
ArtifactType: "",
Config: specV1.DescriptorEmptyJSON,
Layers: artifactManifestLayers,
Config: specV1.DescriptorEmptyJSON,
Layers: artifactManifestLayers,
}
if len(options.ArtifactType) != 0 {
artifactManifest.ArtifactType = options.ArtifactType
}

rawData, err := json.Marshal(artifactManifest)
Expand Down
6 changes: 6 additions & 0 deletions pkg/libartifact/types/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,9 @@ package types
// GetArtifactOptions is a struct containing options that for obtaining artifacts.
// It is meant for future growth or changes required without wacking the API
type GetArtifactOptions struct{}

// AddOptions are additional descriptors of an artifact file
type AddOptions struct {
Annotations map[string]string `json:"annotations,omitempty"`
ArtifactType string `json:",omitempty"`
}
20 changes: 20 additions & 0 deletions test/e2e/artifact_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,26 @@ var _ = Describe("Podman artifact", func() {
Expect(addAgain.ErrorToString()).To(Equal(fmt.Sprintf("Error: artifact %s already exists", artifact1Name)))
})

It("podman artifact add with options", func() {
artifact1Name := "localhost/test/artifact1"
artifact1File, err := createArtifactFile(1024)
Expect(err).ToNot(HaveOccurred())

artifactType := "octet/foobar"
annotation1 := "color=blue"
annotation2 := "flavor=lemon"

podmanTest.PodmanExitCleanly([]string{"artifact", "add", "--type", artifactType, "--annotation", annotation1, "--annotation", annotation2, artifact1Name, artifact1File}...)
inspectSingleSession := podmanTest.PodmanExitCleanly([]string{"artifact", "inspect", artifact1Name}...)
a := libartifact.Artifact{}
err = json.Unmarshal([]byte(inspectSingleSession.OutputToString()), &a)
Expect(err).ToNot(HaveOccurred())
Expect(a.Name).To(Equal(artifact1Name))
Expect(a.Manifests[0].ArtifactType).To(Equal(artifactType))
Expect(a.Manifests[0].Layers[0].Annotations["color"]).To(Equal("blue"))
Expect(a.Manifests[0].Layers[0].Annotations["flavor"]).To(Equal("lemon"))
})

It("podman artifact add multiple", func() {
artifact1File1, err := createArtifactFile(1024)
Expect(err).ToNot(HaveOccurred())
Expand Down

0 comments on commit 822d6be

Please sign in to comment.