feat: Validate userdata (#593)

* feat: Validate userdata

Signed-off-by: Brad Beam <brad.beam@talos-systems.com>
This commit is contained in:
Brad Beam 2019-05-02 13:10:16 -05:00 committed by GitHub
parent e4c5385f3d
commit a5d31d97ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1068 additions and 322 deletions

1
go.mod
View File

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

2
go.sum
View File

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

38
pkg/userdata/errors.go Normal file
View 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")
)

View File

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

49
pkg/userdata/install.go Normal file
View 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
View 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
}

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

View 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
View 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"`
}

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

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

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

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

View File

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

View File

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

27
pkg/userdata/version.go Normal file
View 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()
}