Skip to content

Commit

Permalink
Add 'kit remove' command
Browse files Browse the repository at this point in the history
Add 'kit remove' to allow deleting models from local storage.
  • Loading branch information
amisevsk committed Feb 29, 2024
1 parent 0cb1a56 commit 4d9d785
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 0 deletions.
2 changes: 2 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"kitops/pkg/cmd/logout"
"kitops/pkg/cmd/pull"
"kitops/pkg/cmd/push"
"kitops/pkg/cmd/remove"
"kitops/pkg/cmd/version"
"kitops/pkg/lib/constants"
"kitops/pkg/output"
Expand Down Expand Up @@ -72,6 +73,7 @@ func addSubcommands(rootCmd *cobra.Command) {
rootCmd.AddCommand(push.PushCommand())
rootCmd.AddCommand(list.ListCommand())
rootCmd.AddCommand(export.ExportCommand())
rootCmd.AddCommand(remove.RemoveCommand())
rootCmd.AddCommand(version.VersionCommand())
}

Expand Down
100 changes: 100 additions & 0 deletions pkg/cmd/remove/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package remove

import (
"context"
"fmt"
"kitops/pkg/lib/constants"
"kitops/pkg/lib/repo"
"kitops/pkg/output"
"strings"

"github.com/spf13/cobra"
"oras.land/oras-go/v2/registry"
)

const (
shortDesc = `Remove a modelkit from local storage`
longDesc = `Remove a modelkit from local storage.
Description:
Removes a modelkit from storage on the local disk.
The model to be removed may be specifed either by a tag or by a digest. If
specified by digest, that modelkit will be removed along with any tags that
might refer to it. If specified by tag, the modelkit will only be removed if
no other tags refer to it; otherwise it is only untagged.`

examples = ` kit remove my-registry.com/my-org/my-repo:my-tag
kit remove my-registry.com/my-org/my-repo@sha256:<digest>
kit remove my-registry.com/my-org/my-repo:tag1,tag2,tag3`
)

type removeOptions struct {
configHome string
modelRef *registry.Reference
extraTags []string
}

func (opts *removeOptions) complete(ctx context.Context, args []string) error {
configHome, ok := ctx.Value(constants.ConfigKey{}).(string)
if !ok {
return fmt.Errorf("default config path not set on command context")
}
opts.configHome = configHome

modelRef, extraTags, err := repo.ParseReference(args[0])
if err != nil {
return fmt.Errorf("failed to parse reference %s: %w", modelRef, err)
}
opts.modelRef = modelRef
opts.extraTags = extraTags

printConfig(opts)
return nil
}

func RemoveCommand() *cobra.Command {
opts := &removeOptions{}
cmd := &cobra.Command{
Use: "remove [flags] <reference>",
Short: shortDesc,
Long: longDesc,
Example: examples,
Run: runCommand(opts),
}
cmd.Args = cobra.ExactArgs(1)
return cmd
}

func runCommand(opts *removeOptions) func(*cobra.Command, []string) {
return func(cmd *cobra.Command, args []string) {
if err := opts.complete(cmd.Context(), args); err != nil {
output.Fatalf("Failed to process arguments: %s", err)
}
storageRoot := constants.StoragePath(opts.configHome)
localStore, err := repo.NewLocalStore(storageRoot, opts.modelRef)
if err != nil {
output.Fatalf("Failed to read local storage: %s", storageRoot)
}
desc, err := removeModel(cmd.Context(), localStore, opts.modelRef)
if err != nil {
output.Fatalf("Failed to remove: %s", err)
}
output.Infof("Removed %s (digest %s)", opts.modelRef.String(), desc.Digest)

for _, tag := range opts.extraTags {
ref := *opts.modelRef
ref.Reference = tag
desc, err := removeModel(cmd.Context(), localStore, &ref)
if err != nil {
output.Errorf("Failed to remove: %s", err)
}
output.Infof("Removed %s (digest %s)", ref.String(), desc.Digest)
}
}
}

func printConfig(opts *removeOptions) {
output.Debugf("Using config path: %s", opts.configHome)
output.Debugf("Removing %s and additional tags: [%s]", opts.modelRef.String(), strings.Join(opts.extraTags, ", "))
}
52 changes: 52 additions & 0 deletions pkg/cmd/remove/remove.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package remove

import (
"context"
"fmt"
"kitops/pkg/lib/repo"
"kitops/pkg/output"
"strings"

ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2"
"oras.land/oras-go/v2/errdef"
"oras.land/oras-go/v2/registry"
)

func removeModel(ctx context.Context, store repo.LocalStorage, ref *registry.Reference) (ocispec.Descriptor, error) {
desc, err := oras.Resolve(ctx, store, ref.Reference, oras.ResolveOptions{})
if err != nil {
if err == errdef.ErrNotFound {
return ocispec.DescriptorEmptyJSON, fmt.Errorf("model %s not found", ref.String())
}
return ocispec.DescriptorEmptyJSON, fmt.Errorf("error resolving model: %s", err)
}

// If reference passed in is a digest, remove the manifest ignoring any tags the manifest might have
if err := ref.ValidateReferenceAsDigest(); err == nil {
output.Debugf("Deleting manifest with digest %s", ref.Reference)
if err := store.Delete(ctx, desc); err != nil {
return ocispec.DescriptorEmptyJSON, fmt.Errorf("failed to delete model: %ws", err)
}
return desc, nil
}

tags, err := repo.GetTagsForDescriptor(ctx, store, desc)
if err != nil {
return ocispec.DescriptorEmptyJSON, err
}
if len(tags) <= 1 {
output.Debugf("Deleting manifest tagged %s", ref.Reference)
if err := store.Delete(ctx, desc); err != nil {
return ocispec.DescriptorEmptyJSON, fmt.Errorf("failed to delete model: %w", err)
}
} else {
output.Debugf("Found other tags for manifest: [%s]", strings.Join(tags, ", "))
output.Debugf("Untagging %s", ref.Reference)
if err := store.Untag(ctx, ref.Reference); err != nil {
return ocispec.DescriptorEmptyJSON, fmt.Errorf("failed to untag model: %w", err)
}
}

return desc, nil
}
3 changes: 3 additions & 0 deletions pkg/lib/repo/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/content/oci"
"oras.land/oras-go/v2/registry"
)
Expand All @@ -19,6 +20,8 @@ type LocalStorage interface {
GetRepo() string
GetIndex() (*ocispec.Index, error)
oras.Target
content.Deleter
content.Untagger
}

type LocalStore struct {
Expand Down
14 changes: 14 additions & 0 deletions pkg/lib/repo/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,20 @@ func GetConfig(ctx context.Context, store content.Storage, configDesc ocispec.De
return config, nil
}

func GetTagsForDescriptor(ctx context.Context, store LocalStorage, desc ocispec.Descriptor) ([]string, error) {
index, err := store.GetIndex()
if err != nil {
return nil, err
}
var tags []string
for _, manifest := range index.Manifests {
if manifest.Digest == desc.Digest {
tags = append(tags, manifest.Annotations[ocispec.AnnotationRefName])
}
}
return tags, nil
}

func ValidateTag(tag string) error {
if !validTagRegex.MatchString(tag) {
return fmt.Errorf("invalid tag")
Expand Down

0 comments on commit 4d9d785

Please sign in to comment.