Skip to content

Commit 1b65174

Browse files
committed
mkctr: add --env flag, fix volume support
This adds support for setting environment variables and fixes two problems with volume support (merged in #27): * if no volumes were specified, it added a bogus "" (empty string) volume which made docker fail to run the container * if no args were given, volumes were omitted This also pulls out some common code into a new withPlatformPrefix helper, and removes some log spam by silently skipping over "unknown" OS layers in the source image when discovering what OSes to build for. Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
1 parent e1b53cd commit 1b65174

1 file changed

Lines changed: 117 additions & 9 deletions

File tree

mkctr.go

Lines changed: 117 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"io"
1616
"io/fs"
1717
"log"
18+
"maps"
1819
"os"
1920
"os/exec"
2021
"path/filepath"
@@ -42,6 +43,14 @@ func withPrefix(f logf, prefix string) logf {
4243
}
4344
}
4445

46+
func withPlatformPrefix(f logf, p v1.Platform) logf {
47+
var variantSlash string
48+
if v := p.Variant; v != "" {
49+
variantSlash = "/" + v
50+
}
51+
return withPrefix(f, fmt.Sprintf("%v/%v%s: ", p.OS, p.Architecture, variantSlash))
52+
}
53+
4554
// parseFiles parses a comma-separated list of colon-separated pairs
4655
// into a map of filePathOnDisk -> filePathInContainer.
4756
func parseFiles(s string) (map[string]string, error) {
@@ -87,6 +96,7 @@ type buildParams struct {
8796
verbose bool
8897
annotations map[string]string // OCI image annotations
8998
volumes map[string]struct{}
99+
envVars []string // Environment variables to add to image config
90100
}
91101

92102
func main() {
@@ -107,6 +117,7 @@ func main() {
107117
Annotations must be comma separated key=value pairs, i.e key1=val1,key2=val2. For a single image manifest annotations will get added to the image manifest.
108118
For an image index (a multi-platform manifest list) annotations will get added to each image manifest as well as the image index.
109119
Annotations with empty values are not supported.`)
120+
envArg = flag.String("env", "", "comma-separated list of environment variables in KEY=value form to add to the image config")
110121
)
111122
flag.Parse()
112123
if *tagArg == "" {
@@ -139,11 +150,13 @@ func main() {
139150
log.Fatal("at least one of --files or --gopaths must be set")
140151
}
141152
var vols map[string]struct{}
142-
for vol := range strings.SplitSeq(*volumes, ",") {
143-
if vols == nil {
144-
vols = make(map[string]struct{})
153+
if *volumes != "" {
154+
for vol := range strings.SplitSeq(*volumes, ",") {
155+
if vols == nil {
156+
vols = make(map[string]struct{})
157+
}
158+
vols[strings.TrimSpace(vol)] = struct{}{}
145159
}
146-
vols[strings.TrimSpace(vol)] = struct{}{}
147160
}
148161

149162
bp := &buildParams{
@@ -159,6 +172,7 @@ func main() {
159172
goarch: strings.Split(*goarch, ","),
160173
annotations: parseAnnotations(*annotations),
161174
volumes: vols,
175+
envVars: parseEnv(*envArg),
162176
}
163177

164178
if err := fetchAndBuild(bp); err != nil {
@@ -250,11 +264,15 @@ func fetchAndBuild(bp *buildParams) error {
250264
if err := bp.verifyPlatform(p); err != nil {
251265
return err
252266
}
253-
logf := withPrefix(logf, fmt.Sprintf("%v/%v: ", p.OS, p.Architecture))
267+
logf := withPlatformPrefix(logf, p)
254268
img, err := createImageForBase(bp, logf, baseImage, p)
255269
if err != nil {
256270
return err
257271
}
272+
img, err = applyEnvVars(img, bp.envVars)
273+
if err != nil {
274+
return err
275+
}
258276
if !bp.publish {
259277
logf("not pushing")
260278
return nil
@@ -292,10 +310,13 @@ func fetchAndBuild(bp *buildParams) error {
292310
var adds []mutate.IndexAddendum
293311
// Try to build images for all supported platforms.
294312
for _, id := range im.Manifests {
295-
logf := withPrefix(logf, fmt.Sprintf("%v/%v: ", id.Platform.OS, id.Platform.Architecture))
296313
if id.Platform == nil {
297314
return fmt.Errorf("unknown platform for image: %v", bp.baseImage)
298315
}
316+
if id.Platform.OS == "unknown" {
317+
continue
318+
}
319+
logf := withPlatformPrefix(logf, *id.Platform)
299320
if err := bp.verifyPlatform(*id.Platform); err != nil {
300321
logf("skipping: %v", err)
301322
continue
@@ -314,15 +335,31 @@ func fetchAndBuild(bp *buildParams) error {
314335
// Ensure that any provided OCI annotations are added to each OCI image manifest.
315336
img = mutate.Annotations(img, bp.annotations).(v1.Image)
316337

338+
img, err = applyEnvVars(img, bp.envVars)
339+
if err != nil {
340+
return err
341+
}
342+
343+
if bp.volumes != nil {
344+
img, err = mutateConfig(img, func(c *v1.Config) error {
345+
c.Volumes = bp.volumes
346+
return nil
347+
})
348+
if err != nil {
349+
return err
350+
}
351+
}
352+
317353
if args := flag.Args(); len(args) > 0 {
318-
img, err = mutate.Config(img, v1.Config{
319-
Cmd: args,
320-
Volumes: bp.volumes,
354+
img, err = mutateConfig(img, func(c *v1.Config) error {
355+
c.Cmd = args
356+
return nil
321357
})
322358
if err != nil {
323359
return err
324360
}
325361
}
362+
326363
d, err := img.Digest()
327364
if err != nil {
328365
return err
@@ -634,3 +671,74 @@ func parseAnnotations(s string) map[string]string {
634671
}
635672
return annotations
636673
}
674+
675+
// parseEnv accepts a string with comma separated KEY=value pairs of environment variables
676+
// and returns them as a slice of "KEY=value" strings.
677+
func parseEnv(s string) []string {
678+
if len(s) == 0 {
679+
return nil
680+
}
681+
var envVars []string
682+
for env := range strings.SplitSeq(s, ",") {
683+
env = strings.TrimSpace(env)
684+
if len(env) == 0 {
685+
continue
686+
}
687+
if !strings.Contains(env, "=") {
688+
continue
689+
}
690+
envVars = append(envVars, env)
691+
}
692+
return envVars
693+
}
694+
695+
// applyEnvVars applies environment variables to an image config, merging with existing env vars.
696+
// New env vars override existing ones with the same key.
697+
func applyEnvVars(img v1.Image, newEnvVars []string) (v1.Image, error) {
698+
if len(newEnvVars) == 0 {
699+
return img, nil
700+
}
701+
config, err := img.ConfigFile()
702+
if err != nil {
703+
return nil, fmt.Errorf("error getting config: %w", err)
704+
}
705+
706+
envMap := make(map[string]string)
707+
for _, kv := range config.Config.Env {
708+
if k, v, ok := strings.Cut(kv, "="); ok {
709+
envMap[k] = v
710+
}
711+
}
712+
713+
for _, env := range newEnvVars {
714+
if k, v, ok := strings.Cut(env, "="); ok {
715+
envMap[k] = v
716+
}
717+
}
718+
719+
// Apply the merged env vars
720+
return mutateConfig(img, func(c *v1.Config) error {
721+
c.Env = nil
722+
for _, k := range slices.Sorted(maps.Keys(envMap)) {
723+
c.Env = append(c.Env, k+"="+envMap[k])
724+
}
725+
return nil
726+
})
727+
}
728+
729+
// mutateConfig returns img with its config mutated by f.
730+
//
731+
// The pointer given to f is a deep copy of the existing config,
732+
// so any fields untouched by f will be preserved.
733+
func mutateConfig(img v1.Image, f func(*v1.Config) error) (v1.Image, error) {
734+
config, err := img.ConfigFile()
735+
if err != nil {
736+
return nil, fmt.Errorf("error getting config: %w", err)
737+
}
738+
739+
confCopy := config.DeepCopy()
740+
if err := f(&confCopy.Config); err != nil {
741+
return nil, err
742+
}
743+
return mutate.Config(img, confCopy.Config)
744+
}

0 commit comments

Comments
 (0)