mirror of
https://github.com/apple/container.git
synced 2026-07-13 04:57:08 +00:00
Add support for build secrets (#1300)
docker-compatible `--secret id=key,...` arg for `container build`, that works with Dockerfiles with `RUN --mount=type=secret` Requires https://github.com/apple/container-builder-shim/pull/69 ## Type of Change - [ ] Bug fix - [x] New feature - [ ] Breaking change - [ ] Documentation update ## Motivation and Context Adds support for Dockerfiles that use build secrets (e.g. `RUN --mount=type=secret ...`) ## Testing - [x] Tested locally - [x] Added/updated tests - [x] Added/updated docs
This commit is contained in:
@@ -264,6 +264,7 @@ public struct Builder: Sendable {
|
||||
public let buildID: String
|
||||
public let contentStore: ContentStore
|
||||
public let buildArgs: [String]
|
||||
public let secrets: [String: Data]
|
||||
public let contextDir: String
|
||||
public let dockerfile: Data
|
||||
public let hiddenDockerDir: String?
|
||||
@@ -283,6 +284,7 @@ public struct Builder: Sendable {
|
||||
buildID: String,
|
||||
contentStore: ContentStore,
|
||||
buildArgs: [String],
|
||||
secrets: [String: Data],
|
||||
contextDir: String,
|
||||
dockerfile: Data,
|
||||
hiddenDockerDir: String?,
|
||||
@@ -301,6 +303,7 @@ public struct Builder: Sendable {
|
||||
self.buildID = buildID
|
||||
self.contentStore = contentStore
|
||||
self.buildArgs = buildArgs
|
||||
self.secrets = secrets
|
||||
self.contextDir = contextDir
|
||||
self.dockerfile = dockerfile
|
||||
self.hiddenDockerDir = hiddenDockerDir
|
||||
@@ -344,6 +347,9 @@ public struct Builder: Sendable {
|
||||
for buildArg in config.buildArgs {
|
||||
metadata.addString(buildArg, forKey: "build-args")
|
||||
}
|
||||
for (id, data) in config.secrets {
|
||||
metadata.addString(id + "=" + data.base64EncodedString(), forKey: "secrets")
|
||||
}
|
||||
for output in config.exports {
|
||||
metadata.addString(try output.stringValue, forKey: "outputs")
|
||||
}
|
||||
|
||||
@@ -46,6 +46,11 @@ extension Application {
|
||||
case tty
|
||||
}
|
||||
|
||||
enum SecretType: Decodable {
|
||||
case data(Data)
|
||||
case file(String)
|
||||
}
|
||||
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
help: ArgumentHelp("Add the architecture type to the build", valueName: "value"),
|
||||
@@ -115,6 +120,11 @@ extension Application {
|
||||
@Flag(name: .shortAndLong, help: "Suppress build output")
|
||||
var quiet: Bool = false
|
||||
|
||||
@Option(name: .long, help: ArgumentHelp("Set build-time secrets (format: id=<key>[,env=<ENV_VAR>|,src=<local/path>])", valueName: "id=key,..."))
|
||||
var secret: [String] = []
|
||||
|
||||
var secrets: [String: SecretType] = [:]
|
||||
|
||||
@Option(name: [.short, .customLong("tag")], help: ArgumentHelp("Name for the built image", valueName: "name"))
|
||||
var targetImageNames: [String] = {
|
||||
[UUID().uuidString.lowercased()]
|
||||
@@ -258,6 +268,15 @@ extension Application {
|
||||
}
|
||||
}
|
||||
|
||||
let secretsData: [String: Data] = try self.secrets.mapValues { secret in
|
||||
switch secret {
|
||||
case .data(let data):
|
||||
return data
|
||||
case .file(let path):
|
||||
return try Data(contentsOf: URL(fileURLWithPath: path))
|
||||
}
|
||||
}
|
||||
|
||||
let systemHealth = try await ClientHealthCheck.ping(timeout: .seconds(10))
|
||||
let exportPath = systemHealth.appRoot
|
||||
.appendingPathComponent(Application.BuilderCommand.builderResourceDir)
|
||||
@@ -331,11 +350,12 @@ extension Application {
|
||||
}
|
||||
return results
|
||||
}()
|
||||
group.addTask { [terminal, buildArg, contextDir, hiddenDockerDir, label, noCache, target, quiet, cacheIn, cacheOut, pull] in
|
||||
group.addTask { [terminal, buildArg, secretsData, contextDir, hiddenDockerDir, label, noCache, target, quiet, cacheIn, cacheOut, pull] in
|
||||
let config = Builder.BuildConfig(
|
||||
buildID: buildID,
|
||||
contentStore: RemoteContentStoreClient(),
|
||||
buildArgs: buildArg,
|
||||
secrets: secretsData,
|
||||
contextDir: contextDir,
|
||||
dockerfile: buildFileData,
|
||||
hiddenDockerDir: hiddenDockerDir,
|
||||
@@ -463,6 +483,32 @@ extension Application {
|
||||
dockerfile = defaultDockerfile
|
||||
break
|
||||
}
|
||||
|
||||
// Parse --secret args
|
||||
for secret in self.secret {
|
||||
let parts = secret.split(separator: ",", maxSplits: 1, omittingEmptySubsequences: false)
|
||||
guard parts[0].hasPrefix("id=") else {
|
||||
throw ValidationError("secret must start with id=<key> \(secret)")
|
||||
}
|
||||
let key = String(parts[0].dropFirst(3))
|
||||
guard !key.contains("=") else {
|
||||
throw ValidationError("secret id cannot contain '=' \(key)")
|
||||
}
|
||||
if parts.count == 1 || parts[1].hasPrefix("env=") {
|
||||
let env = parts.count == 1 ? key : String(parts[1].dropFirst(4))
|
||||
// Using getenv/strlen over processInfo.environment to support
|
||||
// non-UTF-8 env var data.
|
||||
guard let ptr = getenv(env) else {
|
||||
throw ValidationError("secret env var doesn't exist \(env)")
|
||||
}
|
||||
self.secrets[key] = .data(Data(bytes: ptr, count: strlen(ptr)))
|
||||
} else if parts[1].hasPrefix("src=") {
|
||||
let path = String(parts[1].dropFirst(4))
|
||||
self.secrets[key] = .file(path)
|
||||
} else {
|
||||
throw ValidationError("secret bad value \(parts[1])")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,12 @@ class TestCLIBuildBase: CLITest {
|
||||
return tempDir
|
||||
}
|
||||
|
||||
func createTempFile(suffix: String, contents: Data) throws -> URL {
|
||||
let tempFile = testDir.appendingPathComponent(UUID().uuidString + suffix)
|
||||
try contents.write(to: tempFile, options: .atomic)
|
||||
return tempFile
|
||||
}
|
||||
|
||||
func createContext(tempDir: URL, dockerfile: String, context: [FileSystemEntry]? = nil) throws {
|
||||
let dockerfileBytes = dockerfile.data(using: .utf8)!
|
||||
try dockerfileBytes.write(to: tempDir.appendingPathComponent("Dockerfile"), options: .atomic)
|
||||
|
||||
@@ -163,6 +163,44 @@ extension TestCLIBuildBase {
|
||||
#expect(try self.inspectImage(imageName) == imageName, "expected to have successfully built \(imageName)")
|
||||
}
|
||||
|
||||
@Test func testBuildSecret() throws {
|
||||
let tempDir: URL = try createTempDir()
|
||||
let dockerfile: String =
|
||||
"""
|
||||
FROM ghcr.io/linuxcontainers/alpine:3.20
|
||||
RUN --mount=type=secret,id=ENV1 \
|
||||
--mount=type=secret,id=env2 \
|
||||
--mount=type=secret,id=env3 \
|
||||
test xyyzzz = "`cat /run/secrets/ENV1 /run/secrets/env2 /run/secrets/env3`"
|
||||
RUN --mount=type=secret,id=file \
|
||||
awk 'BEGIN {for(i=0; i<17; i++) for(c=0; c<256; c++) printf("%c", c)}' > /tmp/foo && \
|
||||
cmp /tmp/foo /run/secrets/file && \
|
||||
rm /tmp/foo
|
||||
RUN --mount=type=secret,id=empty \
|
||||
test \\! -e /run/secrets/file && \
|
||||
test -e /run/secrets/empty && \
|
||||
cmp /dev/null /run/secrets/empty
|
||||
"""
|
||||
try createContext(tempDir: tempDir, dockerfile: dockerfile)
|
||||
setenv("ENV1", "x", 1)
|
||||
setenv("ENV_VAR", "yy", 1)
|
||||
setenv("env3", "zzz", 1)
|
||||
let testData = Data((0..<17).flatMap { _ in Array(0...255) })
|
||||
let tempFile: URL = try createTempFile(suffix: " _f,i=l.e+ ", contents: testData)
|
||||
let tempFile2: URL = try createTempFile(suffix: "file2", contents: Data())
|
||||
let imageName: String = "registry.local/secrets:\(UUID().uuidString)"
|
||||
try self.build(
|
||||
tag: imageName, tempDir: tempDir,
|
||||
otherArgs: [
|
||||
"--secret", "id=ENV1",
|
||||
"--secret", "id=env2,env=ENV_VAR",
|
||||
"--secret", "id=env3,env=env3",
|
||||
"--secret", "id=file,src=" + tempFile.path,
|
||||
"--secret", "id=empty,src=" + tempFile2.path,
|
||||
])
|
||||
#expect(try self.inspectImage(imageName) == imageName, "expected to have successfully built \(imageName)")
|
||||
}
|
||||
|
||||
@Test func testBuildNetworkAccess() throws {
|
||||
let tempDir: URL = try createTempDir()
|
||||
let dockerfile: String =
|
||||
|
||||
@@ -144,6 +144,7 @@ container build [<options>] [<context-dir>]
|
||||
* `--progress <type>`: Progress type (format: auto|plain|tty) (default: auto)
|
||||
* `--pull`: Pull latest image
|
||||
* `-q, --quiet`: Suppress build output
|
||||
* `--secret <id=key,...>`: Set build-time secrets (format: id=<key>[,env=<ENV_VAR>|,src=<local/path>])
|
||||
* `-t, --tag <name>`: Name for the built image (can be specified multiple times)
|
||||
* `--target <stage>`: Set the target build stage
|
||||
* `--vsock-port <port>`: Builder shim vsock port (default: 8088)
|
||||
|
||||
Reference in New Issue
Block a user