Skip to content

Commit 42e8bc7

Browse files
committed
Add image tag command for existing local images
1 parent e8aedc0 commit 42e8bc7

2 files changed

Lines changed: 86 additions & 0 deletions

File tree

pkg/cmd/imagecmd.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ var imageCmd = cli.Command{
2020
&imageCreateCmd,
2121
&imageListCmd,
2222
&imageGetCmd,
23+
&imageTagCmd,
2324
&imageDeleteCmd,
2425
},
2526
HideHelpCommand: true,
@@ -60,6 +61,14 @@ var imageGetCmd = cli.Command{
6061
HideHelpCommand: true,
6162
}
6263

64+
var imageTagCmd = cli.Command{
65+
Name: "tag",
66+
Usage: "Tag an existing local image without pulling or converting",
67+
ArgsUsage: "<source> <target>",
68+
Action: handleImageTag,
69+
HideHelpCommand: true,
70+
}
71+
6372
var imageDeleteCmd = cli.Command{
6473
Name: "delete",
6574
Aliases: []string{"rm"},
@@ -255,6 +264,49 @@ func handleImageGet(ctx context.Context, cmd *cli.Command) error {
255264
return ShowJSON(os.Stdout, "image get", obj, format, transform)
256265
}
257266

267+
// imageTagBody is the POST /images/{name}/tag request body, defined locally
268+
// until the generated SDK gains a typed method for the endpoint.
269+
type imageTagBody struct {
270+
Name string `json:"name"`
271+
}
272+
273+
func handleImageTag(ctx context.Context, cmd *cli.Command) error {
274+
args := cmd.Args().Slice()
275+
if len(args) < 2 {
276+
return fmt.Errorf("source and target required\nUsage: hypeman image tag <source> <target>")
277+
}
278+
source, target := args[0], args[1]
279+
280+
client := hypeman.NewClient(getDefaultRequestOptions(cmd)...)
281+
282+
var opts []option.RequestOption
283+
if cmd.Root().Bool("debug") {
284+
opts = append(opts, debugMiddlewareOption)
285+
}
286+
287+
path := fmt.Sprintf("images/%s/tag", url.PathEscape(source))
288+
body := imageTagBody{Name: target}
289+
290+
format := cmd.Root().String("format")
291+
transform := cmd.Root().String("transform")
292+
293+
if format != "auto" {
294+
var res []byte
295+
opts = append(opts, option.WithResponseBodyInto(&res))
296+
if err := client.Post(ctx, path, body, nil, opts...); err != nil {
297+
return err
298+
}
299+
return ShowJSON(os.Stdout, "image tag", gjson.ParseBytes(res), format, transform)
300+
}
301+
302+
var img hypeman.Image
303+
if err := client.Post(ctx, path, body, &img, opts...); err != nil {
304+
return err
305+
}
306+
fmt.Println(img.Name)
307+
return nil
308+
}
309+
258310
func handleImageDelete(ctx context.Context, cmd *cli.Command) error {
259311
args := cmd.Args().Slice()
260312
if len(args) < 1 {

pkg/cmd/imagecmd_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
package cmd
22

33
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
"net/http/httptest"
49
"testing"
510

611
"github.com/stretchr/testify/assert"
@@ -37,3 +42,32 @@ func TestPlatformOrDash(t *testing.T) {
3742
assert.Equal(t, "linux/amd64", platformOrDash("linux/amd64"))
3843
assert.Equal(t, "-", platformOrDash(""))
3944
}
45+
46+
func TestImageTagCommand_RequiresSourceAndTarget(t *testing.T) {
47+
err := Command.Run(context.Background(), []string{"hypeman", "image", "tag", "only-source"})
48+
require.EqualError(t, err, "source and target required\nUsage: hypeman image tag <source> <target>")
49+
}
50+
51+
func TestImageTagCommand_PostsToTagEndpoint(t *testing.T) {
52+
var gotMethod, gotPath string
53+
var gotBody []byte
54+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
55+
gotMethod = r.Method
56+
gotPath = r.URL.EscapedPath()
57+
gotBody, _ = io.ReadAll(r.Body)
58+
w.Header().Set("Content-Type", "application/json")
59+
fmt.Fprint(w, `{"name":"registry.example.com/app:v1","digest":"sha256:abc","status":"ready","created_at":"2026-01-01T00:00:00Z"}`)
60+
}))
61+
defer srv.Close()
62+
63+
err := Command.Run(context.Background(), []string{
64+
"hypeman", "--base-url", srv.URL,
65+
"image", "tag",
66+
"docker.io/library/alpine:latest", "registry.example.com/app:v1",
67+
})
68+
require.NoError(t, err)
69+
70+
assert.Equal(t, http.MethodPost, gotMethod)
71+
assert.Equal(t, "/images/docker.io%2Flibrary%2Falpine:latest/tag", gotPath)
72+
assert.JSONEq(t, `{"name":"registry.example.com/app:v1"}`, string(gotBody))
73+
}

0 commit comments

Comments
 (0)