feat: Add support for build-secrets (#604)

Enables passing of build-secrets through the 'secrets' block inside
'build'.
The feature is only available when using Buildkit.

Co-authored-by: Martin <Junkern@users.noreply.github.com>
This commit is contained in:
Emanuel Skrenković 2025-04-15 09:15:04 +02:00 committed by GitHub
parent cfcb6f2cf6
commit 64b95701e3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 153 additions and 2 deletions

View file

@ -131,6 +131,7 @@ Optional:
- `pull_parent` (Boolean) Attempt to pull the image even if an older image exists locally
- `remote_context` (String) A Git repository URI or HTTP/HTTPS context URI
- `remove` (Boolean) Remove intermediate containers after a successful build. Defaults to `true`.
- `secrets` (Block List) Set build-time secrets (see [below for nested schema](#nestedblock--build--secrets))
- `security_opt` (List of String) The security options
- `session_id` (String) Set an ID for the build session
- `shm_size` (Number) Size of /dev/shm in bytes. The size must be greater than 0
@ -159,6 +160,19 @@ Optional:
- `user_name` (String) the registry user name
<a id="nestedblock--build--secrets"></a>
### Nested Schema for `build.secrets`
Required:
- `id` (String) ID of the secret. By default, secrets are mounted to /run/secrets/<id>
Optional:
- `env` (String) Environment variable source of the secret
- `src` (String) File source of the secret
<a id="nestedblock--build--ulimit"></a>
### Nested Schema for `build.ulimit`

1
go.mod
View file

@ -229,6 +229,7 @@ require (
github.com/timonwong/loggercheck v0.9.4 // indirect
github.com/tomarrell/wrapcheck/v2 v2.8.3 // indirect
github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect
github.com/ultraware/funlen v0.1.0 // indirect
github.com/ultraware/whitespace v0.1.1 // indirect
github.com/uudashr/gocognit v1.1.2 // indirect

2
go.sum
View file

@ -1184,6 +1184,8 @@ github.com/tomarrell/wrapcheck/v2 v2.8.3 h1:5ov+Cbhlgi7s/a42BprYoxsr73CbdMUTzE3b
github.com/tomarrell/wrapcheck/v2 v2.8.3/go.mod h1:g9vNIyhb5/9TQgumxQyOEqDHsmGYcGsVMOx/xGkqdMo=
github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw=
github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw=
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea h1:SXhTLE6pb6eld/v/cCndK0AMpt1wiVFb/YYmqB3/QG0=
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea/go.mod h1:WPnis/6cRcDZSUvVmezrxJPkiO87ThFYsoUiMwWNDJk=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ultraware/funlen v0.1.0 h1:BuqclbkY6pO+cvxoq7OsktIXZpgBSkYTQtmwhAK81vI=
github.com/ultraware/funlen v0.1.0/go.mod h1:XJqmOQja6DpxarLj6Jj1U7JuoS8PvL4nEqDaQhy22p4=

View file

@ -88,6 +88,36 @@ func resourceDockerImage() *schema.Resource {
Default: true,
Optional: true,
},
"secrets": {
Type: schema.TypeList,
Description: "Set build-time secrets",
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"id": {
Type: schema.TypeString,
Description: "ID of the secret. By default, secrets are mounted to /run/secrets/<id>",
Optional: false,
Required: true,
ForceNew: true,
},
"src": {
Type: schema.TypeString,
Description: "File source of the secret",
Optional: true,
Required: false,
ForceNew: true,
},
"env": {
Type: schema.TypeString,
Description: "Environment variable source of the secret",
Optional: true,
Required: false,
ForceNew: true,
},
},
},
},
"label": {
Type: schema.TypeMap,
Description: "Set metadata for an image",

View file

@ -24,6 +24,7 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/mitchellh/go-homedir"
"github.com/moby/buildkit/session"
"github.com/moby/buildkit/session/secrets/secretsprovider"
"github.com/pkg/errors"
)
@ -333,7 +334,22 @@ func buildDockerImage(ctx context.Context, rawBuild map[string]interface{}, imag
buildContext := rawBuild["context"].(string)
enableBuildKitIfSupported(ctx, client, &buildOptions)
buildKitSession := enableBuildKitIfSupported(ctx, client, &buildOptions)
// If Buildkit is enabled, try to parse and use secrets if present.
if buildKitSession != nil {
if secretsRaw, secretsDefined := rawBuild["secrets"]; secretsDefined {
parsedSecrets := parseBuildSecrets(secretsRaw)
store, err := secretsprovider.NewStore(parsedSecrets)
if err != nil {
return err
}
provider := secretsprovider.NewSecretProvider(store)
buildKitSession.Allow(provider)
}
}
buildCtx, relDockerfile, err := prepareBuildContext(buildContext, buildOptions.Dockerfile)
if err != nil {
@ -355,7 +371,11 @@ func buildDockerImage(ctx context.Context, rawBuild map[string]interface{}, imag
return nil
}
func enableBuildKitIfSupported(ctx context.Context, client *client.Client, buildOptions *types.ImageBuildOptions) {
func enableBuildKitIfSupported(
ctx context.Context,
client *client.Client,
buildOptions *types.ImageBuildOptions,
) *session.Session {
dockerClientVersion := client.ClientVersion()
log.Printf("[DEBUG] DockerClientVersion: %v, minBuildKitDockerVersion: %v\n", dockerClientVersion, minBuildkitDockerVersion)
if versions.GreaterThanOrEqualTo(dockerClientVersion, minBuildkitDockerVersion) {
@ -369,8 +389,10 @@ func enableBuildKitIfSupported(ctx context.Context, client *client.Client, build
defer s.Close()
buildOptions.SessionID = s.ID()
buildOptions.Version = types.BuilderBuildKit
return s
} else {
buildOptions.Version = types.BuilderV1
return nil
}
}
@ -461,3 +483,20 @@ func decodeBuildMessages(response types.ImageBuildResponse) (string, error) {
return buf.String(), buildErr
}
func parseBuildSecrets(secretsRaw interface{}) []secretsprovider.Source {
options := secretsRaw.([]interface{})
secrets := make([]secretsprovider.Source, len(options))
for i, option := range options {
secretRaw := option.(map[string]interface{})
source := secretsprovider.Source{
ID: secretRaw["id"].(string),
FilePath: secretRaw["src"].(string),
Env: secretRaw["env"].(string),
}
secrets[i] = source
}
return secrets
}

View file

@ -449,6 +449,53 @@ func TestAccDockerImage_build(t *testing.T) {
})
}
func TestAccDockerImageSecrets_build(t *testing.T) {
const testDockerFileWithSecret = `
FROM python:3-bookworm
WORKDIR /app
ARG test_arg
RUN echo ${test_arg} > test_arg.txt
RUN --mount=type=secret,id=TEST_SECRET_SRC \
--mount=type=secret,id=TEST_SECRET_ENV \
apt-get update -qq`
ctx := context.Background()
wd, _ := os.Getwd()
dfPath := filepath.Join(wd, "Dockerfile")
if err := os.WriteFile(dfPath, []byte(testDockerFileWithSecret), 0o644); err != nil {
t.Fatalf("failed to create a Dockerfile %s for test: %+v", dfPath, err)
}
defer os.Remove(dfPath)
const secretContent = "THIS IS A SECRET"
sPath := filepath.Join(wd, "secret")
if err := os.WriteFile(sPath, []byte(secretContent), 0o644); err != nil {
t.Fatalf("failed to create a secret file %s for test: %+v", sPath, err)
}
defer os.Remove(sPath)
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: providerFactories,
CheckDestroy: func(state *terraform.State) error {
return testAccDockerImageDestroy(ctx, state)
},
Steps: []resource.TestStep{
{
Config: loadTestConfiguration(t, RESOURCE, "docker_image", "testDockerImageBuildSecrets"),
Check: resource.ComposeTestCheckFunc(
resource.TestMatchResourceAttr("docker_image.test", "name", contentDigestRegexp),
),
},
},
})
}
const testDockerFileExample = `
FROM python:3-bookworm

View file

@ -0,0 +1,18 @@
resource "docker_image" "test" {
name = "ubuntu:11"
build {
context = "."
dockerfile = "Dockerfile"
force_remove = true
secrets {
id = "TEST_SECRET_SRC"
src = "./secret"
}
secrets {
id = "TEST_SECRET_ENV"
env = "PATH"
}
}
}