feat: support version contract for Talos config generation

This allows to generating current version Talos configs (by default) or
backwards compatible configuration (e.g. for Talos 0.8).

`talosctl gen config` defaults to current version, but explicit version
can be passed to the command via flags.

`talosctl cluster create` defaults to install/container image version,
but that can be overridden. This makes `talosctl cluster create` now
compatible with 0.8.1 images out of the box.

Upgrade tests use contract based on source version in the test.

When used as a library, `VersionContract` can be omitted (defaults to
current version) or passed explicitly. `VersionContract` can be
convienietly parsed from Talos version string or specified as one of the
constants.

Fixes #3130

Signed-off-by: Andrey Smirnov <smirnov.andrey@gmail.com>
This commit is contained in:
Andrey Smirnov 2021-02-09 23:50:39 +03:00 committed by talos-bot
parent f9896777fc
commit daea9d3811
12 changed files with 259 additions and 31 deletions

View File

@ -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)
}

View File

@ -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(&registryMirrors, "registry-mirror", []string{}, "list of registry mirrors to use in format: <registry host>=<mirror URL>")

View File

@ -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)

View File

@ -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
}

View File

@ -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)
}

View File

@ -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())
}

View File

@ -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
}

View File

@ -15,7 +15,6 @@ type InputOptions struct {
Endpoint string
KubeVersion string
GenOptions []generate.GenOption
UseRSAKeys bool
}
// Options describes generate parameters.

View File

@ -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.

View File

@ -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)
}

View File

@ -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.

View File

@ -108,6 +108,7 @@ talosctl cluster create [flags]
--registry-mirror strings list of registry mirrors to use in format: <registry host>=<mirror URL>
--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: <mount_point1>:<size1>:<mount_point2>:<size2>
--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 <cluster name> <cluster endpoint> [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: <registry host>=<mirror URL>
--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")
```