diff --git a/cmd/talosctl/cmd/mgmt/cluster/create.go b/cmd/talosctl/cmd/mgmt/cluster/create.go index a42d2e061..5aed3a300 100644 --- a/cmd/talosctl/cmd/mgmt/cluster/create.go +++ b/cmd/talosctl/cmd/mgmt/cluster/create.go @@ -86,6 +86,7 @@ var ( crashdumpOnFailure bool skipKubeconfig bool skipInjectingConfig bool + talosVersion string ) // createCmd represents the cluster up command. @@ -280,6 +281,29 @@ func create(ctx context.Context) (err error) { genOptions = append(genOptions, generate.WithUserDisks(machineDisks)) } + if talosVersion == "" { + if provisionerName == "docker" { + parts := strings.Split(nodeImage, ":") + + talosVersion = parts[len(parts)-1] + } else { + parts := strings.Split(nodeInstallImage, ":") + + talosVersion = parts[len(parts)-1] + } + } + + if talosVersion != "latest" { + var versionContract *config.VersionContract + + versionContract, err = config.ParseContractFromVersion(talosVersion) + if err != nil { + return fmt.Errorf("error parsing Talos version %q: %w", talosVersion, err) + } + + genOptions = append(genOptions, generate.WithVersionContract(versionContract)) + } + defaultInternalLB, defaultEndpoint := provisioner.GetLoadBalancers(request.Network) if defaultInternalLB == "" { @@ -677,5 +701,6 @@ func init() { createCmd.Flags().BoolVar(&crashdumpOnFailure, "crashdump", false, "print debug crashdump to stderr when cluster startup fails") createCmd.Flags().BoolVar(&skipKubeconfig, "skip-kubeconfig", false, "skip merging kubeconfig from the created cluster") createCmd.Flags().BoolVar(&skipInjectingConfig, "skip-injecting-config", false, "skip injecting config from embedded metadata server, write config files to current directory") + createCmd.Flags().StringVar(&talosVersion, "talos-version", "", "the desired Talos version to generate config for (if not set, defaults to image version)") Cmd.AddCommand(createCmd) } diff --git a/cmd/talosctl/cmd/mgmt/config.go b/cmd/talosctl/cmd/mgmt/config.go index fcd147a6f..05a421544 100644 --- a/cmd/talosctl/cmd/mgmt/config.go +++ b/cmd/talosctl/cmd/mgmt/config.go @@ -18,6 +18,7 @@ import ( "github.com/talos-systems/talos/cmd/talosctl/pkg/mgmt/helpers" "github.com/talos-systems/talos/pkg/images" + "github.com/talos-systems/talos/pkg/machinery/config" "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1/bundle" "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1/generate" "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1/machine" @@ -29,6 +30,7 @@ var ( configVersion string dnsDomain string kubernetesVersion string + talosVersion string installDisk string installImage string outputDir string @@ -127,6 +129,17 @@ func genV1Alpha1Config(args []string) error { genOptions = append(genOptions, generate.WithRegistryMirror(components[0], components[1])) } + if talosVersion != "" { + var versionContract *config.VersionContract + + versionContract, err = config.ParseContractFromVersion(talosVersion) + if err != nil { + return fmt.Errorf("invalid talos-version: %w", err) + } + + genOptions = append(genOptions, generate.WithVersionContract(versionContract)) + } + configBundle, err := bundle.NewConfigBundle( bundle.WithInputOptions( &bundle.InputOptions{ @@ -177,6 +190,7 @@ func init() { genConfigCmd.Flags().StringSliceVar(&additionalSANs, "additional-sans", []string{}, "additional Subject-Alt-Names for the APIServer certificate") genConfigCmd.Flags().StringVar(&dnsDomain, "dns-domain", "cluster.local", "the dns domain to use for cluster") genConfigCmd.Flags().StringVar(&configVersion, "version", "v1alpha1", "the desired machine config version to generate") + genConfigCmd.Flags().StringVar(&talosVersion, "talos-version", "", "the desired Talos version to generate config for (backwards compatibility, e.g. v0.8)") genConfigCmd.Flags().StringVar(&kubernetesVersion, "kubernetes-version", "", "desired kubernetes version to run") genConfigCmd.Flags().StringVarP(&outputDir, "output-dir", "o", "", "destination to output generated files") genConfigCmd.Flags().StringSliceVar(®istryMirrors, "registry-mirror", []string{}, "list of registry mirrors to use in format: =") diff --git a/internal/integration/provision/upgrade.go b/internal/integration/provision/upgrade.go index 0e45cc83f..08f9b66d0 100644 --- a/internal/integration/provision/upgrade.go +++ b/internal/integration/provision/upgrade.go @@ -34,6 +34,7 @@ import ( machineapi "github.com/talos-systems/talos/pkg/machinery/api/machine" talosclient "github.com/talos-systems/talos/pkg/machinery/client" clientconfig "github.com/talos-systems/talos/pkg/machinery/client/config" + "github.com/talos-systems/talos/pkg/machinery/config" "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1" "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1/bundle" "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1/generate" @@ -62,7 +63,6 @@ type upgradeSpec struct { UpgradePreserve bool UpgradeStage bool - UseRSAKeys bool } const ( @@ -106,7 +106,6 @@ func upgradeBetweenTwoLastReleases() upgradeSpec { MasterNodes: DefaultSettings.MasterNodes, WorkerNodes: DefaultSettings.WorkerNodes, - UseRSAKeys: true, // ECDSA is supported with Talos >= 0.9 } } @@ -127,7 +126,6 @@ func upgradeStableReleaseToCurrent() upgradeSpec { MasterNodes: DefaultSettings.MasterNodes, WorkerNodes: DefaultSettings.WorkerNodes, - UseRSAKeys: true, // ECDSA is supported with Talos >= 0.9 } } @@ -149,7 +147,6 @@ func upgradeSingeNodePreserve() upgradeSpec { MasterNodes: 1, WorkerNodes: 0, UpgradePreserve: true, - UseRSAKeys: true, // ECDSA is supported with Talos >= 0.9 } } @@ -172,7 +169,6 @@ func upgradeSingeNodeStage() upgradeSpec { WorkerNodes: 0, UpgradePreserve: true, UpgradeStage: true, - UseRSAKeys: true, // ECDSA is supported with Talos >= 0.9 } } @@ -319,17 +315,20 @@ func (suite *UpgradeSuite) setupCluster() { })) } + versionContract, err := config.ParseContractFromVersion(suite.spec.SourceVersion) + suite.Require().NoError(err) + suite.configBundle, err = bundle.NewConfigBundle(bundle.WithInputOptions( &bundle.InputOptions{ ClusterName: clusterName, Endpoint: suite.controlPlaneEndpoint, KubeVersion: "", // keep empty so that default version is used per Talos version - UseRSAKeys: suite.spec.UseRSAKeys, GenOptions: append( genOptions, generate.WithEndpointList(masterEndpoints), generate.WithInstallImage(suite.spec.SourceInstallerImage), generate.WithDNSDomain("cluster.local"), + generate.WithVersionContract(versionContract), ), })) suite.Require().NoError(err) diff --git a/internal/pkg/configuration/configuration.go b/internal/pkg/configuration/configuration.go index 6d4f8ad88..d4337bc8d 100644 --- a/internal/pkg/configuration/configuration.go +++ b/internal/pkg/configuration/configuration.go @@ -112,7 +112,7 @@ func Generate(ctx context.Context, in *machine.GenerateConfigurationRequest) (re switch { case os.IsNotExist(err): - secrets, err = generate.NewSecretsBundle(clock, false) + secrets, err = generate.NewSecretsBundle(clock) if err != nil { return nil, err } diff --git a/pkg/machinery/config/contract.go b/pkg/machinery/config/contract.go new file mode 100644 index 000000000..cdc99ee71 --- /dev/null +++ b/pkg/machinery/config/contract.go @@ -0,0 +1,74 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package config + +import ( + "fmt" + "regexp" + "strconv" +) + +// VersionContract describes Talos version to generate config for. +// +// Config generation only supports backwards compatibility (e.g. Talos 0.9 can generate configs for Talos 0.9 and 0.8). +// Matching version of the machinery package is required to generate configs for the current version of Talos. +// +// Nil value of *VersionContract always describes current version of Talos. +type VersionContract struct { + Major int + Minor int +} + +// Well-known Talos version contracts. +var ( + TalosVersionCurrent = (*VersionContract)(nil) + TalosVersion0_9 = &VersionContract{0, 9} + TalosVersion0_8 = &VersionContract{0, 8} +) + +var versionRegexp = regexp.MustCompile(`^v(\d+)\.(\d+)($|\.)`) + +// ParseContractFromVersion parses Talos version into VersionContract. +func ParseContractFromVersion(version string) (*VersionContract, error) { + matches := versionRegexp.FindStringSubmatch(version) + if len(matches) < 3 { + return nil, fmt.Errorf("error parsing version %q", version) + } + + var contract VersionContract + + contract.Major, _ = strconv.Atoi(matches[1]) //nolint: errcheck + contract.Minor, _ = strconv.Atoi(matches[2]) //nolint: errcheck + + return &contract, nil +} + +// Greater compares contract to another contract. +func (contract *VersionContract) Greater(other *VersionContract) bool { + if contract == nil { + return other != nil + } + + if other == nil { + return false + } + + return contract.Major > other.Major || (contract.Major == other.Major && contract.Minor > other.Minor) +} + +// SupportsECDSAKeys returns true if version of Talos supports ECDSA keys (vs. RSA keys). +func (contract *VersionContract) SupportsECDSAKeys() bool { + return contract.Greater(TalosVersion0_8) +} + +// SupportsAggregatorCA returns true if version of Talos supports AggregatorCA in the config. +func (contract *VersionContract) SupportsAggregatorCA() bool { + return contract.Greater(TalosVersion0_8) +} + +// SupportsServiceAccount returns true if version of Talos supports ServiceAccount in the config. +func (contract *VersionContract) SupportsServiceAccount() bool { + return contract.Greater(TalosVersion0_8) +} diff --git a/pkg/machinery/config/contract_test.go b/pkg/machinery/config/contract_test.go new file mode 100644 index 000000000..b9bb3d38e --- /dev/null +++ b/pkg/machinery/config/contract_test.go @@ -0,0 +1,59 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package config_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/talos-systems/talos/pkg/machinery/config" +) + +func TestContractGreater(t *testing.T) { + assert.True(t, config.TalosVersion0_9.Greater(config.TalosVersion0_8)) + assert.True(t, config.TalosVersionCurrent.Greater(config.TalosVersion0_8)) + assert.True(t, config.TalosVersionCurrent.Greater(config.TalosVersion0_9)) + + assert.False(t, config.TalosVersion0_8.Greater(config.TalosVersion0_9)) + assert.False(t, config.TalosVersion0_8.Greater(config.TalosVersion0_8)) + assert.False(t, config.TalosVersionCurrent.Greater(config.TalosVersionCurrent)) +} + +func TestContractParseVersion(t *testing.T) { + contract, err := config.ParseContractFromVersion("v0.8") + assert.NoError(t, err) + assert.Equal(t, config.TalosVersion0_8, contract) + + contract, err = config.ParseContractFromVersion("v0.8.1") + assert.NoError(t, err) + assert.Equal(t, config.TalosVersion0_8, contract) + + contract, err = config.ParseContractFromVersion("v0.88") + assert.NoError(t, err) + assert.NotEqual(t, config.TalosVersion0_8, contract) + + contract, err = config.ParseContractFromVersion("v0.8.3-alpha.4") + assert.NoError(t, err) + assert.Equal(t, config.TalosVersion0_8, contract) +} + +func TestContractCurrent(t *testing.T) { + assert.True(t, config.TalosVersionCurrent.SupportsAggregatorCA()) + assert.True(t, config.TalosVersionCurrent.SupportsECDSAKeys()) + assert.True(t, config.TalosVersionCurrent.SupportsServiceAccount()) +} + +func TestContract0_9(t *testing.T) { + assert.True(t, config.TalosVersion0_9.SupportsAggregatorCA()) + assert.True(t, config.TalosVersion0_9.SupportsECDSAKeys()) + assert.True(t, config.TalosVersion0_9.SupportsServiceAccount()) +} + +func TestContract0_8(t *testing.T) { + assert.False(t, config.TalosVersion0_8.SupportsAggregatorCA()) + assert.False(t, config.TalosVersion0_8.SupportsECDSAKeys()) + assert.False(t, config.TalosVersion0_8.SupportsServiceAccount()) +} diff --git a/pkg/machinery/config/types/v1alpha1/bundle/bundle.go b/pkg/machinery/config/types/v1alpha1/bundle/bundle.go index 421f0c025..3733e4ad2 100644 --- a/pkg/machinery/config/types/v1alpha1/bundle/bundle.go +++ b/pkg/machinery/config/types/v1alpha1/bundle/bundle.go @@ -80,7 +80,7 @@ func NewConfigBundle(opts ...Option) (*v1alpha1.ConfigBundle, error) { fmt.Println("generating PKI and tokens") } - secrets, err := generate.NewSecretsBundle(generate.NewClock(), options.InputOptions.UseRSAKeys) + secrets, err := generate.NewSecretsBundle(generate.NewClock(), options.InputOptions.GenOptions...) if err != nil { return bundle, err } diff --git a/pkg/machinery/config/types/v1alpha1/bundle/options.go b/pkg/machinery/config/types/v1alpha1/bundle/options.go index 69943f850..796ed2a16 100644 --- a/pkg/machinery/config/types/v1alpha1/bundle/options.go +++ b/pkg/machinery/config/types/v1alpha1/bundle/options.go @@ -15,7 +15,6 @@ type InputOptions struct { Endpoint string KubeVersion string GenOptions []generate.GenOption - UseRSAKeys bool } // Options describes generate parameters. diff --git a/pkg/machinery/config/types/v1alpha1/generate/generate.go b/pkg/machinery/config/types/v1alpha1/generate/generate.go index ba6a350cf..61621e711 100644 --- a/pkg/machinery/config/types/v1alpha1/generate/generate.go +++ b/pkg/machinery/config/types/v1alpha1/generate/generate.go @@ -185,7 +185,15 @@ func (c *SystemClock) SetFixedTimestamp(t time.Time) { // NewSecretsBundle creates secrets bundle generating all secrets. // //nolint: gocyclo -func NewSecretsBundle(clock Clock, useRSA bool) (*SecretsBundle, error) { +func NewSecretsBundle(clock Clock, opts ...GenOption) (*SecretsBundle, error) { + options := DefaultGenOptions() + + for _, opt := range opts { + if err := opt(&options); err != nil { + return nil, err + } + } + var ( etcd *x509.CertificateAuthority kubernetesCA *x509.CertificateAuthority @@ -197,24 +205,28 @@ func NewSecretsBundle(clock Clock, useRSA bool) (*SecretsBundle, error) { err error ) - etcd, err = NewEtcdCA(clock.Now(), useRSA) + etcd, err = NewEtcdCA(clock.Now(), !options.VersionContract.SupportsECDSAKeys()) if err != nil { return nil, err } - kubernetesCA, err = NewKubernetesCA(clock.Now(), useRSA) + kubernetesCA, err = NewKubernetesCA(clock.Now(), !options.VersionContract.SupportsECDSAKeys()) if err != nil { return nil, err } - aggregatorCA, err = NewAggregatorCA(clock.Now()) - if err != nil { - return nil, err + if options.VersionContract.SupportsAggregatorCA() { + aggregatorCA, err = NewAggregatorCA(clock.Now()) + if err != nil { + return nil, err + } } - serviceAccount, err = x509.NewECDSAKey() - if err != nil { - return nil, err + if options.VersionContract.SupportsServiceAccount() { + serviceAccount, err = x509.NewECDSAKey() + if err != nil { + return nil, err + } } talosCA, err = NewTalosCA(clock.Now()) @@ -243,7 +255,7 @@ func NewSecretsBundle(clock Clock, useRSA bool) (*SecretsBundle, error) { return nil, err } - return &SecretsBundle{ + result := &SecretsBundle{ Clock: clock, Secrets: kubeadmTokens, TrustdInfo: trustdInfo, @@ -256,19 +268,27 @@ func NewSecretsBundle(clock Clock, useRSA bool) (*SecretsBundle, error) { Crt: kubernetesCA.CrtPEM, Key: kubernetesCA.KeyPEM, }, - K8sAggregator: &x509.PEMEncodedCertificateAndKey{ - Crt: aggregatorCA.CrtPEM, - Key: aggregatorCA.KeyPEM, - }, - K8sServiceAccount: &x509.PEMEncodedKey{ - Key: serviceAccount.KeyPEM, - }, OS: &x509.PEMEncodedCertificateAndKey{ Crt: talosCA.CrtPEM, Key: talosCA.KeyPEM, }, }, - }, nil + } + + if aggregatorCA != nil { + result.Certs.K8sAggregator = &x509.PEMEncodedCertificateAndKey{ + Crt: aggregatorCA.CrtPEM, + Key: aggregatorCA.KeyPEM, + } + } + + if serviceAccount != nil { + result.Certs.K8sServiceAccount = &x509.PEMEncodedKey{ + Key: serviceAccount.KeyPEM, + } + } + + return result, nil } // NewSecretsBundleFromConfig creates secrets bundle using existing config. diff --git a/pkg/machinery/config/types/v1alpha1/generate/generate_test.go b/pkg/machinery/config/types/v1alpha1/generate/generate_test.go index 4a0cb8706..4f7aa81fe 100644 --- a/pkg/machinery/config/types/v1alpha1/generate/generate_test.go +++ b/pkg/machinery/config/types/v1alpha1/generate/generate_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/suite" + "github.com/talos-systems/talos/pkg/machinery/config" genv1alpha1 "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1/generate" "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1/machine" "github.com/talos-systems/talos/pkg/machinery/constants" @@ -17,18 +18,42 @@ import ( type GenerateSuite struct { suite.Suite - input *genv1alpha1.Input + input *genv1alpha1.Input + genOptions []genv1alpha1.GenOption } func TestGenerateSuite(t *testing.T) { - suite.Run(t, new(GenerateSuite)) + for _, tt := range []struct { + label string + genOptions []genv1alpha1.GenOption + }{ + { + label: "current", + }, + { + label: "0.9", + genOptions: []genv1alpha1.GenOption{genv1alpha1.WithVersionContract(config.TalosVersion0_9)}, + }, + { + label: "0.8", + genOptions: []genv1alpha1.GenOption{genv1alpha1.WithVersionContract(config.TalosVersion0_8)}, + }, + } { + tt := tt + + t.Run(tt.label, func(t *testing.T) { + suite.Run(t, &GenerateSuite{ + genOptions: tt.genOptions, + }) + }) + } } func (suite *GenerateSuite) SetupSuite() { var err error - secrets, err := genv1alpha1.NewSecretsBundle(genv1alpha1.NewClock(), false) + secrets, err := genv1alpha1.NewSecretsBundle(genv1alpha1.NewClock(), suite.genOptions...) suite.Require().NoError(err) - suite.input, err = genv1alpha1.NewInput("test", "10.0.1.5", constants.DefaultKubernetesVersion, secrets) + suite.input, err = genv1alpha1.NewInput("test", "10.0.1.5", constants.DefaultKubernetesVersion, secrets, suite.genOptions...) suite.Require().NoError(err) } diff --git a/pkg/machinery/config/types/v1alpha1/generate/options.go b/pkg/machinery/config/types/v1alpha1/generate/options.go index f9aaa4058..d0f6503b8 100644 --- a/pkg/machinery/config/types/v1alpha1/generate/options.go +++ b/pkg/machinery/config/types/v1alpha1/generate/options.go @@ -5,6 +5,7 @@ package generate import ( + "github.com/talos-systems/talos/pkg/machinery/config" v1alpha1 "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1" ) @@ -153,6 +154,15 @@ func WithAllowSchedulingOnMasters(enabled bool) GenOption { } } +// WithVersionContract specifies version contract to use when generating. +func WithVersionContract(versionContract *config.VersionContract) GenOption { + return func(o *GenOptions) error { + o.VersionContract = versionContract + + return nil + } +} + // GenOptions describes generate parameters. type GenOptions struct { EndpointList []string @@ -169,6 +179,7 @@ type GenOptions struct { Persist bool AllowSchedulingOnMasters bool MachineDisks []*v1alpha1.MachineDisk + VersionContract *config.VersionContract } // DefaultGenOptions returns default options. diff --git a/website/content/docs/v0.9/Reference/cli.md b/website/content/docs/v0.9/Reference/cli.md index 5cf37de32..167ee97f9 100644 --- a/website/content/docs/v0.9/Reference/cli.md +++ b/website/content/docs/v0.9/Reference/cli.md @@ -108,6 +108,7 @@ talosctl cluster create [flags] --registry-mirror strings list of registry mirrors to use in format: = --skip-injecting-config skip injecting config from embedded metadata server, write config files to current directory --skip-kubeconfig skip merging kubeconfig from the created cluster + --talos-version string the desired Talos version to generate config for (if not set, defaults to image version) --user-disk strings list of disks to create for each VM in format: ::: --vmlinuz-path string the compressed kernel image to use (default "_out/vmlinuz-${ARCH}") --wait wait for the cluster to be ready before returning (default true) @@ -854,6 +855,7 @@ talosctl gen config [flags] -o, --output-dir string destination to output generated files -p, --persist the desired persist value for configs (default true) --registry-mirror strings list of registry mirrors to use in format: = + --talos-version string the desired Talos version to generate config for (backwards compatibility, e.g. v0.8) --version string the desired machine config version to generate (default "v1alpha1") ```