mirror of
https://github.com/siderolabs/talos.git
synced 2025-08-26 00:51:11 +02:00
feat: Validate userdata (#593)
* feat: Validate userdata Signed-off-by: Brad Beam <brad.beam@talos-systems.com>
This commit is contained in:
parent
e4c5385f3d
commit
a5d31d97ff
1
go.mod
1
go.mod
@ -70,6 +70,7 @@ require (
|
|||||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313
|
golang.org/x/sys v0.0.0-20190312061237-fead79001313
|
||||||
golang.org/x/text v0.3.0
|
golang.org/x/text v0.3.0
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c // indirect
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c // indirect
|
||||||
|
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373
|
||||||
google.golang.org/genproto v0.0.0-20181221175505-bd9b4fb69e2f // indirect
|
google.golang.org/genproto v0.0.0-20181221175505-bd9b4fb69e2f // indirect
|
||||||
google.golang.org/grpc v1.17.0
|
google.golang.org/grpc v1.17.0
|
||||||
gopkg.in/airbrake/gobrake.v2 v2.0.9 // indirect
|
gopkg.in/airbrake/gobrake.v2 v2.0.9 // indirect
|
||||||
|
2
go.sum
2
go.sum
@ -172,6 +172,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg=
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373 h1:PPwnA7z1Pjf7XYaBP9GL1VAMZmcIWyFz7QCMSIIa3Bg=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/appengine v1.1.0 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs=
|
google.golang.org/appengine v1.1.0 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs=
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
|
38
pkg/userdata/errors.go
Normal file
38
pkg/userdata/errors.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
/* 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 userdata
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
// General
|
||||||
|
|
||||||
|
// ErrRequiredSection denotes a section is required
|
||||||
|
ErrRequiredSection = errors.New("required userdata section")
|
||||||
|
// ErrInvalidVersion denotes that the config file version is invalid
|
||||||
|
ErrInvalidVersion = errors.New("invalid config version")
|
||||||
|
|
||||||
|
// Security
|
||||||
|
|
||||||
|
// ErrInvalidCert denotes that the certificate specified is invalid
|
||||||
|
ErrInvalidCert = errors.New("certificate is invalid")
|
||||||
|
// ErrInvalidCertType denotes that the certificate type is invalid
|
||||||
|
ErrInvalidCertType = errors.New("certificate type is invalid")
|
||||||
|
|
||||||
|
// Services
|
||||||
|
|
||||||
|
// ErrUnsupportedCNI denotes that the specified CNI is invalid
|
||||||
|
ErrUnsupportedCNI = errors.New("unsupported CNI driver")
|
||||||
|
// ErrInvalidTrustdToken denotes that a trustd token has not been specified
|
||||||
|
ErrInvalidTrustdToken = errors.New("trustd token is invalid")
|
||||||
|
|
||||||
|
// Networking
|
||||||
|
|
||||||
|
// ErrBadAddressing denotes that an incorrect combination of network
|
||||||
|
// address methods have been specified
|
||||||
|
ErrBadAddressing = errors.New("invalid network device addressing method")
|
||||||
|
// ErrInvalidAddress denotes that a bad address was provided
|
||||||
|
ErrInvalidAddress = errors.New("invalid network address")
|
||||||
|
)
|
@ -204,11 +204,33 @@ func Userdata(t Type, in *Input) (string, error) {
|
|||||||
return "", errors.New("failed to determine userdata type to generate")
|
return "", errors.New("failed to determine userdata type to generate")
|
||||||
}
|
}
|
||||||
|
|
||||||
ud, err := renderTemplate(in, template)
|
var err error
|
||||||
|
var ud string
|
||||||
|
|
||||||
|
ud, err = renderTemplate(in, template)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: We cant implement this currently because of
|
||||||
|
// issues with kubeadm dependency mismatch between
|
||||||
|
// talos and clusterapi//kubebuilder.
|
||||||
|
// We should figure out way we can work around/through
|
||||||
|
// this
|
||||||
|
/*
|
||||||
|
// Create an actual userdata struct from the
|
||||||
|
// generated data so we can call validate
|
||||||
|
// and ensure we are providing proper data
|
||||||
|
data := &userdata.UserData{}
|
||||||
|
if err = yaml.Unmarshal([]byte(ud), data); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = data.Validate(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
return ud, nil
|
return ud, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
49
pkg/userdata/install.go
Normal file
49
pkg/userdata/install.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
/* 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 userdata
|
||||||
|
|
||||||
|
// Install represents the installation options for preparing a node.
|
||||||
|
type Install struct {
|
||||||
|
Boot *BootDevice `yaml:"boot,omitempty"`
|
||||||
|
Root *RootDevice `yaml:"root"`
|
||||||
|
Data *InstallDevice `yaml:"data,omitempty"`
|
||||||
|
ExtraDevices []*ExtraDevice `yaml:"extraDevices,omitempty"`
|
||||||
|
Wipe bool `yaml:"wipe"`
|
||||||
|
Force bool `yaml:"force"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BootDevice represents the install options specific to the boot partition.
|
||||||
|
type BootDevice struct {
|
||||||
|
InstallDevice `yaml:",inline"`
|
||||||
|
|
||||||
|
Kernel string `yaml:"kernel"`
|
||||||
|
Initramfs string `yaml:"initramfs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RootDevice represents the install options specific to the root partition.
|
||||||
|
type RootDevice struct {
|
||||||
|
InstallDevice `yaml:",inline"`
|
||||||
|
|
||||||
|
Rootfs string `yaml:"rootfs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// InstallDevice represents the specific directions for each partition.
|
||||||
|
type InstallDevice struct {
|
||||||
|
Device string `yaml:"device,omitempty"`
|
||||||
|
Size uint `yaml:"size,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtraDevice represents the options available for partitioning, formatting,
|
||||||
|
// and mounting extra disks.
|
||||||
|
type ExtraDevice struct {
|
||||||
|
Device string `yaml:"device,omitempty"`
|
||||||
|
Partitions []*ExtraDevicePartition `yaml:"partitions,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtraDevicePartition represents the options for a device partition.
|
||||||
|
type ExtraDevicePartition struct {
|
||||||
|
Size uint `yaml:"size,omitempty"`
|
||||||
|
MountPoint string `yaml:"mountpoint,omitempty"`
|
||||||
|
}
|
106
pkg/userdata/kubeadm.go
Normal file
106
pkg/userdata/kubeadm.go
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
/* 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 userdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
|
||||||
|
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
|
||||||
|
kubeadmscheme "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/scheme"
|
||||||
|
kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util"
|
||||||
|
configutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Kubeadm describes the set of configuration options available for kubeadm.
|
||||||
|
type Kubeadm struct {
|
||||||
|
CommonServiceOptions `yaml:",inline"`
|
||||||
|
|
||||||
|
// ConfigurationStr is converted to Configuration and back in Marshal/UnmarshalYAML
|
||||||
|
Configuration runtime.Object `yaml:"-"`
|
||||||
|
ConfigurationStr string `yaml:"configuration"`
|
||||||
|
|
||||||
|
ExtraArgs []string `yaml:"extraArgs,omitempty"`
|
||||||
|
CertificateKey string `yaml:"certificateKey,omitempty"`
|
||||||
|
IgnorePreflightErrors []string `yaml:"ignorePreflightErrors,omitempty"`
|
||||||
|
bootstrap bool
|
||||||
|
controlPlane bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalYAML implements the yaml.Marshaler interface.
|
||||||
|
func (kdm *Kubeadm) MarshalYAML() (interface{}, error) {
|
||||||
|
b, err := configutil.MarshalKubeadmConfigObject(kdm.Configuration)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
gvks, err := kubeadmutil.GroupVersionKindsFromBytes(b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if kubeadmutil.GroupVersionKindsHasInitConfiguration(gvks...) {
|
||||||
|
kdm.bootstrap = true
|
||||||
|
}
|
||||||
|
if kubeadmutil.GroupVersionKindsHasJoinConfiguration(gvks...) {
|
||||||
|
kdm.bootstrap = false
|
||||||
|
}
|
||||||
|
|
||||||
|
kdm.ConfigurationStr = string(b)
|
||||||
|
|
||||||
|
type KubeadmAlias Kubeadm
|
||||||
|
|
||||||
|
return (*KubeadmAlias)(kdm), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalYAML implements the yaml.Unmarshaler interface.
|
||||||
|
func (kdm *Kubeadm) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
|
type KubeadmAlias Kubeadm
|
||||||
|
|
||||||
|
if err := unmarshal((*KubeadmAlias)(kdm)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
b := []byte(kdm.ConfigurationStr)
|
||||||
|
|
||||||
|
gvks, err := kubeadmutil.GroupVersionKindsFromBytes(b)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if kubeadmutil.GroupVersionKindsHasInitConfiguration(gvks...) {
|
||||||
|
// Since the ClusterConfiguration is embedded in the InitConfiguration
|
||||||
|
// struct, it is required to (un)marshal it a special way. The kubeadm
|
||||||
|
// API exposes one function (MarshalKubeadmConfigObject) to handle the
|
||||||
|
// marshaling, but does not yet have that convenience for
|
||||||
|
// unmarshaling.
|
||||||
|
cfg, err := configutil.BytesToInitConfiguration(b)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
kdm.Configuration = cfg
|
||||||
|
kdm.bootstrap = true
|
||||||
|
}
|
||||||
|
if kubeadmutil.GroupVersionKindsHasJoinConfiguration(gvks...) {
|
||||||
|
cfg, err := kubeadmutil.UnmarshalFromYamlForCodecs(b, kubeadmapi.SchemeGroupVersion, kubeadmscheme.Codecs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
kdm.Configuration = cfg
|
||||||
|
kdm.bootstrap = false
|
||||||
|
joinConfiguration, ok := cfg.(*kubeadm.JoinConfiguration)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("expected JoinConfiguration")
|
||||||
|
}
|
||||||
|
if joinConfiguration.ControlPlane == nil {
|
||||||
|
kdm.controlPlane = false
|
||||||
|
} else {
|
||||||
|
kdm.controlPlane = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
85
pkg/userdata/kubernetes_security.go
Normal file
85
pkg/userdata/kubernetes_security.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
/* 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 userdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/pem"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
"github.com/talos-systems/talos/pkg/crypto/x509"
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// KubernetesSecurity represents the set of security options specific to
|
||||||
|
// Kubernetes.
|
||||||
|
type KubernetesSecurity struct {
|
||||||
|
CA *x509.PEMEncodedCertificateAndKey `yaml:"ca"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// KubernetesSecurityCheck defines the function type for checks
|
||||||
|
type KubernetesSecurityCheck func(*KubernetesSecurity) error
|
||||||
|
|
||||||
|
// Validate triggers the specified validation checks to run
|
||||||
|
func (k *KubernetesSecurity) Validate(checks ...KubernetesSecurityCheck) error {
|
||||||
|
var result *multierror.Error
|
||||||
|
|
||||||
|
for _, check := range checks {
|
||||||
|
result = multierror.Append(result, check(k))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.ErrorOrNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckKubernetesCA verfies the KubernetesSecurity settings are valid
|
||||||
|
// nolint: dupl
|
||||||
|
func CheckKubernetesCA() KubernetesSecurityCheck {
|
||||||
|
return func(k *KubernetesSecurity) error {
|
||||||
|
var result *multierror.Error
|
||||||
|
|
||||||
|
// Verify the required sections are present
|
||||||
|
if k.CA == nil {
|
||||||
|
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.kubernetes.ca", "", ErrRequiredSection))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bail early since we're already missing the required sections
|
||||||
|
if result.ErrorOrNil() != nil {
|
||||||
|
return result.ErrorOrNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
if k.CA.Crt == nil {
|
||||||
|
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.kubernetes.ca.crt", "", ErrRequiredSection))
|
||||||
|
}
|
||||||
|
|
||||||
|
if k.CA.Key == nil {
|
||||||
|
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.kubernetes.ca.key", "", ErrRequiredSection))
|
||||||
|
}
|
||||||
|
|
||||||
|
// test if k.CA fields are present ( x509 package handles the b64 decode
|
||||||
|
// during yaml unmarshal, so we have the bytes if it was successful )
|
||||||
|
var block *pem.Block
|
||||||
|
block, _ = pem.Decode(k.CA.Crt)
|
||||||
|
// nolint: gocritic
|
||||||
|
if block == nil {
|
||||||
|
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.kubernetes.ca.crt", k.CA.Crt, ErrInvalidCert))
|
||||||
|
} else {
|
||||||
|
if block.Type != "CERTIFICATE" {
|
||||||
|
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.kubernetes.ca.crt", k.CA.Crt, ErrInvalidCertType))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
block, _ = pem.Decode(k.CA.Key)
|
||||||
|
// nolint: gocritic
|
||||||
|
if block == nil {
|
||||||
|
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.kubernetes.ca.key", k.CA.Key, ErrInvalidCert))
|
||||||
|
} else {
|
||||||
|
if !strings.HasSuffix(block.Type, "PRIVATE KEY") {
|
||||||
|
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.kubernetes.ca.key", k.CA.Key, ErrInvalidCertType))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.ErrorOrNil()
|
||||||
|
}
|
||||||
|
}
|
43
pkg/userdata/kubernetes_security_test.go
Normal file
43
pkg/userdata/kubernetes_security_test.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
/* 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/. */
|
||||||
|
|
||||||
|
// nolint: dupl
|
||||||
|
package userdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
"github.com/talos-systems/talos/pkg/crypto/x509"
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (suite *validateSuite) TestValidateKubernetesSecurity() {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Test for missing required sections
|
||||||
|
kube := &KubernetesSecurity{}
|
||||||
|
err = kube.Validate(CheckKubernetesCA())
|
||||||
|
suite.Require().Error(err)
|
||||||
|
// Embedding the check in suite.Assert().Equal(true, xerrors.Is had issues )
|
||||||
|
if !xerrors.Is(err.(*multierror.Error).Errors[0], ErrRequiredSection) {
|
||||||
|
suite.T().Errorf("%+v", err)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
kube.CA = &x509.PEMEncodedCertificateAndKey{}
|
||||||
|
err = kube.Validate(CheckKubernetesCA())
|
||||||
|
suite.Require().Error(err)
|
||||||
|
suite.Assert().Equal(4, len(err.(*multierror.Error).Errors))
|
||||||
|
|
||||||
|
// Test for invalid certs
|
||||||
|
kube.CA.Crt = []byte("-----BEGIN Rubbish-----\n-----END Rubbish-----")
|
||||||
|
kube.CA.Key = []byte("-----BEGIN EC Fluffy KEY-----\n-----END EC Fluffy KEY-----")
|
||||||
|
err = kube.Validate(CheckKubernetesCA())
|
||||||
|
suite.Require().Error(err)
|
||||||
|
|
||||||
|
// Successful test
|
||||||
|
kube.CA.Crt = []byte("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----")
|
||||||
|
kube.CA.Key = []byte("-----BEGIN EC PRIVATE KEY-----\n-----END EC PRIVATE KEY-----")
|
||||||
|
err = kube.Validate(CheckKubernetesCA())
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
}
|
113
pkg/userdata/networking.go
Normal file
113
pkg/userdata/networking.go
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
/* 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 userdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Device represents a network interface
|
||||||
|
type Device struct {
|
||||||
|
Interface string `yaml:"interface"`
|
||||||
|
CIDR string `yaml:"cidr"`
|
||||||
|
DHCP bool `yaml:"dhcp"`
|
||||||
|
Routes []Route `yaml:"routes"`
|
||||||
|
Bond *Bond `yaml:"bond"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkDeviceCheck defines the function type for checks
|
||||||
|
type NetworkDeviceCheck func(*Device) error
|
||||||
|
|
||||||
|
// Validate triggers the specified validation checks to run
|
||||||
|
func (d *Device) Validate(checks ...NetworkDeviceCheck) error {
|
||||||
|
var result *multierror.Error
|
||||||
|
|
||||||
|
for _, check := range checks {
|
||||||
|
result = multierror.Append(result, check(d))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.ErrorOrNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckDeviceInterface ensures that the interface has been specified
|
||||||
|
func CheckDeviceInterface() NetworkDeviceCheck {
|
||||||
|
return func(d *Device) error {
|
||||||
|
var result *multierror.Error
|
||||||
|
|
||||||
|
if d.Interface == "" {
|
||||||
|
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "networking.os.device.interface", "", ErrRequiredSection))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.ErrorOrNil()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckDeviceAddressing ensures that an appropriate addressing method
|
||||||
|
// has been specified
|
||||||
|
func CheckDeviceAddressing() NetworkDeviceCheck {
|
||||||
|
return func(d *Device) error {
|
||||||
|
var result *multierror.Error
|
||||||
|
|
||||||
|
// Test for both dhcp and cidr specified
|
||||||
|
if d.DHCP && d.CIDR != "" {
|
||||||
|
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "networking.os.device", "", ErrBadAddressing))
|
||||||
|
}
|
||||||
|
|
||||||
|
// test for neither dhcp nor cidr specified
|
||||||
|
if !d.DHCP && d.CIDR == "" {
|
||||||
|
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "networking.os.device", "", ErrBadAddressing))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure cidr is a valid address
|
||||||
|
if d.CIDR != "" {
|
||||||
|
if _, _, err := net.ParseCIDR(d.CIDR); err != nil {
|
||||||
|
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "networking.os.device.CIDR", "", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.ErrorOrNil()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckDeviceRoutes ensures that the specified routes are valid
|
||||||
|
func CheckDeviceRoutes() NetworkDeviceCheck {
|
||||||
|
return func(d *Device) error {
|
||||||
|
var result *multierror.Error
|
||||||
|
|
||||||
|
if len(d.Routes) == 0 {
|
||||||
|
return result.ErrorOrNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx, route := range d.Routes {
|
||||||
|
if _, _, err := net.ParseCIDR(route.Network); err != nil {
|
||||||
|
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "networking.os.device.route["+strconv.Itoa(idx)+"].Network", route.Network, ErrInvalidAddress))
|
||||||
|
}
|
||||||
|
|
||||||
|
if ip := net.ParseIP(route.Gateway); ip == nil {
|
||||||
|
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "networking.os.device.route["+strconv.Itoa(idx)+"].Gateway", route.Gateway, ErrInvalidAddress))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.ErrorOrNil()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bond contains the various options for configuring a
|
||||||
|
// bonded interface
|
||||||
|
type Bond struct {
|
||||||
|
Mode string `yaml:"mode"`
|
||||||
|
HashPolicy string `yaml:"hashpolicy"`
|
||||||
|
LACPRate string `yaml:"lacprate"`
|
||||||
|
Interfaces []string `yaml:"interfaces"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route represents a network route
|
||||||
|
type Route struct {
|
||||||
|
Network string `yaml:"network"`
|
||||||
|
Gateway string `yaml:"gateway"`
|
||||||
|
}
|
60
pkg/userdata/networking_test.go
Normal file
60
pkg/userdata/networking_test.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
/* 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 userdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (suite *validateSuite) TestValidateDevice() {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Test for missing required sections
|
||||||
|
dev := &Device{}
|
||||||
|
err = dev.Validate(CheckDeviceInterface())
|
||||||
|
suite.Require().Error(err)
|
||||||
|
// Embedding the check in suite.Assert().Equal(true, xerrors.Is had issues )
|
||||||
|
if !xerrors.Is(err.(*multierror.Error).Errors[0], ErrRequiredSection) {
|
||||||
|
suite.T().Errorf("%+v", err)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
dev.Interface = "eth0"
|
||||||
|
err = dev.Validate(CheckDeviceInterface())
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
err = dev.Validate(CheckDeviceAddressing())
|
||||||
|
suite.Require().Error(err)
|
||||||
|
|
||||||
|
// Ensure only a single addressing scheme is specified
|
||||||
|
dev.DHCP = true
|
||||||
|
dev.CIDR = "1.0.0.1/32"
|
||||||
|
err = dev.Validate(CheckDeviceAddressing())
|
||||||
|
suite.Require().Error(err)
|
||||||
|
|
||||||
|
dev.DHCP = false
|
||||||
|
err = dev.Validate(CheckDeviceAddressing())
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
dev.Routes = []Route{}
|
||||||
|
err = dev.Validate(CheckDeviceRoutes())
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
// nolint: gofmt
|
||||||
|
dev.Routes = []Route{Route{Gateway: "yolo"}}
|
||||||
|
err = dev.Validate(CheckDeviceRoutes())
|
||||||
|
suite.Require().Error(err)
|
||||||
|
|
||||||
|
// nolint: gofmt
|
||||||
|
dev.Routes = []Route{Route{Gateway: "yolo", Network: "totes"}}
|
||||||
|
err = dev.Validate(CheckDeviceRoutes())
|
||||||
|
suite.Require().Error(err)
|
||||||
|
|
||||||
|
// nolint: gofmt
|
||||||
|
dev.Routes = []Route{Route{Gateway: "192.168.1.1", Network: "192.168.1.0/24"}}
|
||||||
|
err = dev.Validate(CheckDeviceRoutes())
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
}
|
85
pkg/userdata/os_security.go
Normal file
85
pkg/userdata/os_security.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
/* 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 userdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/pem"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
"github.com/talos-systems/talos/pkg/crypto/x509"
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OSSecurity represents the set of security options specific to the OS.
|
||||||
|
type OSSecurity struct {
|
||||||
|
CA *x509.PEMEncodedCertificateAndKey `yaml:"ca"`
|
||||||
|
Identity *x509.PEMEncodedCertificateAndKey `yaml:"identity"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// OSSecurityCheck defines the function type for checks
|
||||||
|
type OSSecurityCheck func(*OSSecurity) error
|
||||||
|
|
||||||
|
// Validate triggers the specified validation checks to run
|
||||||
|
func (o *OSSecurity) Validate(checks ...OSSecurityCheck) error {
|
||||||
|
var result *multierror.Error
|
||||||
|
|
||||||
|
for _, check := range checks {
|
||||||
|
result = multierror.Append(result, check(o))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.ErrorOrNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckOSCA verfies the OSSecurity settings are valid
|
||||||
|
// nolint: dupl
|
||||||
|
func CheckOSCA() OSSecurityCheck {
|
||||||
|
return func(o *OSSecurity) error {
|
||||||
|
var result *multierror.Error
|
||||||
|
|
||||||
|
// Verify the required sections are present
|
||||||
|
if o.CA == nil {
|
||||||
|
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.os.ca", "", ErrRequiredSection))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bail early since we're already missing the required sections
|
||||||
|
if result.ErrorOrNil() != nil {
|
||||||
|
return result.ErrorOrNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.CA.Crt == nil {
|
||||||
|
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.os.ca.crt", "", ErrRequiredSection))
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.CA.Key == nil {
|
||||||
|
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.os.ca.key", "", ErrRequiredSection))
|
||||||
|
}
|
||||||
|
|
||||||
|
// test if o.CA fields are present ( x509 package handles the b64 decode
|
||||||
|
// during yaml unmarshal, so we have the bytes if it was successful )
|
||||||
|
var block *pem.Block
|
||||||
|
block, _ = pem.Decode(o.CA.Crt)
|
||||||
|
// nolint: gocritic
|
||||||
|
if block == nil {
|
||||||
|
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.os.ca.crt", o.CA.Crt, ErrInvalidCert))
|
||||||
|
} else {
|
||||||
|
if block.Type != "CERTIFICATE" {
|
||||||
|
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.os.ca.crt", o.CA.Crt, ErrInvalidCertType))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
block, _ = pem.Decode(o.CA.Key)
|
||||||
|
// nolint: gocritic
|
||||||
|
if block == nil {
|
||||||
|
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.os.ca.key", o.CA.Key, ErrInvalidCert))
|
||||||
|
} else {
|
||||||
|
if !strings.HasSuffix(block.Type, "PRIVATE KEY") {
|
||||||
|
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.os.ca.key", o.CA.Key, ErrInvalidCertType))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.ErrorOrNil()
|
||||||
|
}
|
||||||
|
}
|
43
pkg/userdata/os_security_test.go
Normal file
43
pkg/userdata/os_security_test.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
/* 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/. */
|
||||||
|
|
||||||
|
// nolint: dupl
|
||||||
|
package userdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
"github.com/talos-systems/talos/pkg/crypto/x509"
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (suite *validateSuite) TestValidateOSSecurity() {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Test for missing required sections
|
||||||
|
os := &OSSecurity{}
|
||||||
|
err = os.Validate(CheckOSCA())
|
||||||
|
suite.Require().Error(err)
|
||||||
|
// Embedding the check in suite.Assert().Equal(true, xerrors.Is had issues )
|
||||||
|
if !xerrors.Is(err.(*multierror.Error).Errors[0], ErrRequiredSection) {
|
||||||
|
suite.T().Errorf("%+v", err)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
os.CA = &x509.PEMEncodedCertificateAndKey{}
|
||||||
|
err = os.Validate(CheckOSCA())
|
||||||
|
suite.Require().Error(err)
|
||||||
|
suite.Assert().Equal(4, len(err.(*multierror.Error).Errors))
|
||||||
|
|
||||||
|
// Test for invalid certs
|
||||||
|
os.CA.Crt = []byte("-----BEGIN Rubbish-----\n-----END Rubbish-----")
|
||||||
|
os.CA.Key = []byte("-----BEGIN EC Fluffy KEY-----\n-----END EC Fluffy KEY-----")
|
||||||
|
err = os.Validate(CheckOSCA())
|
||||||
|
suite.Require().Error(err)
|
||||||
|
|
||||||
|
// Successful test
|
||||||
|
os.CA.Crt = []byte("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----")
|
||||||
|
os.CA.Key = []byte("-----BEGIN EC PRIVATE KEY-----\n-----END EC PRIVATE KEY-----")
|
||||||
|
err = os.Validate(CheckOSCA())
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
}
|
197
pkg/userdata/services.go
Normal file
197
pkg/userdata/services.go
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
/* 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 userdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Env represents a set of environment variables.
|
||||||
|
type Env = map[string]string
|
||||||
|
|
||||||
|
// Services represents the set of services available to configure.
|
||||||
|
type Services struct {
|
||||||
|
Init *Init `yaml:"init"`
|
||||||
|
Kubelet *Kubelet `yaml:"kubelet"`
|
||||||
|
Kubeadm *Kubeadm `yaml:"kubeadm"`
|
||||||
|
Trustd *Trustd `yaml:"trustd"`
|
||||||
|
Proxyd *Proxyd `yaml:"proxyd"`
|
||||||
|
OSD *OSD `yaml:"osd"`
|
||||||
|
CRT *CRT `yaml:"crt"`
|
||||||
|
NTPd *NTPd `yaml:"ntp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate triggers the specified validation checks to run
|
||||||
|
func (s *Services) Validate(checks ...ServiceCheck) error {
|
||||||
|
var result *multierror.Error
|
||||||
|
|
||||||
|
for _, check := range checks {
|
||||||
|
result = multierror.Append(result, check(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.ErrorOrNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceCheck defines the function type for checks
|
||||||
|
type ServiceCheck func(*Services) error
|
||||||
|
|
||||||
|
// CheckServices ensures the minimum necessary services config has been provided
|
||||||
|
func CheckServices() ServiceCheck {
|
||||||
|
return func(s *Services) error {
|
||||||
|
var result *multierror.Error
|
||||||
|
|
||||||
|
if s.Kubeadm == nil {
|
||||||
|
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "services.kubeadm", "", ErrRequiredSection))
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Trustd == nil {
|
||||||
|
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "services.trustd", "", ErrRequiredSection))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.ErrorOrNil()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OSD describes the configuration of the osd service.
|
||||||
|
type OSD struct {
|
||||||
|
CommonServiceOptions `yaml:",inline"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proxyd describes the configuration of the proxyd service.
|
||||||
|
type Proxyd struct {
|
||||||
|
CommonServiceOptions `yaml:",inline"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CRT describes the configuration of the container runtime service.
|
||||||
|
type CRT struct {
|
||||||
|
CommonServiceOptions `yaml:",inline"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommonServiceOptions represents the set of options common to all services.
|
||||||
|
type CommonServiceOptions struct {
|
||||||
|
Env Env `yaml:"env,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NTPd describes the configuration of the ntp service.
|
||||||
|
type NTPd struct {
|
||||||
|
CommonServiceOptions `yaml:",inline"`
|
||||||
|
|
||||||
|
Server string `yaml:"server,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kubelet describes the configuration of the kubelet service.
|
||||||
|
type Kubelet struct {
|
||||||
|
CommonServiceOptions `yaml:",inline"`
|
||||||
|
ExtraMounts []specs.Mount `yaml:"extraMounts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trustd describes the configuration of the Root of Trust (RoT) service. The
|
||||||
|
// username and password are used by master nodes, and worker nodes. The master
|
||||||
|
// nodes use them to authenticate clients, while the workers use them to
|
||||||
|
// authenticate as a client. The endpoints should only be specified in the
|
||||||
|
// worker user data, and should include all master nodes participating as a RoT.
|
||||||
|
type Trustd struct {
|
||||||
|
CommonServiceOptions `yaml:",inline"`
|
||||||
|
|
||||||
|
Token string `yaml:"token"`
|
||||||
|
Username string `yaml:"username"`
|
||||||
|
Password string `yaml:"password"`
|
||||||
|
Endpoints []string `yaml:"endpoints,omitempty"`
|
||||||
|
CertSANs []string `yaml:"certSANs,omitempty"`
|
||||||
|
BootstrapNode string `yaml:"bootstrapNode,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrustdCheck defines the function type for checks
|
||||||
|
type TrustdCheck func(*Trustd) error
|
||||||
|
|
||||||
|
// Validate triggers the specified validation checks to run
|
||||||
|
func (t *Trustd) Validate(checks ...TrustdCheck) error {
|
||||||
|
var result *multierror.Error
|
||||||
|
|
||||||
|
for _, check := range checks {
|
||||||
|
result = multierror.Append(result, check(t))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.ErrorOrNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckTrustdAuth ensures that a trustd token has been specified
|
||||||
|
func CheckTrustdAuth() TrustdCheck {
|
||||||
|
return func(t *Trustd) error {
|
||||||
|
var result *multierror.Error
|
||||||
|
|
||||||
|
if t.Token == "" && (t.Username == "" || t.Password == "") {
|
||||||
|
if t.Token == "" {
|
||||||
|
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "services.trustd.token", t.Token, ErrRequiredSection))
|
||||||
|
} else {
|
||||||
|
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "services.trustd.username:password", t.Username+":"+t.Password, ErrRequiredSection))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.ErrorOrNil()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckTrustdEndpoints ensures that the trustd endpoints have been specified
|
||||||
|
func CheckTrustdEndpoints() TrustdCheck {
|
||||||
|
return func(t *Trustd) error {
|
||||||
|
var result *multierror.Error
|
||||||
|
|
||||||
|
if len(t.Endpoints) == 0 {
|
||||||
|
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "services.trustd.endpoints", t.Endpoints, ErrRequiredSection))
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx, endpoint := range t.Endpoints {
|
||||||
|
if ip := net.ParseIP(endpoint); ip == nil {
|
||||||
|
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "services.trustd.endpoints["+strconv.Itoa(idx)+"]", endpoint, ErrInvalidAddress))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.ErrorOrNil()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init describes the configuration of the init service.
|
||||||
|
type Init struct {
|
||||||
|
CNI string `yaml:"cni,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitCheck defines the function type for checks
|
||||||
|
type InitCheck func(*Init) error
|
||||||
|
|
||||||
|
// Validate triggers the specified validation checks to run
|
||||||
|
func (i *Init) Validate(checks ...InitCheck) error {
|
||||||
|
var result *multierror.Error
|
||||||
|
|
||||||
|
for _, check := range checks {
|
||||||
|
result = multierror.Append(result, check(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.ErrorOrNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckInitCNI ensures that a valid cni driver has been specified
|
||||||
|
func CheckInitCNI() InitCheck {
|
||||||
|
return func(i *Init) error {
|
||||||
|
var result *multierror.Error
|
||||||
|
|
||||||
|
switch i.CNI {
|
||||||
|
case "calico":
|
||||||
|
return result.ErrorOrNil()
|
||||||
|
case "flannel":
|
||||||
|
return result.ErrorOrNil()
|
||||||
|
default:
|
||||||
|
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "services.init.cni", i.CNI, ErrUnsupportedCNI))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.ErrorOrNil()
|
||||||
|
}
|
||||||
|
}
|
67
pkg/userdata/services_test.go
Normal file
67
pkg/userdata/services_test.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
/* 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 userdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (suite *validateSuite) TestValidateServices() {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Test for missing required sections
|
||||||
|
svc := &Services{}
|
||||||
|
err = svc.Validate(CheckServices())
|
||||||
|
suite.Require().Error(err)
|
||||||
|
// services.kubeadm
|
||||||
|
if !xerrors.Is(err.(*multierror.Error).Errors[0], ErrRequiredSection) {
|
||||||
|
suite.T().Errorf("%+v", err)
|
||||||
|
}
|
||||||
|
// services.trustd
|
||||||
|
if !xerrors.Is(err.(*multierror.Error).Errors[1], ErrRequiredSection) {
|
||||||
|
suite.T().Errorf("%+v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *validateSuite) TestValidateTrustd() {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
svc := &Services{}
|
||||||
|
svc.Trustd = &Trustd{}
|
||||||
|
err = svc.Trustd.Validate(CheckTrustdAuth(), CheckTrustdEndpoints())
|
||||||
|
suite.Require().Error(err)
|
||||||
|
suite.Assert().Equal(2, len(err.(*multierror.Error).Errors))
|
||||||
|
|
||||||
|
svc.Trustd.Endpoints = []string{"1.2.3.4"}
|
||||||
|
err = svc.Trustd.Validate(CheckTrustdEndpoints())
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
svc.Trustd.Token = "yolo"
|
||||||
|
err = svc.Trustd.Validate(CheckTrustdAuth())
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
svc.Trustd.Token = ""
|
||||||
|
svc.Trustd.Username = "bob"
|
||||||
|
svc.Trustd.Password = "burger"
|
||||||
|
err = svc.Trustd.Validate(CheckTrustdAuth())
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *validateSuite) TestValidateInit() {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
svc := &Services{}
|
||||||
|
svc.Init = &Init{}
|
||||||
|
err = svc.Init.Validate(CheckInitCNI())
|
||||||
|
suite.Require().Error(err)
|
||||||
|
if !xerrors.Is(err.(*multierror.Error).Errors[0], ErrUnsupportedCNI) {
|
||||||
|
suite.T().Errorf("%+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
svc.Init.CNI = "calico"
|
||||||
|
err = svc.Init.Validate(CheckInitCNI())
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
}
|
@ -7,7 +7,6 @@ package userdata
|
|||||||
import (
|
import (
|
||||||
stdlibx509 "crypto/x509"
|
stdlibx509 "crypto/x509"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
@ -18,26 +17,16 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
specs "github.com/opencontainers/runtime-spec/specs-go"
|
"github.com/hashicorp/go-multierror"
|
||||||
"github.com/talos-systems/talos/internal/pkg/net"
|
"github.com/talos-systems/talos/internal/pkg/net"
|
||||||
"github.com/talos-systems/talos/pkg/crypto/x509"
|
"github.com/talos-systems/talos/pkg/crypto/x509"
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
|
||||||
"k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
|
|
||||||
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
|
|
||||||
kubeadmscheme "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/scheme"
|
|
||||||
kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util"
|
|
||||||
configutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config"
|
|
||||||
|
|
||||||
yaml "gopkg.in/yaml.v2"
|
yaml "gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Env represents a set of environment variables.
|
|
||||||
type Env = map[string]string
|
|
||||||
|
|
||||||
// UserData represents the user data.
|
// UserData represents the user data.
|
||||||
type UserData struct {
|
type UserData struct {
|
||||||
Version string `yaml:"version"`
|
Version Version `yaml:"version"`
|
||||||
Security *Security `yaml:"security"`
|
Security *Security `yaml:"security"`
|
||||||
Networking *Networking `yaml:"networking"`
|
Networking *Networking `yaml:"networking"`
|
||||||
Services *Services `yaml:"services"`
|
Services *Services `yaml:"services"`
|
||||||
@ -47,24 +36,54 @@ type UserData struct {
|
|||||||
Install *Install `yaml:"install,omitempty"`
|
Install *Install `yaml:"install,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate ensures the required fields are present in the userdata
|
||||||
|
// nolint: gocyclo
|
||||||
|
func (data *UserData) Validate() error {
|
||||||
|
var result *multierror.Error
|
||||||
|
|
||||||
|
var nodeType string
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case data.IsBootstrap():
|
||||||
|
nodeType = "init"
|
||||||
|
case data.IsMaster():
|
||||||
|
nodeType = "master"
|
||||||
|
case data.IsWorker():
|
||||||
|
nodeType = "worker"
|
||||||
|
default:
|
||||||
|
// TODO make an error
|
||||||
|
return result.ErrorOrNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
// All nodeType checks
|
||||||
|
result = multierror.Append(result, data.Services.Validate(CheckServices()))
|
||||||
|
result = multierror.Append(result, data.Services.Trustd.Validate(CheckTrustdAuth(), CheckTrustdEndpoints()))
|
||||||
|
result = multierror.Append(result, data.Services.Init.Validate(CheckInitCNI()))
|
||||||
|
|
||||||
|
// Surely there's a better way to do this
|
||||||
|
if data.Networking != nil && data.Networking.OS != nil {
|
||||||
|
for _, dev := range data.Networking.OS.Devices {
|
||||||
|
result = multierror.Append(result, dev.Validate(CheckDeviceInterface(), CheckDeviceAddressing(), CheckDeviceRoutes()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch nodeType {
|
||||||
|
case "init":
|
||||||
|
result = multierror.Append(result, data.Security.OS.Validate(CheckOSCA()))
|
||||||
|
result = multierror.Append(result, data.Security.Kubernetes.Validate(CheckKubernetesCA()))
|
||||||
|
case "master":
|
||||||
|
case "worker":
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.ErrorOrNil()
|
||||||
|
}
|
||||||
|
|
||||||
// Security represents the set of options available to configure security.
|
// Security represents the set of options available to configure security.
|
||||||
type Security struct {
|
type Security struct {
|
||||||
OS *OSSecurity `yaml:"os"`
|
OS *OSSecurity `yaml:"os"`
|
||||||
Kubernetes *KubernetesSecurity `yaml:"kubernetes"`
|
Kubernetes *KubernetesSecurity `yaml:"kubernetes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// OSSecurity represents the set of security options specific to the OS.
|
|
||||||
type OSSecurity struct {
|
|
||||||
CA *x509.PEMEncodedCertificateAndKey `yaml:"ca"`
|
|
||||||
Identity *x509.PEMEncodedCertificateAndKey `yaml:"identity"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// KubernetesSecurity represents the set of security options specific to
|
|
||||||
// Kubernetes.
|
|
||||||
type KubernetesSecurity struct {
|
|
||||||
CA *x509.PEMEncodedCertificateAndKey `yaml:"ca"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Networking represents the set of options available to configure networking.
|
// Networking represents the set of options available to configure networking.
|
||||||
type Networking struct {
|
type Networking struct {
|
||||||
Kubernetes struct{} `yaml:"kubernetes"`
|
Kubernetes struct{} `yaml:"kubernetes"`
|
||||||
@ -76,42 +95,6 @@ type OSNet struct {
|
|||||||
Devices []Device `yaml:"devices"`
|
Devices []Device `yaml:"devices"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Device represents a network interface
|
|
||||||
type Device struct {
|
|
||||||
Interface string `yaml:"interface"`
|
|
||||||
CIDR string `yaml:"cidr"`
|
|
||||||
DHCP bool `yaml:"dhcp"`
|
|
||||||
Routes []Route `yaml:"routes"`
|
|
||||||
Bond *Bond `yaml:"bond"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bond contains the various options for configuring a
|
|
||||||
// bonded interface
|
|
||||||
type Bond struct {
|
|
||||||
Mode string `yaml:"mode"`
|
|
||||||
HashPolicy string `yaml:"hashpolicy"`
|
|
||||||
LACPRate string `yaml:"lacprate"`
|
|
||||||
Interfaces []string `yaml:"interfaces"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Route represents a network route
|
|
||||||
type Route struct {
|
|
||||||
Network string `yaml:"network"`
|
|
||||||
Gateway string `yaml:"gateway"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Services represents the set of services available to configure.
|
|
||||||
type Services struct {
|
|
||||||
Init *Init `yaml:"init"`
|
|
||||||
Kubelet *Kubelet `yaml:"kubelet"`
|
|
||||||
Kubeadm *Kubeadm `yaml:"kubeadm"`
|
|
||||||
Trustd *Trustd `yaml:"trustd"`
|
|
||||||
Proxyd *Proxyd `yaml:"proxyd"`
|
|
||||||
OSD *OSD `yaml:"osd"`
|
|
||||||
CRT *CRT `yaml:"crt"`
|
|
||||||
NTPd *NTPd `yaml:"ntp"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// File represents a file to write to disk.
|
// File represents a file to write to disk.
|
||||||
type File struct {
|
type File struct {
|
||||||
Contents string `yaml:"contents"`
|
Contents string `yaml:"contents"`
|
||||||
@ -119,197 +102,10 @@ type File struct {
|
|||||||
Path string `yaml:"path"`
|
Path string `yaml:"path"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Install represents the installation options for preparing a node.
|
|
||||||
type Install struct {
|
|
||||||
Boot *BootDevice `yaml:"boot,omitempty"`
|
|
||||||
Root *RootDevice `yaml:"root"`
|
|
||||||
Data *InstallDevice `yaml:"data,omitempty"`
|
|
||||||
ExtraDevices []*ExtraDevice `yaml:"extraDevices,omitempty"`
|
|
||||||
Wipe bool `yaml:"wipe"`
|
|
||||||
Force bool `yaml:"force"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// BootDevice represents the install options specific to the boot partition.
|
|
||||||
type BootDevice struct {
|
|
||||||
InstallDevice `yaml:",inline"`
|
|
||||||
|
|
||||||
Kernel string `yaml:"kernel"`
|
|
||||||
Initramfs string `yaml:"initramfs"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// RootDevice represents the install options specific to the root partition.
|
|
||||||
type RootDevice struct {
|
|
||||||
InstallDevice `yaml:",inline"`
|
|
||||||
|
|
||||||
Rootfs string `yaml:"rootfs"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// InstallDevice represents the specific directions for each partition.
|
|
||||||
type InstallDevice struct {
|
|
||||||
Device string `yaml:"device,omitempty"`
|
|
||||||
Size uint `yaml:"size,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExtraDevice represents the options available for partitioning, formatting,
|
|
||||||
// and mounting extra disks.
|
|
||||||
type ExtraDevice struct {
|
|
||||||
Device string `yaml:"device,omitempty"`
|
|
||||||
Partitions []*ExtraDevicePartition `yaml:"partitions,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExtraDevicePartition represents the options for a device partition.
|
|
||||||
type ExtraDevicePartition struct {
|
|
||||||
Size uint `yaml:"size,omitempty"`
|
|
||||||
MountPoint string `yaml:"mountpoint,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init describes the configuration of the init service.
|
|
||||||
type Init struct {
|
|
||||||
CNI string `yaml:"cni,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Kubelet describes the configuration of the kubelet service.
|
|
||||||
type Kubelet struct {
|
|
||||||
CommonServiceOptions `yaml:",inline"`
|
|
||||||
ExtraMounts []specs.Mount `yaml:"extraMounts"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Kubeadm describes the set of configuration options available for kubeadm.
|
|
||||||
type Kubeadm struct {
|
|
||||||
CommonServiceOptions `yaml:",inline"`
|
|
||||||
|
|
||||||
// ConfigurationStr is converted to Configuration and back in Marshal/UnmarshalYAML
|
|
||||||
Configuration runtime.Object `yaml:"-"`
|
|
||||||
ConfigurationStr string `yaml:"configuration"`
|
|
||||||
|
|
||||||
ExtraArgs []string `yaml:"extraArgs,omitempty"`
|
|
||||||
CertificateKey string `yaml:"certificateKey,omitempty"`
|
|
||||||
IgnorePreflightErrors []string `yaml:"ignorePreflightErrors,omitempty"`
|
|
||||||
bootstrap bool
|
|
||||||
controlPlane bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalYAML implements the yaml.Marshaler interface.
|
|
||||||
func (kdm *Kubeadm) MarshalYAML() (interface{}, error) {
|
|
||||||
b, err := configutil.MarshalKubeadmConfigObject(kdm.Configuration)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
gvks, err := kubeadmutil.GroupVersionKindsFromBytes(b)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if kubeadmutil.GroupVersionKindsHasInitConfiguration(gvks...) {
|
|
||||||
kdm.bootstrap = true
|
|
||||||
}
|
|
||||||
if kubeadmutil.GroupVersionKindsHasJoinConfiguration(gvks...) {
|
|
||||||
kdm.bootstrap = false
|
|
||||||
}
|
|
||||||
|
|
||||||
kdm.ConfigurationStr = string(b)
|
|
||||||
|
|
||||||
type KubeadmAlias Kubeadm
|
|
||||||
|
|
||||||
return (*KubeadmAlias)(kdm), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalYAML implements the yaml.Unmarshaler interface.
|
|
||||||
func (kdm *Kubeadm) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|
||||||
type KubeadmAlias Kubeadm
|
|
||||||
|
|
||||||
if err := unmarshal((*KubeadmAlias)(kdm)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
b := []byte(kdm.ConfigurationStr)
|
|
||||||
|
|
||||||
gvks, err := kubeadmutil.GroupVersionKindsFromBytes(b)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if kubeadmutil.GroupVersionKindsHasInitConfiguration(gvks...) {
|
|
||||||
// Since the ClusterConfiguration is embedded in the InitConfiguration
|
|
||||||
// struct, it is required to (un)marshal it a special way. The kubeadm
|
|
||||||
// API exposes one function (MarshalKubeadmConfigObject) to handle the
|
|
||||||
// marshaling, but does not yet have that convenience for
|
|
||||||
// unmarshaling.
|
|
||||||
cfg, err := configutil.BytesToInitConfiguration(b)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
kdm.Configuration = cfg
|
|
||||||
kdm.bootstrap = true
|
|
||||||
}
|
|
||||||
if kubeadmutil.GroupVersionKindsHasJoinConfiguration(gvks...) {
|
|
||||||
cfg, err := kubeadmutil.UnmarshalFromYamlForCodecs(b, kubeadmapi.SchemeGroupVersion, kubeadmscheme.Codecs)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
kdm.Configuration = cfg
|
|
||||||
kdm.bootstrap = false
|
|
||||||
joinConfiguration, ok := cfg.(*kubeadm.JoinConfiguration)
|
|
||||||
if !ok {
|
|
||||||
return errors.New("expected JoinConfiguration")
|
|
||||||
}
|
|
||||||
if joinConfiguration.ControlPlane == nil {
|
|
||||||
kdm.controlPlane = false
|
|
||||||
} else {
|
|
||||||
kdm.controlPlane = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trustd describes the configuration of the Root of Trust (RoT) service. The
|
|
||||||
// username and password are used by master nodes, and worker nodes. The master
|
|
||||||
// nodes use them to authenticate clients, while the workers use them to
|
|
||||||
// authenticate as a client. The endpoints should only be specified in the
|
|
||||||
// worker user data, and should include all master nodes participating as a RoT.
|
|
||||||
type Trustd struct {
|
|
||||||
CommonServiceOptions `yaml:",inline"`
|
|
||||||
|
|
||||||
Token string `yaml:"token"`
|
|
||||||
Username string `yaml:"username"`
|
|
||||||
Password string `yaml:"password"`
|
|
||||||
Endpoints []string `yaml:"endpoints,omitempty"`
|
|
||||||
CertSANs []string `yaml:"certSANs,omitempty"`
|
|
||||||
BootstrapNode string `yaml:"bootstrapNode,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// OSD describes the configuration of the osd service.
|
|
||||||
type OSD struct {
|
|
||||||
CommonServiceOptions `yaml:",inline"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Proxyd describes the configuration of the proxyd service.
|
|
||||||
type Proxyd struct {
|
|
||||||
CommonServiceOptions `yaml:",inline"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CRT describes the configuration of the container runtime service.
|
|
||||||
type CRT struct {
|
|
||||||
CommonServiceOptions `yaml:",inline"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CommonServiceOptions represents the set of options common to all services.
|
|
||||||
type CommonServiceOptions struct {
|
|
||||||
Env Env `yaml:"env,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// NTPd describes the configuration of the ntp service.
|
|
||||||
type NTPd struct {
|
|
||||||
CommonServiceOptions `yaml:",inline"`
|
|
||||||
|
|
||||||
Server string `yaml:"server,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteFiles writes the requested files to disk.
|
// WriteFiles writes the requested files to disk.
|
||||||
func (data *UserData) WriteFiles() (err error) {
|
func (data *UserData) WriteFiles() (err error) {
|
||||||
for _, f := range data.Files {
|
for _, f := range data.Files {
|
||||||
|
// TODO isnt there a const for the data mountpoint
|
||||||
p := path.Join("/var", f.Path)
|
p := path.Join("/var", f.Path)
|
||||||
if err = os.MkdirAll(path.Dir(p), os.ModeDir); err != nil {
|
if err = os.MkdirAll(path.Dir(p), os.ModeDir); err != nil {
|
||||||
return
|
return
|
||||||
@ -322,6 +118,30 @@ func (data *UserData) WriteFiles() (err error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsBootstrap indicates if the current kubeadm configuration is a master init
|
||||||
|
// configuration.
|
||||||
|
func (data *UserData) IsBootstrap() bool {
|
||||||
|
return data.Services.Kubeadm.bootstrap
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsControlPlane indicates if the current kubeadm configuration is a worker
|
||||||
|
// acting as a master.
|
||||||
|
func (data *UserData) IsControlPlane() bool {
|
||||||
|
return data.Services.Kubeadm.controlPlane
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsMaster indicates if the current kubeadm configuration is a master
|
||||||
|
// configuration.
|
||||||
|
func (data *UserData) IsMaster() bool {
|
||||||
|
return data.Services.Kubeadm.bootstrap || data.Services.Kubeadm.controlPlane
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsWorker indicates if the current kubeadm configuration is a worker
|
||||||
|
// configuration.
|
||||||
|
func (data *UserData) IsWorker() bool {
|
||||||
|
return !data.IsMaster()
|
||||||
|
}
|
||||||
|
|
||||||
// NewIdentityCSR creates a new CSR for the node's identity certificate.
|
// NewIdentityCSR creates a new CSR for the node's identity certificate.
|
||||||
func (data *UserData) NewIdentityCSR() (csr *x509.CertificateSigningRequest, err error) {
|
func (data *UserData) NewIdentityCSR() (csr *x509.CertificateSigningRequest, err error) {
|
||||||
var key *x509.Key
|
var key *x509.Key
|
||||||
@ -377,7 +197,7 @@ func Download(url string, headers *map[string]string) (data *UserData, err error
|
|||||||
client := &http.Client{}
|
client := &http.Client{}
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return data, err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if headers != nil {
|
if headers != nil {
|
||||||
@ -386,10 +206,11 @@ func Download(url string, headers *map[string]string) (data *UserData, err error
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var resp *http.Response
|
||||||
for attempt := 0; attempt < maxRetries; attempt++ {
|
for attempt := 0; attempt < maxRetries; attempt++ {
|
||||||
resp, err := client.Do(req)
|
resp, err = client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return data, err
|
return
|
||||||
}
|
}
|
||||||
// nolint: errcheck
|
// nolint: errcheck
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
@ -413,8 +234,7 @@ func Download(url string, headers *map[string]string) (data *UserData, err error
|
|||||||
if err := yaml.Unmarshal(dataBytes, data); err != nil {
|
if err := yaml.Unmarshal(dataBytes, data); err != nil {
|
||||||
return data, fmt.Errorf("unmarshal user data: %s", err.Error())
|
return data, fmt.Errorf("unmarshal user data: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
return data, data.Validate()
|
||||||
return data, nil
|
|
||||||
}
|
}
|
||||||
return data, fmt.Errorf("failed to download userdata from: %s", url)
|
return data, fmt.Errorf("failed to download userdata from: %s", url)
|
||||||
}
|
}
|
||||||
@ -434,27 +254,3 @@ func Open(p string) (data *UserData, err error) {
|
|||||||
|
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsBootstrap indicates if the current kubeadm configuration is a master init
|
|
||||||
// configuration.
|
|
||||||
func (data *UserData) IsBootstrap() bool {
|
|
||||||
return data.Services.Kubeadm.bootstrap
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsControlPlane indicates if the current kubeadm configuration is a worker
|
|
||||||
// acting as a master.
|
|
||||||
func (data *UserData) IsControlPlane() bool {
|
|
||||||
return data.Services.Kubeadm.controlPlane
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsMaster indicates if the current kubeadm configuration is a master
|
|
||||||
// configuration.
|
|
||||||
func (data *UserData) IsMaster() bool {
|
|
||||||
return data.Services.Kubeadm.bootstrap || data.Services.Kubeadm.controlPlane
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsWorker indicates if the current kubeadm configuration is a worker
|
|
||||||
// configuration.
|
|
||||||
func (data *UserData) IsWorker() bool {
|
|
||||||
return !data.IsMaster()
|
|
||||||
}
|
|
||||||
|
@ -5,17 +5,70 @@
|
|||||||
package userdata
|
package userdata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
yaml "gopkg.in/yaml.v2"
|
yaml "gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type validateSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(validateSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *validateSuite) TestDownloadRetry() {
|
||||||
|
// Disable logging for test
|
||||||
|
log.SetOutput(ioutil.Discard)
|
||||||
|
ts := testUDServer()
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
_, err := Download(ts.URL, nil)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
log.SetOutput(os.Stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *validateSuite) TestKubeadmMarshal() {
|
||||||
|
var kubeadm Kubeadm
|
||||||
|
|
||||||
|
err := yaml.Unmarshal([]byte(kubeadmConfig), &kubeadm)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
assert.Equal(suite.T(), "test", kubeadm.CertificateKey)
|
||||||
|
|
||||||
|
out, err := yaml.Marshal(&kubeadm)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
assert.Equal(suite.T(), kubeadmConfig, string(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testUDServer() *httptest.Server {
|
||||||
|
var count int
|
||||||
|
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
count++
|
||||||
|
log.Printf("Request %d\n", count)
|
||||||
|
if count == 4 {
|
||||||
|
// nolint: errcheck
|
||||||
|
w.Write([]byte(testConfig))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
}))
|
||||||
|
|
||||||
|
return ts
|
||||||
|
}
|
||||||
|
|
||||||
// nolint: lll
|
// nolint: lll
|
||||||
const testConfig = `version: ""
|
const testConfig = `version: "1"
|
||||||
security:
|
security:
|
||||||
os:
|
os:
|
||||||
ca:
|
ca:
|
||||||
@ -58,7 +111,7 @@ services:
|
|||||||
trustd:
|
trustd:
|
||||||
username: 'test'
|
username: 'test'
|
||||||
password: 'test'
|
password: 'test'
|
||||||
endpoints: []
|
endpoints: [ "1.2.3.4" ]
|
||||||
certSANs: []
|
certSANs: []
|
||||||
install:
|
install:
|
||||||
wipe: true
|
wipe: true
|
||||||
@ -77,16 +130,6 @@ install:
|
|||||||
size: 1024000000
|
size: 1024000000
|
||||||
`
|
`
|
||||||
|
|
||||||
func TestDownloadRetry(t *testing.T) {
|
|
||||||
ts := testUDServer()
|
|
||||||
defer ts.Close()
|
|
||||||
|
|
||||||
_, err := Download(ts.URL, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Error("Failed to download userdata", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// nolint: lll
|
// nolint: lll
|
||||||
const kubeadmConfig = `configuration: |
|
const kubeadmConfig = `configuration: |
|
||||||
apiVersion: kubeadm.k8s.io/v1beta1
|
apiVersion: kubeadm.k8s.io/v1beta1
|
||||||
@ -174,34 +217,3 @@ const kubeadmConfig = `configuration: |
|
|||||||
sourceVip: ""
|
sourceVip: ""
|
||||||
certificateKey: test
|
certificateKey: test
|
||||||
`
|
`
|
||||||
|
|
||||||
func TestKubeadmMarshal(t *testing.T) {
|
|
||||||
var kubeadm Kubeadm
|
|
||||||
|
|
||||||
err := yaml.Unmarshal([]byte(kubeadmConfig), &kubeadm)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, "test", kubeadm.CertificateKey)
|
|
||||||
|
|
||||||
out, err := yaml.Marshal(&kubeadm)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, kubeadmConfig, string(out))
|
|
||||||
}
|
|
||||||
|
|
||||||
func testUDServer() *httptest.Server {
|
|
||||||
var count int
|
|
||||||
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
count++
|
|
||||||
log.Printf("Request %d\n", count)
|
|
||||||
if count == 4 {
|
|
||||||
// nolint: errcheck
|
|
||||||
w.Write([]byte(testConfig))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
}))
|
|
||||||
|
|
||||||
return ts
|
|
||||||
}
|
|
||||||
|
27
pkg/userdata/version.go
Normal file
27
pkg/userdata/version.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
/* 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 userdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Version represents the config file version
|
||||||
|
type Version string
|
||||||
|
|
||||||
|
// Validate triggers the specified validation checks to run
|
||||||
|
func (v Version) Validate() error {
|
||||||
|
var result *multierror.Error
|
||||||
|
if v == "" {
|
||||||
|
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "version", "", ErrRequiredSection))
|
||||||
|
}
|
||||||
|
|
||||||
|
if v != "1" {
|
||||||
|
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "version", v, ErrInvalidVersion))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.ErrorOrNil()
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user