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
98 changes: 96 additions & 2 deletions cmd/tag.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
package cmd

import (
"encoding/json"
"errors"
"fmt"
"os"

"github.com/meigma/blob"
"github.com/spf13/cobra"
"github.com/spf13/viper"

internalcfg "github.com/meigma/blob-cli/internal/config"
)

var tagCmd = &cobra.Command{
Expand All @@ -15,7 +24,92 @@ new reference to the existing content.`,
Example: ` blob tag ghcr.io/acme/configs:v1.0.0 ghcr.io/acme/configs:latest
blob tag ghcr.io/acme/configs@sha256:abc... ghcr.io/acme/configs:stable`,
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
RunE: runTag,
}

// tagResult contains the result of a tag operation.
type tagResult struct {
SrcRef string `json:"src_ref"`
ResolvedSrcRef string `json:"resolved_src_ref,omitempty"`
DstRef string `json:"dst_ref"`
ResolvedDstRef string `json:"resolved_dst_ref,omitempty"`
Digest string `json:"digest"`
Status string `json:"status"`
}

func runTag(cmd *cobra.Command, args []string) error {
cfg := internalcfg.FromContext(cmd.Context())
if cfg == nil {
return errors.New("configuration not loaded")
}

srcRef := args[0]
dstRef := args[1]

resolvedSrcRef := cfg.ResolveAlias(srcRef)
resolvedDstRef := cfg.ResolveAlias(dstRef)

client, err := blob.NewClient(blob.WithDockerConfig())
if err != nil {
return fmt.Errorf("creating client: %w", err)
}

ctx := cmd.Context()

manifest, err := client.Fetch(ctx, resolvedSrcRef)
if err != nil {
return fmt.Errorf("fetching source manifest: %w", err)
}

digest := manifest.Digest()

if err := client.Tag(ctx, resolvedDstRef, digest); err != nil {
return fmt.Errorf("tagging manifest: %w", err)
}

result := tagResult{
SrcRef: srcRef,
DstRef: dstRef,
Digest: digest,
Status: "success",
}

if srcRef != resolvedSrcRef {
result.ResolvedSrcRef = resolvedSrcRef
}
if dstRef != resolvedDstRef {
result.ResolvedDstRef = resolvedDstRef
}

return outputTagResult(cfg, &result)
}

// outputTagResult formats and outputs the tag result.
func outputTagResult(cfg *internalcfg.Config, result *tagResult) error {
if cfg.Quiet {
return nil
},
}
if viper.GetString("output") == internalcfg.OutputJSON {
return tagJSON(result)
}
return tagText(result)
}

func tagJSON(result *tagResult) error {
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
return enc.Encode(result)
}

func tagText(result *tagResult) error {
fmt.Printf("Tagged %s\n", result.DstRef)
if result.ResolvedDstRef != "" {
fmt.Printf(" Resolved: %s\n", result.ResolvedDstRef)
}
fmt.Printf("Source: %s\n", result.SrcRef)
if result.ResolvedSrcRef != "" {
fmt.Printf(" Resolved: %s\n", result.ResolvedSrcRef)
}
fmt.Printf("Digest: %s\n", result.Digest)
return nil
}
148 changes: 148 additions & 0 deletions cmd/tag_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package cmd

import (
"bytes"
"context"
"encoding/json"
"os"
"testing"

"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestTagCmd_NilConfig(t *testing.T) {
// Reset viper and restore after test to avoid affecting other tests.
viper.Reset()
t.Cleanup(viper.Reset)

ctx := context.Background()

tagCmd.SetContext(ctx)
err := tagCmd.RunE(tagCmd, []string{"ghcr.io/test:v1", "ghcr.io/test:latest"})

require.Error(t, err)
assert.Contains(t, err.Error(), "configuration not loaded")
}

func TestTagText_Basic(t *testing.T) {
result := &tagResult{
SrcRef: "ghcr.io/acme/configs:v1.0.0",
DstRef: "ghcr.io/acme/configs:latest",
Digest: "sha256:abc123def456",
Status: "success",
}

oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w

err := tagText(result)

w.Close()
os.Stdout = oldStdout

var buf bytes.Buffer
buf.ReadFrom(r)

require.NoError(t, err)
got := buf.String()
assert.Contains(t, got, "Tagged ghcr.io/acme/configs:latest")
assert.Contains(t, got, "Source: ghcr.io/acme/configs:v1.0.0")
assert.Contains(t, got, "Digest: sha256:abc123def456")
assert.NotContains(t, got, "Resolved:")
}

func TestTagText_WithResolvedRefs(t *testing.T) {
result := &tagResult{
SrcRef: "src-alias",
ResolvedSrcRef: "ghcr.io/acme/configs:v1.0.0",
DstRef: "dst-alias",
ResolvedDstRef: "ghcr.io/acme/configs:latest",
Digest: "sha256:abc123",
Status: "success",
}

oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w

err := tagText(result)

w.Close()
os.Stdout = oldStdout

var buf bytes.Buffer
buf.ReadFrom(r)

require.NoError(t, err)
got := buf.String()
assert.Contains(t, got, "Tagged dst-alias")
assert.Contains(t, got, "Resolved: ghcr.io/acme/configs:latest")
assert.Contains(t, got, "Source: src-alias")
assert.Contains(t, got, "Resolved: ghcr.io/acme/configs:v1.0.0")
}

func TestTagJSON(t *testing.T) {
result := &tagResult{
SrcRef: "src-alias",
ResolvedSrcRef: "ghcr.io/acme/configs:v1.0.0",
DstRef: "dst-alias",
ResolvedDstRef: "ghcr.io/acme/configs:latest",
Digest: "sha256:abc123def456",
Status: "success",
}

oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w

err := tagJSON(result)

w.Close()
os.Stdout = oldStdout

var buf bytes.Buffer
buf.ReadFrom(r)

require.NoError(t, err)

var got tagResult
err = json.Unmarshal(buf.Bytes(), &got)
require.NoError(t, err)

assert.Equal(t, "src-alias", got.SrcRef)
assert.Equal(t, "ghcr.io/acme/configs:v1.0.0", got.ResolvedSrcRef)
assert.Equal(t, "dst-alias", got.DstRef)
assert.Equal(t, "ghcr.io/acme/configs:latest", got.ResolvedDstRef)
assert.Equal(t, "sha256:abc123def456", got.Digest)
assert.Equal(t, "success", got.Status)
}

func TestTagJSON_OmitsEmpty(t *testing.T) {
result := &tagResult{
SrcRef: "ghcr.io/acme/configs:v1.0.0",
DstRef: "ghcr.io/acme/configs:latest",
Digest: "sha256:abc123",
Status: "success",
}

oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w

err := tagJSON(result)

w.Close()
os.Stdout = oldStdout

var buf bytes.Buffer
buf.ReadFrom(r)

require.NoError(t, err)

jsonStr := buf.String()
assert.NotContains(t, jsonStr, "resolved_src_ref")
assert.NotContains(t, jsonStr, "resolved_dst_ref")
}