Skip to content

Commit 1be251f

Browse files
weltekialexellis
authored andcommitted
Add support for build secrets in local Docker builds
Previously, build_secrets defined in stack.yml were only used when building with the remote builder. For local docker build and docker buildx build commands, the secrets were silently ignored. This change ports the build secrets support from the pro plugin so that local builds pass --secret id=<key>,src=<path> flags to Docker. DOCKER_BUILDKIT=1 is also set automatically when build secrets are present, since BuildKit is required for the --secret flag. Signed-off-by: Han Verstraete (OpenFaaS Ltd) <han@openfaas.com>
1 parent bb85f62 commit 1be251f

File tree

3 files changed

+193
-1
lines changed

3 files changed

+193
-1
lines changed

builder/build.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,11 @@ func BuildImage(image string, handler string, functionName string, language stri
114114

115115
fmt.Printf("Building: %s with %s template. Please wait..\n", imageName, language)
116116

117+
buildSecrets, err = resolveSecretPaths(buildSecrets)
118+
if err != nil {
119+
return err
120+
}
121+
117122
if remoteBuilder != "" {
118123
tempDir, err := os.MkdirTemp(os.TempDir(), "openfaas-build-*")
119124
if err != nil {
@@ -152,12 +157,13 @@ func BuildImage(image string, handler string, functionName string, language stri
152157
BuildArgMap: buildArgMap,
153158
BuildLabelMap: buildLabelMap,
154159
ForcePull: forcePull,
160+
BuildSecrets: buildSecrets,
155161
}
156162

157163
command, args := getDockerBuildCommand(dockerBuildVal)
158164

159165
envs := os.Environ()
160-
if mountSSH {
166+
if mountSSH || len(buildSecrets) > 0 {
161167
envs = append(envs, "DOCKER_BUILDKIT=1")
162168
}
163169
log.Printf("Build flags: %+v\n", args)
@@ -289,6 +295,12 @@ func getDockerBuildCommand(build dockerBuild) (string, []string) {
289295

290296
args = append(args, "--tag", build.Image, ".")
291297

298+
if len(build.BuildSecrets) > 0 {
299+
for k, v := range build.BuildSecrets {
300+
args = append(args, "--secret", fmt.Sprintf("id=%s,src=%s", k, v))
301+
}
302+
}
303+
292304
command := "docker"
293305

294306
return command, args
@@ -311,6 +323,8 @@ type dockerBuild struct {
311323
ExtraTags []string
312324

313325
ForcePull bool
326+
327+
BuildSecrets map[string]string
314328
}
315329

316330
// pathInScope returns the absolute path to `path` and ensures that it is located within the
@@ -450,6 +464,26 @@ func getPackages(availableBuildOptions []stack.BuildOption, requestedBuildOption
450464
return deDuplicate(buildPackages), true
451465
}
452466

467+
// resolveSecretPaths converts relative build secret file paths to absolute
468+
// paths. Relative paths are resolved against the current working directory,
469+
// consistent with how other relative paths (e.g. handler) are handled in
470+
// faas-cli. Absolute paths are returned unchanged.
471+
func resolveSecretPaths(secrets map[string]string) (map[string]string, error) {
472+
if len(secrets) == 0 {
473+
return secrets, nil
474+
}
475+
476+
resolved := make(map[string]string, len(secrets))
477+
for k, v := range secrets {
478+
absPath, err := filepath.Abs(v)
479+
if err != nil {
480+
return nil, fmt.Errorf("unable to resolve path for build secret %q: %w", k, err)
481+
}
482+
resolved[k] = absPath
483+
}
484+
return resolved, nil
485+
}
486+
453487
func deDuplicate(buildOptPackages []string) []string {
454488
seenPackages := map[string]bool{}
455489
retPackages := []string{}

builder/build_test.go

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package builder
22

33
import (
44
"fmt"
5+
"os"
56
"path/filepath"
67
"reflect"
78
"strings"
@@ -115,6 +116,87 @@ func Test_getDockerBuildCommand_WithBuildArg(t *testing.T) {
115116
}
116117
}
117118

119+
func Test_getDockerBuildCommand_WithBuildSecrets(t *testing.T) {
120+
dockerBuildVal := dockerBuild{
121+
Image: "imagename:latest",
122+
NoCache: false,
123+
Squash: false,
124+
BuildArgMap: make(map[string]string),
125+
BuildSecrets: map[string]string{
126+
"npmrc": "/home/user/.npmrc",
127+
"netrc": "/home/user/.netrc",
128+
},
129+
}
130+
131+
_, values := getDockerBuildCommand(dockerBuildVal)
132+
133+
joined := strings.Join(values, " ")
134+
wantSecret1 := "--secret id=npmrc,src=/home/user/.npmrc"
135+
wantSecret2 := "--secret id=netrc,src=/home/user/.netrc"
136+
137+
if !strings.Contains(joined, wantSecret1) {
138+
t.Errorf("want %s in %s, but didn't find it", wantSecret1, joined)
139+
}
140+
if !strings.Contains(joined, wantSecret2) {
141+
t.Errorf("want %s in %s, but didn't find it", wantSecret2, joined)
142+
}
143+
}
144+
145+
func Test_getDockerBuildCommand_NoBuildSecrets(t *testing.T) {
146+
dockerBuildVal := dockerBuild{
147+
Image: "imagename:latest",
148+
NoCache: false,
149+
Squash: false,
150+
BuildArgMap: make(map[string]string),
151+
}
152+
153+
_, values := getDockerBuildCommand(dockerBuildVal)
154+
155+
joined := strings.Join(values, " ")
156+
if strings.Contains(joined, "--secret") {
157+
t.Errorf("did not expect --secret in %s", joined)
158+
}
159+
}
160+
161+
func Test_getDockerBuildxCommand_WithBuildSecrets(t *testing.T) {
162+
dockerBuildVal := dockerBuild{
163+
Image: "imagename:latest",
164+
NoCache: false,
165+
Squash: false,
166+
BuildArgMap: make(map[string]string),
167+
Platforms: "linux/amd64",
168+
BuildSecrets: map[string]string{
169+
"pipconf": "/home/user/.config/pip/pip.conf",
170+
},
171+
}
172+
173+
_, values := getDockerBuildxCommand(dockerBuildVal)
174+
175+
joined := strings.Join(values, " ")
176+
wantSecret := "--secret id=pipconf,src=/home/user/.config/pip/pip.conf"
177+
178+
if !strings.Contains(joined, wantSecret) {
179+
t.Errorf("want %s in %s, but didn't find it", wantSecret, joined)
180+
}
181+
}
182+
183+
func Test_getDockerBuildxCommand_NoBuildSecrets(t *testing.T) {
184+
dockerBuildVal := dockerBuild{
185+
Image: "imagename:latest",
186+
NoCache: false,
187+
Squash: false,
188+
BuildArgMap: make(map[string]string),
189+
Platforms: "linux/amd64",
190+
}
191+
192+
_, values := getDockerBuildxCommand(dockerBuildVal)
193+
194+
joined := strings.Join(values, " ")
195+
if strings.Contains(joined, "--secret") {
196+
t.Errorf("did not expect --secret in %s", joined)
197+
}
198+
}
199+
118200
func Test_buildFlagSlice(t *testing.T) {
119201

120202
var buildFlagOpts = []struct {
@@ -612,3 +694,67 @@ func Test_appendAdditionalPackages(t *testing.T) {
612694
})
613695
}
614696
}
697+
698+
func Test_resolveSecretPaths_RelativePaths(t *testing.T) {
699+
secrets := map[string]string{
700+
"npmrc": ".secrets/npmrc",
701+
"api_key": "secrets/api_key.txt",
702+
}
703+
704+
resolved, err := resolveSecretPaths(secrets)
705+
if err != nil {
706+
t.Fatalf("resolveSecretPaths returned error: %v", err)
707+
}
708+
709+
for k, v := range resolved {
710+
if !filepath.IsAbs(v) {
711+
t.Errorf("expected absolute path for %q, got %q", k, v)
712+
}
713+
}
714+
715+
// Verify relative paths were resolved against CWD
716+
cwd, _ := os.Getwd()
717+
if want := filepath.Join(cwd, ".secrets/npmrc"); resolved["npmrc"] != want {
718+
t.Errorf("want %q, got %q", want, resolved["npmrc"])
719+
}
720+
if want := filepath.Join(cwd, "secrets/api_key.txt"); resolved["api_key"] != want {
721+
t.Errorf("want %q, got %q", want, resolved["api_key"])
722+
}
723+
}
724+
725+
func Test_resolveSecretPaths_AbsolutePaths(t *testing.T) {
726+
secrets := map[string]string{
727+
"npmrc": "/home/user/.npmrc",
728+
"netrc": "/home/user/.netrc",
729+
}
730+
731+
resolved, err := resolveSecretPaths(secrets)
732+
if err != nil {
733+
t.Fatalf("resolveSecretPaths returned error: %v", err)
734+
}
735+
736+
if resolved["npmrc"] != "/home/user/.npmrc" {
737+
t.Errorf("expected absolute path unchanged, got %q", resolved["npmrc"])
738+
}
739+
if resolved["netrc"] != "/home/user/.netrc" {
740+
t.Errorf("expected absolute path unchanged, got %q", resolved["netrc"])
741+
}
742+
}
743+
744+
func Test_resolveSecretPaths_EmptyMap(t *testing.T) {
745+
resolved, err := resolveSecretPaths(nil)
746+
if err != nil {
747+
t.Fatalf("resolveSecretPaths returned error: %v", err)
748+
}
749+
if resolved != nil {
750+
t.Errorf("expected nil for nil input, got %v", resolved)
751+
}
752+
753+
resolved, err = resolveSecretPaths(map[string]string{})
754+
if err != nil {
755+
t.Fatalf("resolveSecretPaths returned error: %v", err)
756+
}
757+
if len(resolved) != 0 {
758+
t.Errorf("expected empty map, got %v", resolved)
759+
}
760+
}

builder/publish.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ func PublishImage(image string, handler string, functionName string, language st
6767

6868
fmt.Printf("Building: %s with %s template. Please wait..\n", imageName, language)
6969

70+
buildSecrets, err = resolveSecretPaths(buildSecrets)
71+
if err != nil {
72+
return err
73+
}
74+
7075
if remoteBuilder != "" {
7176

7277
if forcePull {
@@ -114,6 +119,7 @@ func PublishImage(image string, handler string, functionName string, language st
114119
Platforms: platforms,
115120
ExtraTags: extraTags,
116121
ForcePull: forcePull,
122+
BuildSecrets: buildSecrets,
117123
}
118124

119125
command, args := getDockerBuildxCommand(dockerBuildVal)
@@ -170,6 +176,12 @@ func getDockerBuildxCommand(build dockerBuild) (string, []string) {
170176
args = append(args, "--tag", tag)
171177
}
172178

179+
if len(build.BuildSecrets) > 0 {
180+
for k, v := range build.BuildSecrets {
181+
args = append(args, "--secret", fmt.Sprintf("id=%s,src=%s", k, v))
182+
}
183+
}
184+
173185
command := "docker"
174186

175187
return command, args

0 commit comments

Comments
 (0)