Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions cmd/docker-variant/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package main

import (
"fmt"

"github.com/spf13/cobra"

"github.com/achimnol/docker-variant/pkg/oci"
)

// registryFlags carries the flags shared by every registry-touching
// subcommand.
type registryFlags struct {
plainHTTP bool
}

func (f *registryFlags) add(cmd *cobra.Command) {
cmd.Flags().BoolVar(&f.plainHTTP, "plain-http", false, "use HTTP for the registry (loopback registries always do)")
}

func (f *registryFlags) client() (*oci.Client, error) {
return oci.NewClient(f.plainHTTP)
}

// versionRef parses a "<registry>/<repository>:<base-version>" argument.
func versionRef(arg string) (oci.Ref, error) {
ref, err := oci.ParseRef(arg)
if err != nil {
return oci.Ref{}, err
}
if ref.Tag == "" {
return oci.Ref{}, fmt.Errorf("reference %q must include the base version as its tag", arg)
}
return ref, nil
}
62 changes: 62 additions & 0 deletions cmd/docker-variant/indexcmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package main

import (
"fmt"
"maps"
"slices"

"github.com/spf13/cobra"

"github.com/achimnol/docker-variant/pkg/variant"
)

func newIndexCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "index",
Short: "Manage variant index artifacts",
}
cmd.AddCommand(newIndexUpdateCommand())
return cmd
}

func newIndexUpdateCommand() *cobra.Command {
var regFlags registryFlags
cmd := &cobra.Command{
Use: "update REGISTRY/REPOSITORY:VERSION",
Short: "Rebuild the variant index by scanning the repository's tags",
Long: "Scans the repository for <version>-<label> tags, reads each image's\n" +
"variant labels, and replaces the <version>-variants index artifact\n" +
"with the result. Use to reconcile after plain `docker push`es or\n" +
"conflicting index updates.",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
ref, err := versionRef(args[0])
if err != nil {
return err
}
client, err := regFlags.client()
if err != nil {
return err
}
ix, warnings, err := client.BuildIndex(ctx, ref, ref.Tag)
if err != nil {
return err
}
for _, warning := range warnings {
fmt.Fprintln(cmd.ErrOrStderr(), "Warning:", warning)
}
if len(ix.Variants) == 0 {
return fmt.Errorf("no variant tags found for %s version %s", ref.Name(), ref.Tag)
}
if _, err := client.PushIndex(ctx, ref, ix); err != nil {
return err
}
fmt.Fprintf(cmd.OutOrStdout(), "Rebuilt index %s with variants: %v\n",
ref.WithTag(variant.IndexTag(ref.Tag)), slices.Sorted(maps.Keys(ix.Variants)))
return nil
},
}
regFlags.add(cmd)
return cmd
}
38 changes: 38 additions & 0 deletions cmd/docker-variant/inspect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package main

import (
"fmt"

"github.com/spf13/cobra"
)

func newInspectCommand() *cobra.Command {
var regFlags registryFlags
cmd := &cobra.Command{
Use: "inspect REGISTRY/REPOSITORY:VERSION",
Short: "Print the raw variant index document",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ref, err := versionRef(args[0])
if err != nil {
return err
}
client, err := regFlags.client()
if err != nil {
return err
}
ix, err := client.FetchIndex(cmd.Context(), ref, ref.Tag)
if err != nil {
return err
}
data, err := ix.Marshal()
if err != nil {
return err
}
fmt.Fprintln(cmd.OutOrStdout(), string(data))
return nil
},
}
regFlags.add(cmd)
return cmd
}
78 changes: 78 additions & 0 deletions cmd/docker-variant/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package main

import (
"fmt"
"maps"
"slices"
"strings"
"text/tabwriter"

"github.com/spf13/cobra"

"github.com/achimnol/docker-variant/pkg/variant"
)

func newListCommand() *cobra.Command {
var (
regFlags registryFlags
propertiesFile string
)
cmd := &cobra.Command{
Use: "list REGISTRY/REPOSITORY:VERSION",
Short: "List the variants of a base version and their match ranking",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
ref, err := versionRef(args[0])
if err != nil {
return err
}
client, err := regFlags.client()
if err != nil {
return err
}
ix, err := client.FetchIndex(ctx, ref, ref.Tag)
if err != nil {
return err
}
sys, err := systemProperties(cmd, propertiesFile)
if err != nil {
return err
}
rank := map[string]int{}
for i, r := range variant.Rank(ix, sys) {
rank[r.Label] = i + 1
}
w := tabwriter.NewWriter(cmd.OutOrStdout(), 2, 8, 2, ' ', 0)
fmt.Fprintln(w, "LABEL\tTAG\tDIGEST\tMATCH\tPROPERTIES")
for _, label := range slices.Sorted(maps.Keys(ix.Variants)) {
e := ix.Variants[label]
match := "-"
if pos, ok := rank[label]; ok {
match = fmt.Sprintf("#%d", pos)
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n",
label, e.Tag, shortDigest(e.Digest), match, strings.Join(e.Properties.Triples(), "; "))
}
if err := w.Flush(); err != nil {
return err
}
if best := variant.Rank(ix, sys); len(best) > 0 {
fmt.Fprintf(cmd.OutOrStdout(), "\nBest match for this system: %s (%s)\n", best[0].Label, best[0].Entry.Tag)
} else {
fmt.Fprintf(cmd.OutOrStdout(), "\nNo compatible variant; `pull` would fall back to %s\n", args[0])
}
return nil
},
}
regFlags.add(cmd)
cmd.Flags().StringVar(&propertiesFile, "properties-file", "", "read system properties from a JSON file instead of detecting")
return cmd
}

func shortDigest(d string) string {
if rest, ok := strings.CutPrefix(d, "sha256:"); ok && len(rest) >= 12 {
return rest[:12]
}
return d
}
8 changes: 7 additions & 1 deletion cmd/docker-variant/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ func newVariantCommand() *cobra.Command {
"following the PEP 817 wheel-variant model mapped onto image labels,\n" +
"tags, and a per-version variant index artifact.",
}
cmd.AddCommand(newDetectCommand())
cmd.AddCommand(
newDetectCommand(),
newPushCommand(),
newListCommand(),
newInspectCommand(),
newIndexCommand(),
)
return cmd
}
82 changes: 82 additions & 0 deletions cmd/docker-variant/push.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package main

import (
"fmt"
"maps"
"slices"

"github.com/spf13/cobra"

"github.com/achimnol/docker-variant/pkg/docker"
"github.com/achimnol/docker-variant/pkg/variant"
)

func newPushCommand() *cobra.Command {
var (
regFlags registryFlags
skipImagePush bool
)
cmd := &cobra.Command{
Use: "push REGISTRY/REPOSITORY:VERSION-LABEL",
Short: "Push a variant image and update the repository's variant index",
Long: "Pushes the given variant tag with `docker push`, reads the variant\n" +
"label and properties from the local image's config labels, and\n" +
"upserts the corresponding entry in the <version>-variants index\n" +
"artifact of the repository.",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
ref, err := versionRef(args[0])
if err != nil {
return err
}
labels, err := docker.ImageLabels(ctx, args[0])
if err != nil {
return err
}
label, props, err := variant.ParseImageLabels(labels)
if err != nil {
return fmt.Errorf("%s: %w (variant images must carry %s labels)", args[0], err, variant.PropertyLabelPrefix+"*")
}
version, err := variant.BaseVersion(ref.Tag, label)
if err != nil {
return err
}
if !skipImagePush {
if err := docker.Push(ctx, args[0]); err != nil {
return err
}
}
client, err := regFlags.client()
if err != nil {
return err
}
repo, err := client.Repo(ref)
if err != nil {
return err
}
desc, err := repo.Resolve(ctx, ref.Tag)
if err != nil {
return fmt.Errorf("resolving pushed tag %s: %w", args[0], err)
}
ix, err := client.FetchIndex(ctx, ref, version)
if err != nil {
ix = variant.NewIndex(ref.Name(), version)
}
if err := ix.Upsert(label, variant.Entry{Properties: props, Tag: ref.Tag, Digest: desc.Digest.String()}); err != nil {
return err
}
if _, err := client.PushIndex(ctx, ref, ix); err != nil {
return err
}
fmt.Fprintf(cmd.OutOrStdout(), "Indexed variant %q (%s) for %s version %s\n",
label, desc.Digest, ref.Name(), version)
fmt.Fprintf(cmd.OutOrStdout(), "Index %s now lists: %v\n",
ref.WithTag(variant.IndexTag(version)), slices.Sorted(maps.Keys(ix.Variants)))
return nil
},
}
regFlags.add(cmd)
cmd.Flags().BoolVar(&skipImagePush, "skip-image-push", false, "only update the index, assuming the tag is already pushed")
return cmd
}
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ module github.com/achimnol/docker-variant
go 1.26.4

require (
github.com/opencontainers/image-spec v1.1.1
github.com/spf13/cobra v1.10.2
golang.org/x/sys v0.47.0
oras.land/oras-go/v2 v2.6.2
)

require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/spf13/pflag v1.0.9 // indirect
golang.org/x/sync v0.22.0 // indirect
)
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
oras.land/oras-go/v2 v2.6.2 h1:N04RXngAp1LJKTG6ifz3xHPipasEkWr+hFmInja5YKo=
oras.land/oras-go/v2 v2.6.2/go.mod h1:PlTtg4JTDJkDe8yVHpM2wz7/YDc00GVas+i4jAW2TZ4=
Loading
Loading