v2.31.0-dev23 58a92f3

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	dockerfile, err := resolveDockerfile(req.Build.Dockerfile, req.Build.Enabled)
61	if err != nil {
62		return err
63	}
64
65	inferredRemote, err := inferRemoteForAttachmentImage(ctx, req.ImageArg)
66	if err != nil {
67		return err
68	}
69
70	var handle *controlPlaneHandle
71	if inferredRemote != nil {
72		handle = newRemoteControlPlaneHandle(inferredRemote)
73		if err := cliWritef(out, "Remote:           %s %s\n", styles.Theme.Bold.Render(inferredRemote.DisplayName()), styles.Theme.Muted.Render("(auto-detected)")); err != nil {
74			return err
75		}
76	} else {
77		handle, err = resolveControlPlaneFn(configPath)
78		if err != nil {
79			return err
80		}
81	}
82	defer handle.close()
83
84	for _, ba := range req.Build.BuildArgs {
85		if err := validateBuildArg(ba); err != nil {
86			return err
87		}
88	}
89
90	registry, imageName, targets, err := resolveAttachmentImage(ctx, handle.plane, req.ImageArg)
91	if err != nil {
92		return err
93	}
94
95	version, err := resolveVersionWithFn(ctx, req.Tag)
96	if err != nil {
97		return err
98	}
99
100	img := imagePush{
101		Registry:  registry,
102		ImageName: imageName,
103		Version:   version,
104	}
105	img.VersionRef, img.LatestRef = resolveImageRefs(registry, imageName, version)
106
107	var imageOps pushImageOps
108	if inferredRemote != nil {
109		imageOps, err = newImageOpsForResolvedRemote(inferredRemote)
110	} else {
111		imageOps, err = newImageOpsFn()
112	}
113	if err != nil {
114		return err
115	}
116
117	if err := printAttachmentPushInfo(out, req.ImageArg, targets, img.VersionRef, img.LatestRef, version); err != nil {
118		return err
119	}
120
121	build := buildConfig{
122		Enabled:    req.Build.Enabled,
123		Platform:   req.Build.Platform,
124		Dockerfile: dockerfile,
125		BuildArgs:  req.Build.BuildArgs,
126	}
127
128	if err := performAttachmentPush(ctx, imageOps, build, img); err != nil {
129		return err
130	}
131
132	return cliWriteLine(out, styles.RenderSuccess("Push complete"))
133}
134
135func printAttachmentPushInfo(out io.Writer, imageArg string, targets []string, versionRef, latestRef, version string) error {
136	if err := cliWritef(out, "Attachment image: %s\n", styles.Theme.Bold.Render(imageArg)); err != nil {
137		return err
138	}
139	if err := cliWritef(out, "Targets:          %s\n", styles.Theme.Bold.Render(formatAttachmentTargets(targets))); err != nil {
140		return err
141	}
142	if err := cliWritef(out, "Image:            %s\n", styles.Theme.Bold.Render(versionRef)); err != nil {
143		return err
144	}
145	if version != "latest" {
146		if err := cliWritef(out, "Also:             %s\n", styles.Theme.Bold.Render(latestRef)); err != nil {
147			return err
148		}
149	}
150	return nil
151}
152
153func performAttachmentPush(ctx context.Context, ops pushImageOps, build buildConfig, img imagePush) error {
154	if build.Enabled {
155		return buildAndPush(ctx, ops, build, img)
156	}
157	return tagAndPush(ctx, ops, img)
158}
159
160func resolveAttachmentImage(ctx context.Context, cp ControlPlane, imageArg string) (registry, imageName string, targets []string, err error) {
161	imageName = normalizeAttachmentImageName(imageArg)
162
163	targets, err = cp.FindAttachmentTargetsByImage(ctx, imageArg)
164	if err != nil {
165		return "", "", nil, fmt.Errorf("failed to find attachment targets for image %q: %w", imageArg, err)
166	}
167	if len(targets) == 0 {
168		return "", "", nil, fmt.Errorf("image %q is not configured as an attachment", imageArg)
169	}
170
171	status, err := cp.GetStatus(ctx)
172	if err != nil {
173		return "", "", nil, fmt.Errorf("failed to get status: %w", err)
174	}
175
176	return status.RegistryDomain, imageName, targets, nil
177}
178
179func normalizeAttachmentImageName(imageArg string) string {
180	name := imageArg
181	if idx := strings.Index(name, "@"); idx != -1 {
182		name = name[:idx]
183	}
184	if idx := strings.LastIndex(name, ":"); idx != -1 {
185		slashIdx := strings.LastIndex(name, "/")
186		if idx > slashIdx {
187			name = name[:idx]
188		}
189	}
190	parts := strings.SplitN(name, "/", 2)
191	if len(parts) == 2 {
192		host := parts[0]
193		if strings.ContainsAny(host, ".:") || host == "localhost" {
194			return parts[1]
195		}
196	}
197	return name
198}
199
200func formatAttachmentTargets(targets []string) string {
201	cloned := append([]string(nil), targets...)
202	sort.Strings(cloned)
203	return fmt.Sprintf("%v", cloned)
204}
205
206func resolveVersionWithFn(ctx context.Context, tag string) (string, error) {
207	version := determineVersionFn(ctx, tag)
208	if version != "latest" {
209		if err := validateVersionTag(version); err != nil {
210			return "", err
211		}
212	}
213	return version, nil
214}
215
216func validateVersionTag(version string) error {
217	if err := validation.ValidateReference(version); err != nil {
218		return fmt.Errorf("invalid version tag %q: %w", version, err)
219	}
220	return nil
221}

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