Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion builder/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ func BuildImage(image string, handler string, functionName string, language stri

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

buildSecrets, err = resolveSecretPaths(buildSecrets)
if err != nil {
return err
}

if remoteBuilder != "" {
tempDir, err := os.MkdirTemp(os.TempDir(), "openfaas-build-*")
if err != nil {
Expand Down Expand Up @@ -152,12 +157,13 @@ func BuildImage(image string, handler string, functionName string, language stri
BuildArgMap: buildArgMap,
BuildLabelMap: buildLabelMap,
ForcePull: forcePull,
BuildSecrets: buildSecrets,
}

command, args := getDockerBuildCommand(dockerBuildVal)

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

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

if len(build.BuildSecrets) > 0 {
for k, v := range build.BuildSecrets {
args = append(args, "--secret", fmt.Sprintf("id=%s,src=%s", k, v))
}
}

command := "docker"

return command, args
Expand All @@ -311,6 +323,8 @@ type dockerBuild struct {
ExtraTags []string

ForcePull bool

BuildSecrets map[string]string
}

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

// resolveSecretPaths converts relative build secret file paths to absolute
// paths. Relative paths are resolved against the current working directory,
// consistent with how other relative paths (e.g. handler) are handled in
// faas-cli. Absolute paths are returned unchanged.
func resolveSecretPaths(secrets map[string]string) (map[string]string, error) {
if len(secrets) == 0 {
return secrets, nil
}

resolved := make(map[string]string, len(secrets))
for k, v := range secrets {
absPath, err := filepath.Abs(v)
if err != nil {
return nil, fmt.Errorf("unable to resolve path for build secret %q: %w", k, err)
}
resolved[k] = absPath
}
return resolved, nil
}

func deDuplicate(buildOptPackages []string) []string {
seenPackages := map[string]bool{}
retPackages := []string{}
Expand Down
146 changes: 146 additions & 0 deletions builder/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package builder

import (
"fmt"
"os"
"path/filepath"
"reflect"
"strings"
Expand Down Expand Up @@ -115,6 +116,87 @@ func Test_getDockerBuildCommand_WithBuildArg(t *testing.T) {
}
}

func Test_getDockerBuildCommand_WithBuildSecrets(t *testing.T) {
dockerBuildVal := dockerBuild{
Image: "imagename:latest",
NoCache: false,
Squash: false,
BuildArgMap: make(map[string]string),
BuildSecrets: map[string]string{
"npmrc": "/home/user/.npmrc",
"netrc": "/home/user/.netrc",
},
}

_, values := getDockerBuildCommand(dockerBuildVal)

joined := strings.Join(values, " ")
wantSecret1 := "--secret id=npmrc,src=/home/user/.npmrc"
wantSecret2 := "--secret id=netrc,src=/home/user/.netrc"

if !strings.Contains(joined, wantSecret1) {
t.Errorf("want %s in %s, but didn't find it", wantSecret1, joined)
}
if !strings.Contains(joined, wantSecret2) {
t.Errorf("want %s in %s, but didn't find it", wantSecret2, joined)
}
}

func Test_getDockerBuildCommand_NoBuildSecrets(t *testing.T) {
dockerBuildVal := dockerBuild{
Image: "imagename:latest",
NoCache: false,
Squash: false,
BuildArgMap: make(map[string]string),
}

_, values := getDockerBuildCommand(dockerBuildVal)

joined := strings.Join(values, " ")
if strings.Contains(joined, "--secret") {
t.Errorf("did not expect --secret in %s", joined)
}
}

func Test_getDockerBuildxCommand_WithBuildSecrets(t *testing.T) {
dockerBuildVal := dockerBuild{
Image: "imagename:latest",
NoCache: false,
Squash: false,
BuildArgMap: make(map[string]string),
Platforms: "linux/amd64",
BuildSecrets: map[string]string{
"pipconf": "/home/user/.config/pip/pip.conf",
},
}

_, values := getDockerBuildxCommand(dockerBuildVal)

joined := strings.Join(values, " ")
wantSecret := "--secret id=pipconf,src=/home/user/.config/pip/pip.conf"

if !strings.Contains(joined, wantSecret) {
t.Errorf("want %s in %s, but didn't find it", wantSecret, joined)
}
}

func Test_getDockerBuildxCommand_NoBuildSecrets(t *testing.T) {
dockerBuildVal := dockerBuild{
Image: "imagename:latest",
NoCache: false,
Squash: false,
BuildArgMap: make(map[string]string),
Platforms: "linux/amd64",
}

_, values := getDockerBuildxCommand(dockerBuildVal)

joined := strings.Join(values, " ")
if strings.Contains(joined, "--secret") {
t.Errorf("did not expect --secret in %s", joined)
}
}

func Test_buildFlagSlice(t *testing.T) {

var buildFlagOpts = []struct {
Expand Down Expand Up @@ -612,3 +694,67 @@ func Test_appendAdditionalPackages(t *testing.T) {
})
}
}

func Test_resolveSecretPaths_RelativePaths(t *testing.T) {
secrets := map[string]string{
"npmrc": ".secrets/npmrc",
"api_key": "secrets/api_key.txt",
}

resolved, err := resolveSecretPaths(secrets)
if err != nil {
t.Fatalf("resolveSecretPaths returned error: %v", err)
}

for k, v := range resolved {
if !filepath.IsAbs(v) {
t.Errorf("expected absolute path for %q, got %q", k, v)
}
}

// Verify relative paths were resolved against CWD
cwd, _ := os.Getwd()
if want := filepath.Join(cwd, ".secrets/npmrc"); resolved["npmrc"] != want {
t.Errorf("want %q, got %q", want, resolved["npmrc"])
}
if want := filepath.Join(cwd, "secrets/api_key.txt"); resolved["api_key"] != want {
t.Errorf("want %q, got %q", want, resolved["api_key"])
}
}

func Test_resolveSecretPaths_AbsolutePaths(t *testing.T) {
secrets := map[string]string{
"npmrc": "/home/user/.npmrc",
"netrc": "/home/user/.netrc",
}

resolved, err := resolveSecretPaths(secrets)
if err != nil {
t.Fatalf("resolveSecretPaths returned error: %v", err)
}

if resolved["npmrc"] != "/home/user/.npmrc" {
t.Errorf("expected absolute path unchanged, got %q", resolved["npmrc"])
}
if resolved["netrc"] != "/home/user/.netrc" {
t.Errorf("expected absolute path unchanged, got %q", resolved["netrc"])
}
}

func Test_resolveSecretPaths_EmptyMap(t *testing.T) {
resolved, err := resolveSecretPaths(nil)
if err != nil {
t.Fatalf("resolveSecretPaths returned error: %v", err)
}
if resolved != nil {
t.Errorf("expected nil for nil input, got %v", resolved)
}

resolved, err = resolveSecretPaths(map[string]string{})
if err != nil {
t.Fatalf("resolveSecretPaths returned error: %v", err)
}
if len(resolved) != 0 {
t.Errorf("expected empty map, got %v", resolved)
}
}
12 changes: 12 additions & 0 deletions builder/publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ func PublishImage(image string, handler string, functionName string, language st

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

buildSecrets, err = resolveSecretPaths(buildSecrets)
if err != nil {
return err
}

if remoteBuilder != "" {

if forcePull {
Expand Down Expand Up @@ -114,6 +119,7 @@ func PublishImage(image string, handler string, functionName string, language st
Platforms: platforms,
ExtraTags: extraTags,
ForcePull: forcePull,
BuildSecrets: buildSecrets,
}

command, args := getDockerBuildxCommand(dockerBuildVal)
Expand Down Expand Up @@ -170,6 +176,12 @@ func getDockerBuildxCommand(build dockerBuild) (string, []string) {
args = append(args, "--tag", tag)
}

if len(build.BuildSecrets) > 0 {
for k, v := range build.BuildSecrets {
args = append(args, "--secret", fmt.Sprintf("id=%s,src=%s", k, v))
}
}

command := "docker"

return command, args
Expand Down
21 changes: 20 additions & 1 deletion builder/remote_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@ func runRemoteBuild(builderURL *url.URL, tarPath, payloadSecretPath, builderPubl

var stream *sdkbuilder.BuildResultStream
if len(buildSecrets) > 0 {
stream, err = b.BuildWithSecretsStream(tarPath, buildSecrets)
resolvedSecrets, err := readBuildSecrets(buildSecrets)
if err != nil {
return err
}
stream, err = b.BuildWithSecretsStream(tarPath, resolvedSecrets)
} else {
stream, err = b.BuildWithStream(tarPath)
}
Expand All @@ -57,6 +61,21 @@ func runRemoteBuild(builderURL *url.URL, tarPath, payloadSecretPath, builderPubl
return consumeBuildStream(stream, quietBuild, functionName, imageName)
}

// readBuildSecrets resolves build secret values by reading file contents.
// Each value in the buildSecrets map is treated as a file path. The file
// is read and its contents replace the path in the returned map.
func readBuildSecrets(buildSecrets map[string]string) (map[string]string, error) {
resolved := make(map[string]string, len(buildSecrets))
for k, v := range buildSecrets {
data, err := os.ReadFile(v)
if err != nil {
return nil, fmt.Errorf("unable to read build secret %q from %s: %w", k, v, err)
}
resolved[k] = string(data)
}
return resolved, nil
}

func resolveRemoteBuilderPublicKey(builderURL *url.URL, builderPublicKeyPath string) (*remoteBuilderPublicKeyResponse, error) {
if builderPublicKeyPath == "" {
return fetchRemoteBuilderPublicKey(builderURL)
Expand Down
57 changes: 54 additions & 3 deletions builder/remote_builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ func TestRunRemoteBuildWithSecrets(t *testing.T) {
}

if err := runRemoteBuild(builderURL, tarPath, payloadSecretPath, "", map[string]string{
"pip_token": "s3cr3t",
"pip_token": writeTempSecret(t, "s3cr3t"),
}, true, "fn", "ttl.sh/test:latest"); err != nil {
t.Fatalf("runRemoteBuild returned error: %v", err)
}
Expand Down Expand Up @@ -160,7 +160,7 @@ func TestRunRemoteBuildWithPinnedPublicKey(t *testing.T) {
}

if err := runRemoteBuild(builderURL, tarPath, payloadSecretPath, publicKeyPath, map[string]string{
"pip_token": "s3cr3t",
"pip_token": writeTempSecret(t, "s3cr3t"),
}, true, "fn", "ttl.sh/test:latest"); err != nil {
t.Fatalf("runRemoteBuild returned error: %v", err)
}
Expand Down Expand Up @@ -198,7 +198,7 @@ func TestRunRemoteBuildWithLiteralPublicKey(t *testing.T) {
}

if err := runRemoteBuild(builderURL, tarPath, payloadSecretPath, string(pub), map[string]string{
"pip_token": "s3cr3t",
"pip_token": writeTempSecret(t, "s3cr3t"),
}, true, "fn", "ttl.sh/test:latest"); err != nil {
t.Fatalf("runRemoteBuild returned error: %v", err)
}
Expand All @@ -224,3 +224,54 @@ func createTestTar(t *testing.T) []byte {
}
return buf.Bytes()
}

func writeTempSecret(t *testing.T, content string) string {
t.Helper()
p := filepath.Join(t.TempDir(), "secret")
if err := os.WriteFile(p, []byte(content), 0o600); err != nil {
t.Fatalf("os.WriteFile returned error: %v", err)
}
return p
}

func TestReadBuildSecrets(t *testing.T) {
secretDir := t.TempDir()

npmrcPath := filepath.Join(secretDir, ".npmrc")
if err := os.WriteFile(npmrcPath, []byte("//registry.npmjs.org/:_authToken=TOKEN123"), 0o600); err != nil {
t.Fatal(err)
}

netrcPath := filepath.Join(secretDir, ".netrc")
if err := os.WriteFile(netrcPath, []byte("machine github.com login user password pat"), 0o600); err != nil {
t.Fatal(err)
}

secrets := map[string]string{
"npmrc": npmrcPath,
"netrc": netrcPath,
}

resolved, err := readBuildSecrets(secrets)
if err != nil {
t.Fatalf("readBuildSecrets returned error: %v", err)
}

if got := resolved["npmrc"]; got != "//registry.npmjs.org/:_authToken=TOKEN123" {
t.Errorf("want npmrc %q, got %q", "//registry.npmjs.org/:_authToken=TOKEN123", got)
}
if got := resolved["netrc"]; got != "machine github.com login user password pat" {
t.Errorf("want netrc %q, got %q", "machine github.com login user password pat", got)
}
}

func TestReadBuildSecrets_FileNotFound(t *testing.T) {
secrets := map[string]string{
"missing": "/nonexistent/path/secret.txt",
}

_, err := readBuildSecrets(secrets)
if err == nil {
t.Fatal("expected error for missing file, got nil")
}
}
Loading