Skip to content

Commit b9c68da

Browse files
committed
Add gen command to docker command
Generates initial arkade.yaml with list of images to upgrade scanned from a Dockerfile Signed-off-by: Alex Ellis (OpenFaaS Ltd) <alexellis2@gmail.com>
1 parent 4621387 commit b9c68da

File tree

3 files changed

+331
-0
lines changed

3 files changed

+331
-0
lines changed

cmd/docker/docker.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ func MakeDocker() *cobra.Command {
2020
}
2121

2222
command.AddCommand(MakeUpgrade())
23+
command.AddCommand(MakeGen())
2324

2425
return command
2526
}

cmd/docker/gen.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package docker
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path"
7+
"strings"
8+
9+
"github.com/alexellis/arkade/pkg/dockerfile"
10+
"github.com/spf13/cobra"
11+
)
12+
13+
func MakeGen() *cobra.Command {
14+
var command = &cobra.Command{
15+
Use: "gen",
16+
Short: "Generate an arkade.yaml from detected images in a Dockerfile",
17+
Long: `Generate an arkade.yaml from detected images in a Dockerfile.
18+
19+
This command scans a Dockerfile and generates an arkade.yaml file
20+
with the detected images. You can then edit this file and use it
21+
with 'arkade docker upgrade' to automatically upgrade images.
22+
23+
Only images with explicit tags are detected. Images using variable
24+
substitution (e.g., ${VERSION}) are skipped.
25+
`,
26+
Example: ` # Generate arkade.yaml from current directory's Dockerfile
27+
arkade docker gen
28+
29+
# Generate from a specific Dockerfile
30+
arkade docker gen -f ./Dockerfile.prod
31+
32+
# Output to stdout only (don't create file)
33+
arkade docker gen --stdout
34+
arkade docker gen -f Dockerfile | tee arkade.yaml
35+
`,
36+
SilenceUsage: true,
37+
}
38+
39+
command.Flags().StringP("file", "f", "Dockerfile", "Path to Dockerfile")
40+
command.Flags().BoolP("stdout", "s", false, "Output to stdout instead of creating file")
41+
42+
command.RunE = func(cmd *cobra.Command, args []string) error {
43+
file, _ := cmd.Flags().GetString("file")
44+
toStdout, _ := cmd.Flags().GetBool("stdout")
45+
46+
content, err := os.ReadFile(file)
47+
if err != nil {
48+
return fmt.Errorf("failed to read %s: %w", file, err)
49+
}
50+
51+
found := dockerfile.FindImages(string(content))
52+
if len(found) == 0 {
53+
return fmt.Errorf("no images found in %s", file)
54+
}
55+
56+
// Sort images for consistent output
57+
images := make([]string, len(found))
58+
for i, img := range found {
59+
images[i] = img.Image
60+
}
61+
62+
var yamlBuilder strings.Builder
63+
yamlBuilder.WriteString("images:\n")
64+
for _, img := range images {
65+
yamlBuilder.WriteString(fmt.Sprintf("- %s\n", img))
66+
}
67+
68+
output := yamlBuilder.String()
69+
70+
if toStdout {
71+
fmt.Fprint(cmd.OutOrStdout(), output)
72+
return nil
73+
}
74+
75+
basePath := path.Dir(file)
76+
outputPath := path.Join(basePath, "arkade.yaml")
77+
78+
if err := os.WriteFile(outputPath, []byte(output), 0644); err != nil {
79+
return fmt.Errorf("failed to write %s: %w", outputPath, err)
80+
}
81+
82+
fmt.Fprintf(cmd.OutOrStdout(), "Generated %s with %d images:\n", outputPath, len(images))
83+
for _, img := range images {
84+
fmt.Fprintf(cmd.OutOrStdout(), " - %s\n", img)
85+
}
86+
87+
return nil
88+
}
89+
90+
return command
91+
}

cmd/docker/gen_test.go

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
package docker
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"strings"
7+
"testing"
8+
)
9+
10+
func TestGenCommand_StdoutMode(t *testing.T) {
11+
tmpDir, err := os.MkdirTemp("", "arkade-docker-gen-test-*")
12+
if err != nil {
13+
t.Fatalf("failed to create temp dir: %v", err)
14+
}
15+
defer os.RemoveAll(tmpDir)
16+
17+
dockerfilePath := filepath.Join(tmpDir, "Dockerfile")
18+
dockerfileContent := `FROM golang:1.24 AS builder
19+
FROM alpine:3.19
20+
FROM ghcr.io/openfaas/of-watchdog:0.25.0`
21+
22+
if err := os.WriteFile(dockerfilePath, []byte(dockerfileContent), 0644); err != nil {
23+
t.Fatalf("failed to write dockerfile: %v", err)
24+
}
25+
26+
cmd := MakeGen()
27+
cmd.SetArgs([]string{"-f", dockerfilePath, "--stdout"})
28+
29+
var stdout, stderr strings.Builder
30+
cmd.SetOut(&stdout)
31+
cmd.SetErr(&stderr)
32+
33+
if err := cmd.Execute(); err != nil {
34+
t.Fatalf("command failed: %v", err)
35+
}
36+
37+
result := stdout.String()
38+
39+
// Check that images are present in output
40+
expectedImages := []string{"golang", "alpine", "ghcr.io/openfaas/of-watchdog"}
41+
for _, img := range expectedImages {
42+
if !strings.Contains(result, "- "+img) {
43+
t.Errorf("stdout missing image: %s, stdout=%q, stderr=%q", img, result, stderr.String())
44+
}
45+
}
46+
47+
// Check that it has the images: header
48+
if !strings.HasPrefix(result, "images:\n") {
49+
t.Errorf("expected stdout to start with 'images:\n', got: %q", result)
50+
}
51+
}
52+
53+
func TestGenCommand_WriteToFile(t *testing.T) {
54+
tmpDir, err := os.MkdirTemp("", "arkade-docker-gen-test-*")
55+
if err != nil {
56+
t.Fatalf("failed to create temp dir: %v", err)
57+
}
58+
defer os.RemoveAll(tmpDir)
59+
60+
dockerfilePath := filepath.Join(tmpDir, "Dockerfile")
61+
dockerfileContent := `FROM golang:1.24 AS builder
62+
FROM alpine:3.19`
63+
64+
if err := os.WriteFile(dockerfilePath, []byte(dockerfileContent), 0644); err != nil {
65+
t.Fatalf("failed to write dockerfile: %v", err)
66+
}
67+
68+
cmd := MakeGen()
69+
cmd.SetArgs([]string{"-f", dockerfilePath})
70+
71+
var stdout, stderr strings.Builder
72+
cmd.SetOut(&stdout)
73+
cmd.SetErr(&stderr)
74+
75+
if err := cmd.Execute(); err != nil {
76+
t.Fatalf("command failed: %v", err)
77+
}
78+
79+
// Check output contains success message (goes to stderr)
80+
output := stdout.String() + stderr.String()
81+
if !strings.Contains(output, "Generated") {
82+
t.Errorf("expected success message, stdout=%q, stderr=%q", stdout.String(), stderr.String())
83+
}
84+
85+
// Check that arkade.yaml was created
86+
arkadeYamlPath := filepath.Join(tmpDir, "arkade.yaml")
87+
content, err := os.ReadFile(arkadeYamlPath)
88+
if err != nil {
89+
t.Fatalf("failed to read arkade.yaml: %v", err)
90+
}
91+
92+
contentStr := string(content)
93+
if !strings.Contains(contentStr, "images:") {
94+
t.Errorf("arkade.yaml missing 'images:' header")
95+
}
96+
if !strings.Contains(contentStr, "- golang") {
97+
t.Errorf("arkade.yaml missing golang image")
98+
}
99+
if !strings.Contains(contentStr, "- alpine") {
100+
t.Errorf("arkade.yaml missing alpine image")
101+
}
102+
}
103+
104+
func TestGenCommand_NoImagesFound(t *testing.T) {
105+
tmpDir, err := os.MkdirTemp("", "arkade-docker-gen-test-*")
106+
if err != nil {
107+
t.Fatalf("failed to create temp dir: %v", err)
108+
}
109+
defer os.RemoveAll(tmpDir)
110+
111+
dockerfilePath := filepath.Join(tmpDir, "Dockerfile")
112+
dockerfileContent := `# Just a comment
113+
RUN echo "hello"`
114+
115+
if err := os.WriteFile(dockerfilePath, []byte(dockerfileContent), 0644); err != nil {
116+
t.Fatalf("failed to write dockerfile: %v", err)
117+
}
118+
119+
cmd := MakeGen()
120+
cmd.SetArgs([]string{"-f", dockerfilePath})
121+
122+
var output strings.Builder
123+
cmd.SetOut(&output)
124+
cmd.SetErr(&output)
125+
126+
if err := cmd.Execute(); err == nil {
127+
t.Fatal("expected error for no images found, got nil")
128+
}
129+
130+
if !strings.Contains(output.String(), "no images found") {
131+
t.Errorf("expected error about no images, got: %s", output.String())
132+
}
133+
}
134+
135+
func TestGenCommand_VariableImagesSkipped(t *testing.T) {
136+
tmpDir, err := os.MkdirTemp("", "arkade-docker-gen-test-*")
137+
if err != nil {
138+
t.Fatalf("failed to create temp dir: %v", err)
139+
}
140+
defer os.RemoveAll(tmpDir)
141+
142+
dockerfilePath := filepath.Join(tmpDir, "Dockerfile")
143+
dockerfileContent := `ARG VERSION=1.0
144+
FROM golang:${VERSION}
145+
FROM alpine:3.19`
146+
147+
if err := os.WriteFile(dockerfilePath, []byte(dockerfileContent), 0644); err != nil {
148+
t.Fatalf("failed to write dockerfile: %v", err)
149+
}
150+
151+
cmd := MakeGen()
152+
cmd.SetArgs([]string{"-f", dockerfilePath, "--stdout"})
153+
154+
var stdout, stderr strings.Builder
155+
cmd.SetOut(&stdout)
156+
cmd.SetErr(&stderr)
157+
158+
if err := cmd.Execute(); err != nil {
159+
t.Fatalf("command failed: %v", err)
160+
}
161+
162+
result := stdout.String()
163+
164+
// golang:${VERSION} should be skipped, only alpine should be present
165+
if strings.Contains(result, "golang") {
166+
t.Errorf("expected golang to be skipped (has variable tag), got: %s", result)
167+
}
168+
if !strings.Contains(result, "- alpine") {
169+
t.Errorf("expected alpine to be present, got: %s, stderr=%s", result, stderr.String())
170+
}
171+
}
172+
173+
func TestGenCommand_RegistryWithPort(t *testing.T) {
174+
tmpDir, err := os.MkdirTemp("", "arkade-docker-gen-test-*")
175+
if err != nil {
176+
t.Fatalf("failed to create temp dir: %v", err)
177+
}
178+
defer os.RemoveAll(tmpDir)
179+
180+
dockerfilePath := filepath.Join(tmpDir, "Dockerfile")
181+
dockerfileContent := `FROM myregistry.com:5000/myimage:1.2.3`
182+
183+
if err := os.WriteFile(dockerfilePath, []byte(dockerfileContent), 0644); err != nil {
184+
t.Fatalf("failed to write dockerfile: %v", err)
185+
}
186+
187+
cmd := MakeGen()
188+
cmd.SetArgs([]string{"-f", dockerfilePath, "--stdout"})
189+
190+
var stdout, stderr strings.Builder
191+
cmd.SetOut(&stdout)
192+
cmd.SetErr(&stderr)
193+
194+
if err := cmd.Execute(); err != nil {
195+
t.Fatalf("command failed: %v", err)
196+
}
197+
198+
result := stdout.String()
199+
200+
if !strings.Contains(result, "- myregistry.com:5000/myimage") {
201+
t.Errorf("expected registry:port/image format, got: %s, stderr=%s", result, stderr.String())
202+
}
203+
}
204+
205+
func TestGenCommand_DeduplicatesImages(t *testing.T) {
206+
tmpDir, err := os.MkdirTemp("", "arkade-docker-gen-test-*")
207+
if err != nil {
208+
t.Fatalf("failed to create temp dir: %v", err)
209+
}
210+
defer os.RemoveAll(tmpDir)
211+
212+
dockerfilePath := filepath.Join(tmpDir, "Dockerfile")
213+
dockerfileContent := `FROM alpine:3.19 AS builder
214+
FROM alpine:3.19 AS runtime
215+
FROM golang:1.24`
216+
217+
if err := os.WriteFile(dockerfilePath, []byte(dockerfileContent), 0644); err != nil {
218+
t.Fatalf("failed to write dockerfile: %v", err)
219+
}
220+
221+
cmd := MakeGen()
222+
cmd.SetArgs([]string{"-f", dockerfilePath, "--stdout"})
223+
224+
var stdout, stderr strings.Builder
225+
cmd.SetOut(&stdout)
226+
cmd.SetErr(&stderr)
227+
228+
if err := cmd.Execute(); err != nil {
229+
t.Fatalf("command failed: %v", err)
230+
}
231+
232+
result := stdout.String()
233+
234+
// Count occurrences of alpine
235+
count := strings.Count(result, "- alpine")
236+
if count != 1 {
237+
t.Errorf("expected alpine to appear once (deduplicated), got %d times, stdout=%q, stderr=%q", count, result, stderr.String())
238+
}
239+
}

0 commit comments

Comments
 (0)