From a150c5493b09ee7477b440788e03f2f728390088 Mon Sep 17 00:00:00 2001 From: e-minguez Date: Fri, 5 Dec 2025 16:27:54 +0100 Subject: [PATCH] feat: Introduce schema validation and generation tools for EIB definitions. --- contrib/README.md | 12 + contrib/schema-generator/.gitignore | 1 + contrib/schema-generator/Makefile | 16 + contrib/schema-generator/README.md | 40 + contrib/schema-generator/go.mod | 21 + contrib/schema-generator/go.sum | 36 + contrib/schema-generator/schema-generator.go | 531 ++++++++++++ contrib/schema-generator/schema.json | 822 +++++++++++++++++++ contrib/schema-validator/.gitignore | 1 + contrib/schema-validator/Makefile | 17 + contrib/schema-validator/README.md | 53 ++ contrib/schema-validator/eib.yaml | 68 ++ contrib/schema-validator/go.mod | 13 + contrib/schema-validator/go.sum | 17 + contrib/schema-validator/schema-validator.go | 109 +++ contrib/schema-validator/schema.json | 807 ++++++++++++++++++ 16 files changed, 2564 insertions(+) create mode 100644 contrib/README.md create mode 100644 contrib/schema-generator/.gitignore create mode 100644 contrib/schema-generator/Makefile create mode 100644 contrib/schema-generator/README.md create mode 100644 contrib/schema-generator/go.mod create mode 100644 contrib/schema-generator/go.sum create mode 100644 contrib/schema-generator/schema-generator.go create mode 100644 contrib/schema-generator/schema.json create mode 100644 contrib/schema-validator/.gitignore create mode 100644 contrib/schema-validator/Makefile create mode 100644 contrib/schema-validator/README.md create mode 100644 contrib/schema-validator/eib.yaml create mode 100644 contrib/schema-validator/go.mod create mode 100644 contrib/schema-validator/go.sum create mode 100644 contrib/schema-validator/schema-validator.go create mode 100644 contrib/schema-validator/schema.json diff --git a/contrib/README.md b/contrib/README.md new file mode 100644 index 00000000..693241e6 --- /dev/null +++ b/contrib/README.md @@ -0,0 +1,12 @@ +# Contrib + +This directory contains tools and scripts that are contributed by the community. + +## Tools + +- **[schema-generator](./schema-generator)**: Generates the JSON schema for the Edge Image Builder configuration file. +- **[schema-validator](./schema-validator)**: Validates an Edge Image Builder configuration file (YAML) against the generated JSON schema. + +## Disclaimer + +Please note that the tools and scripts in this directory are contributions and are **not maintained by the core developers**. They are provided as-is and may not be kept up-to-date with the main project. diff --git a/contrib/schema-generator/.gitignore b/contrib/schema-generator/.gitignore new file mode 100644 index 00000000..f8d28b74 --- /dev/null +++ b/contrib/schema-generator/.gitignore @@ -0,0 +1 @@ +schema-generator \ No newline at end of file diff --git a/contrib/schema-generator/Makefile b/contrib/schema-generator/Makefile new file mode 100644 index 00000000..d2255fe9 --- /dev/null +++ b/contrib/schema-generator/Makefile @@ -0,0 +1,16 @@ +.PHONY: all build run clean + +BINARY_NAME=schema-generator +SOURCE_FILE=schema-generator.go +OUTPUT_FILE=schema.json + +all: build + +build: + go build -o $(BINARY_NAME) $(SOURCE_FILE) + +run: + go run $(SOURCE_FILE) > $(OUTPUT_FILE) + +clean: + rm -f $(BINARY_NAME) $(OUTPUT_FILE) diff --git a/contrib/schema-generator/README.md b/contrib/schema-generator/README.md new file mode 100644 index 00000000..f69cbf39 --- /dev/null +++ b/contrib/schema-generator/README.md @@ -0,0 +1,40 @@ +# Schema Generator + +This tool generates the JSON schema for the Edge Image Builder configuration file. It inspects the Go structs in the `pkg/image` package and applies additional validations to match the logic enforced by the application. + +## Files + +- `schema-generator.go`: The source code for the tool. +- `schema.json`: The generated JSON schema. +- `go.mod` & `go.sum`: These files define the Go module for this tool. They are necessary because this tool is a standalone Go program with its own dependencies (like `github.com/invopop/jsonschema`) that might differ from or be independent of the main project's dependencies. This keeps the tool isolated and reproducible. +- `Makefile`: Helper script to build and run the tool. + +## Usage + +You can use the provided `Makefile` to interact with the tool. + +### Generate the Schema + +To run the tool and generate the `schema.json` file: + +```bash +make run +``` + +This will execute `go run schema-generator.go` and redirect the output to `schema.json`. + +### Build the Binary + +To compile the tool into a binary named `schema-generator`: + +```bash +make build +``` + +### Clean + +To remove the generated binary and the `schema.json` file: + +```bash +make clean +``` diff --git a/contrib/schema-generator/go.mod b/contrib/schema-generator/go.mod new file mode 100644 index 00000000..ab52a6f8 --- /dev/null +++ b/contrib/schema-generator/go.mod @@ -0,0 +1,21 @@ +module github.com/suse-edge/edge-image-builder/contrib/schema-generator + +go 1.24.0 + +require ( + github.com/invopop/jsonschema v0.12.0 + github.com/suse-edge/edge-image-builder v0.0.0 + github.com/wk8/go-ordered-map/v2 v2.1.8 +) + +require ( + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/buger/jsonparser v1.1.1 // indirect + github.com/kr/pretty v0.3.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/rogpeppe/go-internal v1.11.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/suse-edge/edge-image-builder => ../../ diff --git a/contrib/schema-generator/go.sum b/contrib/schema-generator/go.sum new file mode 100644 index 00000000..3c53b34e --- /dev/null +++ b/contrib/schema-generator/go.sum @@ -0,0 +1,36 @@ +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI= +github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/contrib/schema-generator/schema-generator.go b/contrib/schema-generator/schema-generator.go new file mode 100644 index 00000000..1383f2d6 --- /dev/null +++ b/contrib/schema-generator/schema-generator.go @@ -0,0 +1,531 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + + "github.com/invopop/jsonschema" + "github.com/suse-edge/edge-image-builder/pkg/image" + orderedmap "github.com/wk8/go-ordered-map/v2" +) + +func main() { + // Helper variables for schema properties that require pointers. + // The jsonschema library uses pointers for primitive types (like integers and booleans) + // to distinguish between a zero value (e.g., 0 or false) and an omitted value (nil). + // For example, MinItems: &uint64_1 means "minimum items is 1", whereas MinItems: nil + // means "no minimum items constraint". + uint64_1 := uint64(1) + minItems1 := &uint64_1 + minLength1 := &uint64_1 + minPriority := json.Number("0") + maxPriority := json.Number("99") + + // By default, the library looks for `json` tags. We need to configure it + // to use `yaml` tags, as that's what the edge-image-builder structs use. + reflector := &jsonschema.Reflector{ + // Treat all fields as optional unless they have a `validate:"required"` tag. + // This allows us to manually specify required fields later, which gives us more control + // over conditional requirements. + RequiredFromJSONSchemaTags: true, + FieldNameTag: "yaml", + } + + schema := reflector.Reflect(&image.Definition{}) + + // Manually mark fields as required that are not tagged with `validate:"required"` + // but are validated programmatically in the codebase. + // See: https://github.com/suse-edge/edge-image-builder/blob/main/pkg/image/validation.go + if definition, ok := schema.Definitions["Definition"]; ok { + definition.AdditionalProperties = jsonschema.FalseSchema + definition.Required = append(definition.Required, "image", "operatingSystem", "apiVersion") + definition.Description = `Edge Image Builder Configuration. +ROOT OBJECT. All other configurations must be nested within this object. + +Example: +{ + "apiVersion": "1.0", + "image": { + "imageType": "iso", + "arch": "x86_64", + "baseImage": "sles15sp5.iso", + "outputImageName": "my-image" + }, + "operatingSystem": { + "users": [{"username": "root", "encryptedPassword": "..."}], + "isoConfiguration": { "installDevice": "/dev/sda" } + } +}` + + // Add conditional requirements based on imageType. + // We use AllOf to apply multiple independent schemas. + // Each schema in the list checks a specific condition (If) and applies constraints (Then). + definition.AllOf = []*jsonschema.Schema{ + { + // Condition: If image.imageType is "iso" + If: &jsonschema.Schema{ + Properties: func() *orderedmap.OrderedMap[string, *jsonschema.Schema] { + p := jsonschema.NewProperties() + p.Set("image", &jsonschema.Schema{ + Properties: func() *orderedmap.OrderedMap[string, *jsonschema.Schema] { + p := jsonschema.NewProperties() + p.Set("imageType", &jsonschema.Schema{Const: "iso"}) + return p + }(), + }) + return p + }(), + }, + // Then: Ensure certain fields in operatingSystem.rawConfiguration are NOT present. + // This prevents users from configuring RAW-specific settings when building an ISO. + // Then: Ensure operatingSystem.isoConfiguration is NOT present and rawConfiguration IS present. + Then: &jsonschema.Schema{ + Properties: func() *orderedmap.OrderedMap[string, *jsonschema.Schema] { + p := jsonschema.NewProperties() + p.Set("operatingSystem", &jsonschema.Schema{ + Properties: func() *orderedmap.OrderedMap[string, *jsonschema.Schema] { + p := jsonschema.NewProperties() + p.Set("isoConfiguration", &jsonschema.Schema{ + Description: "Configuration specific to ISO image builds. Optional when imageType is 'iso'.", + }) + p.Set("rawConfiguration", &jsonschema.Schema{ + Not: &jsonschema.Schema{}, + Description: "Configuration specific to RAW image builds. Forbidden when imageType is 'iso'.", + }) + return p + }(), + }) + return p + }(), + }, + }, + { + // Condition: If image.imageType is "raw" + If: &jsonschema.Schema{ + Properties: func() *orderedmap.OrderedMap[string, *jsonschema.Schema] { + p := jsonschema.NewProperties() + p.Set("image", &jsonschema.Schema{ + Properties: func() *orderedmap.OrderedMap[string, *jsonschema.Schema] { + p := jsonschema.NewProperties() + p.Set("imageType", &jsonschema.Schema{Const: "raw"}) + return p + }(), + }) + return p + }(), + }, + // Then: Ensure operatingSystem.isoConfiguration.installDevice is NOT present. + // This prevents users from configuring ISO-specific settings when building a RAW image. + // Then: Ensure operatingSystem.rawConfiguration IS present and isoConfiguration is NOT present. + Then: &jsonschema.Schema{ + Properties: func() *orderedmap.OrderedMap[string, *jsonschema.Schema] { + p := jsonschema.NewProperties() + p.Set("operatingSystem", &jsonschema.Schema{ + Properties: func() *orderedmap.OrderedMap[string, *jsonschema.Schema] { + p := jsonschema.NewProperties() + p.Set("isoConfiguration", &jsonschema.Schema{ + Not: &jsonschema.Schema{}, + Description: "Configuration specific to ISO image builds. Forbidden when imageType is 'raw'.", + }) + p.Set("rawConfiguration", &jsonschema.Schema{ + Description: "Configuration specific to RAW image builds. Optional when imageType is 'raw'.", + }) + return p + }(), + }) + return p + }(), + }, + }, + } + } + // Manually add enum validation for apiVersion. + // See: https://github.com/suse-edge/edge-image-builder/blob/main/pkg/version/version.go + if definition, ok := schema.Definitions["Definition"]; ok { + if apiVersion, ok := definition.Properties.Get("apiVersion"); ok { + apiVersion.Enum = []interface{}{"1.0", "1.1", "1.2", "1.3"} + } + } + + // Manually add enum validation for fields with `oneof` tags. + // See: https://github.com/suse-edge/edge-image-builder/blob/main/pkg/image/definition.go + if imageDefinition, ok := schema.Definitions["Image"]; ok { + if imageType, ok := imageDefinition.Properties.Get("imageType"); ok { + imageType.Enum = []interface{}{"iso", "raw"} + imageType.Description = "Type of image to build. Must be 'iso' or 'raw'." + } + if arch, ok := imageDefinition.Properties.Get("arch"); ok { + arch.Enum = []interface{}{"x86_64", "aarch64"} + arch.Description = "Target architecture. Must be 'x86_64' or 'aarch64'." + } + if baseImage, ok := imageDefinition.Properties.Get("baseImage"); ok { + baseImage.Description = "Name of the base image to use (e.g., 'sles15sp5-x86_64'). Required." + } + if outputImageName, ok := imageDefinition.Properties.Get("outputImageName"); ok { + outputImageName.Description = "Name of the output image file. Required." + } + imageDefinition.Required = append(imageDefinition.Required, "imageType", "arch", "baseImage", "outputImageName") + + // Add conditional requirements for the 'image' definition. + // (The existing imageDefinition.AllOf block for imageType/baseImage/iso is correct and remains here) + } + + if k8sDefinition, ok := schema.Definitions["Kubernetes"]; ok { + k8sDefinition.AdditionalProperties = jsonschema.FalseSchema + k8sDefinition.Required = append(k8sDefinition.Required, "version") + k8sDefinition.Description = `Kubernetes configuration. +Example: +{ + "version": "1.29.0", + "network": { "apiVIP": "1.2.3.4" }, + "nodes": [ + { "hostname": "node1", "type": "server", "initializer": true }, + { "hostname": "node2", "type": "server" } + ] +}` + if nodes, ok := k8sDefinition.Properties.Get("nodes"); ok { + nodes.Description = "List of nodes. Required for multi-node. Example: [{ 'hostname': 'n1', 'type': 'server' }]" + } + if network, ok := k8sDefinition.Properties.Get("network"); ok { + network.Description = "Network config. Example: { 'apiVIP': '1.2.3.4' }" + } + } + + if osDefinition, ok := schema.Definitions["OperatingSystem"]; ok { + osDefinition.Description = `Operating System configuration. +Example (ISO): +{ + "users": [{"username": "root", "encryptedPassword": "..."}], + "isoConfiguration": { "installDevice": "/dev/sda" }, + "time": { "timezone": "UTC", "ntp": { "servers": ["pool.ntp.org"] } } +}` + if time, ok := osDefinition.Properties.Get("time"); ok { + time.Description = "Time configuration. Example: { 'timezone': 'UTC', 'ntp': { 'servers': ['pool.ntp.org'] } }" + } + if isoConfig, ok := osDefinition.Properties.Get("isoConfiguration"); ok { + isoConfig.Description = "ISO configuration object. MUST be nested here. Example: { 'installDevice': '/dev/sda' }" + } + if rawConfig, ok := osDefinition.Properties.Get("rawConfiguration"); ok { + rawConfig.Description = "RAW configuration object. MUST be nested here. Example: { 'diskSize': '10G' }" + } + if users, ok := osDefinition.Properties.Get("users"); ok { + users.Description = "List of users. Example: [{ 'username': 'user', 'password': '...' }]" + } + } + + if timeDefinition, ok := schema.Definitions["Time"]; ok { + if ntp, ok := timeDefinition.Properties.Get("ntp"); ok { + ntp.Description = "NTP config. 'servers' and 'pools' are lists of STRINGS. Example: { 'servers': ['1.2.3.4'] }" + } + } + + // Add conditional requirements for the 'operatingSystem' definition. + // if osDefinition, ok := schema.Definitions["OperatingSystem"]; ok { + // // We removed the OneOf constraint here because it was too generic. + // // Instead, we enforce the presence/absence of isoConfiguration/rawConfiguration + // // in the top-level Definition.AllOf block based on image.imageType. + // } + + if userDefinition, ok := schema.Definitions["OperatingSystemUser"]; ok { + if username, ok := userDefinition.Properties.Get("username"); ok { + username.Description = "Username for the user. Required." + } + if password, ok := userDefinition.Properties.Get("encryptedPassword"); ok { + password.Description = "Encrypted password for the user. Required if sshKey is not provided." + } + if sshKey, ok := userDefinition.Properties.Get("sshKeys"); ok { + sshKey.Description = "List of SSH keys for the user. Required if encryptedPassword is not provided." + } + } + + if ntpDefinition, ok := schema.Definitions["NtpConfiguration"]; ok { + if servers, ok := ntpDefinition.Properties.Get("servers"); ok { + servers.Description = "List of NTP server addresses (e.g., ['pool.ntp.org']). Must be a list of strings." + } + if pools, ok := ntpDefinition.Properties.Get("pools"); ok { + pools.Description = "List of NTP pool addresses. Must be a list of strings." + } + } + + // Add conditional requirements for the 'Packages' definition. + + // Add conditional requirements for the 'Packages' definition. + if packagesDefinition, ok := schema.Definitions["Packages"]; ok { + // If packageList is present (and has at least 1 item)... + packagesDefinition.If = &jsonschema.Schema{ + Properties: newProperties("packageList", &jsonschema.Schema{ + MinItems: minItems1, + }), + Required: []string{"packageList"}, + } + // ...then either sccRegistrationCode OR additionalRepos (or both) must be present. + // AnyOf allows one or more of the sub-schemas to match. + packagesDefinition.Then = &jsonschema.Schema{ + AnyOf: []*jsonschema.Schema{ + {Required: []string{"sccRegistrationCode"}}, + {Required: []string{"additionalRepos"}}, + }, + } + } + + if addRepoDefinition, ok := schema.Definitions["AddRepo"]; ok { + addRepoDefinition.Required = append(addRepoDefinition.Required, "url") + if priority, ok := addRepoDefinition.Properties.Get("priority"); ok { + priority.Minimum = minPriority + priority.Maximum = maxPriority + } + } + + // Add conditional requirements for the 'Time' definition. + // See: https://github.com/suse-edge/edge-image-builder/blob/main/pkg/image/validation/os.go + // Timezone is not strictly required by validation logic, so we make it optional to avoid Gemini errors. + + if ntpDefinition, ok := schema.Definitions["NtpConfiguration"]; ok { + ntpDefinition.OneOf = []*jsonschema.Schema{ + { + Properties: func() *orderedmap.OrderedMap[string, *jsonschema.Schema] { + p := newProperties("pools", &jsonschema.Schema{MinItems: minItems1}) + return p + }(), + Required: []string{"pools"}, + }, + { + Properties: func() *orderedmap.OrderedMap[string, *jsonschema.Schema] { + p := newProperties("servers", &jsonschema.Schema{MinItems: minItems1}) + return p + }(), + Required: []string{"servers"}, + }, + } + ntpDefinition.If = &jsonschema.Schema{ + Properties: newProperties("forceWait", &jsonschema.Schema{Const: true}), + Required: []string{"forceWait"}, + } + ntpDefinition.Then = &jsonschema.Schema{ + AnyOf: []*jsonschema.Schema{ + {Required: []string{"pools"}}, + {Required: []string{"servers"}}, + }, + } + } + + // Add conditional requirements for the 'Users' definition. + // See: https://github.com/suse-edge/edge-image-builder/blob/main/pkg/image/validation/os.go + if userDefinition, ok := schema.Definitions["OperatingSystemUser"]; ok { + userDefinition.AdditionalProperties = jsonschema.FalseSchema + userDefinition.Description = `User configuration. +Allowed fields: "username", "uid", "encryptedPassword", "sshKeys", "primaryGroup", "secondaryGroups", "createHomeDir". +DO NOT use "name", "password", "sshKey" (singular).` + userDefinition.Required = append(userDefinition.Required, "username") + userDefinition.OneOf = []*jsonschema.Schema{ + {Required: []string{"encryptedPassword"}}, + {Required: []string{"sshKeys"}}, + } + } + + // Add conditional requirements for the 'Suma' definition. + if sumaDefinition, ok := schema.Definitions["Suma"]; ok { + sumaDefinition.Required = append(sumaDefinition.Required, "host", "activationKey") + } + + // Add conditional requirements for the 'FIPS' definition. + // See: https://github.com/suse-edge/edge-image-builder/blob/main/pkg/image/validation/os.go + if fipsDefinition, ok := schema.Definitions["FIPS"]; ok { + fipsDefinition.Not = &jsonschema.Schema{ + Properties: func() *orderedmap.OrderedMap[string, *jsonschema.Schema] { + p := newProperties("enable", &jsonschema.Schema{ + Const: true, + }) + p.Set("disable", &jsonschema.Schema{ + Const: true, + }) + return p + }(), + } + fipsDefinition.Not.Properties.Set("disable", &jsonschema.Schema{Const: true}) + } + + // Add conditional requirements for the 'RawConfiguration' definition. + // See: https://github.com/suse-edge/edge-image-builder/blob/main/pkg/image/validation/os.go + if rawConfigDefinition, ok := schema.Definitions["RawConfiguration"]; ok { + if diskSize, ok := rawConfigDefinition.Properties.Get("diskSize"); ok { + diskSize.Pattern = "^([1-9]\\d+|[1-9])+([MGT])$" + } + rawConfigDefinition.If = &jsonschema.Schema{ + Properties: newProperties("expandEncryptedPartition", &jsonschema.Schema{Const: true}), + Required: []string{"expandEncryptedPartition"}, + } + rawConfigDefinition.Then = &jsonschema.Schema{ + Required: []string{"luksKey"}, + } + } + + // Add conditional requirements for the 'Group' definition. + // See: https://github.com/suse-edge/edge-image-builder/blob/main/pkg/image/validation/os.go + if groupDefinition, ok := schema.Definitions["Group"]; ok { + groupDefinition.Required = append(groupDefinition.Required, "name") + } + + // Add conditional requirements for the 'Systemd' definition. + // See: https://github.com/suse-edge/edge-image-builder/blob/main/pkg/image/validation/os.go + if systemdDefinition, ok := schema.Definitions["Systemd"]; ok { + if enabled, ok := systemdDefinition.Properties.Get("enable"); ok { + enabled.Items.MinLength = minLength1 + } + if disabled, ok := systemdDefinition.Properties.Get("disable"); ok { + disabled.Items.MinLength = minLength1 + } + } + + // Add conditional requirements for kernel arguments. + if osDefinition, ok := schema.Definitions["OperatingSystem"]; ok { + if kernelArgs, ok := osDefinition.Properties.Get("kernelArgs"); ok { + kernelArgs.Items.MinLength = minLength1 + } + } + + // Manually add format validation for IP address fields. + // See: https://github.com/suse-edge/edge-image-builder/blob/main/pkg/kubernetes/cluster.go + if networkDefinition, ok := schema.Definitions["Network"]; ok { + if apiVIP, ok := networkDefinition.Properties.Get("apiVIP"); ok { + apiVIP.Format = "ipv4" + } + if apiVIP6, ok := networkDefinition.Properties.Get("apiVIP6"); ok { + apiVIP6.Format = "ipv6" + } + } + + // Add conditional requirements for the 'Node' definition. + if nodeDefinition, ok := schema.Definitions["Node"]; ok { + nodeDefinition.AdditionalProperties = jsonschema.FalseSchema + nodeDefinition.Description = "Node configuration. DO NOT include IP addresses here (they are not supported)." + nodeDefinition.Required = append(nodeDefinition.Required, "hostname", "type") + if nodeType, ok := nodeDefinition.Properties.Get("type"); ok { + nodeType.Enum = []interface{}{"server", "agent"} + } + // Initializer implies server + nodeDefinition.AllOf = append(nodeDefinition.AllOf, &jsonschema.Schema{ + If: &jsonschema.Schema{ + Properties: newProperties("initializer", &jsonschema.Schema{Const: true}), + Required: []string{"initializer"}, + }, + Then: &jsonschema.Schema{ + Properties: newProperties("type", &jsonschema.Schema{Const: "server"}), + }, + }) + } + + if timeDefinition, ok := schema.Definitions["Time"]; ok { + timeDefinition.AdditionalProperties = jsonschema.FalseSchema + if timezone, ok := timeDefinition.Properties.Get("timezone"); ok { + timezone.Description = "Timezone (e.g., 'UTC'). Note: field name is lowercase 'timezone'." + } + } + + if ntpDefinition, ok := schema.Definitions["NtpConfiguration"]; ok { + ntpDefinition.AdditionalProperties = jsonschema.FalseSchema + } + + if manifestsDefinition, ok := schema.Definitions["Manifests"]; ok { + if urls, ok := manifestsDefinition.Properties.Get("urls"); ok { + urls.Items.Pattern = "^http(s)?://" + } + } + + if helmChartDefinition, ok := schema.Definitions["HelmChart"]; ok { + helmChartDefinition.AdditionalProperties = jsonschema.FalseSchema + helmChartDefinition.Description = `Helm chart configuration. +Example: +{ + "name": "rancher", + "repositoryName": "rancher-prime", + "version": "2.10.0", + "targetNamespace": "cattle-system", + "createNamespace": true, + "installationNamespace": "kube-system", + "valuesFile": "rancher-values.yaml" +}` + helmChartDefinition.Required = append(helmChartDefinition.Required, "name", "repositoryName", "version") + if repoName, ok := helmChartDefinition.Properties.Get("repositoryName"); ok { + repoName.Description = "Name of the repository to use. Must match a repository defined in 'repositories'. Required. DO NOT use 'repoUrl'." + } + helmChartDefinition.If = &jsonschema.Schema{ + Properties: newProperties("createNamespace", &jsonschema.Schema{Const: true}), + Required: []string{"createNamespace"}, + } + helmChartDefinition.Then = &jsonschema.Schema{ + Required: []string{"targetNamespace"}, + } + } + + if helmRepoDefinition, ok := schema.Definitions["HelmRepository"]; ok { + helmRepoDefinition.AdditionalProperties = jsonschema.FalseSchema + helmRepoDefinition.Description = `Helm repository configuration. +Example: +{ + "name": "rancher-prime", + "url": "https://charts.rancher.com/server-charts/prime" +}` + helmRepoDefinition.Required = append(helmRepoDefinition.Required, "name", "url") + if url, ok := helmRepoDefinition.Properties.Get("url"); ok { + url.Pattern = "^(oci|http|https)://" + } + // plainHTTP and skipTLSVerify cannot both be true + helmRepoDefinition.AllOf = append(helmRepoDefinition.AllOf, &jsonschema.Schema{ + Not: &jsonschema.Schema{ + Properties: func() *orderedmap.OrderedMap[string, *jsonschema.Schema] { + p := jsonschema.NewProperties() + p.Set("plainHTTP", &jsonschema.Schema{Const: true}) + p.Set("skipTLSVerify", &jsonschema.Schema{Const: true}) + return p + }(), + Required: []string{"plainHTTP", "skipTLSVerify"}, + }, + }) + } + + if helmDefinition, ok := schema.Definitions["Helm"]; ok { + // If charts are defined, repositories must also be defined. + helmDefinition.If = &jsonschema.Schema{ + Properties: newProperties("charts", &jsonschema.Schema{ + MinItems: minItems1, + }), + Required: []string{"charts"}, + } + helmDefinition.Then = &jsonschema.Schema{ + Properties: newProperties("repositories", &jsonschema.Schema{ + MinItems: minItems1, + }), + Required: []string{"repositories"}, + } + } + + // Add top-level schema details. + schema.Version = "http://json-schema.org/draft-07/schema#" + schema.Title = "Edge Image Builder Configuration" + schema.Description = "Schema for the configuration file used by the SUSE Edge Image Builder." + + // Fix root type to be object (required for some MCP clients) + if schema.Ref != "" { + schema.AllOf = []*jsonschema.Schema{{Ref: schema.Ref}} + schema.Ref = "" + } + schema.Type = "object" + + // Marshal the schema to nicely formatted JSON. + schemaBytes, err := json.MarshalIndent(schema, "", " ") + if err != nil { + log.Fatalf("Error marshalling schema to JSON: %v", err) + } + + // Print the final schema to standard output. + fmt.Println(string(schemaBytes)) +} + +// newProperties is a helper function to create a new ordered map for properties. +func newProperties(key string, value *jsonschema.Schema) *orderedmap.OrderedMap[string, *jsonschema.Schema] { + p := jsonschema.NewProperties() + p.Set(key, value) + return p +} diff --git a/contrib/schema-generator/schema.json b/contrib/schema-generator/schema.json new file mode 100644 index 00000000..bccbeab3 --- /dev/null +++ b/contrib/schema-generator/schema.json @@ -0,0 +1,822 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://github.com/suse-edge/edge-image-builder/pkg/image/definition", + "$defs": { + "AddRepo": { + "properties": { + "url": { + "type": "string" + }, + "unsigned": { + "type": "boolean" + }, + "priority": { + "type": "integer", + "maximum": 99, + "minimum": 0 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "url" + ] + }, + "ContainerImage": { + "properties": { + "name": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + }, + "Definition": { + "allOf": [ + { + "if": { + "properties": { + "image": { + "properties": { + "imageType": { + "const": "iso" + } + } + } + } + }, + "then": { + "properties": { + "operatingSystem": { + "properties": { + "isoConfiguration": { + "description": "Configuration specific to ISO image builds. Optional when imageType is 'iso'." + }, + "rawConfiguration": { + "not": true, + "description": "Configuration specific to RAW image builds. Forbidden when imageType is 'iso'." + } + } + } + } + } + }, + { + "if": { + "properties": { + "image": { + "properties": { + "imageType": { + "const": "raw" + } + } + } + } + }, + "then": { + "properties": { + "operatingSystem": { + "properties": { + "isoConfiguration": { + "not": true, + "description": "Configuration specific to ISO image builds. Forbidden when imageType is 'raw'." + }, + "rawConfiguration": { + "description": "Configuration specific to RAW image builds. Optional when imageType is 'raw'." + } + } + } + } + } + } + ], + "properties": { + "apiVersion": { + "type": "string", + "enum": [ + "1.0", + "1.1", + "1.2", + "1.3" + ] + }, + "image": { + "$ref": "#/$defs/Image" + }, + "operatingSystem": { + "$ref": "#/$defs/OperatingSystem" + }, + "embeddedArtifactRegistry": { + "$ref": "#/$defs/EmbeddedArtifactRegistry" + }, + "kubernetes": { + "$ref": "#/$defs/Kubernetes" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "image", + "operatingSystem", + "apiVersion" + ], + "description": "Edge Image Builder Configuration.\nROOT OBJECT. All other configurations must be nested within this object.\n\nExample:\n{\n \"apiVersion\": \"1.0\",\n \"image\": {\n \"imageType\": \"iso\",\n \"arch\": \"x86_64\",\n \"baseImage\": \"sles15sp5.iso\",\n \"outputImageName\": \"my-image\"\n },\n \"operatingSystem\": {\n \"users\": [{\"username\": \"root\", \"encryptedPassword\": \"...\"}],\n \"isoConfiguration\": { \"installDevice\": \"/dev/sda\" }\n }\n}" + }, + "EmbeddedArtifactRegistry": { + "properties": { + "images": { + "items": { + "$ref": "#/$defs/ContainerImage" + }, + "type": "array" + }, + "registries": { + "items": { + "$ref": "#/$defs/Registry" + }, + "type": "array" + } + }, + "additionalProperties": false, + "type": "object" + }, + "Helm": { + "if": { + "properties": { + "charts": { + "minItems": 1 + } + }, + "required": [ + "charts" + ] + }, + "then": { + "properties": { + "repositories": { + "minItems": 1 + } + }, + "required": [ + "repositories" + ] + }, + "properties": { + "charts": { + "items": { + "$ref": "#/$defs/HelmChart" + }, + "type": "array" + }, + "repositories": { + "items": { + "$ref": "#/$defs/HelmRepository" + }, + "type": "array" + } + }, + "additionalProperties": false, + "type": "object" + }, + "HelmAuthentication": { + "properties": { + "username": { + "type": "string" + }, + "password": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + }, + "HelmChart": { + "if": { + "properties": { + "createNamespace": { + "const": true + } + }, + "required": [ + "createNamespace" + ] + }, + "then": { + "required": [ + "targetNamespace" + ] + }, + "properties": { + "name": { + "type": "string" + }, + "releaseName": { + "type": "string" + }, + "repositoryName": { + "type": "string", + "description": "Name of the repository to use. Must match a repository defined in 'repositories'. Required. DO NOT use 'repoUrl'." + }, + "version": { + "type": "string" + }, + "targetNamespace": { + "type": "string" + }, + "createNamespace": { + "type": "boolean" + }, + "installationNamespace": { + "type": "string" + }, + "valuesFile": { + "type": "string" + }, + "apiVersions": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name", + "repositoryName", + "version" + ], + "description": "Helm chart configuration.\nExample:\n{\n \"name\": \"rancher\",\n \"repositoryName\": \"rancher-prime\",\n \"version\": \"2.10.0\",\n \"targetNamespace\": \"cattle-system\",\n \"createNamespace\": true,\n \"installationNamespace\": \"kube-system\",\n \"valuesFile\": \"rancher-values.yaml\"\n}" + }, + "HelmRepository": { + "allOf": [ + { + "not": { + "properties": { + "plainHTTP": { + "const": true + }, + "skipTLSVerify": { + "const": true + } + }, + "required": [ + "plainHTTP", + "skipTLSVerify" + ] + } + } + ], + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string", + "pattern": "^(oci|http|https)://" + }, + "authentication": { + "$ref": "#/$defs/HelmAuthentication" + }, + "plainHTTP": { + "type": "boolean" + }, + "skipTLSVerify": { + "type": "boolean" + }, + "caFile": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name", + "url" + ], + "description": "Helm repository configuration.\nExample:\n{\n \"name\": \"rancher-prime\",\n \"url\": \"https://charts.rancher.com/server-charts/prime\"\n}" + }, + "Image": { + "properties": { + "imageType": { + "type": "string", + "enum": [ + "iso", + "raw" + ], + "description": "Type of image to build. Must be 'iso' or 'raw'." + }, + "arch": { + "type": "string", + "enum": [ + "x86_64", + "aarch64" + ], + "description": "Target architecture. Must be 'x86_64' or 'aarch64'." + }, + "baseImage": { + "type": "string", + "description": "Name of the base image to use (e.g., 'sles15sp5-x86_64'). Required." + }, + "outputImageName": { + "type": "string", + "description": "Name of the output image file. Required." + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "imageType", + "arch", + "baseImage", + "outputImageName" + ] + }, + "IsoConfiguration": { + "properties": { + "installDevice": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + }, + "Kubernetes": { + "properties": { + "version": { + "type": "string" + }, + "network": { + "$ref": "#/$defs/Network", + "description": "Network config. Example: { 'apiVIP': '1.2.3.4' }" + }, + "nodes": { + "items": { + "$ref": "#/$defs/Node" + }, + "type": "array", + "description": "List of nodes. Required for multi-node. Example: [{ 'hostname': 'n1', 'type': 'server' }]" + }, + "manifests": { + "$ref": "#/$defs/Manifests" + }, + "helm": { + "$ref": "#/$defs/Helm" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "version" + ], + "description": "Kubernetes configuration.\nExample:\n{\n \"version\": \"1.29.0\",\n \"network\": { \"apiVIP\": \"1.2.3.4\" },\n \"nodes\": [\n { \"hostname\": \"node1\", \"type\": \"server\", \"initializer\": true },\n { \"hostname\": \"node2\", \"type\": \"server\" }\n ]\n}" + }, + "Manifests": { + "properties": { + "urls": { + "items": { + "type": "string", + "pattern": "^http(s)?://" + }, + "type": "array" + } + }, + "additionalProperties": false, + "type": "object" + }, + "Network": { + "properties": { + "apiHost": { + "type": "string" + }, + "apiVIP": { + "type": "string", + "format": "ipv4" + }, + "apiVIP6": { + "type": "string", + "format": "ipv6" + } + }, + "additionalProperties": false, + "type": "object" + }, + "Node": { + "allOf": [ + { + "if": { + "properties": { + "initializer": { + "const": true + } + }, + "required": [ + "initializer" + ] + }, + "then": { + "properties": { + "type": { + "const": "server" + } + } + } + } + ], + "properties": { + "hostname": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "server", + "agent" + ] + }, + "initializer": { + "type": "boolean" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "hostname", + "type" + ], + "description": "Node configuration. DO NOT include IP addresses here (they are not supported)." + }, + "NtpConfiguration": { + "oneOf": [ + { + "properties": { + "pools": { + "minItems": 1 + } + }, + "required": [ + "pools" + ] + }, + { + "properties": { + "servers": { + "minItems": 1 + } + }, + "required": [ + "servers" + ] + } + ], + "if": { + "properties": { + "forceWait": { + "const": true + } + }, + "required": [ + "forceWait" + ] + }, + "then": { + "anyOf": [ + { + "required": [ + "pools" + ] + }, + { + "required": [ + "servers" + ] + } + ] + }, + "properties": { + "forceWait": { + "type": "boolean" + }, + "pools": { + "items": { + "type": "string" + }, + "type": "array", + "description": "List of NTP pool addresses. Must be a list of strings." + }, + "servers": { + "items": { + "type": "string" + }, + "type": "array", + "description": "List of NTP server addresses (e.g., ['pool.ntp.org']). Must be a list of strings." + } + }, + "additionalProperties": false, + "type": "object" + }, + "OperatingSystem": { + "properties": { + "kernelArgs": { + "items": { + "type": "string", + "minLength": 1 + }, + "type": "array" + }, + "groups": { + "items": { + "$ref": "#/$defs/OperatingSystemGroup" + }, + "type": "array" + }, + "users": { + "items": { + "$ref": "#/$defs/OperatingSystemUser" + }, + "type": "array", + "description": "List of users. Example: [{ 'username': 'user', 'password': '...' }]" + }, + "systemd": { + "$ref": "#/$defs/Systemd" + }, + "suma": { + "$ref": "#/$defs/Suma" + }, + "packages": { + "$ref": "#/$defs/Packages" + }, + "isoConfiguration": { + "$ref": "#/$defs/IsoConfiguration", + "description": "ISO configuration object. MUST be nested here. Example: { 'installDevice': '/dev/sda' }" + }, + "rawConfiguration": { + "$ref": "#/$defs/RawConfiguration", + "description": "RAW configuration object. MUST be nested here. Example: { 'diskSize': '10G' }" + }, + "time": { + "$ref": "#/$defs/Time", + "description": "Time configuration. Example: { 'timezone': 'UTC', 'ntp': { 'servers': ['pool.ntp.org'] } }" + }, + "proxy": { + "$ref": "#/$defs/Proxy" + }, + "keymap": { + "type": "string" + }, + "enableFIPS": { + "type": "boolean" + } + }, + "additionalProperties": false, + "type": "object", + "description": "Operating System configuration.\nExample (ISO):\n{\n \"users\": [{\"username\": \"root\", \"encryptedPassword\": \"...\"}],\n \"isoConfiguration\": { \"installDevice\": \"/dev/sda\" },\n \"time\": { \"timezone\": \"UTC\", \"ntp\": { \"servers\": [\"pool.ntp.org\"] } }\n}" + }, + "OperatingSystemGroup": { + "properties": { + "name": { + "type": "string" + }, + "gid": { + "type": "integer" + } + }, + "additionalProperties": false, + "type": "object" + }, + "OperatingSystemUser": { + "oneOf": [ + { + "required": [ + "encryptedPassword" + ] + }, + { + "required": [ + "sshKeys" + ] + } + ], + "properties": { + "username": { + "type": "string", + "description": "Username for the user. Required." + }, + "uid": { + "type": "integer" + }, + "encryptedPassword": { + "type": "string", + "description": "Encrypted password for the user. Required if sshKey is not provided." + }, + "sshKeys": { + "items": { + "type": "string" + }, + "type": "array", + "description": "List of SSH keys for the user. Required if encryptedPassword is not provided." + }, + "primaryGroup": { + "type": "string" + }, + "secondaryGroups": { + "items": { + "type": "string" + }, + "type": "array" + }, + "createHomeDir": { + "type": "boolean" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "username" + ], + "description": "User configuration.\nAllowed fields: \"username\", \"uid\", \"encryptedPassword\", \"sshKeys\", \"primaryGroup\", \"secondaryGroups\", \"createHomeDir\".\nDO NOT use \"name\", \"password\", \"sshKey\" (singular)." + }, + "Packages": { + "if": { + "properties": { + "packageList": { + "minItems": 1 + } + }, + "required": [ + "packageList" + ] + }, + "then": { + "anyOf": [ + { + "required": [ + "sccRegistrationCode" + ] + }, + { + "required": [ + "additionalRepos" + ] + } + ] + }, + "properties": { + "noGPGCheck": { + "type": "boolean" + }, + "enableExtras": { + "type": "boolean" + }, + "packageList": { + "items": { + "type": "string" + }, + "type": "array" + }, + "additionalRepos": { + "items": { + "$ref": "#/$defs/AddRepo" + }, + "type": "array" + }, + "sccRegistrationCode": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + }, + "Proxy": { + "properties": { + "httpProxy": { + "type": "string" + }, + "httpsProxy": { + "type": "string" + }, + "noProxy": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "additionalProperties": false, + "type": "object" + }, + "RawConfiguration": { + "if": { + "properties": { + "expandEncryptedPartition": { + "const": true + } + }, + "required": [ + "expandEncryptedPartition" + ] + }, + "then": { + "required": [ + "luksKey" + ] + }, + "properties": { + "diskSize": { + "type": "string", + "pattern": "^([1-9]\\d+|[1-9])+([MGT])$" + }, + "luksKey": { + "type": "string" + }, + "expandEncryptedPartition": { + "type": "boolean" + } + }, + "additionalProperties": false, + "type": "object" + }, + "Registry": { + "properties": { + "uri": { + "type": "string" + }, + "authentication": { + "$ref": "#/$defs/RegistryAuthentication" + } + }, + "additionalProperties": false, + "type": "object" + }, + "RegistryAuthentication": { + "properties": { + "username": { + "type": "string" + }, + "password": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + }, + "Suma": { + "properties": { + "host": { + "type": "string" + }, + "activationKey": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "host", + "activationKey" + ] + }, + "Systemd": { + "properties": { + "enable": { + "items": { + "type": "string", + "minLength": 1 + }, + "type": "array" + }, + "disable": { + "items": { + "type": "string", + "minLength": 1 + }, + "type": "array" + } + }, + "additionalProperties": false, + "type": "object" + }, + "Time": { + "properties": { + "timezone": { + "type": "string", + "description": "Timezone (e.g., 'UTC'). Note: field name is lowercase 'timezone'." + }, + "ntp": { + "$ref": "#/$defs/NtpConfiguration", + "description": "NTP config. 'servers' and 'pools' are lists of STRINGS. Example: { 'servers': ['1.2.3.4'] }" + } + }, + "additionalProperties": false, + "type": "object" + } + }, + "allOf": [ + { + "$ref": "#/$defs/Definition" + } + ], + "type": "object", + "title": "Edge Image Builder Configuration", + "description": "Schema for the configuration file used by the SUSE Edge Image Builder." +} diff --git a/contrib/schema-validator/.gitignore b/contrib/schema-validator/.gitignore new file mode 100644 index 00000000..f55c1a05 --- /dev/null +++ b/contrib/schema-validator/.gitignore @@ -0,0 +1 @@ +schema-validator \ No newline at end of file diff --git a/contrib/schema-validator/Makefile b/contrib/schema-validator/Makefile new file mode 100644 index 00000000..d5b7f430 --- /dev/null +++ b/contrib/schema-validator/Makefile @@ -0,0 +1,17 @@ +.PHONY: all build run clean + +BINARY_NAME=schema-validator +SOURCE_FILE=schema-validator.go +YAML_FILE=eib.yaml +SCHEMA_FILE=../schema-generator/schema.json + +all: build + +build: + go build -o $(BINARY_NAME) $(SOURCE_FILE) + +run: + go run $(SOURCE_FILE) -d $(YAML_FILE) -s $(SCHEMA_FILE) + +clean: + rm -f $(BINARY_NAME) diff --git a/contrib/schema-validator/README.md b/contrib/schema-validator/README.md new file mode 100644 index 00000000..5e5b3838 --- /dev/null +++ b/contrib/schema-validator/README.md @@ -0,0 +1,53 @@ +# Schema Validator + +This tool validates an Edge Image Builder configuration file (YAML) against the generated JSON schema. + +## Files + +- `schema-validator.go`: The source code for the tool. +- `eib.yaml`: A sample configuration file used for testing validation. +- `go.mod` & `go.sum`: Module definitions for the validator tool. +- `Makefile`: Helper script to build and run the tool. + +## Usage + +You can use the provided `Makefile` to interact with the tool. + +### Validate a Configuration + +To run the tool and validate the `eib.yaml` file against the schema generated by the `schema-generator` tool: + +```bash +make run +``` + +This assumes the schema file is located at `../schema-generator/schema.json`. + +### Build the Binary + +To compile the tool into a binary named `schema-validator`: + +```bash +make build +``` + +### Clean + +To remove the generated binary: + +```bash +make clean +``` + +### Manual Usage +You can also run the tool manually: + +```bash +go run schema-validator.go -d -s +``` + +Or using long flags: + +```bash +go run schema-validator.go --definition --schema +``` diff --git a/contrib/schema-validator/eib.yaml b/contrib/schema-validator/eib.yaml new file mode 100644 index 00000000..d688f6f3 --- /dev/null +++ b/contrib/schema-validator/eib.yaml @@ -0,0 +1,68 @@ +apiVersion: 1.1 +image: + imageType: raw + arch: aarch64 + baseImage: SL-Micro.aarch64-6.0-Default-GM2.raw + outputImageName: rke2-cluster-image.raw +operatingSystem: + rawConfiguration: + diskSize: 30G + packages: + additionalRepos: + - url: http://download.nue.suse.com/ibs/SUSE:/CA/SLE_15/ + packageList: + - jq + - ca-certificates-suse + - qemu-guest-agent + sccRegistrationCode: XXXX + systemd: + disable: + - rebootmgr.service + - transactional-update.timer + - transactional-update-cleanup.timer + enable: + - qemu-guest-agent + users: + - username: root + createHomeDir: true + encryptedPassword: XXXX + sshKeys: + - ssh-rsa XXXX +kubernetes: + network: + apiVIP: 192.168.205.100 + apiHost: 192-168-205-100.sslip.io + nodes: + - hostname: vm1 + initializer: true + type: server + - hostname: vm2 + type: server + - hostname: vm3 + type: server + helm: + charts: + - createNamespace: true + installationNamespace: kube-system + name: rancher + repositoryName: rancher-prime + targetNamespace: cattle-system + valuesFile: rancher.yaml + version: 2.10.3 + - createNamespace: true + installationNamespace: kube-system + name: cert-manager + repositoryName: jetstack + targetNamespace: cert-manager + valuesFile: certmanager.yaml + version: 1.14.2 + repositories: + - name: rancher-prime + plainHTTP: false + skipTLSVerify: true + url: https://charts.rancher.com/server-charts/prime + - name: jetstack + plainHTTP: false + skipTLSVerify: true + url: https://charts.jetstack.io + version: v1.31.6+rke2r1 diff --git a/contrib/schema-validator/go.mod b/contrib/schema-validator/go.mod new file mode 100644 index 00000000..42384f98 --- /dev/null +++ b/contrib/schema-validator/go.mod @@ -0,0 +1,13 @@ +module schema-validator + +go 1.25.5 + +require ( + github.com/xeipuuv/gojsonschema v1.2.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect +) diff --git a/contrib/schema-validator/go.sum b/contrib/schema-validator/go.sum new file mode 100644 index 00000000..f7cd70a1 --- /dev/null +++ b/contrib/schema-validator/go.sum @@ -0,0 +1,17 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/contrib/schema-validator/schema-validator.go b/contrib/schema-validator/schema-validator.go new file mode 100644 index 00000000..f169b0aa --- /dev/null +++ b/contrib/schema-validator/schema-validator.go @@ -0,0 +1,109 @@ +package main + +import ( + "flag" + "fmt" + "os" + + "github.com/xeipuuv/gojsonschema" + "gopkg.in/yaml.v3" +) + +func main() { + // 1. Parse command-line flags. + var yamlFilePath string + var schemaFilePath string + + flag.StringVar(&yamlFilePath, "d", "", "Path to the EIB definition YAML file (shorthand)") + flag.StringVar(&yamlFilePath, "definition", "", "Path to the EIB definition YAML file") + flag.StringVar(&schemaFilePath, "s", "", "Path to the JSON schema file (shorthand)") + flag.StringVar(&schemaFilePath, "schema", "", "Path to the JSON schema file") + + flag.Usage = func() { + fmt.Fprintf(flag.CommandLine.Output(), "Usage: schema-validator -d|--definition -s|--schema \n") + fmt.Fprintf(flag.CommandLine.Output(), "\nFlags:\n") + fmt.Fprintf(flag.CommandLine.Output(), " -d (or --definition) \n") + fmt.Fprintf(flag.CommandLine.Output(), " Path to the EIB definition YAML file\n") + fmt.Fprintf(flag.CommandLine.Output(), " -s (or --schema) \n") + fmt.Fprintf(flag.CommandLine.Output(), " Path to the JSON schema file\n") + } + + flag.Parse() + + if yamlFilePath == "" || schemaFilePath == "" { + flag.Usage() + os.Exit(1) + } + + // 2. Read the YAML file from the provided path. + yamlFile, err := os.ReadFile(yamlFilePath) + if err != nil { + fmt.Printf("Error reading YAML file '%s': %s\n", yamlFilePath, err) + os.Exit(1) + } + + // 3. Unmarshal the YAML into a generic interface{} which can represent any YAML structure. + var yamlData any + if err := yaml.Unmarshal(yamlFile, &yamlData); err != nil { + fmt.Printf("Error unmarshalling YAML: %s\n", err) + os.Exit(1) + } + + // Recursively convert numeric apiVersion to string to accommodate YAML's type inference. + yamlData = convertNumericApiVersionToString(yamlData) + + // 4. The gojsonschema library requires loaders for both the schema and the document to be validated. + // We create a loader for the schema from its file path. + schemaLoader := gojsonschema.NewReferenceLoader("file://" + schemaFilePath) + // We create a loader for the YAML data that has been unmarshalled into a Go type. + documentLoader := gojsonschema.NewGoLoader(yamlData) + + // 5. Perform the validation. + result, err := gojsonschema.Validate(schemaLoader, documentLoader) + if err != nil { + fmt.Printf("Error during validation: %s\n", err) + os.Exit(1) + } + + // 6. Check the validation result and provide feedback to the user. + if result.Valid() { + fmt.Printf("The document '%s' is valid.\n", yamlFilePath) + } else { + fmt.Printf("The document '%s' is not valid. see errors:\n", yamlFilePath) + for _, err := range result.Errors() { + // Filter out generic conditional errors that are not actionable for the user. + // The 'allOf' and 'condition_then' errors are implementation details of the schema. + // The specific error, like a missing required field, is what's truly useful. + if err.Type() == "allOf" || err.Type() == "condition_then" { + continue + } + fmt.Printf("- %s\n", err) + } + os.Exit(1) + } +} + +// convertNumericApiVersionToString traverses the unmarshalled YAML data and converts any 'apiVersion' +// field that has been parsed as a number into a string representation. This handles cases +// where the YAML parser interprets values like `1.1` as a float. +func convertNumericApiVersionToString(data any) any { + switch v := data.(type) { + case map[string]any: + // Check for the apiVersion key at the current level. + if apiVersion, ok := v["apiVersion"]; ok { + // If it's a number, convert it to a string. + if num, isNum := apiVersion.(float64); isNum { + v["apiVersion"] = fmt.Sprintf("%.1f", num) + } + } + // Recurse into the rest of the map. + for key, val := range v { + v[key] = convertNumericApiVersionToString(val) + } + case []any: + for i, item := range v { + v[i] = convertNumericApiVersionToString(item) + } + } + return data +} diff --git a/contrib/schema-validator/schema.json b/contrib/schema-validator/schema.json new file mode 100644 index 00000000..2380329c --- /dev/null +++ b/contrib/schema-validator/schema.json @@ -0,0 +1,807 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://github.com/suse-edge/edge-image-builder/pkg/image/definition", + "$defs": { + "AddRepo": { + "properties": { + "url": { + "type": "string" + }, + "unsigned": { + "type": "boolean" + }, + "priority": { + "type": "integer", + "maximum": 99, + "minimum": 0 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "url" + ] + }, + "ContainerImage": { + "properties": { + "name": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + }, + "Definition": { + "allOf": [ + { + "if": { + "properties": { + "image": { + "properties": { + "imageType": { + "const": "iso" + } + } + } + } + }, + "then": { + "properties": { + "operatingSystem": { + "properties": { + "isoConfiguration": { + "required": [ + "installDevice" + ], + "description": "Configuration specific to ISO image builds. Required when imageType is 'iso'. MUST contain 'installDevice'." + }, + "rawConfiguration": { + "not": true, + "description": "Configuration specific to RAW image builds. Forbidden when imageType is 'iso'." + } + }, + "required": [ + "isoConfiguration" + ] + } + } + } + }, + { + "if": { + "properties": { + "image": { + "properties": { + "imageType": { + "const": "raw" + } + } + } + } + }, + "then": { + "properties": { + "operatingSystem": { + "properties": { + "isoConfiguration": { + "not": true, + "description": "Configuration specific to ISO image builds. Forbidden when imageType is 'raw'." + }, + "rawConfiguration": { + "description": "Configuration specific to RAW image builds. Required when imageType is 'raw'. MUST contain 'diskSize' and optional 'luksKey'." + } + }, + "required": [ + "rawConfiguration" + ] + } + } + } + } + ], + "properties": { + "apiVersion": { + "type": "string", + "enum": [ + "1.0", + "1.1", + "1.2", + "1.3" + ] + }, + "image": { + "$ref": "#/$defs/Image" + }, + "operatingSystem": { + "$ref": "#/$defs/OperatingSystem" + }, + "embeddedArtifactRegistry": { + "$ref": "#/$defs/EmbeddedArtifactRegistry" + }, + "kubernetes": { + "$ref": "#/$defs/Kubernetes" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "image", + "operatingSystem", + "apiVersion" + ], + "description": "Edge Image Builder Configuration.\nROOT OBJECT. All other configurations must be nested within this object.\n\nExample:\n{\n \"apiVersion\": \"1.0\",\n \"image\": {\n \"imageType\": \"iso\",\n \"arch\": \"x86_64\",\n \"baseImage\": \"sles15sp5.iso\",\n \"outputImageName\": \"my-image\"\n },\n \"operatingSystem\": {\n \"users\": [{\"username\": \"root\", \"encryptedPassword\": \"...\"}],\n \"isoConfiguration\": { \"installDevice\": \"/dev/sda\" }\n }\n}" + }, + "EmbeddedArtifactRegistry": { + "properties": { + "images": { + "items": { + "$ref": "#/$defs/ContainerImage" + }, + "type": "array" + }, + "registries": { + "items": { + "$ref": "#/$defs/Registry" + }, + "type": "array" + } + }, + "additionalProperties": false, + "type": "object" + }, + "Helm": { + "properties": { + "charts": { + "items": { + "$ref": "#/$defs/HelmChart" + }, + "type": "array" + }, + "repositories": { + "items": { + "$ref": "#/$defs/HelmRepository" + }, + "type": "array" + } + }, + "additionalProperties": false, + "type": "object" + }, + "HelmAuthentication": { + "properties": { + "username": { + "type": "string" + }, + "password": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + }, + "HelmChart": { + "if": { + "properties": { + "createNamespace": { + "const": true + } + }, + "required": [ + "createNamespace" + ] + }, + "then": { + "required": [ + "targetNamespace" + ] + }, + "properties": { + "name": { + "type": "string" + }, + "releaseName": { + "type": "string" + }, + "repositoryName": { + "type": "string", + "description": "Name of the repository to use. Must match a repository defined in 'repositories'. Required. DO NOT use 'repoUrl'." + }, + "version": { + "type": "string" + }, + "targetNamespace": { + "type": "string" + }, + "createNamespace": { + "type": "boolean" + }, + "installationNamespace": { + "type": "string" + }, + "valuesFile": { + "type": "string" + }, + "apiVersions": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name", + "repositoryName", + "version" + ], + "description": "Helm chart configuration.\nExample:\n{\n \"name\": \"rancher\",\n \"repositoryName\": \"rancher-prime\",\n \"version\": \"2.10.0\",\n \"targetNamespace\": \"cattle-system\",\n \"createNamespace\": true,\n \"installationNamespace\": \"kube-system\",\n \"valuesFile\": \"rancher-values.yaml\"\n}" + }, + "HelmRepository": { + "allOf": [ + { + "not": { + "properties": { + "plainHTTP": { + "const": true + }, + "skipTLSVerify": { + "const": true + } + }, + "required": [ + "plainHTTP", + "skipTLSVerify" + ] + } + } + ], + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string", + "pattern": "^(oci|http|https)://" + }, + "authentication": { + "$ref": "#/$defs/HelmAuthentication" + }, + "plainHTTP": { + "type": "boolean" + }, + "skipTLSVerify": { + "type": "boolean" + }, + "caFile": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name", + "url" + ], + "description": "Helm repository configuration.\nExample:\n{\n \"name\": \"rancher-prime\",\n \"url\": \"https://charts.rancher.com/server-charts/prime\"\n}" + }, + "Image": { + "properties": { + "imageType": { + "type": "string", + "enum": [ + "iso", + "raw" + ], + "description": "Type of image to build. Must be 'iso' or 'raw'." + }, + "arch": { + "type": "string", + "enum": [ + "x86_64", + "aarch64" + ], + "description": "Target architecture. Must be 'x86_64' or 'aarch64'." + }, + "baseImage": { + "type": "string", + "description": "Name of the base image to use (e.g., 'sles15sp5-x86_64'). Required." + }, + "outputImageName": { + "type": "string", + "description": "Name of the output image file. Required." + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "imageType", + "arch", + "baseImage", + "outputImageName" + ] + }, + "IsoConfiguration": { + "properties": { + "installDevice": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + }, + "Kubernetes": { + "properties": { + "version": { + "type": "string" + }, + "network": { + "$ref": "#/$defs/Network", + "description": "Network config. Example: { 'apiVIP': '1.2.3.4' }" + }, + "nodes": { + "items": { + "$ref": "#/$defs/Node" + }, + "type": "array", + "description": "List of nodes. Required for multi-node. Example: [{ 'hostname': 'n1', 'type': 'server' }]" + }, + "manifests": { + "$ref": "#/$defs/Manifests" + }, + "helm": { + "$ref": "#/$defs/Helm" + } + }, + "additionalProperties": true, + "type": "object", + "description": "Kubernetes configuration.\nExample:\n{\n \"version\": \"1.29.0\",\n \"network\": { \"apiVIP\": \"1.2.3.4\" },\n \"nodes\": [\n { \"hostname\": \"node1\", \"type\": \"server\", \"initializer\": true },\n { \"hostname\": \"node2\", \"type\": \"server\" }\n ]\n}" + }, + "Manifests": { + "properties": { + "urls": { + "items": { + "type": "string", + "pattern": "^http(s)?://" + }, + "type": "array" + } + }, + "additionalProperties": false, + "type": "object" + }, + "Network": { + "properties": { + "apiHost": { + "type": "string" + }, + "apiVIP": { + "type": "string", + "format": "ipv4" + }, + "apiVIP6": { + "type": "string", + "format": "ipv6" + } + }, + "additionalProperties": false, + "type": "object" + }, + "Node": { + "allOf": [ + { + "if": { + "properties": { + "initializer": { + "const": true + } + }, + "required": [ + "initializer" + ] + }, + "then": { + "properties": { + "type": { + "const": "server" + } + } + } + } + ], + "properties": { + "hostname": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "server", + "agent" + ] + }, + "initializer": { + "type": "boolean" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "hostname" + ], + "description": "Node configuration. DO NOT include IP addresses here (they are not supported)." + }, + "NtpConfiguration": { + "oneOf": [ + { + "properties": { + "pools": { + "minItems": 1 + } + }, + "required": [ + "pools" + ] + }, + { + "properties": { + "servers": { + "minItems": 1 + } + }, + "required": [ + "servers" + ] + } + ], + "if": { + "properties": { + "forceWait": { + "const": true + } + }, + "required": [ + "forceWait" + ] + }, + "then": { + "anyOf": [ + { + "required": [ + "pools" + ] + }, + { + "required": [ + "servers" + ] + } + ] + }, + "properties": { + "forceWait": { + "type": "boolean" + }, + "pools": { + "items": { + "type": "string" + }, + "type": "array", + "description": "List of NTP pool addresses. Must be a list of strings." + }, + "servers": { + "items": { + "type": "string" + }, + "type": "array", + "description": "List of NTP server addresses (e.g., ['pool.ntp.org']). Must be a list of strings." + } + }, + "additionalProperties": false, + "type": "object" + }, + "OperatingSystem": { + "properties": { + "kernelArgs": { + "items": { + "type": "string", + "minLength": 1 + }, + "type": "array" + }, + "groups": { + "items": { + "$ref": "#/$defs/OperatingSystemGroup" + }, + "type": "array" + }, + "users": { + "items": { + "$ref": "#/$defs/OperatingSystemUser" + }, + "type": "array", + "description": "List of users. Example: [{ 'username': 'user', 'password': '...' }]" + }, + "systemd": { + "$ref": "#/$defs/Systemd" + }, + "suma": { + "$ref": "#/$defs/Suma" + }, + "packages": { + "$ref": "#/$defs/Packages" + }, + "isoConfiguration": { + "$ref": "#/$defs/IsoConfiguration", + "description": "ISO configuration object. MUST be nested here. Example: { 'installDevice': '/dev/sda' }" + }, + "rawConfiguration": { + "$ref": "#/$defs/RawConfiguration", + "description": "RAW configuration object. MUST be nested here. Example: { 'diskSize': '10G' }" + }, + "time": { + "$ref": "#/$defs/Time", + "description": "Time configuration. Example: { 'timezone': 'UTC', 'ntp': { 'servers': ['pool.ntp.org'] } }" + }, + "proxy": { + "$ref": "#/$defs/Proxy" + }, + "keymap": { + "type": "string" + }, + "enableFIPS": { + "type": "boolean" + } + }, + "additionalProperties": false, + "type": "object", + "description": "Operating System configuration.\nExample (ISO):\n{\n \"users\": [{\"username\": \"root\", \"encryptedPassword\": \"...\"}],\n \"isoConfiguration\": { \"installDevice\": \"/dev/sda\" },\n \"time\": { \"timezone\": \"UTC\", \"ntp\": { \"servers\": [\"pool.ntp.org\"] } }\n}" + }, + "OperatingSystemGroup": { + "properties": { + "name": { + "type": "string" + }, + "gid": { + "type": "integer" + } + }, + "additionalProperties": false, + "type": "object" + }, + "OperatingSystemUser": { + "oneOf": [ + { + "required": [ + "encryptedPassword" + ] + }, + { + "required": [ + "sshKeys" + ] + } + ], + "properties": { + "username": { + "type": "string", + "description": "Username for the user. Required." + }, + "uid": { + "type": "integer" + }, + "encryptedPassword": { + "type": "string", + "description": "Encrypted password for the user. Required if sshKey is not provided." + }, + "sshKeys": { + "items": { + "type": "string" + }, + "type": "array", + "description": "List of SSH keys for the user. Required if encryptedPassword is not provided." + }, + "primaryGroup": { + "type": "string" + }, + "secondaryGroups": { + "items": { + "type": "string" + }, + "type": "array" + }, + "createHomeDir": { + "type": "boolean" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "username" + ], + "description": "User configuration.\nAllowed fields: \"username\", \"uid\", \"encryptedPassword\", \"sshKeys\", \"primaryGroup\", \"secondaryGroups\", \"createHomeDir\".\nDO NOT use \"name\", \"password\", \"sshKey\" (singular)." + }, + "Packages": { + "if": { + "properties": { + "packageList": { + "minItems": 1 + } + }, + "required": [ + "packageList" + ] + }, + "then": { + "anyOf": [ + { + "required": [ + "sccRegistrationCode" + ] + }, + { + "required": [ + "additionalRepos" + ] + } + ] + }, + "properties": { + "noGPGCheck": { + "type": "boolean" + }, + "enableExtras": { + "type": "boolean" + }, + "packageList": { + "items": { + "type": "string" + }, + "type": "array" + }, + "additionalRepos": { + "items": { + "$ref": "#/$defs/AddRepo" + }, + "type": "array" + }, + "sccRegistrationCode": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + }, + "Proxy": { + "properties": { + "httpProxy": { + "type": "string" + }, + "httpsProxy": { + "type": "string" + }, + "noProxy": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "additionalProperties": false, + "type": "object" + }, + "RawConfiguration": { + "if": { + "properties": { + "expandEncryptedPartition": { + "const": true + } + }, + "required": [ + "expandEncryptedPartition" + ] + }, + "then": { + "required": [ + "luksKey" + ] + }, + "properties": { + "diskSize": { + "type": "string", + "pattern": "^([1-9]\\d+|[1-9])+([MGT])$" + }, + "luksKey": { + "type": "string" + }, + "expandEncryptedPartition": { + "type": "boolean" + } + }, + "additionalProperties": false, + "type": "object" + }, + "Registry": { + "properties": { + "uri": { + "type": "string" + }, + "authentication": { + "$ref": "#/$defs/RegistryAuthentication" + } + }, + "additionalProperties": false, + "type": "object" + }, + "RegistryAuthentication": { + "properties": { + "username": { + "type": "string" + }, + "password": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + }, + "Suma": { + "properties": { + "host": { + "type": "string" + }, + "activationKey": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "host", + "activationKey" + ] + }, + "Systemd": { + "properties": { + "enable": { + "items": { + "type": "string", + "minLength": 1 + }, + "type": "array" + }, + "disable": { + "items": { + "type": "string", + "minLength": 1 + }, + "type": "array" + } + }, + "additionalProperties": false, + "type": "object" + }, + "Time": { + "properties": { + "timezone": { + "type": "string", + "description": "Timezone (e.g., 'UTC'). Note: field name is lowercase 'timezone'." + }, + "ntp": { + "$ref": "#/$defs/NtpConfiguration", + "description": "NTP config. 'servers' and 'pools' are lists of STRINGS. Example: { 'servers': ['1.2.3.4'] }" + } + }, + "additionalProperties": false, + "type": "object" + } + }, + "allOf": [ + { + "$ref": "#/$defs/Definition" + } + ], + "type": "object", + "title": "Edge Image Builder Configuration", + "description": "Schema for the configuration file used by the SUSE Edge Image Builder." +}