From 63ba4fb67d4f579abeba00379f88777f87457848 Mon Sep 17 00:00:00 2001 From: vic1707 <28602203+vic1707@users.noreply.github.com> Date: Sun, 16 Nov 2025 21:13:18 +0100 Subject: [PATCH 1/4] init interface and new package --- translator/common_fields.go | 61 +++++++++++++++++++++++++++++++++++++ translator/interface.go | 23 ++++++++++++++ translator/registery.go | 54 ++++++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+) create mode 100644 translator/common_fields.go create mode 100644 translator/interface.go create mode 100644 translator/registery.go diff --git a/translator/common_fields.go b/translator/common_fields.go new file mode 100644 index 00000000..ae8a25b6 --- /dev/null +++ b/translator/common_fields.go @@ -0,0 +1,61 @@ +// Copyright 2022 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package translator + +import ( + "fmt" + + "github.com/coreos/go-semver/semver" +) + +type commonFields struct { + Variant string `yaml:"variant"` + Version semver.Version `yaml:"version"` +} + +func (c *commonFields) UnmarshalYAML(unmarshal func(interface{}) error) error { + type plain commonFields + var raw plain + + if err := unmarshal(&raw); err != nil { + return err + } + + if raw.Variant == "" { + return fmt.Errorf("variant cannot be empty") + } + + *c = commonFields(raw) + return nil +} + +func (c *commonFields) asKey() string { + return fmt.Sprintf("%s+%s", c.Variant, c.Version.String()) +} + +func newCF(variant, version string) (commonFields, error) { + if variant == "" { + return commonFields{}, fmt.Errorf("variant cannot be empty") + } + + v, err := semver.NewVersion(version) + if err != nil { + return commonFields{}, fmt.Errorf("invalid version: %w", err) + } + + return commonFields{ + Variant: variant, + Version: *v, + }, nil +} diff --git a/translator/interface.go b/translator/interface.go new file mode 100644 index 00000000..f3cec89a --- /dev/null +++ b/translator/interface.go @@ -0,0 +1,23 @@ +// Copyright 2022 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package translator + +import ( + "github.com/coreos/butane/config/common" + "github.com/coreos/vcontext/report" +) + +type Translator interface { + TranslateBytes(input []byte, options common.TranslateBytesOptions) ([]byte, report.Report, error) +} diff --git a/translator/registery.go b/translator/registery.go new file mode 100644 index 00000000..5cddff78 --- /dev/null +++ b/translator/registery.go @@ -0,0 +1,54 @@ +// Copyright 2022 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package translator + +import ( + "fmt" + + "github.com/coreos/butane/config/common" + "github.com/coreos/vcontext/report" + "gopkg.in/yaml.v3" +) + +var TranslatorRegistry = &Registry{ + translators: make(map[string]Translator), +} + +type Registry struct { + translators map[string]Translator +} + +func (r *Registry) RegisterTranslator(variant, version string, trans Translator) { + cf, err := newCF(variant, version) + if err != nil { + panic(fmt.Sprintf("tried to register a translator with an invalid key (%s+%s)", variant, version)) + } + if _, ok := r.translators[cf.asKey()]; ok { + panic(fmt.Sprintf("tried to reregister existing translator (%s+%s)", variant, version)) + } + r.translators[cf.asKey()] = trans +} + +func (r *Registry) TranslateBytes(input []byte, options common.TranslateBytesOptions) ([]byte, report.Report, error) { + // first determine version; this will ignore most fields + cf := commonFields{} + if err := yaml.Unmarshal(input, &cf); err != nil { + return nil, report.Report{}, common.ErrUnmarshal{ + Detail: err.Error(), + } + } + + translator := r.translators[cf.asKey()] + return translator.TranslateBytes(input, options) +} From f2ade01ab875381bf5f7180978f3986274b06ebd Mon Sep 17 00:00:00 2001 From: vic1707 <28602203+vic1707@users.noreply.github.com> Date: Tue, 25 Nov 2025 15:09:42 +0100 Subject: [PATCH 2/4] interfaces v2? --- translator/interface.go | 8 ++++++++ translator/{common_fields.go => metadata.go} | 7 +++++++ translator/registery.go | 11 ++++------- 3 files changed, 19 insertions(+), 7 deletions(-) rename translator/{common_fields.go => metadata.go} (92%) diff --git a/translator/interface.go b/translator/interface.go index f3cec89a..8208ddd6 100644 --- a/translator/interface.go +++ b/translator/interface.go @@ -19,5 +19,13 @@ import ( ) type Translator interface { + Metadata() Metadata + // Parse yml into struct + Parse(input []byte) interface{} + // From yml input to Ignition struct + Translate(input []byte, options common.TranslateBytesOptions) (interface{}, report.Report, error) + // From yml input to Ingition JSON TranslateBytes(input []byte, options common.TranslateBytesOptions) ([]byte, report.Report, error) + // Validates yml struct + Validate(in interface{}) report.Report } diff --git a/translator/common_fields.go b/translator/metadata.go similarity index 92% rename from translator/common_fields.go rename to translator/metadata.go index ae8a25b6..cfc7546a 100644 --- a/translator/common_fields.go +++ b/translator/metadata.go @@ -24,6 +24,13 @@ type commonFields struct { Version semver.Version `yaml:"version"` } +type Metadata struct { + commonFields + Description string + Experimental bool + IgnitionVersion semver.Version +} + func (c *commonFields) UnmarshalYAML(unmarshal func(interface{}) error) error { type plain commonFields var raw plain diff --git a/translator/registery.go b/translator/registery.go index 5cddff78..2c5402c6 100644 --- a/translator/registery.go +++ b/translator/registery.go @@ -22,20 +22,17 @@ import ( ) var TranslatorRegistry = &Registry{ - translators: make(map[string]Translator), + translators: map[string]Translator{}, } type Registry struct { translators map[string]Translator } -func (r *Registry) RegisterTranslator(variant, version string, trans Translator) { - cf, err := newCF(variant, version) - if err != nil { - panic(fmt.Sprintf("tried to register a translator with an invalid key (%s+%s)", variant, version)) - } +func (r *Registry) RegisterTranslator(trans Translator) { + cf := trans.Metadata().commonFields if _, ok := r.translators[cf.asKey()]; ok { - panic(fmt.Sprintf("tried to reregister existing translator (%s+%s)", variant, version)) + panic(fmt.Sprintf("tried to reregister existing translator (%+v)", trans.Metadata())) } r.translators[cf.asKey()] = trans } From 11fe7cc4806898862775d197c9465d9cec2c460f Mon Sep 17 00:00:00 2001 From: Steven Presti Date: Wed, 26 Nov 2025 15:12:50 -0500 Subject: [PATCH 3/4] WIP:translator: update interface and registry --- translator/helpers.go | 40 +++++++++++++++++ translator/interface.go | 24 ++++++---- translator/options.go | 23 ++++++++++ translator/registery.go | 51 ---------------------- translator/registry.go | 97 +++++++++++++++++++++++++++++++++++++++++ translator/result.go | 37 ++++++++++++++++ 6 files changed, 212 insertions(+), 60 deletions(-) create mode 100644 translator/helpers.go create mode 100644 translator/options.go delete mode 100644 translator/registery.go create mode 100644 translator/registry.go create mode 100644 translator/result.go diff --git a/translator/helpers.go b/translator/helpers.go new file mode 100644 index 00000000..aff6b4f1 --- /dev/null +++ b/translator/helpers.go @@ -0,0 +1,40 @@ +// Copyright 2022 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package translator + +import ( + "fmt" + + "gopkg.in/yaml.v3" +) + +// ParseVariantVersion extracts the variant and version from Butane config bytes. +// +// This function only parses the minimal metadata needed to identify which +// translator to use. It does not validate the full config structure. +// +// Returns an error if the variant or version fields are missing or invalid. +func ParseVariantVersion(input []byte) (variant, version string, err error) { + var cf commonFields + if err := yaml.Unmarshal(input, &cf); err != nil { + return "", "", fmt.Errorf("failed to parse config: %w", err) + } + + if cf.Variant == "" { + return "", "", fmt.Errorf("missing 'variant' field in config") + } + + return cf.Variant, cf.Version.String(), nil +} diff --git a/translator/interface.go b/translator/interface.go index 8208ddd6..4799f758 100644 --- a/translator/interface.go +++ b/translator/interface.go @@ -11,21 +11,27 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + package translator import ( - "github.com/coreos/butane/config/common" + "context" + "github.com/coreos/vcontext/report" ) +// Translator translates Butane configuration to Ignition configuration. +// +// Each Butane variant (fcos, flatcar, r4e, openshift, etc.) should implement this +// interface for each supported version. type Translator interface { + // Metadata the variant, version, and target Ignition version. Metadata() Metadata - // Parse yml into struct - Parse(input []byte) interface{} - // From yml input to Ignition struct - Translate(input []byte, options common.TranslateBytesOptions) (interface{}, report.Report, error) - // From yml input to Ingition JSON - TranslateBytes(input []byte, options common.TranslateBytesOptions) ([]byte, report.Report, error) - // Validates yml struct - Validate(in interface{}) report.Report + + // Translate converts Butane config bytes to Ignition config bytes. + Translate(ctx context.Context, input []byte, opts Options) (Result, error) + + // Validate validates a Butane config without performing translation. + Validate(ctx context.Context, input []byte) (report.Report, error) } + diff --git a/translator/options.go b/translator/options.go new file mode 100644 index 00000000..5405d63f --- /dev/null +++ b/translator/options.go @@ -0,0 +1,23 @@ +// Copyright 2022 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package translator + +type Options struct { + FilesDir string + NoResourceAutoCompression bool + DebugPrintTranslations bool + Pretty bool + Raw bool +} diff --git a/translator/registery.go b/translator/registery.go deleted file mode 100644 index 2c5402c6..00000000 --- a/translator/registery.go +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2022 Red Hat, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -package translator - -import ( - "fmt" - - "github.com/coreos/butane/config/common" - "github.com/coreos/vcontext/report" - "gopkg.in/yaml.v3" -) - -var TranslatorRegistry = &Registry{ - translators: map[string]Translator{}, -} - -type Registry struct { - translators map[string]Translator -} - -func (r *Registry) RegisterTranslator(trans Translator) { - cf := trans.Metadata().commonFields - if _, ok := r.translators[cf.asKey()]; ok { - panic(fmt.Sprintf("tried to reregister existing translator (%+v)", trans.Metadata())) - } - r.translators[cf.asKey()] = trans -} - -func (r *Registry) TranslateBytes(input []byte, options common.TranslateBytesOptions) ([]byte, report.Report, error) { - // first determine version; this will ignore most fields - cf := commonFields{} - if err := yaml.Unmarshal(input, &cf); err != nil { - return nil, report.Report{}, common.ErrUnmarshal{ - Detail: err.Error(), - } - } - - translator := r.translators[cf.asKey()] - return translator.TranslateBytes(input, options) -} diff --git a/translator/registry.go b/translator/registry.go new file mode 100644 index 00000000..ed005d2b --- /dev/null +++ b/translator/registry.go @@ -0,0 +1,97 @@ +// Copyright 2022 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package translator + +import ( + "context" + "fmt" +) + +// Global registry for all translators. +// Variants register in init() functions. +var Global = NewRegistry() + +type Registry struct { + translators map[string]Translator +} + +func NewRegistry() *Registry { + return &Registry{ + translators: make(map[string]Translator), + } +} + +// Register adds a translator. Panics if already registered. +func (r *Registry) Register(t Translator) { + meta := t.Metadata() + key := meta.commonFields.asKey() + + if _, exists := r.translators[key]; exists { + panic(fmt.Sprintf("translator already registered: %s version %s", + meta.Variant, meta.Version.String())) + } + + r.translators[key] = t +} + +// Get retrieves a translator by variant and version. +func (r *Registry) Get(variant, version string) (Translator, error) { + cf, err := newCF(variant, version) + if err != nil { + return nil, fmt.Errorf("invalid variant/version: %w", err) + } + + key := cf.asKey() + t, ok := r.translators[key] + if !ok { + return nil, fmt.Errorf("no translator registered for %s version %s", variant, version) + } + + return t, nil +} + +func (r *Registry) IsRegistered(variant, version string) bool { + cf, err := newCF(variant, version) + if err != nil { + return false + } + + _, ok := r.translators[cf.asKey()] + return ok +} + +// List returns all registered translator metadata. +func (r *Registry) List() []Metadata { + result := make([]Metadata, 0, len(r.translators)) + for _, t := range r.translators { + result = append(result, t.Metadata()) + } + return result +} + +// Translate auto-detects variant/version and translates the input. +func (r *Registry) Translate(ctx context.Context, input []byte, opts Options) (Result, error) { + variant, version, err := ParseVariantVersion(input) + if err != nil { + return Result{}, fmt.Errorf("failed to parse variant/version: %w", err) + } + + t, err := r.Get(variant, version) + if err != nil { + return Result{}, err + } + + return t.Translate(ctx, input, opts) +} diff --git a/translator/result.go b/translator/result.go new file mode 100644 index 00000000..9b40b26e --- /dev/null +++ b/translator/result.go @@ -0,0 +1,37 @@ +// Copyright 2022 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package translator + +import ( + "github.com/coreos/butane/translate" + "github.com/coreos/vcontext/report" +) + +// Result contains the output of a translation operation. +// +// This matches the existing return pattern from ToIgnXXBytes functions +// but wraps them in a struct for better extensibility. +type Result struct { + // Output is the translated Ignition configuration as JSON bytes. + Output []byte + + // Report contains warnings and errors from the translation process. + // Use Report.IsFatal() to check if translation failed. + Report report.Report + + // TranslationSet tracks how source paths in the Butane config map to + // output paths in the Ignition config. Used for debugging and tooling. + TranslationSet translate.TranslationSet +} From ed98bfcd85a255ff9c2c58c44c50b8c4873b6737 Mon Sep 17 00:00:00 2001 From: vic1707 <28602203+vic1707@users.noreply.github.com> Date: Fri, 19 Dec 2025 16:53:02 +0100 Subject: [PATCH 4/4] finish interfaces --- translator/interface.go | 15 ++++++--------- translator/registry.go | 38 +++++++++++++++++++++++++++++++++++--- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/translator/interface.go b/translator/interface.go index 4799f758..00e8369b 100644 --- a/translator/interface.go +++ b/translator/interface.go @@ -15,8 +15,6 @@ package translator import ( - "context" - "github.com/coreos/vcontext/report" ) @@ -27,11 +25,10 @@ import ( type Translator interface { // Metadata the variant, version, and target Ignition version. Metadata() Metadata - - // Translate converts Butane config bytes to Ignition config bytes. - Translate(ctx context.Context, input []byte, opts Options) (Result, error) - - // Validate validates a Butane config without performing translation. - Validate(ctx context.Context, input []byte) (report.Report, error) + // Parse yml into schema struct, basically a yaml.Unmarshal wrapper? + Parse(input []byte /*opts?*/) (interface{}, error) + // From inner schema struct to Ignition struct + Translate(input interface{}, options Options) (interface{}, report.Report, error) + // Validates yml inner struct + Validate(in interface{}) (report.Report, error) } - diff --git a/translator/registry.go b/translator/registry.go index ed005d2b..1c88d4cf 100644 --- a/translator/registry.go +++ b/translator/registry.go @@ -16,6 +16,7 @@ package translator import ( "context" + "encoding/json" "fmt" ) @@ -83,15 +84,46 @@ func (r *Registry) List() []Metadata { // Translate auto-detects variant/version and translates the input. func (r *Registry) Translate(ctx context.Context, input []byte, opts Options) (Result, error) { + res := Result{} variant, version, err := ParseVariantVersion(input) if err != nil { - return Result{}, fmt.Errorf("failed to parse variant/version: %w", err) + return res, fmt.Errorf("failed to parse variant/version: %w", err) } t, err := r.Get(variant, version) if err != nil { - return Result{}, err + return res, err } - return t.Translate(ctx, input, opts) + parsed, err := t.Parse(input) + if err != nil { + return res, err + } + + report, err := t.Validate(parsed) + res.Report = report + if err != nil { + return res, err + } + + translated, report, err := t.Translate(parsed, opts) + res.Report.Merge(report) + if err != nil { + return res, err + } + + out, err := marshal(translated, opts.Pretty) + if err != nil { + return res, err + } + res.Output = out + + return res, nil +} + +func marshal(from interface{}, pretty bool) ([]byte, error) { + if pretty { + return json.MarshalIndent(from, "", " ") + } + return json.Marshal(from) }