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 new file mode 100644 index 00000000..00e8369b --- /dev/null +++ b/translator/interface.go @@ -0,0 +1,34 @@ +// 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/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 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/metadata.go b/translator/metadata.go new file mode 100644 index 00000000..cfc7546a --- /dev/null +++ b/translator/metadata.go @@ -0,0 +1,68 @@ +// 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"` +} + +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 + + 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/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/registry.go b/translator/registry.go new file mode 100644 index 00000000..1c88d4cf --- /dev/null +++ b/translator/registry.go @@ -0,0 +1,129 @@ +// 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" + "encoding/json" + "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) { + res := Result{} + variant, version, err := ParseVariantVersion(input) + if err != nil { + return res, fmt.Errorf("failed to parse variant/version: %w", err) + } + + t, err := r.Get(variant, version) + if err != nil { + return res, err + } + + 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) +} 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 +}