From a5d31d97ff55f727b459ddd57435fb5756172b7c Mon Sep 17 00:00:00 2001 From: Brad Beam Date: Thu, 2 May 2019 13:10:16 -0500 Subject: [PATCH] feat: Validate userdata (#593) * feat: Validate userdata Signed-off-by: Brad Beam --- go.mod | 1 + go.sum | 2 + pkg/userdata/errors.go | 38 +++ pkg/userdata/generate/generate.go | 24 +- pkg/userdata/install.go | 49 ++++ pkg/userdata/kubeadm.go | 106 +++++++ pkg/userdata/kubernetes_security.go | 85 ++++++ pkg/userdata/kubernetes_security_test.go | 43 +++ pkg/userdata/networking.go | 113 ++++++++ pkg/userdata/networking_test.go | 60 ++++ pkg/userdata/os_security.go | 85 ++++++ pkg/userdata/os_security_test.go | 43 +++ pkg/userdata/services.go | 197 +++++++++++++ pkg/userdata/services_test.go | 67 +++++ pkg/userdata/userdata.go | 352 +++++------------------ pkg/userdata/userdata_test.go | 98 ++++--- pkg/userdata/version.go | 27 ++ 17 files changed, 1068 insertions(+), 322 deletions(-) create mode 100644 pkg/userdata/errors.go create mode 100644 pkg/userdata/install.go create mode 100644 pkg/userdata/kubeadm.go create mode 100644 pkg/userdata/kubernetes_security.go create mode 100644 pkg/userdata/kubernetes_security_test.go create mode 100644 pkg/userdata/networking.go create mode 100644 pkg/userdata/networking_test.go create mode 100644 pkg/userdata/os_security.go create mode 100644 pkg/userdata/os_security_test.go create mode 100644 pkg/userdata/services.go create mode 100644 pkg/userdata/services_test.go create mode 100644 pkg/userdata/version.go diff --git a/go.mod b/go.mod index fe51db26c..d3c9c2ab8 100644 --- a/go.mod +++ b/go.mod @@ -70,6 +70,7 @@ require ( golang.org/x/sys v0.0.0-20190312061237-fead79001313 golang.org/x/text v0.3.0 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/grpc v1.17.0 gopkg.in/airbrake/gobrake.v2 v2.0.9 // indirect diff --git a/go.sum b/go.sum index c3f2d80b2..2c0c95d51 100644 --- a/go.sum +++ b/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/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 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/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= diff --git a/pkg/userdata/errors.go b/pkg/userdata/errors.go new file mode 100644 index 000000000..3de01bce7 --- /dev/null +++ b/pkg/userdata/errors.go @@ -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") +) diff --git a/pkg/userdata/generate/generate.go b/pkg/userdata/generate/generate.go index 633d39497..ee55afed6 100644 --- a/pkg/userdata/generate/generate.go +++ b/pkg/userdata/generate/generate.go @@ -204,11 +204,33 @@ func Userdata(t Type, in *Input) (string, error) { 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 { 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 } diff --git a/pkg/userdata/install.go b/pkg/userdata/install.go new file mode 100644 index 000000000..feacd9173 --- /dev/null +++ b/pkg/userdata/install.go @@ -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"` +} diff --git a/pkg/userdata/kubeadm.go b/pkg/userdata/kubeadm.go new file mode 100644 index 000000000..4e9ff1997 --- /dev/null +++ b/pkg/userdata/kubeadm.go @@ -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 +} diff --git a/pkg/userdata/kubernetes_security.go b/pkg/userdata/kubernetes_security.go new file mode 100644 index 000000000..0aa05c746 --- /dev/null +++ b/pkg/userdata/kubernetes_security.go @@ -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() + } +} diff --git a/pkg/userdata/kubernetes_security_test.go b/pkg/userdata/kubernetes_security_test.go new file mode 100644 index 000000000..11d6208a2 --- /dev/null +++ b/pkg/userdata/kubernetes_security_test.go @@ -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) +} diff --git a/pkg/userdata/networking.go b/pkg/userdata/networking.go new file mode 100644 index 000000000..929606c56 --- /dev/null +++ b/pkg/userdata/networking.go @@ -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"` +} diff --git a/pkg/userdata/networking_test.go b/pkg/userdata/networking_test.go new file mode 100644 index 000000000..8bb8dcb29 --- /dev/null +++ b/pkg/userdata/networking_test.go @@ -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) +} diff --git a/pkg/userdata/os_security.go b/pkg/userdata/os_security.go new file mode 100644 index 000000000..0a73001b6 --- /dev/null +++ b/pkg/userdata/os_security.go @@ -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() + } +} diff --git a/pkg/userdata/os_security_test.go b/pkg/userdata/os_security_test.go new file mode 100644 index 000000000..8b27c1419 --- /dev/null +++ b/pkg/userdata/os_security_test.go @@ -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) +} diff --git a/pkg/userdata/services.go b/pkg/userdata/services.go new file mode 100644 index 000000000..7134666d3 --- /dev/null +++ b/pkg/userdata/services.go @@ -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() + } +} diff --git a/pkg/userdata/services_test.go b/pkg/userdata/services_test.go new file mode 100644 index 000000000..7f886710a --- /dev/null +++ b/pkg/userdata/services_test.go @@ -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) +} diff --git a/pkg/userdata/userdata.go b/pkg/userdata/userdata.go index beb4dcc62..f8b8f8c8a 100644 --- a/pkg/userdata/userdata.go +++ b/pkg/userdata/userdata.go @@ -7,7 +7,6 @@ package userdata import ( stdlibx509 "crypto/x509" "encoding/pem" - "errors" "fmt" "io/ioutil" "log" @@ -18,26 +17,16 @@ import ( "path" "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/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" ) -// Env represents a set of environment variables. -type Env = map[string]string - // UserData represents the user data. type UserData struct { - Version string `yaml:"version"` + Version Version `yaml:"version"` Security *Security `yaml:"security"` Networking *Networking `yaml:"networking"` Services *Services `yaml:"services"` @@ -47,24 +36,54 @@ type UserData struct { 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. type Security struct { OS *OSSecurity `yaml:"os"` 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. type Networking struct { Kubernetes struct{} `yaml:"kubernetes"` @@ -76,42 +95,6 @@ type OSNet struct { 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. type File struct { Contents string `yaml:"contents"` @@ -119,197 +102,10 @@ type File struct { 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. func (data *UserData) WriteFiles() (err error) { for _, f := range data.Files { + // TODO isnt there a const for the data mountpoint p := path.Join("/var", f.Path) if err = os.MkdirAll(path.Dir(p), os.ModeDir); err != nil { return @@ -322,6 +118,30 @@ func (data *UserData) WriteFiles() (err error) { 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. func (data *UserData) NewIdentityCSR() (csr *x509.CertificateSigningRequest, err error) { var key *x509.Key @@ -377,7 +197,7 @@ func Download(url string, headers *map[string]string) (data *UserData, err error client := &http.Client{} req, err := http.NewRequest("GET", url, nil) if err != nil { - return data, err + return } 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++ { - resp, err := client.Do(req) + resp, err = client.Do(req) if err != nil { - return data, err + return } // nolint: errcheck 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 { return data, fmt.Errorf("unmarshal user data: %s", err.Error()) } - - return data, nil + return data, data.Validate() } 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 } - -// 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() -} diff --git a/pkg/userdata/userdata_test.go b/pkg/userdata/userdata_test.go index 64de10b3a..04a36f4de 100644 --- a/pkg/userdata/userdata_test.go +++ b/pkg/userdata/userdata_test.go @@ -5,17 +5,70 @@ package userdata import ( + "io/ioutil" "log" "net/http" "net/http/httptest" + "os" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" 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 -const testConfig = `version: "" +const testConfig = `version: "1" security: os: ca: @@ -58,7 +111,7 @@ services: trustd: username: 'test' password: 'test' - endpoints: [] + endpoints: [ "1.2.3.4" ] certSANs: [] install: wipe: true @@ -77,16 +130,6 @@ install: 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 const kubeadmConfig = `configuration: | apiVersion: kubeadm.k8s.io/v1beta1 @@ -174,34 +217,3 @@ const kubeadmConfig = `configuration: | sourceVip: "" 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 -} diff --git a/pkg/userdata/version.go b/pkg/userdata/version.go new file mode 100644 index 000000000..ddb36da92 --- /dev/null +++ b/pkg/userdata/version.go @@ -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() +}