diff --git a/README.md b/README.md index a44458b3..fa782fb8 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,32 @@ from different Operating System Vendors (OSVs). The ICT is developed in `golang` and is initially targeting to build custom images for [EMT](https://github.com/open-edge-platform/edge-microvisor-toolkit) (Edge Microvisor Toolkit), [Azure Linux](https://github.com/microsoft/azurelinux) -and WindRiver Linux. +and Wind River eLxr. + +## Documentation + +- [📖 CLI Specification](./image-composer-cli-specification.md) - Complete command-line reference and usage guide +- [🔧 Build Process](./image-composer-build-process.md) - Understanding the five-stage build pipeline +- [⚡ Caching](./image-composer-caching.md) - Package and image caching for performance +- [📋 Templates](./image-composer-templates.md) - Creating and using reusable image templates + +## Quick Start + +```bash +# Build the tool +go build ./cmd/image-composer + +# Or run directly +go run ./cmd/image-composer --help + +# Build an image from template +./image-composer build image-templates/azl3-x86_64-edge-raw.yml + +# Validate a template +./image-composer validate image-templates/azl3-x86_64-edge-raw.yml +``` + +For complete usage instructions, see the [CLI Specification](./image-composer-cli-specification.md). ## Get Started @@ -46,7 +71,7 @@ The Earthly build automatically includes: ### Global Configuration -Image Composer Tool supports global configuration files to set tool-level parameters that apply across all image builds. Image-specific parameters should still be defined in the JSON specification files. +Image Composer Tool supports global configuration files to set tool-level parameters that apply across all image builds. Image-specific parameters are defined in YAML image template files. #### Configuration File Locations @@ -86,7 +111,7 @@ logging: ./image-composer config show # Use specific configuration file -./image-composer --config /path/to/config.yaml build spec.json +./image-composer --config /path/to/config.yaml build template.yml ``` ### Usage @@ -97,14 +122,14 @@ The Image Composer Tool uses a command-line interface with various commands: # Show help ./image-composer --help -# Build command with spec file as positional argument -./image-composer build testdata/valid.json +# Build command with template file as positional argument +./image-composer build image-templates/azl3-x86_64-edge-raw.yml # Override config settings with command-line flags -./image-composer build --workers 16 --cache-dir /tmp/cache testdata/valid.json +./image-composer build --workers 16 --cache-dir /tmp/cache image-templates/azl3-x86_64-edge-raw.yml -# Validate a spec file against the schema -./image-composer validate testdata/valid.json +# Validate a template file against the schema +./image-composer validate image-templates/azl3-x86_64-edge-raw.yml # Display version information ./image-composer version @@ -119,10 +144,10 @@ The Image Composer Tool provides the following commands: #### build -Builds a Linux distribution image based on the specified spec file: +Builds a Linux distribution image based on the specified image template file: ```bash -./image-composer build [flags] SPEC_FILE +./image-composer build [flags] TEMPLATE_FILE ``` Flags: @@ -138,7 +163,7 @@ Flags: Example: ```bash -./image-composer build --workers 12 --cache-dir ./package-cache testdata/valid.json +./image-composer build --workers 12 --cache-dir ./package-cache image-templates/azl3-x86_64-edge-raw.yml ``` #### config @@ -155,13 +180,13 @@ Manages global configuration: #### validate -Validates a JSON spec file against the schema without building an image: +Validates a YAML template file against the schema without building an image: ```bash -./image-composer validate SPEC_FILE +./image-composer validate TEMPLATE_FILE ``` -This is useful for verifying configurations before starting the potentially time-consuming build process. +This is useful for verifying template configurations before starting the potentially time-consuming build process. #### version @@ -220,99 +245,102 @@ image-composer build --[TAB] See the [Shell Completion](#shell-completion) section for more details. -### User Input JSON +### Image Template Format -This section provides a detailed explanation of the JSON input structure used to configure and build a Linux-based operating system. The JSON format allows users to define key parameters such as the operating system distribution, version, architecture, software packages, output format, immutability, and kernel configuration. By customizing these parameters, users can create tailored Linux OS builds, including distributions like Ubuntu, Wind River, and Edge Microvisor Toolkits. +Image templates are written in YAML format and define the requirements for building a specific OS image. The template structure allows users to define key parameters such as the operating system distribution, version, architecture, software packages, output format, and kernel configuration. -```json -{ - "distro": "eLxr", - "version": "12", - "arch": "x86_64", - "packages": [ - "cloud-init", - "cloud-utils-growpart", - "dhcpcd", - "grubby", - "hyperv-daemons", - "netplan", - "python3", - "rsyslog", - "sgx-backwards-compatibility", - "WALinuxAgent", - "wireless-regdb" - ], - "immutable": true, - "output": "iso", - "kernel": { - "version": "5.10.0", - "cmdline": "quiet splash" - } -} +```yaml +image: + name: azl3-x86_64-edge + version: "1.0.0" + +target: + os: azure-linux # Target OS name + dist: azl3 # Target OS distribution + arch: x86_64 # Target OS architecture + imageType: raw # Image type: raw, iso, img, vhd + +systemConfigs: + - name: edge + description: Default configuration for edge image + + # Package Configuration + packages: + # Additional packages beyond the base system + - openssh-server # Remote access + - docker-ce # Container runtime + - vim # Text editor + - curl # HTTP client + - wget # File downloader + + # Kernel Configuration + kernel: + version: "6.12" + cmdline: "quiet splash" ``` #### Key Components -##### 1. `distro` - -**Description:** Specifies the target Linux distribution to be built. -**Examples:** - -- `AzureLinux` -- `eLxr` - -##### 2. `version` - -**Description:** Defines the version of the target operating system. -**Example:** `"12"` +##### 1. `image` -##### 3. `arch` +**Description:** Basic image identification and metadata. +- `name`: Name of the resulting image +- `version`: Version for tracking and naming -**Description:** Specifies the architecture of the target operating system. -**Examples:** +##### 2. `target` -- `x86_64` -- `arm64` +**Description:** Defines the target OS and image configuration. +- `os`: Target operating system (`azure-linux`, `emt`, `elxr`) +- `dist`: Distribution identifier (`azl3`, `emt3`, `elxr12`) +- `arch`: Target architecture (`x86_64`, `aarch64`) +- `imageType`: Output format (`raw`, `iso`, `img`, `vhd`) -##### 4. `packages` +##### 3. `systemConfigs` -**Description:** A list of software packages to be included in the OS build. These packages will be pre-installed in the resulting image. -**Examples:** +**Description:** Array of system configurations that define what goes into the image. +- `name`: Configuration name +- `description`: Human-readable description +- `packages`: List of packages to include in the OS build +- `kernel`: Kernel configuration with version and command-line parameters -- `cloud-init`: Used for initializing cloud instances. -- `python3`: The Python 3 programming language interpreter. -- `rsyslog`: A logging system for Linux. +#### Supported Distributions -##### 5. `immutable` +| OS | Distribution | Version | Provider | +|----|-------------|---------|----------| +| azure-linux | azl3 | 3 | AzureLinux3 | +| emt | emt3 | 3.0 | EMT3.0 | +| elxr | elxr12 | 12 | eLxr12 | -**Description:** Indicates whether the operating system should be immutable. -**Values:** +#### Package Examples -- `true`: The OS is immutable, meaning it cannot be modified after creation. -- `false`: The OS is mutable, allowing modifications after creation. +Common packages that can be included: +- `cloud-init`: Used for initializing cloud instances +- `python3`: The Python 3 programming language interpreter +- `rsyslog`: A logging system for Linux +- `openssh-server`: SSH server for remote access +- `docker-ce`: Docker container runtime -##### 6. `output` +The image template format is validated against a JSON schema to ensure correctness before building. -**Description:** Specifies the format of the output build. -**Values:** +### Legacy JSON Format -- `iso`: The OS will be built as an ISO file, suitable for installation or booting. -- `raw`: The OS will be built as a raw disk image, useful for direct disk writing. -- `vhd`: The OS will be built as a VHD (Virtual Hard Disk) file, commonly used in virtual environments. +**Note:** The tool previously supported JSON build specifications but now exclusively uses YAML image templates. The JSON format has been removed to simplify the architecture and improve maintainability. -##### 7. `kernel` +For reference, the old JSON format looked like this: -**Description:** Defines the kernel version and allows customization of the kernel command line. -**Attributes:** - -- `version`: Specifies the kernel version to be used. - **Example:** `"5.10.0"` -- `cmdline`: Provides additional kernel command-line parameters. - **Example:** `"quiet splash"` +```json +{ + "distro": "eLxr", + "version": "12", + "arch": "x86_64", + "packages": ["cloud-init", "rsyslog"], + "immutable": true, + "output": "iso", + "kernel": { "version": "5.10.0", "cmdline": "quiet splash" } +} +``` -Run the sample JSON files against the defined [schema](schema/os-image-composer.schema.json). -There are two sample JSON files, one [valid](/testdata/valid.json) and one with -[invalid](testdata/invalid.json) content. +This has been replaced with the more flexible and intuitive YAML template format shown above. ### Shell Completion @@ -385,17 +413,101 @@ build completion config help validate version ./image-composer build -- --cache-dir --config --help --log-level --verbose --work-dir --workers -# Tab-complete JSON files for spec file argument +# Tab-complete YAML files for template file argument ./image-composer build -# Will show JSON files in the current directory +# Will show YAML files in the current directory ``` -The tool is specifically configured to suggest JSON files when completing the spec file argument for the build and validate commands. +The tool is specifically configured to suggest YAML files when completing the template file argument for the build and validate commands. + +## Template Examples + +### Minimal Edge Device + +```yaml +image: + name: minimal-edge + version: "1.0.0" + +target: + os: azure-linux + dist: azl3 + arch: x86_64 + imageType: raw + +systemConfigs: + - name: minimal + description: Minimal edge device configuration + packages: + - openssh-server + - ca-certificates + kernel: + version: "6.12" + cmdline: "quiet" +``` + +### Development Environment + +```yaml +image: + name: dev-environment + version: "1.0.0" + +target: + os: azure-linux + dist: azl3 + arch: x86_64 + imageType: raw + +systemConfigs: + - name: development + description: Development environment with tools + packages: + - openssh-server + - git + - docker-ce + - vim + - curl + - wget + - python3 + kernel: + version: "6.12" + cmdline: "quiet splash" +``` + +### Edge Microvisor Toolkit + +```yaml +image: + name: emt-edge-device + version: "1.0.0" + +target: + os: emt + dist: emt3 + arch: x86_64 + imageType: raw + +systemConfigs: + - name: edge + description: Edge Microvisor Toolkit configuration + packages: + - openssh-server + - docker-ce + - edge-runtime + - telemetry-agent + kernel: + version: "6.12" + cmdline: "quiet splash systemd.unified_cgroup_hierarchy=0" +``` ## Getting Help -Run `./image-composer --help` to see all available commands and options. +- **Quick Reference**: Run `./image-composer --help` to see all available commands and options +- **Complete Guide**: See the [CLI Specification](./image-composer-cli-specification.md) for detailed documentation +- **Examples**: Check the [template examples](#template-examples) section below +- **Troubleshooting**: Refer to the [Build Process documentation](./image-composer-build-process.md#troubleshooting-build-issues) ## Contributing -## License Information +## License Information \ No newline at end of file diff --git a/cmd/image-composer/build.go b/cmd/image-composer/build.go index 8b44dd71..f95ac6f7 100644 --- a/cmd/image-composer/build.go +++ b/cmd/image-composer/build.go @@ -28,13 +28,13 @@ var ( // createBuildCommand creates the build subcommand func createBuildCommand() *cobra.Command { buildCmd := &cobra.Command{ - Use: "build [flags] SPEC_FILE", + Use: "build [flags] TEMPLATE_FILE", Short: "Build a Linux distribution image", - Long: `Build a Linux distribution image based on the specified spec file. -The spec file should be in JSON format according to the schema.`, + Long: `Build a Linux distribution image based on the specified image template file. +The template file must be in YAML format following the image template schema.`, Args: cobra.ExactArgs(1), RunE: executeBuild, - ValidArgsFunction: jsonFileCompletion, + ValidArgsFunction: templateFileCompletion, } // Add flags @@ -65,19 +65,22 @@ func executeBuild(cmd *cobra.Command, args []string) error { logger := utils.Logger() - // Check if spec file is provided as first positional argument + // Check if template file is provided as first positional argument if len(args) < 1 { - return fmt.Errorf("no spec file provided, usage: image-composer build [flags] SPEC_FILE") + return fmt.Errorf("no template file provided, usage: image-composer build [flags] TEMPLATE_FILE") } - specFile := args[0] + templateFile := args[0] - // Load and validate the configuration - bc, err := config.Load(specFile) + // Load and validate the image template + template, err := config.LoadTemplate(templateFile) if err != nil { - return fmt.Errorf("loading spec file: %v", err) + return fmt.Errorf("loading template file: %v", err) } - providerName := bc.Distro + bc.Version + providerName := template.GetProviderName() + if providerName == "" { + return fmt.Errorf("no provider found for OS %s with distribution %s", template.Target.OS, template.Target.Dist) + } // Get provider by name p, ok := provider.Get(providerName) @@ -85,8 +88,8 @@ func executeBuild(cmd *cobra.Command, args []string) error { return fmt.Errorf("provider not found: %s", providerName) } - // Initialize provider - if err := p.Init(bc); err != nil { + // Initialize provider with the template + if err := p.Init(template); err != nil { return fmt.Errorf("provider init: %v", err) } @@ -96,8 +99,8 @@ func executeBuild(cmd *cobra.Command, args []string) error { return fmt.Errorf("getting packages: %v", err) } - // Match the packages in the build spec against all the packages - req, err := p.MatchRequested(bc.Packages, all) + // Match the packages in the template against all the packages + req, err := p.MatchRequested(template.GetPackages(), all) if err != nil { return fmt.Errorf("matching packages: %v", err) } @@ -162,7 +165,7 @@ func executeBuild(cmd *cobra.Command, args []string) error { return nil } -// jsonFileCompletion helps with suggesting JSON files for spec file argument -func jsonFileCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return []string{"*.json"}, cobra.ShellCompDirectiveFilterFileExt +// templateFileCompletion helps with suggesting YAML files for template file argument +func templateFileCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{"*.yml", "*.yaml"}, cobra.ShellCompDirectiveFilterFileExt } diff --git a/cmd/image-composer/validate.go b/cmd/image-composer/validate.go index 3b54748c..ba4f1775 100644 --- a/cmd/image-composer/validate.go +++ b/cmd/image-composer/validate.go @@ -2,50 +2,66 @@ package main import ( "fmt" - "os" + "github.com/open-edge-platform/image-composer/internal/config" utils "github.com/open-edge-platform/image-composer/internal/utils/logger" - "github.com/open-edge-platform/image-composer/internal/validate" "github.com/spf13/cobra" ) // createValidateCommand creates the validate subcommand func createValidateCommand() *cobra.Command { validateCmd := &cobra.Command{ - Use: "validate SPEC_FILE", - Short: "Validate a spec file against the schema", - Long: `Validate that the given JSON spec file conforms to the schema.`, + Use: "validate [flags] TEMPLATE_FILE", + Short: "Validate an image template file", + Long: `Validate an image template file against the schema without building it. +The template file must be in YAML format following the image template schema. +This allows checking for errors in your template before committing to a full build process.`, Args: cobra.ExactArgs(1), RunE: executeValidate, - ValidArgsFunction: jsonFileCompletion, + ValidArgsFunction: templateFileCompletion, } return validateCmd } -// executeValidate handles the validate command execution logic +// executeValidate handles the validate command logic func executeValidate(cmd *cobra.Command, args []string) error { logger := utils.Logger() - // Check if spec file is provided as first positional argument + // Check if template file is provided as first positional argument if len(args) < 1 { - return fmt.Errorf("no spec file provided, usage: image-composer validate SPEC_FILE") + return fmt.Errorf("no template file provided, usage: image-composer validate TEMPLATE_FILE") } - specFile := args[0] + templateFile := args[0] - logger.Infof("Validating spec file: %s", specFile) + logger.Infof("validating template file: %s", templateFile) - // Read the file - data, err := os.ReadFile(specFile) + // Load and validate the image template + template, err := config.LoadTemplate(templateFile) if err != nil { - return fmt.Errorf("reading spec file: %v", err) + return fmt.Errorf("template validation failed: %v", err) } - // Validate the JSON against schema - if err := validate.ValidateComposerJSON(data); err != nil { - return fmt.Errorf("validation failed: %v", err) + logger.Infof("✓ Template validation successful") + logger.Infof(" Image: %s v%s", template.Image.Name, template.Image.Version) + logger.Infof(" Target: %s %s (%s)", template.Target.OS, template.Target.Dist, template.Target.Arch) + logger.Infof(" Output: %s", template.Target.ImageType) + logger.Infof(" Provider: %s", template.GetProviderName()) + logger.Infof(" System Configs: %d", len(template.SystemConfigs)) + + if len(template.SystemConfigs) > 0 { + config := template.SystemConfigs[0] + logger.Infof(" Config: %s (%s)", config.Name, config.Description) + logger.Infof(" Packages: %d", len(config.Packages)) + logger.Infof(" Kernel: %s (%s)", config.Kernel.Version, config.Kernel.Cmdline) + + if verbose && len(config.Packages) > 0 { + logger.Infof(" Package list:") + for _, pkg := range config.Packages { + logger.Infof(" - %s", pkg) + } + } } - logger.Info("Spec file is valid") return nil } diff --git a/docs/architecture/image-composer-cli-specification.md b/docs/architecture/image-composer-cli-specification.md index deb9ddda..86cfa961 100644 --- a/docs/architecture/image-composer-cli-specification.md +++ b/docs/architecture/image-composer-cli-specification.md @@ -1,8 +1,8 @@ # Image-Composer CLI Specification `image-composer` is a command-line tool for generating custom OS images for -different operating systems including Edge Microvisor toolkit, Azure, Ubuntu, -and RedHat. It provides a flexible, configurability-first approach to creating production-ready OS images with precise customization. +different operating systems including Edge Microvisor toolkit, Azure Linux, +and Wind River eLxr. It provides a flexible, configurability-first approach to creating production-ready OS images with precise customization. ## Related Documentation @@ -17,7 +17,7 @@ around: 1. A global configuration file that defines system-wide settings like cache locations and provider configurations -2. Build specification files (in YAML format) that define per-image build +2. Image template files (in YAML format) that define per-image build requirements The tool follows a staged build process, supporting package caching, image @@ -35,14 +35,14 @@ flowchart TD Config --> Commands{Commands} Commands -->|build| Build[Build OS Image] - Build --> ReadSpec[Read YAML Spec] - ReadSpec --> CheckCache{Image in Cache?} + Build --> ReadTemplate[Read YAML Template] + ReadTemplate --> CheckCache{Image in Cache?} CheckCache -->|Yes| UseCache[Use Cached Image] CheckCache -->|No| BuildProcess[Run Build Pipeline] BuildProcess --> SaveImage[Save Output Image] UseCache --> SaveImage - Commands -->|validate| Validate[Validate Spec File] + Commands -->|validate| Validate[Validate Template File] Commands -->|cache| Cache[Manage Caches] Cache --> CacheOps[List/Clean/Export/Import] @@ -59,11 +59,11 @@ flowchart TD class Start command; class Build,Validate,Cache,Template,Provider command; class CheckCache decision; - class ReadSpec,BuildProcess,SaveImage,UseCache,CacheOps process; + class ReadTemplate,BuildProcess,SaveImage,UseCache,CacheOps process; ``` -The primary workflow is through the `build` command, which reads a build specification file, checks if an image matching those specifications is already cached, and either uses the cached image or runs the build pipeline to create a new image. +The primary workflow is through the `build` command, which reads an image template file, checks if an image matching those specifications is already cached, and either uses the cached image or runs the build pipeline to create a new image. **_NOTE:_** The build pipeline will have package caching mechanism unless instructed to skip in the command option `--no-package-cache` See also: @@ -98,27 +98,24 @@ taking priority over configuration file settings: ### Build Command -Build an OS image from a build specification file. This is the primary command for creating custom OS images according to your requirements. +Build an OS image from an image template file. This is the primary command for creating custom OS images according to your requirements. ```bash -image-composer build [options] SPEC_FILE +image-composer build [options] TEMPLATE_FILE ``` Options: | Option | Description | |--------|-------------| -| `--output-dir DIR, -o DIR` | Output directory for the finished image (default: ./output). Final images will be placed here with names based on the specification. | +| `--output-dir DIR, -o DIR` | Output directory for the finished image (default: ./output). Final images will be placed here with names based on the template. | | `--force, -f` | Force overwrite existing files. By default, the tool will not overwrite existing images with the same name. | -| `--no-cache` | Disable all caching mechanisms (both package and image caching). Useful when you need a completely fresh build. | -| `--no-package-cache` | Disable package caching only. Packages will be downloaded fresh each time, but previous image builds might still be used. | -| `--no-image-cache` | Disable image caching only. Previous built images won't be reused, but package cache will still be utilized. | | `--keep-temp` | Keep temporary files after build for debugging purposes. These are normally cleaned up automatically. | | `--parallel N` | Run up to N parallel tasks (default: from config). Increases build speed on multi-core systems. | | `--stage NAME` | Build up to specific stage and stop (e.g., "packages"). Useful for debugging or when you need a partially-built image. | | `--skip-stage NAME` | Skip specified stage. Allows bypassing certain build phases when they're not needed. | | `--timeout DURATION` | Maximum build duration (e.g., 1h30m). Prevents builds from running indefinitely due to issues. | -| `--variables FILE` | Load variables from YAML file to customize the build without modifying the spec file. | +| `--variables FILE` | Load variables from YAML file to customize the build without modifying the template file. | | `--set KEY=VALUE` | Set individual variable for the build (can be specified multiple times). | See also: @@ -129,11 +126,11 @@ for information about each build stage ### Validate Command -Validate a build specification file without building it. This allows checking -for errors in your specification before committing to a full build process. +Validate an image template file without building it. This allows checking +for errors in your template before committing to a full build process. ```bash -image-composer validate [options] SPEC_FILE +image-composer validate [options] TEMPLATE_FILE ``` Options: @@ -142,7 +139,7 @@ Options: |--------|-------------| | `--schema-only` | Only validate YAML schema without checking filesystem dependencies or provider compatibility. This performs a quick validation of the syntax only. | | `--strict` | Enable strict validation with additional checks. Enforces best practices and checks for potential issues that might not cause immediate errors. | -| `--list-warnings` | Show all warnings, including minor issues that might not prevent the build. Helpful for creating more robust specification files. | +| `--list-warnings` | Show all warnings, including minor issues that might not prevent the build. Helpful for creating more robust template files. | See also: @@ -163,7 +160,7 @@ Subcommands: | Subcommand | Description | |------------|-------------| | `list` | List cached images with their metadata, timestamps, and storage locations. Helps you understand what's already cached and available for reuse. | -| `info [hash]` | Show detailed cache info for a specific image hash, including build parameters and spec details. | +| `info [hash]` | Show detailed cache info for a specific image hash, including build parameters and template details. | | `clean [--all\|--packages\|--images]` | Clean cache to reclaim disk space. You can selectively clean either packages or images, or both with --all. | | `export [hash] FILE` | Export a cached image to a specific file location. Useful when you need to retrieve a specific cached build. | | `import FILE` | Import an existing image into the cache. Allows pre-populating the cache with images built elsewhere. | @@ -187,7 +184,7 @@ Subcommands: |------------|-------------| | `list` | List available templates with descriptions and supported configurations. Templates provide ready-to-use base configurations for common image types. | | `show TEMPLATE` | Show template details including all settings, variables, and customization options for a specific template. | -| `create SPEC_FILE` | Create a new template from an existing specification file, making it available for future use. | +| `create TEMPLATE_FILE` | Create a new template from an existing template file, making it available for future use. | | `export TEMPLATE FILE` | Export a template to a file for sharing with other users or systems. Templates can be version-controlled and distributed. | See also: @@ -222,19 +219,19 @@ providers are used during the build process ```bash # Build an image with default settings -image-composer build my-image-spec.yml +image-composer build my-image-template.yml # Build with custom global config -image-composer --config=/path/to/config.yaml build my-image-spec.yml +image-composer --config=/path/to/config.yaml build my-image-template.yml # Build with variable substitution -image-composer build --set "version=1.2.3" --set "hostname=edge-device-001" my-image-spec.yml +image-composer build --set "version=1.2.3" --set "hostname=edge-device-001" my-image-template.yml # Build up to a specific stage -image-composer build --stage configuration my-image-spec.yml +image-composer build --stage configuration my-image-template.yml # Build with a timeout -image-composer build --timeout 30m my-image-spec.yml +image-composer build --timeout 30m my-image-template.yml ``` ### Managing Cache @@ -259,8 +256,8 @@ image-composer template list # Show details for a template image-composer template show ubuntu-server-22.04 -# Create a new template from a spec file -image-composer template create my-image-spec.yml +# Create a new template from a template file +image-composer template create my-image-template.yml ``` ## Configuration Files @@ -286,28 +283,24 @@ storage: retention_days: 30 # How long to keep cached packages image_cache: enabled: true # Enable image caching - max_count: 5 # Number of images to keep per spec + max_count: 5 # Number of images to keep per template providers: # OS-specific provider configurations - ubuntu: - repositories: - - name: "main" - url: "http://archive.ubuntu.com/ubuntu/" - debootstrap_path: "/usr/sbin/debootstrap" # Path to debootstrap tool - - redhat: + azure_linux: repositories: - name: "base" - url: "https://cdn.redhat.com/content/dist/rhel8/8/x86_64/baseos/os" + url: "https://packages.microsoft.com/azurelinux/3.0/prod/base/" - azure: - cli_path: "/usr/bin/az" # Path to Azure CLI - gallery_resource_group: "image-gallery-rg" # Azure resource group for images + elxr: + repositories: + - name: "main" + url: "https://mirror.elxr.dev/elxr/dists/aria/main/" - edge_microvisor: - toolkit_source: "https://download.edge-microvisor.io/toolkit/" # Toolkit location - secure_boot_keys_dir: "/etc/image-composer/secure-keys/" # Secure boot key location + emt: + repositories: + - name: "edge-base" + url: "https://files-rs.edgeorchestration.intel.com/files-edge-orch/microvisor/rpm/3.0/" ``` See also: @@ -315,9 +308,9 @@ See also: - [Global Options](#global-options) for command-line options that can override these settings -### Build Specification File +### Image Template File -The build specification file (YAML format) defines the requirements for a +The image template file (YAML format) defines the requirements for a specific image. This is where you define exactly what goes into your custom OS image, including packages, configurations, and customizations. @@ -325,61 +318,38 @@ image, including packages, configurations, and customizations. image: # Basic image identification name: edge-device-image # Name of the resulting image - version: 1.2.0 # Version for tracking and naming - description: Edge device image with Microvisor support # Human-readable description - base: - os: ubuntu # Base operating system - version: 22.04 # OS version - type: minimal # OS variant (minimal, server, desktop) - -build: - # Build process configuration - cache: - use_package_cache: true # Whether to use the package cache - use_image_cache: true # Whether to use the image cache - stages: # Build stages in sequence - - validate # Validate the build spec - - packages # Pull required packages and dependencies - - compose # Compose image - - configuration # Applies configurations - - finalize # verify and output the image -customizations: - # OS customizations - packages: - install: # Packages to install + version: "1.2.0" # Version for tracking and naming + +target: + # Target OS and image configuration + os: azure-linux # Base operating system + dist: azl3 # Distribution identifier + arch: x86_64 # Target architecture + imageType: raw # Output format (raw, iso, img, vhd) + +systemConfigs: + # Array of system configurations + - name: edge # Configuration name + description: Edge device image with Microvisor support # Human-readable description + + # Package configuration + packages: # Packages to install - openssh-server - docker-ce - remove: # Packages to remove - - snapd - services: - enabled: # Services to enable on boot - - ssh - - docker - disabled: # Services to disable - - apparmor - files: # Custom files to add to the image - - source: ./files/sshd_config # Path to source file (relative to spec) - destination: /etc/ssh/sshd_config # Path in the target image - permissions: 0644 # File permissions - owner: root:root # File ownership - scripts: # Scripts to run during build - - path: ./scripts/setup-networking.sh # Path to script (relative to spec) - args: # Arguments to pass to the script - - --interface - - eth0 - run_in_chroot: true # Whether to run in chroot environment - -output: - # Output settings - format: qcow2 # Output format (qcow2, raw, vhd) - compression: gzip # Compression algorithm - destination: ./output/ # Output directory override + - vim + - curl + - wget + + # Kernel configuration + kernel: + version: "6.12" # Kernel version to include + cmdline: "quiet splash" # Additional kernel command-line parameters ``` See also: - [Common Build Patterns](./image-composer-build-process.md#common-build-patterns) -for example build specifications +for example image templates - [Template Structure](./image-composer-templates.md#template-structure) for how templates can be used to generate build specifications @@ -393,7 +363,7 @@ automation: | 0 | Success - The command completed successfully | | 1 | General error - An unspecified error occurred | | 2 | Command line usage error - Invalid options or arguments | -| 3 | Validation error - The specification file failed validation | +| 3 | Validation error - The template file failed validation | | 4 | Build error - The build process failed | | 5 | Configuration error - Error in configuration files | @@ -426,10 +396,10 @@ For detailed logs to troubleshoot issues: ```bash # Enable debug logging -image-composer --log-level debug build my-image-spec.yml +image-composer --log-level debug build my-image-template.yml # Save logs to a file -image-composer --log-level debug build my-image-spec.yml 2>&1 | tee build-log.txt +image-composer --log-level debug build my-image-template.yml 2>&1 | tee build-log.txt ``` See also: diff --git a/image-composer.yml b/image-composer.yml index 3c793939..cbbd2281 100644 --- a/image-composer.yml +++ b/image-composer.yml @@ -19,7 +19,7 @@ work_dir: "/tmp/image-composer" # Cleaned up after successful builds # Requires substantial space during builds (2-10GB typical) -temp_dir: "" +temp_dir: "/tmp" # Temporary directory for short-lived files like GPG keys and metadata parsing # Empty value uses system default (/tmp on Linux, %TEMP% on Windows) # Used for: GPG verification files, decompressed metadata, parsing operations diff --git a/image-templates/azl3-x86_64-edge-raw.yml b/image-templates/azl3-x86_64-edge-raw.yml index 3b8a563f..8d30dc1c 100644 --- a/image-templates/azl3-x86_64-edge-raw.yml +++ b/image-templates/azl3-x86_64-edge-raw.yml @@ -18,11 +18,9 @@ systemConfigs: # Package Configuration packages: # Additional packages beyond the base system - - openssh-server # Remote access - - docker-ce # Container runtime - - vim # Text editor - - curl # HTTP client - - wget # File downloader + - cloud-init + - rsyslog + # Kernel Configuration kernel: @@ -33,4 +31,4 @@ systemConfigs: # Network: Uses DHCP on first interface # Security: Enables distro-appropriate security (SELinux for AzureLinux) # Services: Enables SSH, disables unnecessary services - # Users: Creates default admin user with sudo access \ No newline at end of file + # Users: Creates default admin user with sudo access diff --git a/internal/config/config.go b/internal/config/config.go index 82cace43..ed8f999d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -2,7 +2,6 @@ package config import ( - "bytes" "encoding/json" "fmt" "os" @@ -14,21 +13,33 @@ import ( "gopkg.in/yaml.v3" ) -// BuildSpec represents your JSON schema. -type BuildSpec struct { - Distro string `json:"distro"` - Version string `json:"version"` - Arch string `json:"arch"` - Packages []string `json:"packages"` - Immutable bool `json:"immutable"` - Output string `json:"output"` - Kernel KernelConfig `json:"kernel"` +// ImageTemplate represents the YAML image template structure +type ImageTemplate struct { + Image struct { + Name string `yaml:"name"` + Version string `yaml:"version"` + } `yaml:"image"` + Target struct { + OS string `yaml:"os"` + Dist string `yaml:"dist"` + Arch string `yaml:"arch"` + ImageType string `yaml:"imageType"` + } `yaml:"target"` + SystemConfigs []SystemConfig `yaml:"systemConfigs"` } -// KernelConfig holds the nested "kernel" object. +// SystemConfig represents a system configuration within the template +type SystemConfig struct { + Name string `yaml:"name"` + Description string `yaml:"description"` + Packages []string `yaml:"packages"` + Kernel KernelConfig `yaml:"kernel"` +} + +// KernelConfig holds the kernel configuration type KernelConfig struct { - Version string `json:"version"` - Cmdline string `json:"cmdline"` + Version string `yaml:"version"` + Cmdline string `yaml:"cmdline"` } // GlobalConfig holds essential tool-level configuration parameters @@ -48,37 +59,118 @@ type LoggingConfig struct { Level string `yaml:"level" json:"level"` // Log verbosity level: debug (most verbose), info (default), warn (warnings only), error (errors only) } -// Load loads a BuildSpec from the specified path -func Load(path string) (*BuildSpec, error) { +// LoadTemplate loads an ImageTemplate from the specified YAML template path +func LoadTemplate(path string) (*ImageTemplate, error) { logger := utils.Logger() data, err := os.ReadFile(path) if err != nil { return nil, err } - // validate raw JSON against schema - if err := validate.ValidateComposerJSON(data); err != nil { - return nil, fmt.Errorf("validation error: %w", err) + + // Only support YAML/YML files + ext := strings.ToLower(filepath.Ext(path)) + if ext != ".yml" && ext != ".yaml" { + return nil, fmt.Errorf("unsupported file format: %s (only .yml and .yaml are supported)", ext) + } + + template, err := parseYAMLTemplate(data) + if err != nil { + return nil, fmt.Errorf("loading YAML template: %w", err) + } + + logger.Infof("loaded image template from %s: name=%s, os=%s, dist=%s, arch=%s", + path, template.Image.Name, template.Target.OS, template.Target.Dist, template.Target.Arch) + return template, nil +} + +// parseYAMLTemplate loads an ImageTemplate from YAML data +func parseYAMLTemplate(data []byte) (*ImageTemplate, error) { + // Parse YAML to generic interface for validation + var raw interface{} + if err := yaml.Unmarshal(data, &raw); err != nil { + return nil, fmt.Errorf("parsing YAML: %w", err) + } + + // Convert to JSON for schema validation + jsonData, err := json.Marshal(raw) + if err != nil { + return nil, fmt.Errorf("converting to JSON for validation: %w", err) + } + + // Validate against image template schema + if err := validate.ValidateImageTemplateJSON(jsonData); err != nil { + return nil, fmt.Errorf("template validation error: %w", err) + } + + // Parse into template structure + var template ImageTemplate + if err := yaml.Unmarshal(data, &template); err != nil { + return nil, fmt.Errorf("parsing template: %w", err) + } + + return &template, nil +} + +// GetProviderName returns the provider name for the given template +func (t *ImageTemplate) GetProviderName() string { + // Map OS/dist combinations to provider names + providerMap := map[string]map[string]string{ + "azure-linux": {"azl3": "AzureLinux3"}, + "emt": {"emt3": "EMT3.0"}, + "elxr": {"elxr12": "eLxr12"}, + } + + if providers, ok := providerMap[t.Target.OS]; ok { + if provider, ok := providers[t.Target.Dist]; ok { + return provider + } + } + return "" +} + +// GetDistroVersion returns the version string expected by providers +func (t *ImageTemplate) GetDistroVersion() string { + versionMap := map[string]string{ + "azl3": "3", + "emt3": "3.0", + "elxr12": "12", } - // unmarshal into typed struct - var bc BuildSpec - dec := json.NewDecoder(bytes.NewReader(data)) - dec.DisallowUnknownFields() - if err := dec.Decode(&bc); err != nil { - return nil, fmt.Errorf("parsing config: %w", err) + return versionMap[t.Target.Dist] +} + +// GetPackages returns all packages from the first system configuration +// TODO: In the future, we might want to support multiple configs or allow selection +func (t *ImageTemplate) GetPackages() []string { + if len(t.SystemConfigs) > 0 { + return t.SystemConfigs[0].Packages } + return []string{} +} - logger.Infof("loaded config: \n%s", string(data)) - return &bc, nil +// GetKernel returns the kernel configuration from the first system configuration +func (t *ImageTemplate) GetKernel() KernelConfig { + if len(t.SystemConfigs) > 0 { + return t.SystemConfigs[0].Kernel + } + return KernelConfig{} +} + +// GetSystemConfigName returns the name of the first system configuration +func (t *ImageTemplate) GetSystemConfigName() string { + if len(t.SystemConfigs) > 0 { + return t.SystemConfigs[0].Name + } + return "" } // DefaultGlobalConfig returns a GlobalConfig with sensible defaults func DefaultGlobalConfig() *GlobalConfig { return &GlobalConfig{ Workers: 8, - CacheDir: "./cache", // Changed from ./downloads for consistency - WorkDir: "./workspace", // Changed from ./builds to avoid conflict with build artifacts - TempDir: "", // Will use system temp + CacheDir: "./cache", + WorkDir: "./workspace", + TempDir: "./tmp", Logging: LoggingConfig{ Level: "info", diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 00000000..bf4e0097 --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,255 @@ +package config + +import ( + "os" + "strings" + "testing" +) + +func TestLoadYAMLTemplate(t *testing.T) { + // Create a temporary YAML file + yamlContent := `image: + name: azl3-x86_64-edge + version: "1.0.0" + +target: + os: azure-linux + dist: azl3 + arch: x86_64 + imageType: raw + +systemConfigs: + - name: edge + description: Default yml configuration for edge image + packages: + - openssh-server + - docker-ce + - vim + - curl + - wget + kernel: + version: "6.12" + cmdline: "quiet splash" +` + + tmpFile, err := os.CreateTemp("", "test-*.yml") + if err != nil { + t.Fatalf("failed to create temp file: %v", err) + } + defer os.Remove(tmpFile.Name()) + + if _, err := tmpFile.WriteString(yamlContent); err != nil { + t.Fatalf("failed to write temp file: %v", err) + } + tmpFile.Close() + + // Test loading + template, err := LoadTemplate(tmpFile.Name()) + if err != nil { + t.Fatalf("failed to load YAML template: %v", err) + } + + // Verify the loaded template + if template.Target.OS != "azure-linux" { + t.Errorf("expected OS 'azure-linux', got %s", template.Target.OS) + } + if template.Target.Dist != "azl3" { + t.Errorf("expected dist 'azl3', got %s", template.Target.Dist) + } + if template.Target.Arch != "x86_64" { + t.Errorf("expected arch 'x86_64', got %s", template.Target.Arch) + } + if len(template.GetPackages()) != 5 { + t.Errorf("expected 5 packages, got %d", len(template.GetPackages())) + } + if template.Target.ImageType != "raw" { + t.Errorf("expected imageType 'raw', got %s", template.Target.ImageType) + } + if template.GetKernel().Version != "6.12" { + t.Errorf("expected kernel version '6.12', got %s", template.GetKernel().Version) + } +} + +func TestTemplateHelperMethods(t *testing.T) { + template := &ImageTemplate{ + Image: struct { + Name string `yaml:"name"` + Version string `yaml:"version"` + }{ + Name: "test-image", + Version: "1.0.0", + }, + Target: struct { + OS string `yaml:"os"` + Dist string `yaml:"dist"` + Arch string `yaml:"arch"` + ImageType string `yaml:"imageType"` + }{ + OS: "azure-linux", + Dist: "azl3", + Arch: "x86_64", + ImageType: "iso", + }, + SystemConfigs: []SystemConfig{ + { + Name: "test-config", + Description: "Test configuration", + Packages: []string{"package1", "package2"}, + Kernel: KernelConfig{ + Version: "6.12", + Cmdline: "quiet", + }, + }, + }, + } + + // Test provider name mapping + providerName := template.GetProviderName() + if providerName != "AzureLinux3" { + t.Errorf("expected provider 'AzureLinux3', got %s", providerName) + } + + // Test version mapping + version := template.GetDistroVersion() + if version != "3" { + t.Errorf("expected version '3', got %s", version) + } + + // Test package extraction + packages := template.GetPackages() + if len(packages) != 2 { + t.Errorf("expected 2 packages, got %d", len(packages)) + } + + // Test kernel extraction + kernel := template.GetKernel() + if kernel.Version != "6.12" { + t.Errorf("expected kernel version '6.12', got %s", kernel.Version) + } + + // Test system config name extraction + configName := template.GetSystemConfigName() + if configName != "test-config" { + t.Errorf("expected config name 'test-config', got %s", configName) + } +} + +func TestUnsupportedFileFormat(t *testing.T) { + // Create a temporary file with unsupported extension + tmpFile, err := os.CreateTemp("", "test-*.txt") + if err != nil { + t.Fatalf("failed to create temp file: %v", err) + } + defer os.Remove(tmpFile.Name()) + + if _, err := tmpFile.WriteString("some content"); err != nil { + t.Fatalf("failed to write temp file: %v", err) + } + tmpFile.Close() + + // Test loading should fail + _, err = LoadTemplate(tmpFile.Name()) + if err == nil { + t.Errorf("expected error for unsupported file format") + } + if !strings.Contains(err.Error(), "unsupported file format") { + t.Errorf("expected unsupported file format error, got: %v", err) + } +} + +func TestUnsupportedDistribution(t *testing.T) { + // This test verifies that schema validation catches invalid OS/dist combinations + + invalidYaml := `image: + name: test + version: "1.0.0" +target: + os: azure-linux + dist: emt3 # Invalid: azure-linux should only use azl3 + arch: x86_64 + imageType: raw +systemConfigs: + - name: test + packages: ["test"] + kernel: + version: "6.12" +` + + // This should fail during schema validation + _, err := parseYAMLTemplate([]byte(invalidYaml)) + if err == nil { + t.Errorf("expected schema validation error for invalid OS/dist combination") + } + if !strings.Contains(err.Error(), "template validation error") { + t.Errorf("expected schema validation error, got: %v", err) + } +} + +func TestEmptySystemConfigs(t *testing.T) { + // This test verifies that schema validation catches empty systemConfigs + + invalidYaml := `image: + name: test + version: "1.0.0" +target: + os: azure-linux + dist: azl3 + arch: x86_64 + imageType: raw +systemConfigs: [] # Invalid: minItems is 1 +` + + // This should fail during schema validation + _, err := parseYAMLTemplate([]byte(invalidYaml)) + if err == nil { + t.Errorf("expected schema validation error for empty systemConfigs") + } +} + +func TestAllSupportedProviders(t *testing.T) { + testCases := []struct { + os string + dist string + expected string + version string + }{ + {"azure-linux", "azl3", "AzureLinux3", "3"}, + {"emt", "emt3", "EMT3.0", "3.0"}, + {"elxr", "elxr12", "eLxr12", "12"}, + } + + for _, tc := range testCases { + template := &ImageTemplate{ + Target: struct { + OS string `yaml:"os"` + Dist string `yaml:"dist"` + Arch string `yaml:"arch"` + ImageType string `yaml:"imageType"` + }{ + OS: tc.os, + Dist: tc.dist, + Arch: "x86_64", + ImageType: "iso", + }, + SystemConfigs: []SystemConfig{ + { + Name: "test", + Packages: []string{"test-package"}, + Kernel: KernelConfig{Version: "6.12"}, + }, + }, + } + + // Test provider name + providerName := template.GetProviderName() + if providerName != tc.expected { + t.Errorf("for %s/%s expected provider '%s', got '%s'", tc.os, tc.dist, tc.expected, providerName) + } + + // Test version + version := template.GetDistroVersion() + if version != tc.version { + t.Errorf("for %s/%s expected version '%s', got '%s'", tc.os, tc.dist, tc.version, version) + } + } +} diff --git a/internal/provider/azurelinux3/azurelinux3.go b/internal/provider/azurelinux3/azurelinux3.go index 78fa571d..14b957c6 100644 --- a/internal/provider/azurelinux3/azurelinux3.go +++ b/internal/provider/azurelinux3/azurelinux3.go @@ -37,12 +37,10 @@ type repoConfig struct { // AzureLinux3 implements provider.Provider type AzureLinux3 struct { - repoURL string - repoCfg repoConfig - //repomd string - //primaryURL string - gzHref string - spec *config.BuildSpec + repoURL string + repoCfg repoConfig + gzHref string + template *config.ImageTemplate } func init() { @@ -53,10 +51,9 @@ func init() { func (p *AzureLinux3) Name() string { return "AzureLinux3" } // Init will initialize the provider, fetching repo configuration -func (p *AzureLinux3) Init(spec *config.BuildSpec) error { - +func (p *AzureLinux3) Init(template *config.ImageTemplate) error { logger := utils.Logger() - p.repoURL = baseURL + spec.Arch + "/" + configName + p.repoURL = baseURL + template.Target.Arch + "/" + configName resp, err := http.Get(p.repoURL) if err != nil { @@ -71,14 +68,15 @@ func (p *AzureLinux3) Init(spec *config.BuildSpec) error { return err } - repoDataURL := baseURL + spec.Arch + "/" + repodata + repoDataURL := baseURL + template.Target.Arch + "/" + repodata href, err := fetchPrimaryURL(repoDataURL) if err != nil { logger.Errorf("fetch primary.xml.gz failed: %v", err) + return err } p.repoCfg = cfg - p.spec = spec + p.template = template p.gzHref = href logger.Infof("initialized AzureLinux3 provider repo section=%s", cfg.Section) @@ -87,6 +85,7 @@ func (p *AzureLinux3) Init(spec *config.BuildSpec) error { logger.Infof("primary.xml.gz=%s", p.gzHref) return nil } + func (p *AzureLinux3) Packages() ([]provider.PackageInfo, error) { logger := utils.Logger() logger.Infof("fetching packages from %s", p.repoCfg.URL) @@ -94,6 +93,7 @@ func (p *AzureLinux3) Packages() ([]provider.PackageInfo, error) { packages, err := rpmutils.ParsePrimary(p.repoCfg.URL, p.gzHref) if err != nil { logger.Errorf("parsing primary.xml.gz failed: %v", err) + return nil, err } logger.Infof("found %d packages in AzureLinux3 repo", len(packages)) @@ -111,12 +111,12 @@ func (p *AzureLinux3) MatchRequested(requests []string, all []provider.PackageIn candidates = append(candidates, pi) break } - // 2) prefix by want-version (“acl-”) + // 2) prefix by want-version ("acl-") if strings.HasPrefix(pi.Name, want+"-") { candidates = append(candidates, pi) continue } - // 3) prefix by want.release (“acl-2.3.1-2.”) + // 3) prefix by want.release ("acl-2.3.1-2.") if strings.HasPrefix(pi.Name, want+".") { candidates = append(candidates, pi) } @@ -130,7 +130,7 @@ func (p *AzureLinux3) MatchRequested(requests []string, all []provider.PackageIn out = append(out, candidates[0]) continue } - // Otherwise pick the “highest” by lex sort + // Otherwise pick the "highest" by lex sort sort.Slice(candidates, func(i, j int) bool { return candidates[i].Name > candidates[j].Name }) @@ -138,6 +138,7 @@ func (p *AzureLinux3) MatchRequested(requests []string, all []provider.PackageIn } return out, nil } + func (p *AzureLinux3) Validate(destDir string) error { logger := utils.Logger() @@ -152,7 +153,7 @@ func (p *AzureLinux3) Validate(destDir string) error { if err != nil { return fmt.Errorf("read GPG key body: %w", err) } - logger.Infof("fetched GPG key (%d)", len(keyBytes)) + logger.Infof("fetched GPG key (%d bytes)", len(keyBytes)) logger.Debugf("GPG key: %s\n", keyBytes) // store in a temp file @@ -196,7 +197,6 @@ func (p *AzureLinux3) Validate(destDir string) error { } func (p *AzureLinux3) Resolve(req []provider.PackageInfo, all []provider.PackageInfo) ([]provider.PackageInfo, error) { - logger := utils.Logger() logger.Infof("resolving dependencies for %d RPMs", len(req)) diff --git a/internal/provider/elxr12/elxr12.go b/internal/provider/elxr12/elxr12.go index 71d3db33..253f9064 100644 --- a/internal/provider/elxr12/elxr12.go +++ b/internal/provider/elxr12/elxr12.go @@ -16,7 +16,7 @@ import ( // DEB: https://deb.debian.org/debian/dists/bookworm/main/binary-amd64/Packages.gz // DEB Download Path: https://deb.debian.org/debian/pool/main/0/0ad/0ad_0.0.26-3_amd64.deb // eLxr: https://mirror.elxr.dev/elxr/dists/aria/main/binary-amd64/Packages.gz -// eLxr Donwload Path: https://mirror.elxr.dev/elxr/pool/main/p/python3-defaults/2to3_3.11.2-1_all.deb +// eLxr Download Path: https://mirror.elxr.dev/elxr/pool/main/p/python3-defaults/2to3_3.11.2-1_all.deb const ( baseURL = "https://mirror.elxr.dev/elxr/dists/aria/main/" configName = "Packages.gz" @@ -48,7 +48,7 @@ type eLxr12 struct { repoCfg repoConfig pkgChecksum []pkgChecksum //this is not using for debian gzHref string - spec *config.BuildSpec + template *config.ImageTemplate } func init() { @@ -59,15 +59,15 @@ func init() { func (p *eLxr12) Name() string { return "eLxr12" } // Init will initialize the provider, fetching repo configuration -func (p *eLxr12) Init(spec *config.BuildSpec) error { - +func (p *eLxr12) Init(template *config.ImageTemplate) error { logger := utils.Logger() //todo: need to correct of how to get the arch once finalized - if spec.Arch == "x86_64" { - spec.Arch = "binary-amd64" + arch := template.Target.Arch + if arch == "x86_64" { + arch = "binary-amd64" } - p.repoURL = baseURL + spec.Arch + "/" + configName + p.repoURL = baseURL + arch + "/" + configName cfg, err := loadRepoConfig(p.repoURL) if err != nil { @@ -76,25 +76,24 @@ func (p *eLxr12) Init(spec *config.BuildSpec) error { } p.repoCfg = cfg p.gzHref = cfg.PkgList - p.spec = spec + p.template = template logger.Infof("initialized eLxr provider repo section=%s", cfg.Section) logger.Infof("name=%s", cfg.Name) logger.Infof("package list url=%s", cfg.PkgList) logger.Infof("package download url=%s", cfg.PkgPrefix) return nil - } // Packages returns the list of packages func (p *eLxr12) Packages() ([]provider.PackageInfo, error) { - logger := utils.Logger() logger.Infof("fetching packages from %s", p.repoCfg.PkgList) packages, err := debutils.ParsePrimary(p.repoCfg.PkgPrefix, p.gzHref, p.repoCfg.ReleaseFile, p.repoCfg.ReleaseSign, p.repoCfg.PbGPGKey, p.repoCfg.BuildPath) if err != nil { logger.Errorf("parsing %s failed: %v", p.gzHref, err) + return nil, err } logger.Infof("found %d packages in eLxr repo", len(packages)) @@ -150,7 +149,7 @@ func (p *eLxr12) Resolve(req []provider.PackageInfo, all []provider.PackageInfo) } logger.Infof("requested %d packages, resolved to %d packages", len(req), len(needed)) - logger.Infof("need a total of %d RPMs (including dependencies)", len(needed)) + logger.Infof("need a total of %d DEBs (including dependencies)", len(needed)) for _, pkg := range needed { logger.Debugf("-> %s", pkg.Name) @@ -169,7 +168,6 @@ func (p *eLxr12) Resolve(req []provider.PackageInfo, all []provider.PackageInfo) // MatchRequested matches requested packages func (p *eLxr12) MatchRequested(requests []string, all []provider.PackageInfo) ([]provider.PackageInfo, error) { - logger := utils.Logger() var out []provider.PackageInfo @@ -177,18 +175,17 @@ func (p *eLxr12) MatchRequested(requests []string, all []provider.PackageInfo) ( for _, want := range requests { var candidates []provider.PackageInfo for _, pi := range all { - // 1) exact name match if pi.Name == want || pi.Name == want+".deb" { candidates = append(candidates, pi) break } - // 2) prefix by want-version (“acl-”) + // 2) prefix by want-version ("acl-") if strings.HasPrefix(pi.Name, want+"-") { candidates = append(candidates, pi) continue } - // 3) prefix by want.release (“acl-2.3.1-2.”) + // 3) prefix by want.release ("acl-2.3.1-2.") if strings.HasPrefix(pi.Name, want+".") { candidates = append(candidates, pi) } @@ -203,7 +200,7 @@ func (p *eLxr12) MatchRequested(requests []string, all []provider.PackageInfo) ( out = append(out, candidates[0]) continue } - // Otherwise pick the “highest” by lex sort + // Otherwise pick the "highest" by lex sort sort.Slice(candidates, func(i, j int) bool { return candidates[i].Name > candidates[j].Name }) @@ -212,7 +209,6 @@ func (p *eLxr12) MatchRequested(requests []string, all []provider.PackageInfo) ( logger.Infof("found %d packages in request of %d", len(out), len(requests)) return out, nil - } func loadRepoConfig(repoUrl string) (repoConfig, error) { diff --git a/internal/provider/emt3_0/emt3_0.go b/internal/provider/emt3_0/emt3_0.go index a672e56e..93b7a1ff 100644 --- a/internal/provider/emt3_0/emt3_0.go +++ b/internal/provider/emt3_0/emt3_0.go @@ -34,14 +34,12 @@ type repoConfig struct { GPGKey string } -// emt30 implements provider.Provider +// Emt30 implements provider.Provider type Emt30 struct { - repoURL string - repoCfg repoConfig - //repomd string - //primaryURL string - zstHref string - spec *config.BuildSpec + repoURL string + repoCfg repoConfig + zstHref string + template *config.ImageTemplate } func init() { @@ -52,7 +50,7 @@ func init() { func (p *Emt30) Name() string { return "EMT3.0" } // Init will initialize the provider, fetching repo configuration -func (p *Emt30) Init(spec *config.BuildSpec) error { +func (p *Emt30) Init(template *config.ImageTemplate) error { logger := utils.Logger() resp, err := http.Get(configURL) @@ -76,7 +74,7 @@ func (p *Emt30) Init(spec *config.BuildSpec) error { p.repoURL = configURL p.repoCfg = cfg - p.spec = spec + p.template = template p.zstHref = href logger.Infof("initialized EMT3.0 provider repo section=%s", cfg.Section) @@ -94,6 +92,7 @@ func (p *Emt30) Packages() ([]provider.PackageInfo, error) { packages, err := rpmutils.ParsePrimary(p.repoCfg.URL, p.zstHref) if err != nil { logger.Errorf("parsing primary.xml.zst failed: %v", err) + return nil, err } logger.Infof("found %d packages in EMT30 repo", len(packages)) @@ -112,12 +111,12 @@ func (p *Emt30) MatchRequested(requests []string, all []provider.PackageInfo) ([ candidates = append(candidates, pi) break } - // 2) prefix by want-version (“acl-”) + // 2) prefix by want-version ("acl-") if strings.HasPrefix(pi.Name, want+"-") { candidates = append(candidates, pi) continue } - // 3) prefix by want.release (“acl-2.3.1-2.”) + // 3) prefix by want.release ("acl-2.3.1-2.") if strings.HasPrefix(pi.Name, want+".") { candidates = append(candidates, pi) } @@ -131,7 +130,7 @@ func (p *Emt30) MatchRequested(requests []string, all []provider.PackageInfo) ([ out = append(out, candidates[0]) continue } - // Otherwise pick the “highest” by lex sort + // Otherwise pick the "highest" by lex sort sort.Slice(candidates, func(i, j int) bool { return candidates[i].Name > candidates[j].Name }) @@ -155,7 +154,7 @@ func (p *Emt30) Validate(destDir string) error { if err != nil { return fmt.Errorf("read GPG key body: %w", err) } - logger.Infof("fetched GPG key (%d)", len(keyBytes)) + logger.Infof("fetched GPG key (%d bytes)", len(keyBytes)) logger.Debugf("GPG key: %s\n", keyBytes) // store in a temp file diff --git a/internal/provider/provider.go b/internal/provider/provider.go index d8bbf5b3..0b247456 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -15,11 +15,11 @@ type PackageInfo struct { // Provider is the interface every OSV plugin must implement. type Provider interface { - // Name is a unique ID, e.g. "azurelinux3" or "emt3". + // Name is a unique ID, e.g. "AzureLinux3" or "EMT3.0". Name() string // Init does any one-time setup: import GPG keys, register repos, etc. - Init(spec *config.BuildSpec) error + Init(template *config.ImageTemplate) error // Packages returns the list of PackageInfo for this image build. Packages() ([]PackageInfo, error) @@ -31,7 +31,7 @@ type Provider interface { // the list of PackageInfo that match. MatchRequested(requested []string, all []PackageInfo) ([]PackageInfo, error) - // Resolve walks your local cache in destDir and returns the full + // Resolve walks dependencies and returns the full list of packages needed. Resolve(req []PackageInfo, all []PackageInfo) ([]PackageInfo, error) } diff --git a/internal/validate/validate.go b/internal/validate/validate.go index f1af2996..f6602465 100644 --- a/internal/validate/validate.go +++ b/internal/validate/validate.go @@ -33,20 +33,11 @@ func ValidateAgainstSchema(name string, schemaBytes, data []byte) error { return nil } -// ValidateComposerJSON runs the composer schema against data -func ValidateComposerJSON(data []byte) error { - return ValidateAgainstSchema( - "os-image-composer.schema.json", - schema_pkg.ComposerSchema, - data, - ) -} - -// ValidateTemplateJSON runs the template schema against data -func ValidateImageJSON(data []byte) error { +// ValidateImageTemplateJSON runs the template schema against data +func ValidateImageTemplateJSON(data []byte) error { return ValidateAgainstSchema( "os-image-template.schema.json", - schema_pkg.ImageSchema, + schema_pkg.ImageTemplateSchema, data, ) } diff --git a/internal/validate/validate_test.go b/internal/validate/validate_test.go index 80fc6d12..6b23f4c2 100644 --- a/internal/validate/validate_test.go +++ b/internal/validate/validate_test.go @@ -9,7 +9,7 @@ import ( "sigs.k8s.io/yaml" ) -// loadFile reads a test JSON file from the project root testdata directory. +// loadFile reads a test file from the project root testdata directory. func loadFile(t *testing.T, relPath string) []byte { t.Helper() // Determine project root relative to this test file @@ -22,58 +22,51 @@ func loadFile(t *testing.T, relPath string) []byte { return data } -func TestValid(t *testing.T) { - v := loadFile(t, "testdata/valid.json") - if err := ValidateComposerJSON(v); err != nil { - t.Errorf("expected valid.json to pass, but got: %v", err) - } -} - -func TestInvalid(t *testing.T) { - v := loadFile(t, "testdata/invalid.json") - if err := ValidateComposerJSON(v); err == nil { - t.Errorf("expected invalid.json to fail validation") - } -} - -func TestValidImage(t *testing.T) { - v := loadFile(t, "image-templates/default-image-template.yml") +// Test new YAML image template format +func TestValidImageTemplate(t *testing.T) { + v := loadFile(t, "image-templates/azl3-x86_64-edge-raw.yml") // Parse to generic JSON interface var raw interface{} if err := yaml.Unmarshal(v, &raw); err != nil { t.Errorf("yml parsing error: %v", err) + return } // Re‐marshal to JSON bytes dataJSON, err := json.Marshal(raw) if err != nil { t.Errorf("json marshaling error: %v", err) + return } - if err := ValidateImageJSON(dataJSON); err != nil { - t.Errorf("expected image-templates/default-image-template.yml to pass, but got: %v", err) + if err := ValidateImageTemplateJSON(dataJSON); err != nil { + t.Errorf("expected image-templates/azl3-x86_64-edge-raw.yml to pass, but got: %v", err) } } -func TestInvalidImage(t *testing.T) { +func TestInvalidImageTemplate(t *testing.T) { v := loadFile(t, "testdata/invalid-image.yml") // Parse to generic JSON interface var raw interface{} if err := yaml.Unmarshal(v, &raw); err != nil { t.Errorf("yml parsing error: %v", err) + return } // Re‐marshal to JSON bytes dataJSON, err := json.Marshal(raw) if err != nil { t.Errorf("json marshaling error: %v", err) + return } - if err := ValidateImageJSON(dataJSON); err == nil { + + if err := ValidateImageTemplateJSON(dataJSON); err == nil { t.Errorf("expected testdata/invalid-image.yml to pass, but got: %v", err) } } +// Test global config validation func TestValidConfig(t *testing.T) { v := loadFile(t, "testdata/valid-config.yml") @@ -97,18 +90,118 @@ func TestInvalidConfig(t *testing.T) { var raw interface{} if err := yaml.Unmarshal(v, &raw); err != nil { t.Errorf("yml parsing error: %v", err) + return } // Re‐marshal to JSON bytes dataJSON, err := yaml.YAMLToJSON(v) if err != nil { t.Errorf("json marshaling error: %v", err) + return } if err := ValidateConfigJSON(dataJSON); err == nil { t.Errorf("expected invalid-config.json to fail validation: %v", err) - } else { - t.Logf("expected validation error: %v", err) + } +} + +// Test validation of template structure using external test files +func TestImageTemplateStructure(t *testing.T) { + v := loadFile(t, "testdata/complete-valid-template.yml") + + var raw interface{} + if err := yaml.Unmarshal(v, &raw); err != nil { + t.Fatalf("failed to parse minimal template: %v", err) } + dataJSON, err := json.Marshal(raw) + if err != nil { + t.Fatalf("failed to marshal to JSON: %v", err) + } + + if err := ValidateImageTemplateJSON(dataJSON); err != nil { + t.Errorf("minimal template should be valid, but got: %v", err) + } +} + +func TestImageTemplateMissingFields(t *testing.T) { + v := loadFile(t, "testdata/incomplete-template.yml") + + var raw interface{} + if err := yaml.Unmarshal(v, &raw); err != nil { + t.Fatalf("failed to parse invalid template: %v", err) + } + + dataJSON, err := json.Marshal(raw) + if err != nil { + t.Fatalf("failed to marshal to JSON: %v", err) + } + + if err := ValidateImageTemplateJSON(dataJSON); err == nil { + t.Errorf("incomplete template should fail validation") + } +} + +// Table-driven test for multiple template validation scenarios +func TestImageTemplateValidation(t *testing.T) { + tests := []struct { + name string + file string + shouldPass bool + description string + }{ + { + name: "ValidComplete", + file: "testdata/complete-valid-template.yml", + shouldPass: true, + description: "complete template with all optional fields", + }, + { + name: "InvalidMissingImage", + file: "testdata/missing-image-section.yml", + shouldPass: false, + description: "template missing image section", + }, + { + name: "InvalidMissingTarget", + file: "testdata/missing-target-section.yml", + shouldPass: false, + description: "template missing target section", + }, + { + name: "InvalidMissingSystemConfigs", + file: "testdata/missing-systemconfigs.yml", + shouldPass: false, + description: "template missing systemConfigs section", + }, + { + name: "InvalidWrongTypes", + file: "testdata/wrong-field-types.yml", + shouldPass: false, + description: "template with incorrect field types", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := loadFile(t, tt.file) + + var raw interface{} + if err := yaml.Unmarshal(v, &raw); err != nil { + t.Fatalf("failed to parse template %s: %v", tt.file, err) + } + + dataJSON, err := json.Marshal(raw) + if err != nil { + t.Fatalf("failed to marshal to JSON: %v", err) + } + + err = ValidateImageTemplateJSON(dataJSON) + if tt.shouldPass && err != nil { + t.Errorf("expected %s to pass validation (%s), but got error: %v", tt.file, tt.description, err) + } else if !tt.shouldPass && err == nil { + t.Errorf("expected %s to fail validation (%s), but it passed", tt.file, tt.description) + } + }) + } } diff --git a/schema/embed.go b/schema/embed.go index b7621ecc..c0a24717 100644 --- a/schema/embed.go +++ b/schema/embed.go @@ -2,11 +2,8 @@ package schema import _ "embed" -//go:embed os-image-composer.schema.json -var ComposerSchema []byte - //go:embed os-image-template.schema.json -var ImageSchema []byte +var ImageTemplateSchema []byte //go:embed os-image-composer-config.schema.json var ConfigSchema []byte diff --git a/schema/os-image-composer.schema.json b/schema/os-image-composer.schema.json deleted file mode 100644 index 85cf2350..00000000 --- a/schema/os-image-composer.schema.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://https://raw.githubusercontent.com/intel-innersource/os.linux.tiberos.os-curation-tool/refs/heads/main/schemas/os-image-composer.schema.json", - "title": "OS Image Composer Input", - "description": "Defines packages, OS platform/version, output formats, and kernel configuration for the image composer", - "type": "object", - - "properties": { - "distro": { - "type": "string", - "description": "Which OS platform to build against", - "enum": ["EMT", "AzureLinux", "eLxr"] - }, - "version": { - "type": "string", - "description": "Version of the selected OS platform", - "pattern": "^[0-9]+(\\.[0-9]+)*$" - }, - "arch": { - "type": "string", - "description": "Architecture of the OS image", - "enum": ["x86_64", "aarch64"] - }, - "packages": { - "type": "array", - "description": "List of packages to include", - "items": { "type": "string" }, - "minItems": 1 - }, - "immutable": { - "type": "boolean", - "description": "Flags indicating whether each package is immutable" - }, - "output": { - "type": "string", - "description": "Desired output artifact types", - "items": { - "type": "string", - "enum": ["iso", "img", "vhd"] - }, - "minItems": 1 - }, - "kernel": { - "type": "object", - "description": "Kernel version and optional command-line", - "properties": { - "version": { - "type": "string", - "pattern": "^[0-9]+\\.[0-9]+(\\.[0-9]+)?$", - "description": "Kernel version to include" - }, - "cmdline": { - "type": "string", - "description": "Additional kernel command-line parameters" - } - }, - "required": ["version"], - "additionalProperties": false - } - }, - - "required": ["distro", "version", "arch", "packages", "immutable", "output", "kernel"], - "additionalProperties": false, - - "allOf": [ - { - "if": { "properties": { "distro": { "const": "EMT" } } }, - "then": { "properties": { "version": { "enum": ["3.0"] } } } - }, - { - "if": { "properties": { "distro": { "const": "AzureLinux" } } }, - "then": { "properties": { "version": { "enum": ["3"] } } } - }, - { - "if": { "properties": { "distro": { "const": "eLxr" } } }, - "then": { "properties": { "version": { "enum": ["12"] } } } - } - ] - } - \ No newline at end of file diff --git a/schema/os-image-template.schema.json b/schema/os-image-template.schema.json index 5a5b769b..86a0b82f 100644 --- a/schema/os-image-template.schema.json +++ b/schema/os-image-template.schema.json @@ -1,68 +1,122 @@ { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://https://raw.githubusercontent.com/intel-innersource/os.linux.tiberos.os-curation-tool/refs/heads/main/schemas/os-image-template.schema.json", - "title": "OS Image Template", - "description": "Defines the layout for a raw or VHD/X image", - "type": "object", - "properties": { - "filename": { - "type": "string", - "description": "Name of the output disk image file, e.g. \"disk.raw\"" - }, - "size": { - "type": "string", - "description": "Size of the disk image, e.g. \"4GiB\"" - }, - "partitions": { - "type": "array", - "description": "List of partitions to create", - "minItems": 1, - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Identifier for this partition, e.g. \"EFI\" or \"Root\"" - }, - "type": { - "type": "string", - "enum": [ - "efi", - "primary" - ], - "description": "\"efi\" for the EFI System Partition, \"primary\" for other partitions" - }, - "size": { - "type": "string", - "description": "Size of this partition, e.g. \"512MiB\" or \"rest\"" - }, - "fsType": { - "type": "string", - "enum": [ - "vfat", - "ext4" - ], - "description": "Filesystem type to format this partition with" - }, - "mountPoint": { - "type": "string", - "description": "Mount point inside the chroot, e.g. \"/boot/efi\" or \"/\"" - } - }, - "required": [ - "name", - "type", - "size", - "fsType", - "mountPoint" - ], - "additionalProperties": false - } - } - }, - "required": [ - "filename", - "size", - "partitions" - ] + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/intel-innersource/os.linux.tiberos.os-curation-tool/refs/heads/main/schemas/os-image-template.schema.json", + "title": "OS Image Template", + "description": "Defines image configuration including target OS, packages, kernel, and system configurations", + "type": "object", + + "properties": { + "image": { + "type": "object", + "description": "Image metadata and versioning", + "properties": { + "name": { + "type": "string", + "description": "Name of the image template", + "pattern": "^[a-zA-Z0-9]([a-zA-Z0-9\\-_]*[a-zA-Z0-9])?$" + }, + "version": { + "type": "string", + "description": "Version of the image template", + "pattern": "^[0-9]+(\\.[0-9]+)*$" + } + }, + "required": ["name", "version"], + "additionalProperties": false + }, + "target": { + "type": "object", + "description": "Target OS and image configuration", + "properties": { + "os": { + "type": "string", + "description": "Target operating system", + "enum": ["azure-linux", "emt", "elxr"] + }, + "dist": { + "type": "string", + "description": "Distribution identifier", + "enum": ["azl3", "emt3", "elxr12"] + }, + "arch": { + "type": "string", + "description": "Target architecture", + "enum": ["x86_64", "aarch64"] + }, + "imageType": { + "type": "string", + "description": "Type of image to build", + "enum": ["raw", "iso"] + } + }, + "required": ["os", "dist", "arch", "imageType"], + "additionalProperties": false + }, + "systemConfigs": { + "type": "array", + "description": "Array of system configuration objects", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the system configuration", + "pattern": "^[a-zA-Z0-9]([a-zA-Z0-9\\-_]*[a-zA-Z0-9])?$" + }, + "description": { + "type": "string", + "description": "Description of the system configuration" + }, + "packages": { + "type": "array", + "description": "List of packages to include in the system", + "items": { + "type": "string", + "pattern": "^[a-zA-Z0-9]([a-zA-Z0-9\\-_.+]*[a-zA-Z0-9])?$" + }, + "minItems": 1, + "uniqueItems": true + }, + "kernel": { + "type": "object", + "description": "Kernel configuration", + "properties": { + "version": { + "type": "string", + "description": "Kernel version", + "pattern": "^[0-9]+\\.[0-9]+(\\.[0-9]+)?$" + }, + "cmdline": { + "type": "string", + "description": "Kernel command line parameters" + } + }, + "required": ["version"], + "additionalProperties": false + } + }, + "required": ["name"], + "additionalProperties": false + }, + "minItems": 1 + } + }, + + "required": ["image", "target", "systemConfigs"], + "additionalProperties": false, + + "allOf": [ + { + "if": { "properties": { "target": { "properties": { "os": { "const": "azure-linux" } } } } }, + "then": { "properties": { "target": { "properties": { "dist": { "enum": ["azl3"] } } } } } + }, + { + "if": { "properties": { "target": { "properties": { "os": { "const": "emt" } } } } }, + "then": { "properties": { "target": { "properties": { "dist": { "enum": ["emt3"] } } } } } + }, + { + "if": { "properties": { "target": { "properties": { "os": { "const": "elxr" } } } } }, + "then": { "properties": { "target": { "properties": { "dist": { "enum": ["elxr12"] } } } } } + } + ] } \ No newline at end of file diff --git a/testdata/complete-valid-template.yml b/testdata/complete-valid-template.yml new file mode 100644 index 00000000..4bc38e81 --- /dev/null +++ b/testdata/complete-valid-template.yml @@ -0,0 +1,20 @@ +image: + name: complete-test-image + version: "2.0.0" + +target: + os: azure-linux + dist: azl3 + arch: x86_64 + imageType: raw + +systemConfigs: + - name: complete + packages: + - openssh-server + - curl + - vim + - git + kernel: + version: "6.12" + cmdline: "quiet splash" diff --git a/testdata/empty-system-configs.yml b/testdata/empty-system-configs.yml new file mode 100644 index 00000000..9497cace --- /dev/null +++ b/testdata/empty-system-configs.yml @@ -0,0 +1,9 @@ +image: + name: test + version: "1.0.0" +target: + os: azure-linux + dist: azl3 + arch: x86_64 + imageType: raw +systemConfigs: [] # Invalid: minItems is 1 diff --git a/testdata/incomplete-template.yml b/testdata/incomplete-template.yml new file mode 100644 index 00000000..c7d1e8d4 --- /dev/null +++ b/testdata/incomplete-template.yml @@ -0,0 +1,15 @@ +image: + name: test-image + # Missing version + +target: + os: azure-linux + dist: azl3 + arch: x86_64 + # Missing imageType + +systemConfigs: + - name: incomplete + packages: + - openssh-server + # Missing kernel section which might be required diff --git a/testdata/invalid-os-dist-combination.yml b/testdata/invalid-os-dist-combination.yml new file mode 100644 index 00000000..d0baf197 --- /dev/null +++ b/testdata/invalid-os-dist-combination.yml @@ -0,0 +1,13 @@ +image: + name: test + version: "1.0.0" +target: + os: azure-linux + dist: emt3 # Invalid: azure-linux should only use azl3 + arch: x86_64 + imageType: raw +systemConfigs: + - name: test + packages: ["test"] + kernel: + version: "6.12" diff --git a/testdata/invalid.json b/testdata/invalid.json deleted file mode 100644 index 6027aa68..00000000 --- a/testdata/invalid.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "distro": "UnknownOS", - "version": "1.0", - "packages": [], - "immutable": false, - "output": ["exe"], - "kernel": {} - } \ No newline at end of file diff --git a/testdata/missing-image-section.yml b/testdata/missing-image-section.yml new file mode 100644 index 00000000..3c93f624 --- /dev/null +++ b/testdata/missing-image-section.yml @@ -0,0 +1,15 @@ +# Missing entire image section +target: + os: azure-linux + dist: azl3 + arch: x86_64 + imageType: raw + +systemConfigs: + - name: test + description: Test configuration + packages: + - openssh-server + kernel: + version: "6.12" + cmdline: "quiet" diff --git a/testdata/missing-systemconfigs.yml b/testdata/missing-systemconfigs.yml new file mode 100644 index 00000000..3803db63 --- /dev/null +++ b/testdata/missing-systemconfigs.yml @@ -0,0 +1,11 @@ +image: + name: test-image + version: "1.0.0" + +target: + os: azure-linux + dist: azl3 + arch: x86_64 + imageType: raw + +# Missing systemConfigs section diff --git a/testdata/missing-target-section.yml b/testdata/missing-target-section.yml new file mode 100644 index 00000000..c6792422 --- /dev/null +++ b/testdata/missing-target-section.yml @@ -0,0 +1,13 @@ +image: + name: test-image + version: "1.0.0" + +# Missing target section +systemConfigs: + - name: test + description: Test configuration + packages: + - openssh-server + kernel: + version: "6.12" + cmdline: "quiet" diff --git a/testdata/valid.json b/testdata/valid.json deleted file mode 100644 index c6b0d428..00000000 --- a/testdata/valid.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "distro": "AzureLinux", - "version": "3", - "arch": "x86_64", - "packages": ["cloud-init", "rsyslog"], - "immutable": true, - "output": "iso", - "kernel": { "version": "6.12", "cmdline": "quiet splash" } -} diff --git a/testdata/wrong-field-types.yml b/testdata/wrong-field-types.yml new file mode 100644 index 00000000..cc7cdd8d --- /dev/null +++ b/testdata/wrong-field-types.yml @@ -0,0 +1,17 @@ +image: + name: test-image + version: 1.0 # Should be string, not number + +target: + os: azure-linux + dist: azl3 + arch: x86_64 + imageType: raw + +systemConfigs: + - name: test + description: Test configuration + packages: "single-package" # Should be array, not string + kernel: + version: 6.12 # Should be string, not number + cmdline: "quiet"