v2.31.0-dev12 6a38a35

attachments_push.go RawHistoryFolder

1package cli
2
3import (
4	"context"
5	"fmt"
6	"io"
7	"sort"
8	"strings"
9
10	"github.com/spf13/cobra"
11
12	"github.com/bnema/gordon/internal/adapters/in/cli/ui/styles"
13	"github.com/bnema/gordon/pkg/validation"
14)
15
16var (
17	resolveControlPlaneFn = resolveControlPlane
18	determineVersionFn    = determineVersion
19)
20
21// attachmentPushRequest holds all inputs for the attachments push command.
22type attachmentPushRequest struct {
23	ImageArg string
24	Tag      string
25	Build    buildConfig
26}
27
28func newAttachmentsPushCmd() *cobra.Command {
29	var (
30		build      bool
31		platform   string
32		tag        string
33		dockerfile string
34		buildArgs  []string
35	)
36
37	cmd := &cobra.Command{
38		Use:   "push <image>",
39		Short: "Build, tag, and push an attachment image",
40		Args:  cobra.ExactArgs(1),
41		RunE: func(cmd *cobra.Command, args []string) error {
42			return runAttachmentsPush(cmd.Context(), attachmentPushRequest{
43				ImageArg: args[0],
44				Tag:      tag,
45				Build:    buildConfig{Enabled: build, Platform: platform, Dockerfile: dockerfile, BuildArgs: buildArgs},
46			}, cmd.OutOrStdout())
47		},
48	}
49
50	cmd.Flags().BoolVar(&build, "build", false, "Build the image first using docker buildx")
51	cmd.Flags().StringVar(&platform, "platform", "linux/amd64", "Target platform (used with --build)")
52	cmd.Flags().StringVarP(&dockerfile, "file", "f", "", "Path to Dockerfile (default: ./Dockerfile, used with --build)")
53	cmd.Flags().StringVar(&tag, "tag", "", "Override version tag (default: CI tag ref or git describe)")
54	cmd.Flags().StringArrayVar(&buildArgs, "build-arg", nil, "Additional build args (used with --build)")
55
56	return cmd
57}
58
59func runAttachmentsPush(ctx context.Context, req attachmentPushRequest, out io.Writer) error {
60	handle, err := resolveControlPlaneFn(configPath)
61	if err != nil {
62		return err
63	}
64	defer handle.close()
65
66	dockerfile, err := resolveDockerfile(req.Build.Dockerfile, req.Build.Enabled)
67	if err != nil {
68		return err
69	}
70
71	for _, ba := range req.Build.BuildArgs {
72		if err := validateBuildArg(ba); err != nil {
73			return err
74		}
75	}
76
77	registry, imageName, targets, err := resolveAttachmentImage(ctx, handle.plane, req.ImageArg)
78	if err != nil {
79		return err
80	}
81
82	version, err := resolveVersionWithFn(ctx, req.Tag)
83	if err != nil {
84		return err
85	}
86
87	img := imagePush{
88		Registry:  registry,
89		ImageName: imageName,
90		Version:   version,
91	}
92	img.VersionRef, img.LatestRef = resolveImageRefs(registry, imageName, version)
93
94	imageOps, err := newImageOpsFn()
95	if err != nil {
96		return err
97	}
98
99	if err := printAttachmentPushInfo(out, req.ImageArg, targets, img.VersionRef, img.LatestRef, version); err != nil {
100		return err
101	}
102
103	build := buildConfig{
104		Enabled:    req.Build.Enabled,
105		Platform:   req.Build.Platform,
106		Dockerfile: dockerfile,
107		BuildArgs:  req.Build.BuildArgs,
108	}
109
110	if err := performAttachmentPush(ctx, imageOps, build, img); err != nil {
111		return err
112	}
113
114	return cliWriteLine(out, styles.RenderSuccess("Push complete"))
115}
116
117func printAttachmentPushInfo(out io.Writer, imageArg string, targets []string, versionRef, latestRef, version string) error {
118	if err := cliWritef(out, "Attachment image: %s\n", styles.Theme.Bold.Render(imageArg)); err != nil {
119		return err
120	}
121	if err := cliWritef(out, "Targets:          %s\n", styles.Theme.Bold.Render(formatAttachmentTargets(targets))); err != nil {
122		return err
123	}
124	if err := cliWritef(out, "Image:            %s\n", styles.Theme.Bold.Render(versionRef)); err != nil {
125		return err
126	}
127	if version != "latest" {
128		if err := cliWritef(out, "Also:             %s\n", styles.Theme.Bold.Render(latestRef)); err != nil {
129			return err
130		}
131	}
132	return nil
133}
134
135func performAttachmentPush(ctx context.Context, ops pushImageOps, build buildConfig, img imagePush) error {
136	if build.Enabled {
137		return buildAndPush(ctx, ops, build, img)
138	}
139	return tagAndPush(ctx, ops, img)
140}
141
142func resolveAttachmentImage(ctx context.Context, cp ControlPlane, imageArg string) (registry, imageName string, targets []string, err error) {
143	imageName = normalizeAttachmentImageName(imageArg)
144
145	targets, err = cp.FindAttachmentTargetsByImage(ctx, imageArg)
146	if err != nil {
147		return "", "", nil, fmt.Errorf("failed to find attachment targets for image %q: %w", imageArg, err)
148	}
149	if len(targets) == 0 {
150		return "", "", nil, fmt.Errorf("image %q is not configured as an attachment", imageArg)
151	}
152
153	status, err := cp.GetStatus(ctx)
154	if err != nil {
155		return "", "", nil, fmt.Errorf("failed to get status: %w", err)
156	}
157
158	return status.RegistryDomain, imageName, targets, nil
159}
160
161func normalizeAttachmentImageName(imageArg string) string {
162	name := imageArg
163	if idx := strings.Index(name, "@"); idx != -1 {
164		name = name[:idx]
165	}
166	if idx := strings.LastIndex(name, ":"); idx != -1 {
167		slashIdx := strings.LastIndex(name, "/")
168		if idx > slashIdx {
169			name = name[:idx]
170		}
171	}
172	parts := strings.SplitN(name, "/", 2)
173	if len(parts) == 2 {
174		host := parts[0]
175		if strings.ContainsAny(host, ".:") || host == "localhost" {
176			return parts[1]
177		}
178	}
179	return name
180}
181
182func formatAttachmentTargets(targets []string) string {
183	cloned := append([]string(nil), targets...)
184	sort.Strings(cloned)
185	return fmt.Sprintf("%v", cloned)
186}
187
188func resolveVersionWithFn(ctx context.Context, tag string) (string, error) {
189	version := determineVersionFn(ctx, tag)
190	if version != "latest" {
191		if err := validateVersionTag(version); err != nil {
192			return "", err
193		}
194	}
195	return version, nil
196}
197
198func validateVersionTag(version string) error {
199	if err := validation.ValidateReference(version); err != nil {
200		return fmt.Errorf("invalid version tag %q: %w", version, err)
201	}
202	return nil
203}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close