diff --git a/pkg/controller/common/helpers.go b/pkg/controller/common/helpers.go index 047b682faa..68b8a36de0 100644 --- a/pkg/controller/common/helpers.go +++ b/pkg/controller/common/helpers.go @@ -9,6 +9,7 @@ import ( "encoding/base64" "errors" "fmt" + "github.com/coreos/go-semver/semver" "io" "io/fs" "net/url" @@ -22,30 +23,13 @@ import ( "github.com/clarketm/json" fcctbase "github.com/coreos/fcct/base/v0_1" - "github.com/coreos/ign-converter/translate/v23tov30" - "github.com/coreos/ign-converter/translate/v32tov22" - "github.com/coreos/ign-converter/translate/v32tov31" - "github.com/coreos/ign-converter/translate/v33tov32" - "github.com/coreos/ign-converter/translate/v34tov33" - "github.com/coreos/ign-converter/translate/v35tov34" ign2error "github.com/coreos/ignition/config/shared/errors" ign2 "github.com/coreos/ignition/config/v2_2" ign2types "github.com/coreos/ignition/config/v2_2/types" - ign2_3 "github.com/coreos/ignition/config/v2_3" validate2 "github.com/coreos/ignition/config/validate" ign3error "github.com/coreos/ignition/v2/config/shared/errors" - translate3_1 "github.com/coreos/ignition/v2/config/v3_1/translate" - ign3_1types "github.com/coreos/ignition/v2/config/v3_1/types" - translate3_2 "github.com/coreos/ignition/v2/config/v3_2/translate" - ign3_2types "github.com/coreos/ignition/v2/config/v3_2/types" - translate3_3 "github.com/coreos/ignition/v2/config/v3_3/translate" - ign3_3types "github.com/coreos/ignition/v2/config/v3_3/types" - translate3_4 "github.com/coreos/ignition/v2/config/v3_4/translate" - ign3_4types "github.com/coreos/ignition/v2/config/v3_4/types" ign3 "github.com/coreos/ignition/v2/config/v3_5" - ign3_5 "github.com/coreos/ignition/v2/config/v3_5" - translate3 "github.com/coreos/ignition/v2/config/v3_5/translate" ign3types "github.com/coreos/ignition/v2/config/v3_5/types" validate3 "github.com/coreos/ignition/v2/config/validate" "github.com/ghodss/yaml" @@ -291,9 +275,7 @@ func ConvertRawExtIgnitionToV3_5(inRawExtIgn *runtime.RawExtension) (runtime.Raw return outRawExt, nil } -// ConvertRawExtIgnitionToV3_4 ensures that the Ignition config in -// the RawExtension is spec v3.4, or translates to it. -func ConvertRawExtIgnitionToV3_4(inRawExtIgn *runtime.RawExtension) (runtime.RawExtension, error) { +func ConvertRawExtIgnitionToVersion(inRawExtIgn *runtime.RawExtension, targetVersion semver.Version) (runtime.RawExtension, error) { rawExt, err := ConvertRawExtIgnitionToV3_5(inRawExtIgn) if err != nil { return runtime.RawExtension{}, err @@ -304,262 +286,17 @@ func ConvertRawExtIgnitionToV3_4(inRawExtIgn *runtime.RawExtension) (runtime.Raw return runtime.RawExtension{}, fmt.Errorf("parsing Ignition config failed with error: %w\nReport: %v", errV3, rptV3) } - // TODO(jkyros): someday we should write a recursive chain-downconverter, but until then, - // we're going to do it the hard way - ignCfgV33, err := convertIgnition35to34(ignCfgV3) + conversion, err := ignitionConverter.Convert(ignCfgV3, ign3types.MaxVersion, targetVersion) if err != nil { return runtime.RawExtension{}, err } - outIgnV33, err := json.Marshal(ignCfgV33) + out, err := json.Marshal(conversion) if err != nil { return runtime.RawExtension{}, fmt.Errorf("failed to marshal converted config: %w", err) } - outRawExt := runtime.RawExtension{} - outRawExt.Raw = outIgnV33 - - return outRawExt, nil -} - -// ConvertRawExtIgnitionToV3_3 ensures that the Ignition config in -// the RawExtension is spec v3.3, or translates to it. -func ConvertRawExtIgnitionToV3_3(inRawExtIgn *runtime.RawExtension) (runtime.RawExtension, error) { - rawExt, err := ConvertRawExtIgnitionToV3_5(inRawExtIgn) - if err != nil { - return runtime.RawExtension{}, err - } - - ignCfgV3, rptV3, errV3 := ign3.Parse(rawExt.Raw) - if errV3 != nil || rptV3.IsFatal() { - return runtime.RawExtension{}, fmt.Errorf("parsing Ignition config failed with error: %w\nReport: %v", errV3, rptV3) - } - - // TODO(jkyros): someday we should write a recursive chain-downconverter, but until then, - // we're going to do it the hard way - ignCfgV34, err := convertIgnition35to34(ignCfgV3) - if err != nil { - return runtime.RawExtension{}, err - } - - ignCfgV33, err := convertIgnition34to33(ignCfgV34) - if err != nil { - return runtime.RawExtension{}, err - } - - outIgnV33, err := json.Marshal(ignCfgV33) - if err != nil { - return runtime.RawExtension{}, fmt.Errorf("failed to marshal converted config: %w", err) - } - - outRawExt := runtime.RawExtension{} - outRawExt.Raw = outIgnV33 - - return outRawExt, nil -} - -// ConvertRawExtIgnitionToV3_2 ensures that the Ignition config in -// the RawExtension is spec v3.2, or translates to it. -func ConvertRawExtIgnitionToV3_2(inRawExtIgn *runtime.RawExtension) (runtime.RawExtension, error) { - rawExt, err := ConvertRawExtIgnitionToV3_5(inRawExtIgn) - if err != nil { - return runtime.RawExtension{}, err - } - - ignCfgV3, rptV3, errV3 := ign3.Parse(rawExt.Raw) - if errV3 != nil || rptV3.IsFatal() { - return runtime.RawExtension{}, fmt.Errorf("parsing Ignition config failed with error: %w\nReport: %v", errV3, rptV3) - } - - // TODO(jkyros): someday we should write a recursive chain-downconverter, but until then, - // we're going to do it the hard way - ignCfgV34, err := convertIgnition35to34(ignCfgV3) - if err != nil { - return runtime.RawExtension{}, err - } - - ignCfgV33, err := convertIgnition34to33(ignCfgV34) - if err != nil { - return runtime.RawExtension{}, err - } - - ignCfgV32, err := convertIgnition33to32(ignCfgV33) - if err != nil { - return runtime.RawExtension{}, err - } - - outIgnV32, err := json.Marshal(ignCfgV32) - if err != nil { - return runtime.RawExtension{}, fmt.Errorf("failed to marshal converted config: %w", err) - } - - outRawExt := runtime.RawExtension{} - outRawExt.Raw = outIgnV32 - - return outRawExt, nil -} - -// ConvertRawExtIgnitionToV3_1 ensures that the Ignition config in -// the RawExtension is spec v3.1, or translates to it. -func ConvertRawExtIgnitionToV3_1(inRawExtIgn *runtime.RawExtension) (runtime.RawExtension, error) { - rawExt, err := ConvertRawExtIgnitionToV3_5(inRawExtIgn) - if err != nil { - return runtime.RawExtension{}, err - } - - ignCfgV3, rptV3, errV3 := ign3.Parse(rawExt.Raw) - if errV3 != nil || rptV3.IsFatal() { - return runtime.RawExtension{}, fmt.Errorf("parsing Ignition config failed with error: %w\nReport: %v", errV3, rptV3) - } - - // TODO(jkyros): someday we should write a recursive chain-downconverter, but until then, - // we're going to do it the hard way - ignCfgV34, err := convertIgnition35to34(ignCfgV3) - if err != nil { - return runtime.RawExtension{}, err - } - - ignCfgV33, err := convertIgnition34to33(ignCfgV34) - if err != nil { - return runtime.RawExtension{}, err - } - - ignCfgV32, err := convertIgnition33to32(ignCfgV33) - if err != nil { - return runtime.RawExtension{}, err - } - - ignCfgV31, err := convertIgnition32to31(ignCfgV32) - if err != nil { - return runtime.RawExtension{}, err - } - - outIgnV31, err := json.Marshal(ignCfgV31) - if err != nil { - return runtime.RawExtension{}, fmt.Errorf("failed to marshal converted config: %w", err) - } - - outRawExt := runtime.RawExtension{} - outRawExt.Raw = outIgnV31 - - return outRawExt, nil -} - -// ConvertRawExtIgnitionToV2_2 ensures that the Ignition config in -// the RawExtension is spec v2.2, or translates to it. -func ConvertRawExtIgnitionToV2_2(inRawExtIgn *runtime.RawExtension) (runtime.RawExtension, error) { - ignCfg, rpt, err := ign3.Parse(inRawExtIgn.Raw) - if err != nil || rpt.IsFatal() { - return runtime.RawExtension{}, fmt.Errorf("parsing Ignition config spec v3.5 failed with error: %w\nReport: %v", err, rpt) - } - - converted2, err := convertIgnition35to22(ignCfg) - if err != nil { - return runtime.RawExtension{}, fmt.Errorf("failed to convert config from spec v3.5 to v2.2: %w", err) - } - - outIgnV2, err := json.Marshal(converted2) - if err != nil { - return runtime.RawExtension{}, fmt.Errorf("failed to marshal converted config: %w", err) - } - - outRawExt := runtime.RawExtension{} - outRawExt.Raw = outIgnV2 - - return outRawExt, nil -} - -// convertIgnition22to35 takes an ignition spec v2.2 config and returns a v3.5 config -func convertIgnition22to35(ign2config ign2types.Config) (ign3types.Config, error) { - // only support writing to root file system - fsMap := map[string]string{ - "root": "/", - } - - // Workaround to get v2.3 as input for converter - ign2_3config := ign2_3.Translate(ign2config) - ign3_0config, err := v23tov30.Translate(ign2_3config, fsMap) - if err != nil { - return ign3types.Config{}, fmt.Errorf("unable to convert Ignition spec v2 config to v3: %w", err) - } - // Workaround to get a v3.5 config as output - converted3 := translate3.Translate(translate3_4.Translate(translate3_3.Translate(translate3_2.Translate(translate3_1.Translate(ign3_0config))))) - - klog.V(4).Infof("Successfully translated Ignition spec v2 config to Ignition spec v3 config: %v", converted3) - return converted3, nil -} - -// convertIgnition35to22 takes an ignition spec v3.5 config and returns a v2.2 config -func convertIgnition35to22(ign3config ign3types.Config) (ign2types.Config, error) { - - // TODO(jkyros): that recursive down-converter is looking like a better idea all the time - - converted34, err := convertIgnition35to34(ign3config) - if err != nil { - return ign2types.Config{}, fmt.Errorf("unable to convert Ignition spec v3 config to v2: %w", err) - } - - converted33, err := convertIgnition34to33(converted34) - if err != nil { - return ign2types.Config{}, fmt.Errorf("unable to convert Ignition spec v3 config to v2: %w", err) - } - - converted32, err := convertIgnition33to32(converted33) - if err != nil { - return ign2types.Config{}, fmt.Errorf("unable to convert Ignition spec v3 config to v2: %w", err) - } - - converted2, err := v32tov22.Translate(converted32) - if err != nil { - return ign2types.Config{}, fmt.Errorf("unable to convert Ignition spec v3 config to v2: %w", err) - } - klog.V(4).Infof("Successfully translated Ignition spec v3 config to Ignition spec v2 config: %v", converted2) - - return converted2, nil -} - -// convertIgnition35to34 takes an ignition spec v3.5 config and returns a v3.4 config -func convertIgnition35to34(ign3config ign3types.Config) (ign3_4types.Config, error) { - converted34, err := v35tov34.Translate(ign3config) - if err != nil { - return ign3_4types.Config{}, fmt.Errorf("unable to convert Ignition spec v3.5 config to v3.4: %w", err) - } - klog.V(4).Infof("Successfully translated Ignition spec v3.5 config to Ignition spec v3.4 config: %v", converted34) - - return converted34, nil -} - -// convertIgnition34to33 takes an ignition spec v3.4 config and returns a v3.3 config -func convertIgnition34to33(ign3config ign3_4types.Config) (ign3_3types.Config, error) { - converted33, err := v34tov33.Translate(ign3config) - if err != nil { - return ign3_3types.Config{}, fmt.Errorf("unable to convert Ignition spec v3.4 config to v3.3: %w", err) - } - klog.V(4).Infof("Successfully translated Ignition spec v3.4 config to Ignition spec v3.3 config: %v", converted33) - - return converted33, nil -} - -// convertIgnition33to32 takes an ignition spec v3.3 config and returns a v3.2 config -func convertIgnition33to32(ign3config ign3_3types.Config) (ign3_2types.Config, error) { - converted32, err := v33tov32.Translate(ign3config) - if err != nil { - return ign3_2types.Config{}, fmt.Errorf("unable to convert Ignition spec v3.3 config to v3.2: %w", err) - } - klog.V(4).Infof("Successfully translated Ignition spec v3.3 config to Ignition spec v3.2 config: %v", converted32) - - return converted32, nil -} - -// convertIgnition32to31 takes an ignition spec v3.2 config and returns a v3.1 config -func convertIgnition32to31(ign3config ign3_2types.Config) (ign3_1types.Config, error) { - converted31, err := v32tov31.Translate(ign3config) - if err != nil { - return ign3_1types.Config{}, fmt.Errorf("unable to convert Ignition spec v3.2 config to v3.1: %w", err) - } - klog.V(4).Infof("Successfully translated Ignition spec v3.2 config to Ignition spec v3.1 config: %v", converted31) - - return converted31, nil + return runtime.RawExtension{Raw: out}, nil } // ValidateIgnition wraps the underlying Ignition V2/V3 validation, but explicitly supports @@ -746,7 +483,7 @@ func SupportedExtensions() map[string][]string { // a V2 or V3 Config or an error. This wrapper is necessary since V2 and V3 use different parsers. func IgnParseWrapper(rawIgn []byte) (interface{}, error) { // ParseCompatibleVersion will parse any config <= N to version N - ignCfgV3, rptV3, errV3 := ign3_5.ParseCompatibleVersion(rawIgn) + ignCfgV3, rptV3, errV3 := ign3.ParseCompatibleVersion(rawIgn) if errV3 == nil && !rptV3.IsFatal() { return ignCfgV3, nil } @@ -755,7 +492,8 @@ func IgnParseWrapper(rawIgn []byte) (interface{}, error) { // ErrInvalidVersion ("I can't parse it to find out what it is"), but our old 3.2 logic didn't, so this is here to make sure // our error message for invalid version is still helpful. if errV3.Error() == ign3error.ErrInvalidVersion.Error() { - return ign3types.Config{}, fmt.Errorf("parsing Ignition config failed: invalid version. Supported spec versions: 2.2, 3.0, 3.1, 3.2, 3.3, 3.4, 3.5") + versions := strings.TrimSuffix(strings.Join(IgnitionConverterSingleton().GetSupportedMinorVersions(), ","), ",") + return ign3types.Config{}, fmt.Errorf("parsing Ignition config failed: invalid version. Supported spec versions: %s", versions) } if errV3.Error() == ign3error.ErrUnknownVersion.Error() { @@ -766,7 +504,8 @@ func IgnParseWrapper(rawIgn []byte) (interface{}, error) { // If the error is still UnknownVersion it's not a 3.3/3.2/3.1/3.0 or 2.x config, thus unsupported if errV2.Error() == ign2error.ErrUnknownVersion.Error() { - return ign3types.Config{}, fmt.Errorf("parsing Ignition config failed: unknown version. Supported spec versions: 2.2, 3.0, 3.1, 3.2, 3.3, 3.4, 3.5") + versions := strings.TrimSuffix(strings.Join(IgnitionConverterSingleton().GetSupportedMinorVersions(), ","), ",") + return ign3types.Config{}, fmt.Errorf("parsing Ignition config failed: unknown version. Supported spec versions: %s", versions) } return ign3types.Config{}, fmt.Errorf("parsing Ignition spec v2 failed with error: %v\nReport: %v", errV2, rptV2) } @@ -790,11 +529,11 @@ func ParseAndConvertConfig(rawIgn []byte) (ign3types.Config, error) { if err != nil { return ign3types.Config{}, err } - convertedIgnV3, err := convertIgnition22to35(ignconfv2) + convertedIgnV3, err := ignitionConverter.Convert(ignconfv2, *semver.New(ignconfv2.Ignition.Version), ign3types.MaxVersion) if err != nil { return ign3types.Config{}, fmt.Errorf("failed to convert Ignition config spec v2 to v3: %w", err) } - return convertedIgnV3, nil + return convertedIgnV3.(ign3types.Config), nil default: return ign3types.Config{}, fmt.Errorf("unexpected type for ignition config: %v", typedConfig) } @@ -990,13 +729,15 @@ func TranspileCoreOSConfigToIgn(files, units []string) (*ign3types.Config, error // Add the file to the config var ctCfg fcctbase.Config ctCfg.Storage.Files = append(ctCfg.Storage.Files, *f) - ign3_0config, tSet, err := ctCfg.ToIgn3_0() + ign30Config, tSet, err := ctCfg.ToIgn3_0() if err != nil { return nil, fmt.Errorf("failed to transpile config to Ignition config %w\nTranslation set: %v", err, tSet) } - // TODO(jkyros): do we keep just...adding translations forever as we add more versions? :) - ign3Config := translate3.Translate(translate3_4.Translate(translate3_3.Translate(translate3_2.Translate(translate3_1.Translate(ign3_0config))))) - outConfig = ign3.Merge(outConfig, ign3Config) + ign3Config, err := ignitionConverter.Convert(ign30Config, *semver.New("3.0.0"), ign3types.MaxVersion) + if err != nil { + return nil, fmt.Errorf("failed to convert config from 3.0 to %v. %w", ign3types.MaxVersion, err) + } + outConfig = ign3.Merge(outConfig, ign3Config.(ign3types.Config)) } for _, contents := range units { @@ -1008,12 +749,15 @@ func TranspileCoreOSConfigToIgn(files, units []string) (*ign3types.Config, error // Add the unit to the config var ctCfg fcctbase.Config ctCfg.Systemd.Units = append(ctCfg.Systemd.Units, *u) - ign3_0config, tSet, err := ctCfg.ToIgn3_0() + ign30Config, tSet, err := ctCfg.ToIgn3_0() if err != nil { return nil, fmt.Errorf("failed to transpile config to Ignition config %w\nTranslation set: %v", err, tSet) } - ign3Config := translate3.Translate(translate3_4.Translate(translate3_3.Translate(translate3_2.Translate(translate3_1.Translate(ign3_0config))))) - outConfig = ign3.Merge(outConfig, ign3Config) + ign3Config, err := ignitionConverter.Convert(ign30Config, *semver.New("3.0.0"), ign3types.MaxVersion) + if err != nil { + return nil, fmt.Errorf("failed to convert config from 3.0 to %v. %w", ign3types.MaxVersion, err) + } + outConfig = ign3.Merge(outConfig, ign3Config.(ign3types.Config)) } return &outConfig, nil diff --git a/pkg/controller/common/helpers_test.go b/pkg/controller/common/helpers_test.go index 39fbd021af..dcd2c90a31 100644 --- a/pkg/controller/common/helpers_test.go +++ b/pkg/controller/common/helpers_test.go @@ -1,12 +1,14 @@ package common import ( + "github.com/coreos/go-semver/semver" "reflect" "strings" "testing" "github.com/clarketm/json" ign2types "github.com/coreos/ignition/config/v2_2/types" + ign3utils "github.com/coreos/ignition/v2/config/util" ign3 "github.com/coreos/ignition/v2/config/v3_5" ign3types "github.com/coreos/ignition/v2/config/v3_5/types" validate3 "github.com/coreos/ignition/v2/config/validate" @@ -130,37 +132,132 @@ func TestValidateIgnition(t *testing.T) { require.NotNil(t, isValid3) } -func TestConvertIgnition2to3(t *testing.T) { - // Make a new Ign spec v2 config - testIgn2Config := ign2types.Config{} - - tempUser := ign2types.PasswdUser{Name: "core", SSHAuthorizedKeys: []ign2types.SSHAuthorizedKey{"5678", "abc"}} - testIgn2Config.Passwd.Users = []ign2types.PasswdUser{tempUser} - testIgn2Config.Ignition.Version = "2.2.0" - isValid := ValidateIgnition(testIgn2Config) - require.Nil(t, isValid) +// TestIgnitionConverterGetSupportedMinorVersions tests that the Ignition converter properly returns +// the expected supported minor versions in a sorted slice of strings +func TestIgnitionConverterGetSupportedMinorVersions(t *testing.T) { + converter := newIgnitionConverter(buildConverterList()) + supported := []string{"2.2", "3.0", "3.1", "3.2", "3.3", "3.4", "3.5"} + assert.Equal(t, supported, converter.GetSupportedMinorVersions()) +} - convertedIgn, err := convertIgnition22to35(testIgn2Config) - require.Nil(t, err) - assert.IsType(t, ign3types.Config{}, convertedIgn) - isValid3 := ValidateIgnition(convertedIgn) - require.Nil(t, isValid3) +// TestIgnitionConverterGetSupportedMinorVersion +func TestIgnitionConverterGetSupportedMinorVersion(t *testing.T) { + converter := newIgnitionConverter(buildConverterList()) + v350 := semver.New("3.5.0") + v352 := semver.New("3.5.2") + matchingVersion, err := converter.GetSupportedMinorVersion(*v350) + assert.NoError(t, err) + assert.True(t, matchingVersion.Equal(*v350)) + + matchingMinorVersion, err := converter.GetSupportedMinorVersion(*v352) + assert.NoError(t, err) + assert.True(t, matchingMinorVersion.Equal(*v350)) + + _, err = converter.GetSupportedMinorVersion(*semver.New("7.7.7")) + assert.ErrorIs(t, err, errIgnitionConverterUnknownVersion) } -func TestConvertIgnition3to2(t *testing.T) { - // Make a new Ign3 config - testIgn3Config := ign3types.Config{} - tempUser := ign3types.PasswdUser{Name: "core", SSHAuthorizedKeys: []ign3types.SSHAuthorizedKey{"5678", "abc"}} - testIgn3Config.Passwd.Users = []ign3types.PasswdUser{tempUser} - testIgn3Config.Ignition.Version = InternalMCOIgnitionVersion - isValid := ValidateIgnition(testIgn3Config) - require.Nil(t, isValid) +// TestIgnitionConverterConvert tests that the Ignition converter is able to handle the expected +// conversions and that is able to handle error conditions +func TestIgnitionConverterConvert(t *testing.T) { + ign3Config := ign3types.Config{ + Ignition: ign3types.Ignition{Version: ign3types.MaxVersion.String()}, + Passwd: ign3types.Passwd{ + Users: []ign3types.PasswdUser{ + {Name: "core", SSHAuthorizedKeys: []ign3types.SSHAuthorizedKey{"5678", "abc"}}, + }, + }, + } - convertedIgn, err := convertIgnition35to22(testIgn3Config) - require.Nil(t, err) - assert.IsType(t, ign2types.Config{}, convertedIgn) - isValid2 := ValidateIgnition(convertedIgn) - require.Nil(t, isValid2) + ign2Config := ign2types.Config{ + Ignition: ign2types.Ignition{Version: ign2types.MaxVersion.String()}, + Passwd: ign2types.Passwd{ + Users: []ign2types.PasswdUser{ + {Name: "core", SSHAuthorizedKeys: []ign2types.SSHAuthorizedKey{"5678", "abc"}}, + }, + }, + } + + converter := newIgnitionConverter(buildConverterList()) + tests := []struct { + name string + inputConfig any + inputVersion string + outputVersion string + err error + }{ + { + name: "Conversion from 2.2 to 3.5", + inputConfig: ign2Config, + inputVersion: "2.2.0", + outputVersion: "3.5.0", + }, + { + name: "Conversion from 3.5 to 2.2", + inputConfig: ign3Config, + inputVersion: "3.5.0", + outputVersion: "2.2.0", + }, + { + name: "Conversion from 3.5 to 3.4", + inputConfig: ign3Config, + inputVersion: "3.5.0", + outputVersion: "3.4.0", + }, + { + name: "Conversion from 3.5 to 3.3", + inputConfig: ign3Config, + inputVersion: "3.5.0", + outputVersion: "3.3.0", + }, + { + name: "Conversion from 3.5 to 3.2", + inputConfig: ign3Config, + inputVersion: "3.5.0", + outputVersion: "3.2.0", + }, + { + name: "Conversion from 3.5 to 3.1", + inputConfig: ign3Config, + inputVersion: "3.5.0", + outputVersion: "3.1.0", + }, + { + name: "Conversion wrong source version", + inputConfig: ign2Config, + inputVersion: "3.5.0", + outputVersion: "3.1.0", + err: errIgnitionConverterWrongSourceType, + }, { + name: "Conversion not supported", + inputConfig: ign3Config, + inputVersion: "3.1.0", + outputVersion: "3.5.0", + err: errIgnitionConverterUnsupportedConversion, + }, + } + + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + result, err := converter.Convert(testCase.inputConfig, *semver.New(testCase.inputVersion), *semver.New(testCase.outputVersion)) + if testCase.err == nil { + if testCase.outputVersion == testCase.inputVersion { + assert.Equal(t, testCase.inputConfig, result) + } else { + serialized, err := json.Marshal(result) + assert.NoError(t, err) + // Note: Despite 2.2 uses a different type the version field can be fetched using the v3 functions + version, report, err := ign3utils.GetConfigVersion(serialized) + assert.NoError(t, err) + assert.False(t, report.IsFatal()) + assert.Equal(t, testCase.outputVersion, version.String()) + } + } else { + assert.ErrorIs(t, err, testCase.err) + } + + }) + } } func TestParseAndConvert(t *testing.T) { diff --git a/pkg/controller/common/ignition_converter.go b/pkg/controller/common/ignition_converter.go new file mode 100644 index 0000000000..f27184bfc7 --- /dev/null +++ b/pkg/controller/common/ignition_converter.go @@ -0,0 +1,290 @@ +package common + +import ( + "errors" + "fmt" + "github.com/coreos/go-semver/semver" + "github.com/coreos/ign-converter/translate/v23tov30" + "github.com/coreos/ign-converter/translate/v32tov22" + "github.com/coreos/ign-converter/translate/v32tov31" + "github.com/coreos/ign-converter/translate/v33tov32" + "github.com/coreos/ign-converter/translate/v34tov33" + "github.com/coreos/ign-converter/translate/v35tov34" + ign2types "github.com/coreos/ignition/config/v2_2/types" + ign23 "github.com/coreos/ignition/config/v2_3" + v30types "github.com/coreos/ignition/v2/config/v3_0/types" + v31translate "github.com/coreos/ignition/v2/config/v3_1/translate" + v31types "github.com/coreos/ignition/v2/config/v3_1/types" + v32translate "github.com/coreos/ignition/v2/config/v3_2/translate" + v32types "github.com/coreos/ignition/v2/config/v3_2/types" + v33translate "github.com/coreos/ignition/v2/config/v3_3/translate" + v33types "github.com/coreos/ignition/v2/config/v3_3/types" + v34translate "github.com/coreos/ignition/v2/config/v3_4/translate" + v34types "github.com/coreos/ignition/v2/config/v3_4/types" + v35translate "github.com/coreos/ignition/v2/config/v3_5/translate" + v35types "github.com/coreos/ignition/v2/config/v3_5/types" +) + +var ( + errIgnitionConverterWrongSourceType = errors.New("wrong source type for the conversion") + errIgnitionConverterUnknownVersion = errors.New("the requested version is unknown") + errIgnitionConverterUnsupportedConversion = errors.New("the requested conversion is not supported") + ignitionConverter = newIgnitionConverter(buildConverterList()) +) + +type converterFn func(source any) (any, error) +type converterTypedFn[S any, T any] func(source S) (T, error) +type converterTypedNoErrorFn[S any, T any] func(source S) T +type conversionTuple struct { + sourceVersion semver.Version + targetVersion semver.Version +} + +func (t conversionTuple) String() string { + return fmt.Sprintf("source: %v target: %v", t.sourceVersion, t.targetVersion) +} + +func (t conversionTuple) IsUp() bool { + return t.sourceVersion.Compare(t.targetVersion) <= 0 +} + +type ignitionVersionConverter interface { + Convert(source any) (any, error) + Versions() conversionTuple +} + +type baseConverter struct { + tuple conversionTuple +} + +type functionConverter struct { + baseConverter + conversionFunc converterFn +} + +func newFunctionConverter(sourceVersion semver.Version, targetVersion semver.Version, conversionFunc converterFn) *functionConverter { + return &functionConverter{ + baseConverter: baseConverter{ + tuple: conversionTuple{sourceVersion: sourceVersion, targetVersion: targetVersion}, + }, + conversionFunc: conversionFunc, + } +} + +func newFunctionConverterTyped[S any, T any](sourceVersion semver.Version, targetVersion semver.Version, conversionFunc converterTypedFn[S, T]) *functionConverter { + return newFunctionConverter( + sourceVersion, targetVersion, + func(source any) (any, error) { + if input, ok := source.(S); ok { + return conversionFunc(input) + } + var empty T + return empty, errIgnitionConverterWrongSourceType + }) +} + +func newFunctionConverterTypedNoError[S any, T any](sourceVersion semver.Version, targetVersion semver.Version, conversionFunc converterTypedNoErrorFn[S, T]) *functionConverter { + return newFunctionConverterTyped(sourceVersion, targetVersion, func(s S) (T, error) { return conversionFunc(s), nil }) +} + +func (c *functionConverter) Convert(source any) (any, error) { + result, err := c.conversionFunc(source) + if err != nil && errors.Is(err, errIgnitionConverterWrongSourceType) { + return result, fmt.Errorf("conversion from %v to %v failed: %w", c.tuple.sourceVersion, c.tuple.targetVersion, err) + } + return result, err +} + +func (c *functionConverter) Versions() conversionTuple { + return c.tuple +} + +func buildConverterList() []ignitionVersionConverter { + return []ignitionVersionConverter{ + // v3.5 -> v3.4 + newFunctionConverterTyped[v35types.Config, v34types.Config](v35types.MaxVersion, v34types.MaxVersion, v35tov34.Translate), + // v3.4 -> v3.5 + newFunctionConverterTypedNoError[v34types.Config, v35types.Config](v34types.MaxVersion, v35types.MaxVersion, v35translate.Translate), + // v3.4 -> v3.3 + newFunctionConverterTyped[v34types.Config, v33types.Config](v34types.MaxVersion, v33types.MaxVersion, v34tov33.Translate), + // v3.3 -> v3.4 + newFunctionConverterTypedNoError[v33types.Config, v34types.Config](v33types.MaxVersion, v34types.MaxVersion, v34translate.Translate), + // v3.3 -> v3.2 + newFunctionConverterTyped[v33types.Config, v32types.Config](v33types.MaxVersion, v32types.MaxVersion, v33tov32.Translate), + // v3.2 -> v3.3 + newFunctionConverterTypedNoError[v32types.Config, v33types.Config](v32types.MaxVersion, v33types.MaxVersion, v33translate.Translate), + // v3.2 -> v3.1 + newFunctionConverterTyped[v32types.Config, v31types.Config](v32types.MaxVersion, v31types.MaxVersion, v32tov31.Translate), + // v3.2 -> v2.2 + newFunctionConverterTyped[v32types.Config, ign2types.Config](v32types.MaxVersion, ign2types.MaxVersion, v32tov22.Translate), + // v2.2 -> v3.2 + newFunctionConverterTyped[ign2types.Config, v32types.Config]( + ign2types.MaxVersion, v32types.MaxVersion, + func(source ign2types.Config) (v32types.Config, error) { + v30, err := v23tov30.Translate(ign23.Translate(source), map[string]string{ + "root": "/", + }) + if err != nil { + return v32types.Config{}, fmt.Errorf("unable to convert Ignition spec v2 config to v3: %w", err) + } + return v32translate.Translate(v31translate.Translate(v30)), nil + }, + ), + // v3.0 -> v3.2 + newFunctionConverterTypedNoError[v30types.Config, v32types.Config]( + v30types.MaxVersion, v32types.MaxVersion, + func(source v30types.Config) v32types.Config { + return v32translate.Translate(v31translate.Translate(source)) + }, + )} +} + +type converterConversionPath struct { + tuple conversionTuple + converterChain []ignitionVersionConverter +} + +func newConverterConversionPathFromExisting(conversionPath *converterConversionPath, conv ignitionVersionConverter) *converterConversionPath { + newConversionPath := &converterConversionPath{ + tuple: conversionTuple{sourceVersion: conversionPath.tuple.sourceVersion, targetVersion: conv.Versions().targetVersion}, + // Ensure we make a copy of slice's underlying array + converterChain: append(make([]ignitionVersionConverter, 0, len(conversionPath.converterChain)), conversionPath.converterChain...), + } + newConversionPath.converterChain = append(newConversionPath.converterChain, conv) + return newConversionPath +} + +// IgnitionConverter an Ignition configuration version converter +type IgnitionConverter interface { + // Convert performs the Ignition conversion of source (that uses Ignition version sourceVersion) + // to the version given by targetVersion. + // The conversion may fail with errIgnitionConverterUnsupportedConversion if there is no available conversion + // for the source-target version tuple. + Convert(source any, sourceVersion semver.Version, targetVersion semver.Version) (any, error) + + // GetSupportedMinorVersion retrieves the [semver.Version] that performs a translation of the given version. + // The method may return errIgnitionConverterUnknownVersion if the given version minor version (X.Y) does not + // have a compatible version in the converter. + GetSupportedMinorVersion(version semver.Version) (semver.Version, error) + + // GetSupportedMinorVersions retrieves the list of supported minor versions (X.Y) + GetSupportedMinorVersions() []string +} + +type ignitionConverterImpl struct { + converterPaths map[conversionTuple]*converterConversionPath + supportedVersions []*semver.Version +} + +func newIgnitionConverter(converters []ignitionVersionConverter) *ignitionConverterImpl { + instance := &ignitionConverterImpl{ + converterPaths: map[conversionTuple]*converterConversionPath{}, + supportedVersions: make([]*semver.Version, 0), + } + + // ignitionVersionConverter grouped by source version + versionsConverters := map[semver.Version][]ignitionVersionConverter{} + + // supportedVersions used to ensure the final versions list has no duplicates + supportedVersions := map[semver.Version]struct{}{} + for _, conv := range converters { + versionsTuple := conv.Versions() + instance.registerConverterVersions(versionsTuple, supportedVersions) + + sourceVersion := versionsTuple.sourceVersion + if _, ok := versionsConverters[sourceVersion]; !ok { + versionsConverters[sourceVersion] = []ignitionVersionConverter{} + } + versionsConverters[sourceVersion] = append(versionsConverters[sourceVersion], conv) + } + for ver := range versionsConverters { + instance.recursiveConverterRegistration(ver, versionsConverters, nil) + } + semver.Sort(instance.supportedVersions) + return instance +} + +func (m *ignitionConverterImpl) registerConverterVersions(tuple conversionTuple, versionsMap map[semver.Version]struct{}) { + if _, ok := versionsMap[tuple.sourceVersion]; !ok { + versionsMap[tuple.sourceVersion] = struct{}{} + m.supportedVersions = append(m.supportedVersions, &tuple.sourceVersion) + } + if _, ok := versionsMap[tuple.targetVersion]; !ok { + versionsMap[tuple.targetVersion] = struct{}{} + m.supportedVersions = append(m.supportedVersions, &tuple.targetVersion) + } +} + +func (m *ignitionConverterImpl) recursiveConverterRegistration(version semver.Version, convertersMap map[semver.Version][]ignitionVersionConverter, conversionPath *converterConversionPath) { + conversions, exists := convertersMap[version] + if !exists { + // The version has no converter that translates **from** + // In other words: Conversions from the version exists but there are no + // conversion from that version + return + } + for _, conv := range conversions { + tuple := conversionTuple{sourceVersion: version, targetVersion: conv.Versions().targetVersion} + if conversionPath != nil && (conversionPath.tuple.IsUp() == tuple.IsUp()) { + // Continuation of the conversion chain: + // - Create a new chain and save it pointing to the current version + // - Recursively continue using the new chain as start + newChain := newConverterConversionPathFromExisting(conversionPath, conv) + if _, ok := m.converterPaths[newChain.tuple]; !ok { + m.converterPaths[newChain.tuple] = newChain + m.recursiveConverterRegistration(newChain.tuple.targetVersion, convertersMap, newChain) + } + } else if conversionPath == nil { + // Start of the conversion chain + chain := &converterConversionPath{tuple: tuple, converterChain: []ignitionVersionConverter{conv}} + if _, ok := m.converterPaths[chain.tuple]; !ok { + m.converterPaths[chain.tuple] = chain + m.recursiveConverterRegistration(chain.tuple.targetVersion, convertersMap, chain) + } + } + } +} + +func (m *ignitionConverterImpl) Convert(source any, sourceVersion semver.Version, targetVersion semver.Version) (any, error) { + if sourceVersion.Equal(targetVersion) { + return source, nil + } + + tuple := conversionTuple{sourceVersion: sourceVersion, targetVersion: targetVersion} + conversionPath, ok := m.converterPaths[tuple] + if !ok { + return nil, fmt.Errorf("conversion for source version %v to target version %v is not supported %w", sourceVersion, targetVersion, errIgnitionConverterUnsupportedConversion) + } + + var err error + result := source + for _, conv := range conversionPath.converterChain { + if result, err = conv.Convert(result); err != nil { + break + } + } + return result, err +} + +func (m *ignitionConverterImpl) GetSupportedMinorVersion(version semver.Version) (semver.Version, error) { + minorVersion := semver.Version{Major: version.Major, Minor: version.Minor} + for _, ver := range m.supportedVersions { + if ver.Equal(minorVersion) { + return *ver, nil + } + } + return semver.Version{}, errIgnitionConverterUnknownVersion +} + +func (m *ignitionConverterImpl) GetSupportedMinorVersions() []string { + var minorVersions []string + for _, version := range m.supportedVersions { + minorVersions = append(minorVersions, fmt.Sprintf("%d.%d", version.Major, version.Minor)) + } + return minorVersions +} + +// IgnitionConverterSingleton retrieves the singleton instance of the Ignition converter +func IgnitionConverterSingleton() IgnitionConverter { + return ignitionConverter +} diff --git a/pkg/server/api.go b/pkg/server/api.go index 3a82944614..58b99f32b0 100644 --- a/pkg/server/api.go +++ b/pkg/server/api.go @@ -12,7 +12,6 @@ import ( "github.com/clarketm/json" "github.com/coreos/go-semver/semver" - "k8s.io/apimachinery/pkg/runtime" "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/certwatcher" "sigs.k8s.io/controller-runtime/pkg/log" @@ -152,67 +151,16 @@ func (sh *APIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) return } - // we know we're at 3.5 in code... serve directly, parsing is expensive... - // we're doing it during an HTTP request, and most notably before we write the HTTP headers - var serveConf *runtime.RawExtension - switch { - case reqConfigVer.Equal(*semver.New("3.5.0")): - serveConf = conf - - case reqConfigVer.Equal(*semver.New("3.4.0")): - converted34, err := ctrlcommon.ConvertRawExtIgnitionToV3_4(conf) - if err != nil { - w.Header().Set("Content-Length", "0") - w.WriteHeader(http.StatusInternalServerError) - klog.Errorf("couldn't convert config for req: %v, error: %v", cr, err) - return - } - serveConf = &converted34 - - case reqConfigVer.Equal(*semver.New("3.3.0")): - converted33, err := ctrlcommon.ConvertRawExtIgnitionToV3_3(conf) - if err != nil { - w.Header().Set("Content-Length", "0") - w.WriteHeader(http.StatusInternalServerError) - klog.Errorf("couldn't convert config for req: %v, error: %v", cr, err) - return - } - serveConf = &converted33 - - case reqConfigVer.Equal(*semver.New("3.2.0")): - converted32, err := ctrlcommon.ConvertRawExtIgnitionToV3_2(conf) - if err != nil { - w.Header().Set("Content-Length", "0") - w.WriteHeader(http.StatusInternalServerError) - klog.Errorf("couldn't convert config for req: %v, error: %v", cr, err) - return - } - serveConf = &converted32 - - case reqConfigVer.Equal(*semver.New("3.1.0")): - converted31, err := ctrlcommon.ConvertRawExtIgnitionToV3_1(conf) - if err != nil { - w.Header().Set("Content-Length", "0") - w.WriteHeader(http.StatusInternalServerError) - klog.Errorf("couldn't convert config for req: %v, error: %v", cr, err) - return - } - serveConf = &converted31 - - default: - // Can only be 2.2 here - converted2, err := ctrlcommon.ConvertRawExtIgnitionToV2_2(conf) - if err != nil { - w.Header().Set("Content-Length", "0") - w.WriteHeader(http.StatusInternalServerError) - klog.Errorf("couldn't convert config for req: %v, error: %v", cr, err) - return - } - serveConf = &converted2 + serveConf, err := ctrlcommon.ConvertRawExtIgnitionToVersion(conf, *reqConfigVer) + if err != nil { + w.Header().Set("Content-Length", "0") + w.WriteHeader(http.StatusInternalServerError) + klog.Errorf("couldn't convert config for req: %v, error: %v", cr, err) + return } - data, err := json.Marshal(serveConf) + data, err := json.Marshal(&serveConf) if err != nil { w.Header().Set("Content-Length", "0") w.WriteHeader(http.StatusInternalServerError) @@ -324,49 +272,33 @@ func detectSpecVersionFromAcceptHeader(acceptHeader string) (*semver.Version, er // "application/vnd.coreos.ignition+json; version=2.4.0, application/vnd.coreos.ignition+json; version=1; q=0.5, */*; q=0.1". // For v2.x, it looks like: // "application/vnd.coreos.ignition+json;version=3.2.0, */*;q=0.1". - v2_2 := semver.New("2.2.0") - v3_1 := semver.New("3.1.0") - v3_2 := semver.New("3.2.0") - v3_3 := semver.New("3.3.0") - v3_4 := semver.New("3.4.0") - v3_5 := semver.New("3.5.0") - - var ignVersionError error + + v22Version := semver.New("2.2.0") headers, err := parseAcceptHeader(acceptHeader) if err != nil { // no valid accept headers detected at all, serve default - return v2_2, nil + return v22Version, nil } for _, header := range headers { if header.MIMESubtype == "vnd.coreos.ignition+json" && header.SemVer != nil { - switch { - case !header.SemVer.LessThan(*v3_5) && header.SemVer.LessThan(*semver.New("4.0.0")): - return v3_5, nil - case !header.SemVer.LessThan(*v3_4) && header.SemVer.LessThan(*v3_5): - return v3_4, nil - case !header.SemVer.LessThan(*v3_3) && header.SemVer.LessThan(*v3_4): - return v3_3, nil - case !header.SemVer.LessThan(*v3_2) && header.SemVer.LessThan(*v3_3): - return v3_2, nil - case !header.SemVer.LessThan(*v3_1) && header.SemVer.LessThan(*v3_2): - return v3_1, nil - case !header.SemVer.LessThan(*v2_2) && header.SemVer.LessThan(*semver.New("3.0.0")): - return v2_2, nil - default: - ignVersionError = fmt.Errorf("unsupported Ignition version in Accept header: %s", acceptHeader) + reqVersion := header.SemVer + // If the version is > 2.2, but it's still a v2 -> Assume 2.2 handling + if reqVersion.Compare(*v22Version) >= 0 && reqVersion.Major == v22Version.Major { + reqVersion = v22Version } - } - } + version, err := ctrlcommon.IgnitionConverterSingleton().GetSupportedMinorVersion(*reqVersion) + if err != nil { + return nil, fmt.Errorf("unsupported Ignition version in Accept header: %s", acceptHeader) + } + return &version, nil - // return error if version of Ignition MIME subtype is not supported - if ignVersionError != nil { - return nil, ignVersionError + } } // default to serving spec v2.2 for all non-Ignition headers // as well as Ignition headers without a version specified. - return v2_2, nil + return v22Version, nil } // ServeHTTP handles /healthz requests.