mirror of
https://github.com/apple/container.git
synced 2026-07-15 05:57:01 +00:00
c21068661f
Closes https://github.com/apple/container/issues/430 Signed-off-by: Kathryn Baldauf <k_baldauf@apple.com>
328 lines
13 KiB
Swift
328 lines
13 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// https://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
import ContainerBuildIR
|
|
import ContainerBuildReporting
|
|
import Foundation
|
|
|
|
/// Example demonstrating how to build an IR graph programmatically.
|
|
///
|
|
/// This example creates a multi-stage Node.js application build.
|
|
public enum IRExample {
|
|
|
|
/// Create a simple single-stage build.
|
|
public static func createSimpleBuild() throws -> BuildGraph {
|
|
guard let baseImage = ImageReference(parsing: "ubuntu:22.04") else {
|
|
throw ReferenceError.invalidFormat("ubuntu:22.04")
|
|
}
|
|
|
|
return try GraphBuilder.singleStage(
|
|
from: baseImage,
|
|
platform: .linuxAMD64
|
|
) { builder in
|
|
try builder
|
|
.run("apt-get update && apt-get install -y curl")
|
|
.workdir("/app")
|
|
.copyFromContext(paths: ["package.json", "src/"], to: "/app/")
|
|
.run("npm install")
|
|
.env("NODE_ENV", "production")
|
|
.expose([PortSpec(port: 3000)])
|
|
.cmd(Command.exec(["node", "src/index.js"]))
|
|
}
|
|
}
|
|
|
|
/// Create a realistic multi-stage build showing actual parallelism
|
|
public static func createParallelBuild(reporter: Reporter? = nil) throws -> BuildGraph {
|
|
// In container builds, true parallelism happens between stages or with independent resources
|
|
|
|
guard let nodeImage = ImageReference(parsing: "node:18-alpine"),
|
|
let goImage = ImageReference(parsing: "golang:1.21-alpine"),
|
|
let alpineImage = ImageReference(parsing: "alpine:3.18")
|
|
else {
|
|
throw ReferenceError.invalidFormat("Invalid image reference")
|
|
}
|
|
|
|
return try GraphBuilder.multiStage(reporter: reporter) { builder in
|
|
// Stage 1: Build frontend assets
|
|
try builder
|
|
.stage(name: "frontend-builder", from: nodeImage)
|
|
.workdir("/frontend")
|
|
.copyFromContext(paths: ["frontend/package*.json"], to: "./")
|
|
.run("npm ci")
|
|
.copyFromContext(paths: ["frontend/"], to: "./")
|
|
.run("npm run build")
|
|
|
|
// Stage 2: Build backend
|
|
try builder
|
|
.stage(name: "backend-builder", from: goImage)
|
|
.workdir("/backend")
|
|
.copyFromContext(paths: ["go.mod", "go.sum"], to: "./")
|
|
.run("go mod download")
|
|
.copyFromContext(paths: ["*.go", "cmd/", "internal/"], to: "./")
|
|
.run("CGO_ENABLED=0 go build -o server ./cmd/server")
|
|
|
|
// Stage 3: Runtime - depends on both builders
|
|
try builder
|
|
.stage(name: "runtime", from: alpineImage)
|
|
.copyFromStage(.named("frontend-builder"), paths: ["/frontend/dist"], to: "/app/static")
|
|
.copyFromStage(.named("backend-builder"), paths: ["/backend/server"], to: "/app/server")
|
|
.run("chmod +x /app/server")
|
|
.expose([PortSpec(port: 8080)])
|
|
.cmd(.exec(["/app/server"]))
|
|
}
|
|
}
|
|
|
|
/// Create a multi-stage build for a Go application.
|
|
public static func createMultiStageBuild() throws -> BuildGraph {
|
|
try GraphBuilder.multiStage { builder in
|
|
// Build stage
|
|
guard let builderImage = ImageReference(parsing: "golang:1.21-alpine") else {
|
|
throw ReferenceError.invalidFormat("golang:1.21-alpine")
|
|
}
|
|
|
|
try builder
|
|
.stage(
|
|
name: "builder",
|
|
from: builderImage
|
|
)
|
|
.workdir("/build")
|
|
.copyFromContext(paths: ["go.mod", "go.sum"], to: "./")
|
|
.run("go mod download")
|
|
.copyFromContext(paths: ["*.go"], to: "./")
|
|
.run("CGO_ENABLED=0 go build -o app")
|
|
|
|
// Runtime stage
|
|
try builder
|
|
.scratch(name: "runtime")
|
|
.copyFromStage(
|
|
.named("builder"),
|
|
paths: ["/build/app"],
|
|
to: "/app",
|
|
chmod: .mode(0o755)
|
|
)
|
|
.copyFromStage(
|
|
.named("builder"),
|
|
paths: ["/etc/ssl/certs/ca-certificates.crt"],
|
|
to: "/etc/ssl/certs/"
|
|
)
|
|
.user(.uid(1000))
|
|
.entrypoint(.exec(["/app"]))
|
|
}
|
|
}
|
|
|
|
/// Create a Python application with best practices.
|
|
public static func createPythonBuild() throws -> BuildGraph {
|
|
let graph = try BuildGraph(
|
|
stages: [
|
|
// Dependencies stage
|
|
BuildStage(
|
|
name: "dependencies",
|
|
base: {
|
|
guard let pythonImage = ImageReference(parsing: "python:3.11-slim") else {
|
|
throw ReferenceError.invalidFormat("python:3.11-slim")
|
|
}
|
|
return ImageOperation(
|
|
source: .registry(pythonImage),
|
|
platform: .linuxAMD64
|
|
)
|
|
}(),
|
|
nodes: [
|
|
BuildNode(
|
|
operation: MetadataOperation(
|
|
action: .setWorkdir(path: "/app")
|
|
)
|
|
),
|
|
BuildNode(
|
|
operation: FilesystemOperation(
|
|
action: .copy,
|
|
source: .context(ContextSource(paths: ["requirements.txt"])),
|
|
destination: "/app/"
|
|
)
|
|
),
|
|
BuildNode(
|
|
operation: ExecOperation(
|
|
command: .shell("pip install --user --no-cache-dir -r requirements.txt")
|
|
)
|
|
),
|
|
]
|
|
),
|
|
|
|
// Application stage
|
|
BuildStage(
|
|
name: "app",
|
|
base: {
|
|
guard let pythonImage = ImageReference(parsing: "python:3.11-slim") else {
|
|
throw ReferenceError.invalidFormat("python:3.11-slim")
|
|
}
|
|
return ImageOperation(
|
|
source: .registry(pythonImage),
|
|
platform: .linuxAMD64
|
|
)
|
|
}(),
|
|
nodes: [
|
|
// Copy dependencies from first stage
|
|
BuildNode(
|
|
operation: FilesystemOperation(
|
|
action: .copy,
|
|
source: .stage(.named("dependencies"), paths: ["/root/.local"]),
|
|
destination: "/root/.local"
|
|
)
|
|
),
|
|
// Set up application
|
|
BuildNode(
|
|
operation: MetadataOperation(
|
|
action: .setWorkdir(path: "/app")
|
|
)
|
|
),
|
|
BuildNode(
|
|
operation: FilesystemOperation(
|
|
action: .copy,
|
|
source: .context(ContextSource(paths: ["*.py", "src/"])),
|
|
destination: "/app/"
|
|
)
|
|
),
|
|
// Configure runtime
|
|
BuildNode(
|
|
operation: MetadataOperation(
|
|
action: .setEnv(
|
|
key: "PYTHONPATH",
|
|
value: .literal("/root/.local/lib/python3.11/site-packages")
|
|
)
|
|
)
|
|
),
|
|
BuildNode(
|
|
operation: MetadataOperation(
|
|
action: .setUser(user: .uidGid(uid: 1000, gid: 1000))
|
|
)
|
|
),
|
|
BuildNode(
|
|
operation: MetadataOperation(
|
|
action: .expose(ports: [PortSpec(port: 8000)])
|
|
)
|
|
),
|
|
BuildNode(
|
|
operation: MetadataOperation(
|
|
action: .setHealthcheck(
|
|
healthcheck: Healthcheck(
|
|
test: .shell("curl -f http://localhost:8000/health || exit 1"),
|
|
interval: 30,
|
|
timeout: 3,
|
|
retries: 3
|
|
)
|
|
)
|
|
)
|
|
),
|
|
BuildNode(
|
|
operation: MetadataOperation(
|
|
action: .setCmd(command: .exec(["python", "main.py"]))
|
|
)
|
|
),
|
|
]
|
|
),
|
|
],
|
|
targetPlatforms: [.linuxAMD64, .linuxARM64]
|
|
)
|
|
|
|
return graph
|
|
}
|
|
|
|
/// Demonstrate advanced features like cache mounts and secrets.
|
|
public static func createAdvancedBuild() throws -> BuildGraph {
|
|
guard let nodeImage = ImageReference(parsing: "node:18-alpine") else {
|
|
throw ReferenceError.invalidFormat("node:18-alpine")
|
|
}
|
|
|
|
return try GraphBuilder.singleStage(
|
|
from: nodeImage
|
|
) { builder in
|
|
try builder
|
|
.workdir("/app")
|
|
|
|
// Cache mount for package manager
|
|
.run(
|
|
"npm ci",
|
|
mounts: [
|
|
Mount(
|
|
type: .cache,
|
|
target: "/root/.npm",
|
|
options: MountOptions(sharing: .shared)
|
|
)
|
|
]
|
|
)
|
|
|
|
// Secret mount for private registry
|
|
.run(
|
|
"npm install @private/package",
|
|
mounts: [
|
|
Mount(
|
|
type: .secret,
|
|
target: "/root/.npmrc",
|
|
source: .secret("npm-token"),
|
|
options: MountOptions(readOnly: true, mode: 0o600)
|
|
)
|
|
]
|
|
)
|
|
|
|
// Build with tmpfs for temporary files
|
|
.run(
|
|
"npm run build",
|
|
mounts: [
|
|
Mount(
|
|
type: .tmpfs,
|
|
target: "/tmp",
|
|
options: MountOptions(size: 1024 * 1024 * 1024) // 1GB
|
|
)
|
|
]
|
|
)
|
|
|
|
// Multi-platform metadata
|
|
.label("org.opencontainers.image.source", "https://github.com/example/app")
|
|
.label("org.opencontainers.image.version", "${VERSION}")
|
|
.label("org.opencontainers.image.created", "${BUILD_DATE}")
|
|
}
|
|
}
|
|
|
|
/// Validate and analyze a build graph.
|
|
public static func analyzeGraph(_ graph: BuildGraph) {
|
|
// Validate
|
|
let validator = StandardValidator()
|
|
let validationResult = validator.validate(graph)
|
|
|
|
print("Validation Results:")
|
|
print(" Errors: \(validationResult.errors.count)")
|
|
for error in validationResult.errors {
|
|
print(" - \(error)")
|
|
}
|
|
print(" Warnings: \(validationResult.warnings.count)")
|
|
for warning in validationResult.warnings {
|
|
print(" - \(warning.message)")
|
|
if let suggestion = warning.suggestion {
|
|
print(" Suggestion: \(suggestion)")
|
|
}
|
|
}
|
|
|
|
// Analyze with reporter
|
|
print("\nSemantic Analysis:")
|
|
|
|
// Graph statistics
|
|
let stats = graph.analyze()
|
|
print("\nGraph Statistics:")
|
|
print(" Stages: \(stats.stageCount)")
|
|
print(" Operations: \(stats.operationCount)")
|
|
print(" Critical Path: \(stats.criticalPathLength) operations")
|
|
}
|
|
}
|