did stuff
Some checks failed
Tests / 🛃 Unit tests (push) Failing after 13s
Tests / 🗄 DB tests (push) Failing after 19s
Tests / 🐍 Lint python scripts (push) Failing after 1s
Tests / ⌨ Golang Lint (push) Failing after 1s
Tests / 📦 Packit config lint (push) Failing after 1s
Tests / 🔍 Check source preparation (push) Failing after 1s
Tests / 🔍 Check for valid snapshot urls (push) Failing after 1s
Tests / 🔍 Check for missing or unused runner repos (push) Failing after 1s
Tests / 🐚 Shellcheck (push) Failing after 1s
Tests / 📦 RPMlint (push) Failing after 1s
Tests / Gitlab CI trigger helper (push) Failing after 1s
Tests / 🎀 kube-linter (push) Failing after 1s
Tests / 🧹 cloud-cleaner-is-enabled (push) Successful in 3s
Tests / 🔍 Check spec file osbuild/images dependencies (push) Failing after 1s
Some checks failed
Tests / 🛃 Unit tests (push) Failing after 13s
Tests / 🗄 DB tests (push) Failing after 19s
Tests / 🐍 Lint python scripts (push) Failing after 1s
Tests / ⌨ Golang Lint (push) Failing after 1s
Tests / 📦 Packit config lint (push) Failing after 1s
Tests / 🔍 Check source preparation (push) Failing after 1s
Tests / 🔍 Check for valid snapshot urls (push) Failing after 1s
Tests / 🔍 Check for missing or unused runner repos (push) Failing after 1s
Tests / 🐚 Shellcheck (push) Failing after 1s
Tests / 📦 RPMlint (push) Failing after 1s
Tests / Gitlab CI trigger helper (push) Failing after 1s
Tests / 🎀 kube-linter (push) Failing after 1s
Tests / 🧹 cloud-cleaner-is-enabled (push) Successful in 3s
Tests / 🔍 Check spec file osbuild/images dependencies (push) Failing after 1s
This commit is contained in:
parent
d228f6d30f
commit
4eeaa43c39
47 changed files with 21390 additions and 31 deletions
573
internal/container/bootc_generator.go
Normal file
573
internal/container/bootc_generator.go
Normal file
|
|
@ -0,0 +1,573 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type BootcGenerator struct {
|
||||
logger *logrus.Logger
|
||||
config *BootcConfig
|
||||
registry *ContainerRegistry
|
||||
signer *ContainerSigner
|
||||
validator *ContainerValidator
|
||||
}
|
||||
|
||||
type BootcConfig struct {
|
||||
BaseImage string `json:"base_image"`
|
||||
OutputDir string `json:"output_dir"`
|
||||
RegistryURL string `json:"registry_url"`
|
||||
SigningKey string `json:"signing_key"`
|
||||
Compression string `json:"compression"`
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
}
|
||||
|
||||
type ContainerRegistry struct {
|
||||
URL string `json:"url"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Insecure bool `json:"insecure"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
}
|
||||
|
||||
type ContainerSigner struct {
|
||||
PrivateKey string `json:"private_key"`
|
||||
PublicKey string `json:"public_key"`
|
||||
Algorithm string `json:"algorithm"`
|
||||
}
|
||||
|
||||
type ContainerValidator struct {
|
||||
SecurityScan bool `json:"security_scan"`
|
||||
PolicyCheck bool `json:"policy_check"`
|
||||
VulnerabilityScan bool `json:"vulnerability_scan"`
|
||||
}
|
||||
|
||||
type BootcContainer struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Tag string `json:"tag"`
|
||||
Digest string `json:"digest"`
|
||||
Size int64 `json:"size"`
|
||||
Architecture string `json:"architecture"`
|
||||
OS string `json:"os"`
|
||||
Variant string `json:"variant"`
|
||||
Layers []ContainerLayer `json:"layers"`
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Signed bool `json:"signed"`
|
||||
Signature string `json:"signature,omitempty"`
|
||||
}
|
||||
|
||||
type ContainerLayer struct {
|
||||
Digest string `json:"digest"`
|
||||
Size int64 `json:"size"`
|
||||
MediaType string `json:"media_type"`
|
||||
Created time.Time `json:"created"`
|
||||
}
|
||||
|
||||
type ContainerVariant struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Packages []string `json:"packages"`
|
||||
Services []string `json:"services"`
|
||||
Config map[string]interface{} `json:"config"`
|
||||
}
|
||||
|
||||
func NewBootcGenerator(config *BootcConfig, logger *logrus.Logger) *BootcGenerator {
|
||||
generator := &BootcGenerator{
|
||||
logger: logger,
|
||||
config: config,
|
||||
registry: NewContainerRegistry(),
|
||||
signer: NewContainerSigner(),
|
||||
validator: NewContainerValidator(),
|
||||
}
|
||||
|
||||
return generator
|
||||
}
|
||||
|
||||
func NewContainerRegistry() *ContainerRegistry {
|
||||
return &ContainerRegistry{
|
||||
URL: "localhost:5000",
|
||||
Insecure: true,
|
||||
Headers: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
func NewContainerSigner() *ContainerSigner {
|
||||
return &ContainerSigner{
|
||||
Algorithm: "sha256",
|
||||
}
|
||||
}
|
||||
|
||||
func NewContainerValidator() *ContainerValidator {
|
||||
return &ContainerValidator{
|
||||
SecurityScan: true,
|
||||
PolicyCheck: true,
|
||||
VulnerabilityScan: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (bg *BootcGenerator) GenerateContainer(blueprint *Blueprint, variant string) (*BootcContainer, error) {
|
||||
bg.logger.Infof("Generating bootc container for blueprint: %s, variant: %s", blueprint.Name, variant)
|
||||
|
||||
// Create container structure
|
||||
container := &BootcContainer{
|
||||
ID: generateContainerID(),
|
||||
Name: fmt.Sprintf("%s-%s", blueprint.Name, variant),
|
||||
Tag: "latest",
|
||||
Architecture: blueprint.Architecture,
|
||||
OS: "linux",
|
||||
Variant: variant,
|
||||
Layers: []ContainerLayer{},
|
||||
Metadata: make(map[string]interface{}),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Generate container layers
|
||||
if err := bg.generateLayers(container, blueprint, variant); err != nil {
|
||||
return nil, fmt.Errorf("failed to generate layers: %w", err)
|
||||
}
|
||||
|
||||
// Build container image
|
||||
if err := bg.buildContainer(container); err != nil {
|
||||
return nil, fmt.Errorf("failed to build container: %w", err)
|
||||
}
|
||||
|
||||
// Sign container if configured
|
||||
if bg.config.SigningKey != "" {
|
||||
if err := bg.signContainer(container); err != nil {
|
||||
bg.logger.Warnf("Failed to sign container: %v", err)
|
||||
} else {
|
||||
container.Signed = true
|
||||
}
|
||||
}
|
||||
|
||||
// Validate container
|
||||
if err := bg.validateContainer(container); err != nil {
|
||||
bg.logger.Warnf("Container validation failed: %v", err)
|
||||
}
|
||||
|
||||
// Push to registry if configured
|
||||
if bg.config.RegistryURL != "" {
|
||||
if err := bg.pushToRegistry(container); err != nil {
|
||||
bg.logger.Warnf("Failed to push to registry: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
bg.logger.Infof("Successfully generated container: %s", container.ID)
|
||||
return container, nil
|
||||
}
|
||||
|
||||
func (bg *BootcGenerator) generateLayers(container *BootcContainer, blueprint *Blueprint, variant string) error {
|
||||
// Create base layer
|
||||
baseLayer := ContainerLayer{
|
||||
Digest: generateDigest(),
|
||||
Size: 0,
|
||||
MediaType: "application/vnd.oci.image.layer.v1.tar+gzip",
|
||||
Created: time.Now(),
|
||||
}
|
||||
container.Layers = append(container.Layers, baseLayer)
|
||||
|
||||
// Create package layer
|
||||
packageLayer := ContainerLayer{
|
||||
Digest: generateDigest(),
|
||||
Size: 0,
|
||||
MediaType: "application/vnd.oci.image.layer.v1.tar+gzip",
|
||||
Created: time.Now(),
|
||||
}
|
||||
container.Layers = append(container.Layers, packageLayer)
|
||||
|
||||
// Create configuration layer
|
||||
configLayer := ContainerLayer{
|
||||
Digest: generateDigest(),
|
||||
Size: 0,
|
||||
MediaType: "application/vnd.oci.image.layer.v1.tar+gzip",
|
||||
Created: time.Now(),
|
||||
}
|
||||
container.Layers = append(container.Layers, configLayer)
|
||||
|
||||
// Set metadata
|
||||
container.Metadata["blueprint"] = blueprint.Name
|
||||
container.Metadata["variant"] = variant
|
||||
container.Metadata["packages"] = blueprint.Packages.Include
|
||||
container.Metadata["users"] = blueprint.Users
|
||||
container.Metadata["customizations"] = blueprint.Customizations
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bg *BootcGenerator) buildContainer(container *BootcContainer) error {
|
||||
// Create output directory
|
||||
outputDir := filepath.Join(bg.config.OutputDir, container.Name)
|
||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create output directory: %w", err)
|
||||
}
|
||||
|
||||
// Generate Dockerfile
|
||||
dockerfilePath := filepath.Join(outputDir, "Dockerfile")
|
||||
if err := bg.generateDockerfile(dockerfilePath, container); err != nil {
|
||||
return fmt.Errorf("failed to generate Dockerfile: %w", err)
|
||||
}
|
||||
|
||||
// Build container image
|
||||
if err := bg.dockerBuild(outputDir, container); err != nil {
|
||||
return fmt.Errorf("failed to build container: %w", err)
|
||||
}
|
||||
|
||||
// Calculate final size
|
||||
container.Size = bg.calculateContainerSize(container)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bg *BootcGenerator) generateDockerfile(path string, container *BootcContainer) error {
|
||||
content := fmt.Sprintf(`# Debian Bootc Container: %s
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
# Set metadata
|
||||
LABEL org.opencontainers.image.title="%s"
|
||||
LABEL org.opencontainers.image.description="Debian atomic container for %s"
|
||||
LABEL org.opencontainers.image.vendor="Debian Forge"
|
||||
LABEL org.opencontainers.image.version="1.0.0"
|
||||
|
||||
# Install packages
|
||||
RUN apt-get update && apt-get install -y \\
|
||||
%s \\
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create users
|
||||
%s
|
||||
|
||||
# Set customizations
|
||||
%s
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /root
|
||||
|
||||
# Default command
|
||||
CMD ["/bin/bash"]
|
||||
`, container.Name, container.Name, container.Variant,
|
||||
strings.Join(container.Metadata["packages"].([]string), " \\\n "),
|
||||
bg.generateUserCommands(container),
|
||||
bg.generateCustomizationCommands(container))
|
||||
|
||||
return os.WriteFile(path, []byte(content), 0644)
|
||||
}
|
||||
|
||||
func (bg *BootcGenerator) generateUserCommands(container *BootcContainer) string {
|
||||
var commands []string
|
||||
|
||||
if users, ok := container.Metadata["users"].([]BlueprintUser); ok {
|
||||
for _, user := range users {
|
||||
commands = append(commands, fmt.Sprintf("RUN useradd -m -s %s %s", user.Shell, user.Name))
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(commands, "\n")
|
||||
}
|
||||
|
||||
func (bg *BootcGenerator) generateCustomizationCommands(container *BootcContainer) string {
|
||||
var commands []string
|
||||
|
||||
if customizations, ok := container.Metadata["customizations"].(BlueprintCustomizations); ok {
|
||||
if customizations.Hostname != "" {
|
||||
commands = append(commands, fmt.Sprintf("RUN echo '%s' > /etc/hostname", customizations.Hostname))
|
||||
}
|
||||
if customizations.Timezone != "" {
|
||||
commands = append(commands, fmt.Sprintf("RUN ln -sf /usr/share/zoneinfo/%s /etc/localtime", customizations.Timezone))
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(commands, "\n")
|
||||
}
|
||||
|
||||
func (bg *BootcGenerator) dockerBuild(context string, container *BootcContainer) error {
|
||||
// Build command
|
||||
cmd := exec.Command("docker", "build", "-t", container.Name+":"+container.Tag, context)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("docker build failed: %w", err)
|
||||
}
|
||||
|
||||
// Get image digest
|
||||
digestCmd := exec.Command("docker", "images", "--digests", "--format", "{{.Digest}}", container.Name+":"+container.Tag)
|
||||
digestOutput, err := digestCmd.Output()
|
||||
if err != nil {
|
||||
bg.logger.Warnf("Failed to get image digest: %v", err)
|
||||
} else {
|
||||
container.Digest = strings.TrimSpace(string(digestOutput))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bg *BootcGenerator) calculateContainerSize(container *BootcContainer) int64 {
|
||||
// Get image size from Docker
|
||||
cmd := exec.Command("docker", "images", "--format", "{{.Size}}", container.Name+":"+container.Tag)
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
bg.logger.Warnf("Failed to get image size: %v", err)
|
||||
return 0
|
||||
}
|
||||
|
||||
// Parse size (e.g., "123.4MB" -> 123400000)
|
||||
sizeStr := strings.TrimSpace(string(output))
|
||||
// Simple size parsing - in production, use proper size parsing library
|
||||
return 0 // Placeholder
|
||||
}
|
||||
|
||||
func (bg *BootcGenerator) signContainer(container *BootcContainer) error {
|
||||
if bg.signer.PrivateKey == "" {
|
||||
return fmt.Errorf("no signing key configured")
|
||||
}
|
||||
|
||||
// Use cosign to sign the container
|
||||
cmd := exec.Command("cosign", "sign", "--key", bg.signer.PrivateKey,
|
||||
container.Name+":"+container.Tag)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("cosign signing failed: %w", err)
|
||||
}
|
||||
|
||||
// Get signature
|
||||
signatureCmd := exec.Command("cosign", "verify", "--key", bg.signer.PublicKey,
|
||||
container.Name+":"+container.Tag)
|
||||
signatureOutput, err := signatureCmd.Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to verify signature: %w", err)
|
||||
}
|
||||
|
||||
container.Signature = strings.TrimSpace(string(signatureOutput))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bg *BootcGenerator) validateContainer(container *BootcContainer) error {
|
||||
if !bg.validator.SecurityScan {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run security scan
|
||||
if err := bg.runSecurityScan(container); err != nil {
|
||||
return fmt.Errorf("security scan failed: %w", err)
|
||||
}
|
||||
|
||||
// Run policy check
|
||||
if bg.validator.PolicyCheck {
|
||||
if err := bg.runPolicyCheck(container); err != nil {
|
||||
return fmt.Errorf("policy check failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Run vulnerability scan
|
||||
if bg.validator.VulnerabilityScan {
|
||||
if err := bg.runVulnerabilityScan(container); err != nil {
|
||||
return fmt.Errorf("vulnerability scan failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bg *BootcGenerator) runSecurityScan(container *BootcContainer) error {
|
||||
// Use Trivy for security scanning
|
||||
cmd := exec.Command("trivy", "image", "--format", "json",
|
||||
container.Name+":"+container.Tag)
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("trivy scan failed: %w", err)
|
||||
}
|
||||
|
||||
// Parse scan results
|
||||
var scanResults map[string]interface{}
|
||||
if err := json.Unmarshal(output, &scanResults); err != nil {
|
||||
return fmt.Errorf("failed to parse scan results: %w", err)
|
||||
}
|
||||
|
||||
// Check for high/critical vulnerabilities
|
||||
if vulnerabilities, ok := scanResults["Vulnerabilities"].([]interface{}); ok {
|
||||
for _, vuln := range vulnerabilities {
|
||||
if vulnMap, ok := vuln.(map[string]interface{}); ok {
|
||||
if severity, ok := vulnMap["Severity"].(string); ok {
|
||||
if severity == "HIGH" || severity == "CRITICAL" {
|
||||
bg.logger.Warnf("High/Critical vulnerability found: %v", vulnMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bg *BootcGenerator) runPolicyCheck(container *BootcContainer) error {
|
||||
// Use Open Policy Agent for policy checking
|
||||
// This is a placeholder - implement actual policy checking
|
||||
bg.logger.Info("Running policy check on container")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bg *BootcGenerator) runVulnerabilityScan(container *BootcContainer) error {
|
||||
// Use Grype for vulnerability scanning
|
||||
cmd := exec.Command("grype", "--output", "json", container.Name+":"+container.Tag)
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("grype scan failed: %w", err)
|
||||
}
|
||||
|
||||
// Parse vulnerability results
|
||||
var vulnResults map[string]interface{}
|
||||
if err := json.Unmarshal(output, &vulnResults); err != nil {
|
||||
return fmt.Errorf("failed to parse vulnerability results: %w", err)
|
||||
}
|
||||
|
||||
// Process vulnerabilities
|
||||
bg.logger.Infof("Vulnerability scan completed for container: %s", container.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bg *BootcGenerator) pushToRegistry(container *BootcContainer) error {
|
||||
// Tag for registry
|
||||
registryTag := fmt.Sprintf("%s/%s:%s", bg.config.RegistryURL, container.Name, container.Tag)
|
||||
|
||||
// Tag image
|
||||
tagCmd := exec.Command("docker", "tag", container.Name+":"+container.Tag, registryTag)
|
||||
if err := tagCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to tag image: %w", err)
|
||||
}
|
||||
|
||||
// Push to registry
|
||||
pushCmd := exec.Command("docker", "push", registryTag)
|
||||
pushCmd.Stdout = os.Stdout
|
||||
pushCmd.Stderr = os.Stderr
|
||||
|
||||
if err := pushCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to push image: %w", err)
|
||||
}
|
||||
|
||||
bg.logger.Infof("Successfully pushed container to registry: %s", registryTag)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bg *BootcGenerator) ListContainerVariants() []ContainerVariant {
|
||||
return []ContainerVariant{
|
||||
{
|
||||
Name: "minimal",
|
||||
Description: "Minimal Debian system without desktop environment",
|
||||
Packages: []string{"task-minimal", "systemd", "systemd-sysv"},
|
||||
Services: []string{"systemd-sysctl", "systemd-random-seed"},
|
||||
Config: map[string]interface{}{
|
||||
"hostname": "debian-minimal",
|
||||
"timezone": "UTC",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "desktop",
|
||||
Description: "Debian with desktop environment",
|
||||
Packages: []string{"task-gnome-desktop", "gnome-core", "systemd"},
|
||||
Services: []string{"gdm", "systemd-sysctl"},
|
||||
Config: map[string]interface{}{
|
||||
"hostname": "debian-desktop",
|
||||
"timezone": "UTC",
|
||||
"desktop": "gnome",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "server",
|
||||
Description: "Server-optimized Debian system",
|
||||
Packages: []string{"task-server", "systemd", "openssh-server"},
|
||||
Services: []string{"ssh", "systemd-sysctl"},
|
||||
Config: map[string]interface{}{
|
||||
"hostname": "debian-server",
|
||||
"timezone": "UTC",
|
||||
"ssh": true,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "development",
|
||||
Description: "Development environment Debian system",
|
||||
Packages: []string{"build-essential", "git", "python3", "nodejs"},
|
||||
Services: []string{"systemd-sysctl"},
|
||||
Config: map[string]interface{}{
|
||||
"hostname": "debian-dev",
|
||||
"timezone": "UTC",
|
||||
"dev_tools": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (bg *BootcGenerator) GetContainerInfo(containerID string) (*BootcContainer, error) {
|
||||
// Get container information from Docker
|
||||
cmd := exec.Command("docker", "inspect", containerID)
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to inspect container: %w", err)
|
||||
}
|
||||
|
||||
// Parse inspect output
|
||||
var inspectResults []map[string]interface{}
|
||||
if err := json.Unmarshal(output, &inspectResults); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse inspect results: %w", err)
|
||||
}
|
||||
|
||||
if len(inspectResults) == 0 {
|
||||
return nil, fmt.Errorf("container not found")
|
||||
}
|
||||
|
||||
// Convert to our container structure
|
||||
container := &BootcContainer{
|
||||
ID: containerID,
|
||||
Name: inspectResults[0]["Name"].(string),
|
||||
CreatedAt: time.Now(), // Parse from inspect results
|
||||
Metadata: make(map[string]interface{}),
|
||||
}
|
||||
|
||||
return container, nil
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
func generateContainerID() string {
|
||||
bytes := make([]byte, 16)
|
||||
rand.Read(bytes)
|
||||
return fmt.Sprintf("%x", bytes)
|
||||
}
|
||||
|
||||
func generateDigest() string {
|
||||
bytes := make([]byte, 32)
|
||||
rand.Read(bytes)
|
||||
return fmt.Sprintf("sha256:%x", bytes)
|
||||
}
|
||||
|
||||
// Blueprint types (imported from blueprintapi)
|
||||
type Blueprint struct {
|
||||
Name string `json:"name"`
|
||||
Packages BlueprintPackages `json:"packages"`
|
||||
Users []BlueprintUser `json:"users"`
|
||||
Customizations BlueprintCustomizations `json:"customizations"`
|
||||
}
|
||||
|
||||
type BlueprintPackages struct {
|
||||
Include []string `json:"include"`
|
||||
}
|
||||
|
||||
type BlueprintUser struct {
|
||||
Name string `json:"name"`
|
||||
Shell string `json:"shell"`
|
||||
}
|
||||
|
||||
type BlueprintCustomizations struct {
|
||||
Hostname string `json:"hostname"`
|
||||
Timezone string `json:"timezone"`
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue