1
0
mirror of https://github.com/regclient/regclient.git synced 2025-04-18 22:44:00 +03:00

feat: Add regctl image create command

Signed-off-by: Brandon Mitchell <git@bmitch.net>
This commit is contained in:
Brandon Mitchell 2024-05-07 15:21:20 -04:00
parent 4e703ff729
commit 52b369010d
No known key found for this signature in database
GPG Key ID: 6E0FF28C767A8BEE
3 changed files with 192 additions and 1 deletions

View File

@ -2,6 +2,7 @@ package main
import (
"archive/tar"
"bytes"
"encoding/json"
"errors"
"fmt"
@ -26,8 +27,11 @@ import (
"github.com/regclient/regclient/pkg/template"
"github.com/regclient/regclient/types"
"github.com/regclient/regclient/types/blob"
"github.com/regclient/regclient/types/descriptor"
"github.com/regclient/regclient/types/docker/schema2"
"github.com/regclient/regclient/types/errs"
"github.com/regclient/regclient/types/manifest"
"github.com/regclient/regclient/types/mediatype"
v1 "github.com/regclient/regclient/types/oci/v1"
"github.com/regclient/regclient/types/platform"
"github.com/regclient/regclient/types/ref"
@ -35,19 +39,25 @@ import (
type imageCmd struct {
rootOpts *rootCmd
annotations []string
byDigest bool
checkBaseRef string
checkBaseDigest string
checkSkipConfig bool
create string
created string
digestTags bool
exportCompress bool
exportRef string
fastCheck bool
forceRecursive bool
format string
formatCreate string
formatFile string
importName string
includeExternal bool
digestTags bool
labels []string
mediaType string
modOpts []mod.Opts
platform string
platforms []string
@ -55,6 +65,11 @@ type imageCmd struct {
replace bool
}
var imageKnownTypes = []string{
mediatype.OCI1Manifest,
mediatype.Docker2Manifest,
}
func NewImageCmd(rootOpts *rootCmd) *cobra.Command {
imageOpts := imageCmd{
rootOpts: rootOpts,
@ -120,6 +135,19 @@ regctl image copy --platform windows/amd64,osver=10.0.17763.4974 --include-exter
ValidArgsFunction: rootOpts.completeArgTag,
RunE: imageOpts.runImageCopy,
}
var imageCreateCmd = &cobra.Command{
Use: "create <image_ref>",
Aliases: []string{"init", "new"},
Short: "create a new image manifest",
Long: `Create a new image manifest from an initially empty (scratch) state.`,
Example: `
# create a scratch image
regctl image create ocidir://new-image:scratch
`,
Args: cobra.ExactArgs(1),
ValidArgsFunction: rootOpts.completeArgTag,
RunE: imageOpts.runImageCreate,
}
var imageDeleteCmd = &cobra.Command{
Use: "delete <image_ref>",
Aliases: []string{"del", "rm", "remove"},
@ -284,6 +312,17 @@ regctl image ratelimit alpine --format '{{.Remain}}'`,
imageCopyCmd.Flags().BoolVarP(&imageOpts.digestTags, "digest-tags", "", false, "Include digest tags (\"sha256-<digest>.*\") when copying manifests")
imageCopyCmd.Flags().BoolVarP(&imageOpts.referrers, "referrers", "", false, "Include referrers")
imageCreateCmd.Flags().StringArrayVar(&imageOpts.annotations, "annotation", []string{}, "Annotation to set on manifest")
imageCreateCmd.Flags().BoolVar(&imageOpts.byDigest, "by-digest", false, "Push manifest by digest instead of tag")
imageCreateCmd.Flags().StringVar(&imageOpts.created, "created", "", "Created timestamp to set (use \"now\" or RFC3339 syntax)")
imageCreateCmd.Flags().StringVar(&imageOpts.formatCreate, "format", "", "Format output with go template syntax")
imageCreateCmd.Flags().StringArrayVar(&imageOpts.labels, "label", []string{}, "Labels to set in the image config")
imageCreateCmd.Flags().StringVar(&imageOpts.mediaType, "media-type", mediatype.OCI1Manifest, "Media-type for manifest")
imageCreateCmd.Flags().StringVar(&imageOpts.platform, "platform", "", "Platform to set on the image")
_ = imageCreateCmd.RegisterFlagCompletionFunc("media-type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return imageKnownTypes, cobra.ShellCompDirectiveNoFileComp
})
imageDeleteCmd.Flags().BoolVarP(&manifestOpts.forceTagDeref, "force-tag-dereference", "", false, "Dereference the a tag to a digest, this is unsafe")
imageDigestCmd.Flags().BoolVarP(&manifestOpts.list, "list", "", true, "Do not resolve platform from manifest list (enabled by default)")
@ -824,6 +863,7 @@ regctl image ratelimit alpine --format '{{.Remain}}'`,
imageTopCmd.AddCommand(imageCheckBaseCmd)
imageTopCmd.AddCommand(imageCopyCmd)
imageTopCmd.AddCommand(imageCreateCmd)
imageTopCmd.AddCommand(imageDeleteCmd)
imageTopCmd.AddCommand(imageDigestCmd)
imageTopCmd.AddCommand(imageExportCmd)
@ -1141,6 +1181,141 @@ func (ip *imageProgress) display(final bool) {
}
}
func (imageOpts *imageCmd) runImageCreate(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
// validate media type
if imageOpts.mediaType != mediatype.OCI1Manifest && imageOpts.mediaType != mediatype.Docker2Manifest {
return fmt.Errorf("unsupported manifest media type: %s%.0w", imageOpts.mediaType, errs.ErrUnsupportedMediaType)
}
// parse ref
r, err := ref.New(args[0])
if err != nil {
return err
}
// setup regclient
rc := imageOpts.rootOpts.newRegClient()
defer rc.Close(ctx, r)
// define the image config
conf := v1.Image{
Config: v1.ImageConfig{},
RootFS: v1.RootFS{
Type: "layers",
DiffIDs: []digest.Digest{},
},
History: []v1.History{},
}
if imageOpts.created == "now" {
now := time.Now().UTC()
conf.Created = &now
} else if imageOpts.created != "" {
t, err := time.Parse(time.RFC3339, imageOpts.created)
if err != nil {
return fmt.Errorf("failed to parse created time %s: %w", imageOpts.created, err)
}
conf.Created = &t
}
labels := map[string]string{}
for _, l := range imageOpts.labels {
lSplit := strings.SplitN(l, "=", 2)
if len(lSplit) == 1 {
labels[lSplit[0]] = ""
} else {
labels[lSplit[0]] = lSplit[1]
}
}
if len(labels) > 0 {
conf.Config.Labels = labels
}
if imageOpts.platform != "" {
p, err := platform.Parse(imageOpts.platform)
if err != nil {
return fmt.Errorf("failed to parse platform: %w", err)
}
conf.Platform = p
}
// TODO: add layers
// push the config
cJSON, err := json.Marshal(conf)
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}
cd, err := rc.BlobPut(ctx, r, descriptor.Descriptor{}, bytes.NewReader(cJSON))
if err != nil {
return fmt.Errorf("failed to push config: %w", err)
}
// parse annotations
annotations := map[string]string{}
for _, a := range imageOpts.annotations {
aSplit := strings.SplitN(a, "=", 2)
if len(aSplit) == 1 {
annotations[aSplit[0]] = ""
} else {
annotations[aSplit[0]] = aSplit[1]
}
}
// build the manifest
mOpts := []manifest.Opts{}
switch imageOpts.mediaType {
case mediatype.OCI1Manifest:
cd.MediaType = mediatype.OCI1ImageConfig
m := v1.Manifest{
Versioned: v1.ManifestSchemaVersion,
MediaType: mediatype.OCI1Manifest,
Config: cd,
}
if len(annotations) > 0 {
m.Annotations = annotations
}
mOpts = append(mOpts, manifest.WithOrig(m))
case mediatype.Docker2Manifest:
cd.MediaType = mediatype.Docker2ImageConfig
m := schema2.Manifest{
Versioned: schema2.ManifestSchemaVersion,
Config: cd,
}
if len(annotations) > 0 {
m.Annotations = annotations
}
mOpts = append(mOpts, manifest.WithOrig(m))
}
mm, err := manifest.New(mOpts...)
if err != nil {
return err
}
// push the image
if imageOpts.byDigest {
r.Tag = ""
r.Digest = mm.GetDescriptor().Digest.String()
}
err = rc.ManifestPut(ctx, r, mm)
if err != nil {
return err
}
// format output
result := struct {
Manifest manifest.Manifest
}{
Manifest: mm,
}
if imageOpts.byDigest && imageOpts.formatCreate == "" {
imageOpts.formatCreate = "{{ printf \"%s\\n\" .Manifest.GetDescriptor.Digest }}"
}
return template.Writer(cmd.OutOrStdout(), imageOpts.formatCreate, result)
}
func (imageOpts *imageCmd) runImageExport(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
r, err := ref.New(args[0])

View File

@ -6,6 +6,19 @@ import (
"testing"
)
func TestImageCreate(t *testing.T) {
tmpDir := t.TempDir()
imageRef := fmt.Sprintf("ocidir://%s/repo:scratch", tmpDir)
out, err := cobraTest(t, nil, "image", "create", imageRef)
if err != nil {
t.Fatalf("failed to run image create: %v", err)
}
if out != "" {
t.Errorf("unexpected output: %v", out)
}
}
func TestImageExportImport(t *testing.T) {
tmpDir := t.TempDir()
srcRef := "ocidir://../../testdata/testrepo:v2"

View File

@ -138,6 +138,7 @@ Usage:
Available Commands:
check-base check if the base image has changed
copy copy or retag image
create create a new image manifest
delete delete image
digest show digest for pinning
export export image
@ -156,6 +157,8 @@ The OCI annotations used to automatically detect the base image are `org.opencon
The `copy` command allows images to be copied between registries, between repositories on the same registry, or retag an image within the same repository, and only pulls the layers when needed (typically not needed with the same registry server).
The `create` command creates a new image manifest and config, starting from scratch.
The `delete` command removes the image manifest from the server.
This will impact all tags pointing to the same manifest and requires a digest to be included in the image reference to be deleted (e.g. `myimage@sha256:abcd...`).
Using `--force-tag-dereference` will automatically lookup the digest for a specific tag, and will delete the underlying image which will delete any other tags pointing to the same image.