mirror of
https://github.com/siderolabs/talos.git
synced 2025-08-26 00:51:11 +02:00
feat: Validate userdata (#593)
* feat: Validate userdata Signed-off-by: Brad Beam <brad.beam@talos-systems.com>
This commit is contained in:
parent
e4c5385f3d
commit
a5d31d97ff
1
go.mod
1
go.mod
@ -70,6 +70,7 @@ require (
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313
|
||||
golang.org/x/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
2
go.sum
@ -172,6 +172,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/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
38
pkg/userdata/errors.go
Normal file
@ -0,0 +1,38 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package userdata
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
// General
|
||||
|
||||
// ErrRequiredSection denotes a section is required
|
||||
ErrRequiredSection = errors.New("required userdata section")
|
||||
// ErrInvalidVersion denotes that the config file version is invalid
|
||||
ErrInvalidVersion = errors.New("invalid config version")
|
||||
|
||||
// Security
|
||||
|
||||
// ErrInvalidCert denotes that the certificate specified is invalid
|
||||
ErrInvalidCert = errors.New("certificate is invalid")
|
||||
// ErrInvalidCertType denotes that the certificate type is invalid
|
||||
ErrInvalidCertType = errors.New("certificate type is invalid")
|
||||
|
||||
// Services
|
||||
|
||||
// ErrUnsupportedCNI denotes that the specified CNI is invalid
|
||||
ErrUnsupportedCNI = errors.New("unsupported CNI driver")
|
||||
// ErrInvalidTrustdToken denotes that a trustd token has not been specified
|
||||
ErrInvalidTrustdToken = errors.New("trustd token is invalid")
|
||||
|
||||
// Networking
|
||||
|
||||
// ErrBadAddressing denotes that an incorrect combination of network
|
||||
// address methods have been specified
|
||||
ErrBadAddressing = errors.New("invalid network device addressing method")
|
||||
// ErrInvalidAddress denotes that a bad address was provided
|
||||
ErrInvalidAddress = errors.New("invalid network address")
|
||||
)
|
@ -204,11 +204,33 @@ func Userdata(t Type, in *Input) (string, error) {
|
||||
return "", errors.New("failed to determine userdata type to generate")
|
||||
}
|
||||
|
||||
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
49
pkg/userdata/install.go
Normal file
@ -0,0 +1,49 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package userdata
|
||||
|
||||
// Install represents the installation options for preparing a node.
|
||||
type Install struct {
|
||||
Boot *BootDevice `yaml:"boot,omitempty"`
|
||||
Root *RootDevice `yaml:"root"`
|
||||
Data *InstallDevice `yaml:"data,omitempty"`
|
||||
ExtraDevices []*ExtraDevice `yaml:"extraDevices,omitempty"`
|
||||
Wipe bool `yaml:"wipe"`
|
||||
Force bool `yaml:"force"`
|
||||
}
|
||||
|
||||
// BootDevice represents the install options specific to the boot partition.
|
||||
type BootDevice struct {
|
||||
InstallDevice `yaml:",inline"`
|
||||
|
||||
Kernel string `yaml:"kernel"`
|
||||
Initramfs string `yaml:"initramfs"`
|
||||
}
|
||||
|
||||
// RootDevice represents the install options specific to the root partition.
|
||||
type RootDevice struct {
|
||||
InstallDevice `yaml:",inline"`
|
||||
|
||||
Rootfs string `yaml:"rootfs"`
|
||||
}
|
||||
|
||||
// InstallDevice represents the specific directions for each partition.
|
||||
type InstallDevice struct {
|
||||
Device string `yaml:"device,omitempty"`
|
||||
Size uint `yaml:"size,omitempty"`
|
||||
}
|
||||
|
||||
// ExtraDevice represents the options available for partitioning, formatting,
|
||||
// and mounting extra disks.
|
||||
type ExtraDevice struct {
|
||||
Device string `yaml:"device,omitempty"`
|
||||
Partitions []*ExtraDevicePartition `yaml:"partitions,omitempty"`
|
||||
}
|
||||
|
||||
// ExtraDevicePartition represents the options for a device partition.
|
||||
type ExtraDevicePartition struct {
|
||||
Size uint `yaml:"size,omitempty"`
|
||||
MountPoint string `yaml:"mountpoint,omitempty"`
|
||||
}
|
106
pkg/userdata/kubeadm.go
Normal file
106
pkg/userdata/kubeadm.go
Normal file
@ -0,0 +1,106 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package userdata
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
|
||||
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
|
||||
kubeadmscheme "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/scheme"
|
||||
kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util"
|
||||
configutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config"
|
||||
)
|
||||
|
||||
// Kubeadm describes the set of configuration options available for kubeadm.
|
||||
type Kubeadm struct {
|
||||
CommonServiceOptions `yaml:",inline"`
|
||||
|
||||
// ConfigurationStr is converted to Configuration and back in Marshal/UnmarshalYAML
|
||||
Configuration runtime.Object `yaml:"-"`
|
||||
ConfigurationStr string `yaml:"configuration"`
|
||||
|
||||
ExtraArgs []string `yaml:"extraArgs,omitempty"`
|
||||
CertificateKey string `yaml:"certificateKey,omitempty"`
|
||||
IgnorePreflightErrors []string `yaml:"ignorePreflightErrors,omitempty"`
|
||||
bootstrap bool
|
||||
controlPlane bool
|
||||
}
|
||||
|
||||
// MarshalYAML implements the yaml.Marshaler interface.
|
||||
func (kdm *Kubeadm) MarshalYAML() (interface{}, error) {
|
||||
b, err := configutil.MarshalKubeadmConfigObject(kdm.Configuration)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gvks, err := kubeadmutil.GroupVersionKindsFromBytes(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if kubeadmutil.GroupVersionKindsHasInitConfiguration(gvks...) {
|
||||
kdm.bootstrap = true
|
||||
}
|
||||
if kubeadmutil.GroupVersionKindsHasJoinConfiguration(gvks...) {
|
||||
kdm.bootstrap = false
|
||||
}
|
||||
|
||||
kdm.ConfigurationStr = string(b)
|
||||
|
||||
type KubeadmAlias Kubeadm
|
||||
|
||||
return (*KubeadmAlias)(kdm), nil
|
||||
}
|
||||
|
||||
// UnmarshalYAML implements the yaml.Unmarshaler interface.
|
||||
func (kdm *Kubeadm) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
type KubeadmAlias Kubeadm
|
||||
|
||||
if err := unmarshal((*KubeadmAlias)(kdm)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b := []byte(kdm.ConfigurationStr)
|
||||
|
||||
gvks, err := kubeadmutil.GroupVersionKindsFromBytes(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if kubeadmutil.GroupVersionKindsHasInitConfiguration(gvks...) {
|
||||
// Since the ClusterConfiguration is embedded in the InitConfiguration
|
||||
// struct, it is required to (un)marshal it a special way. The kubeadm
|
||||
// API exposes one function (MarshalKubeadmConfigObject) to handle the
|
||||
// marshaling, but does not yet have that convenience for
|
||||
// unmarshaling.
|
||||
cfg, err := configutil.BytesToInitConfiguration(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
kdm.Configuration = cfg
|
||||
kdm.bootstrap = true
|
||||
}
|
||||
if kubeadmutil.GroupVersionKindsHasJoinConfiguration(gvks...) {
|
||||
cfg, err := kubeadmutil.UnmarshalFromYamlForCodecs(b, kubeadmapi.SchemeGroupVersion, kubeadmscheme.Codecs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
kdm.Configuration = cfg
|
||||
kdm.bootstrap = false
|
||||
joinConfiguration, ok := cfg.(*kubeadm.JoinConfiguration)
|
||||
if !ok {
|
||||
return errors.New("expected JoinConfiguration")
|
||||
}
|
||||
if joinConfiguration.ControlPlane == nil {
|
||||
kdm.controlPlane = false
|
||||
} else {
|
||||
kdm.controlPlane = true
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
85
pkg/userdata/kubernetes_security.go
Normal file
85
pkg/userdata/kubernetes_security.go
Normal file
@ -0,0 +1,85 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package userdata
|
||||
|
||||
import (
|
||||
"encoding/pem"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/talos-systems/talos/pkg/crypto/x509"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// KubernetesSecurity represents the set of security options specific to
|
||||
// Kubernetes.
|
||||
type KubernetesSecurity struct {
|
||||
CA *x509.PEMEncodedCertificateAndKey `yaml:"ca"`
|
||||
}
|
||||
|
||||
// KubernetesSecurityCheck defines the function type for checks
|
||||
type KubernetesSecurityCheck func(*KubernetesSecurity) error
|
||||
|
||||
// Validate triggers the specified validation checks to run
|
||||
func (k *KubernetesSecurity) Validate(checks ...KubernetesSecurityCheck) error {
|
||||
var result *multierror.Error
|
||||
|
||||
for _, check := range checks {
|
||||
result = multierror.Append(result, check(k))
|
||||
}
|
||||
|
||||
return result.ErrorOrNil()
|
||||
}
|
||||
|
||||
// CheckKubernetesCA verfies the KubernetesSecurity settings are valid
|
||||
// nolint: dupl
|
||||
func CheckKubernetesCA() KubernetesSecurityCheck {
|
||||
return func(k *KubernetesSecurity) error {
|
||||
var result *multierror.Error
|
||||
|
||||
// Verify the required sections are present
|
||||
if k.CA == nil {
|
||||
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.kubernetes.ca", "", ErrRequiredSection))
|
||||
}
|
||||
|
||||
// Bail early since we're already missing the required sections
|
||||
if result.ErrorOrNil() != nil {
|
||||
return result.ErrorOrNil()
|
||||
}
|
||||
|
||||
if k.CA.Crt == nil {
|
||||
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.kubernetes.ca.crt", "", ErrRequiredSection))
|
||||
}
|
||||
|
||||
if k.CA.Key == nil {
|
||||
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.kubernetes.ca.key", "", ErrRequiredSection))
|
||||
}
|
||||
|
||||
// test if k.CA fields are present ( x509 package handles the b64 decode
|
||||
// during yaml unmarshal, so we have the bytes if it was successful )
|
||||
var block *pem.Block
|
||||
block, _ = pem.Decode(k.CA.Crt)
|
||||
// nolint: gocritic
|
||||
if block == nil {
|
||||
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.kubernetes.ca.crt", k.CA.Crt, ErrInvalidCert))
|
||||
} else {
|
||||
if block.Type != "CERTIFICATE" {
|
||||
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.kubernetes.ca.crt", k.CA.Crt, ErrInvalidCertType))
|
||||
}
|
||||
}
|
||||
|
||||
block, _ = pem.Decode(k.CA.Key)
|
||||
// nolint: gocritic
|
||||
if block == nil {
|
||||
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.kubernetes.ca.key", k.CA.Key, ErrInvalidCert))
|
||||
} else {
|
||||
if !strings.HasSuffix(block.Type, "PRIVATE KEY") {
|
||||
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.kubernetes.ca.key", k.CA.Key, ErrInvalidCertType))
|
||||
}
|
||||
}
|
||||
|
||||
return result.ErrorOrNil()
|
||||
}
|
||||
}
|
43
pkg/userdata/kubernetes_security_test.go
Normal file
43
pkg/userdata/kubernetes_security_test.go
Normal file
@ -0,0 +1,43 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
// nolint: dupl
|
||||
package userdata
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/talos-systems/talos/pkg/crypto/x509"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
func (suite *validateSuite) TestValidateKubernetesSecurity() {
|
||||
var err error
|
||||
|
||||
// Test for missing required sections
|
||||
kube := &KubernetesSecurity{}
|
||||
err = kube.Validate(CheckKubernetesCA())
|
||||
suite.Require().Error(err)
|
||||
// Embedding the check in suite.Assert().Equal(true, xerrors.Is had issues )
|
||||
if !xerrors.Is(err.(*multierror.Error).Errors[0], ErrRequiredSection) {
|
||||
suite.T().Errorf("%+v", err)
|
||||
|
||||
}
|
||||
|
||||
kube.CA = &x509.PEMEncodedCertificateAndKey{}
|
||||
err = kube.Validate(CheckKubernetesCA())
|
||||
suite.Require().Error(err)
|
||||
suite.Assert().Equal(4, len(err.(*multierror.Error).Errors))
|
||||
|
||||
// Test for invalid certs
|
||||
kube.CA.Crt = []byte("-----BEGIN Rubbish-----\n-----END Rubbish-----")
|
||||
kube.CA.Key = []byte("-----BEGIN EC Fluffy KEY-----\n-----END EC Fluffy KEY-----")
|
||||
err = kube.Validate(CheckKubernetesCA())
|
||||
suite.Require().Error(err)
|
||||
|
||||
// Successful test
|
||||
kube.CA.Crt = []byte("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----")
|
||||
kube.CA.Key = []byte("-----BEGIN EC PRIVATE KEY-----\n-----END EC PRIVATE KEY-----")
|
||||
err = kube.Validate(CheckKubernetesCA())
|
||||
suite.Require().NoError(err)
|
||||
}
|
113
pkg/userdata/networking.go
Normal file
113
pkg/userdata/networking.go
Normal file
@ -0,0 +1,113 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package userdata
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// Device represents a network interface
|
||||
type Device struct {
|
||||
Interface string `yaml:"interface"`
|
||||
CIDR string `yaml:"cidr"`
|
||||
DHCP bool `yaml:"dhcp"`
|
||||
Routes []Route `yaml:"routes"`
|
||||
Bond *Bond `yaml:"bond"`
|
||||
}
|
||||
|
||||
// NetworkDeviceCheck defines the function type for checks
|
||||
type NetworkDeviceCheck func(*Device) error
|
||||
|
||||
// Validate triggers the specified validation checks to run
|
||||
func (d *Device) Validate(checks ...NetworkDeviceCheck) error {
|
||||
var result *multierror.Error
|
||||
|
||||
for _, check := range checks {
|
||||
result = multierror.Append(result, check(d))
|
||||
}
|
||||
|
||||
return result.ErrorOrNil()
|
||||
}
|
||||
|
||||
// CheckDeviceInterface ensures that the interface has been specified
|
||||
func CheckDeviceInterface() NetworkDeviceCheck {
|
||||
return func(d *Device) error {
|
||||
var result *multierror.Error
|
||||
|
||||
if d.Interface == "" {
|
||||
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "networking.os.device.interface", "", ErrRequiredSection))
|
||||
}
|
||||
|
||||
return result.ErrorOrNil()
|
||||
}
|
||||
}
|
||||
|
||||
// CheckDeviceAddressing ensures that an appropriate addressing method
|
||||
// has been specified
|
||||
func CheckDeviceAddressing() NetworkDeviceCheck {
|
||||
return func(d *Device) error {
|
||||
var result *multierror.Error
|
||||
|
||||
// Test for both dhcp and cidr specified
|
||||
if d.DHCP && d.CIDR != "" {
|
||||
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "networking.os.device", "", ErrBadAddressing))
|
||||
}
|
||||
|
||||
// test for neither dhcp nor cidr specified
|
||||
if !d.DHCP && d.CIDR == "" {
|
||||
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "networking.os.device", "", ErrBadAddressing))
|
||||
}
|
||||
|
||||
// ensure cidr is a valid address
|
||||
if d.CIDR != "" {
|
||||
if _, _, err := net.ParseCIDR(d.CIDR); err != nil {
|
||||
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "networking.os.device.CIDR", "", err))
|
||||
}
|
||||
}
|
||||
|
||||
return result.ErrorOrNil()
|
||||
}
|
||||
}
|
||||
|
||||
// CheckDeviceRoutes ensures that the specified routes are valid
|
||||
func CheckDeviceRoutes() NetworkDeviceCheck {
|
||||
return func(d *Device) error {
|
||||
var result *multierror.Error
|
||||
|
||||
if len(d.Routes) == 0 {
|
||||
return result.ErrorOrNil()
|
||||
}
|
||||
|
||||
for idx, route := range d.Routes {
|
||||
if _, _, err := net.ParseCIDR(route.Network); err != nil {
|
||||
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "networking.os.device.route["+strconv.Itoa(idx)+"].Network", route.Network, ErrInvalidAddress))
|
||||
}
|
||||
|
||||
if ip := net.ParseIP(route.Gateway); ip == nil {
|
||||
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "networking.os.device.route["+strconv.Itoa(idx)+"].Gateway", route.Gateway, ErrInvalidAddress))
|
||||
}
|
||||
}
|
||||
return result.ErrorOrNil()
|
||||
}
|
||||
}
|
||||
|
||||
// Bond contains the various options for configuring a
|
||||
// bonded interface
|
||||
type Bond struct {
|
||||
Mode string `yaml:"mode"`
|
||||
HashPolicy string `yaml:"hashpolicy"`
|
||||
LACPRate string `yaml:"lacprate"`
|
||||
Interfaces []string `yaml:"interfaces"`
|
||||
}
|
||||
|
||||
// Route represents a network route
|
||||
type Route struct {
|
||||
Network string `yaml:"network"`
|
||||
Gateway string `yaml:"gateway"`
|
||||
}
|
60
pkg/userdata/networking_test.go
Normal file
60
pkg/userdata/networking_test.go
Normal file
@ -0,0 +1,60 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package userdata
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
func (suite *validateSuite) TestValidateDevice() {
|
||||
var err error
|
||||
|
||||
// Test for missing required sections
|
||||
dev := &Device{}
|
||||
err = dev.Validate(CheckDeviceInterface())
|
||||
suite.Require().Error(err)
|
||||
// Embedding the check in suite.Assert().Equal(true, xerrors.Is had issues )
|
||||
if !xerrors.Is(err.(*multierror.Error).Errors[0], ErrRequiredSection) {
|
||||
suite.T().Errorf("%+v", err)
|
||||
|
||||
}
|
||||
|
||||
dev.Interface = "eth0"
|
||||
err = dev.Validate(CheckDeviceInterface())
|
||||
suite.Require().NoError(err)
|
||||
|
||||
err = dev.Validate(CheckDeviceAddressing())
|
||||
suite.Require().Error(err)
|
||||
|
||||
// Ensure only a single addressing scheme is specified
|
||||
dev.DHCP = true
|
||||
dev.CIDR = "1.0.0.1/32"
|
||||
err = dev.Validate(CheckDeviceAddressing())
|
||||
suite.Require().Error(err)
|
||||
|
||||
dev.DHCP = false
|
||||
err = dev.Validate(CheckDeviceAddressing())
|
||||
suite.Require().NoError(err)
|
||||
|
||||
dev.Routes = []Route{}
|
||||
err = dev.Validate(CheckDeviceRoutes())
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// nolint: gofmt
|
||||
dev.Routes = []Route{Route{Gateway: "yolo"}}
|
||||
err = dev.Validate(CheckDeviceRoutes())
|
||||
suite.Require().Error(err)
|
||||
|
||||
// nolint: gofmt
|
||||
dev.Routes = []Route{Route{Gateway: "yolo", Network: "totes"}}
|
||||
err = dev.Validate(CheckDeviceRoutes())
|
||||
suite.Require().Error(err)
|
||||
|
||||
// nolint: gofmt
|
||||
dev.Routes = []Route{Route{Gateway: "192.168.1.1", Network: "192.168.1.0/24"}}
|
||||
err = dev.Validate(CheckDeviceRoutes())
|
||||
suite.Require().NoError(err)
|
||||
}
|
85
pkg/userdata/os_security.go
Normal file
85
pkg/userdata/os_security.go
Normal file
@ -0,0 +1,85 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package userdata
|
||||
|
||||
import (
|
||||
"encoding/pem"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/talos-systems/talos/pkg/crypto/x509"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// OSSecurity represents the set of security options specific to the OS.
|
||||
type OSSecurity struct {
|
||||
CA *x509.PEMEncodedCertificateAndKey `yaml:"ca"`
|
||||
Identity *x509.PEMEncodedCertificateAndKey `yaml:"identity"`
|
||||
}
|
||||
|
||||
// OSSecurityCheck defines the function type for checks
|
||||
type OSSecurityCheck func(*OSSecurity) error
|
||||
|
||||
// Validate triggers the specified validation checks to run
|
||||
func (o *OSSecurity) Validate(checks ...OSSecurityCheck) error {
|
||||
var result *multierror.Error
|
||||
|
||||
for _, check := range checks {
|
||||
result = multierror.Append(result, check(o))
|
||||
}
|
||||
|
||||
return result.ErrorOrNil()
|
||||
}
|
||||
|
||||
// CheckOSCA verfies the OSSecurity settings are valid
|
||||
// nolint: dupl
|
||||
func CheckOSCA() OSSecurityCheck {
|
||||
return func(o *OSSecurity) error {
|
||||
var result *multierror.Error
|
||||
|
||||
// Verify the required sections are present
|
||||
if o.CA == nil {
|
||||
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.os.ca", "", ErrRequiredSection))
|
||||
}
|
||||
|
||||
// Bail early since we're already missing the required sections
|
||||
if result.ErrorOrNil() != nil {
|
||||
return result.ErrorOrNil()
|
||||
}
|
||||
|
||||
if o.CA.Crt == nil {
|
||||
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.os.ca.crt", "", ErrRequiredSection))
|
||||
}
|
||||
|
||||
if o.CA.Key == nil {
|
||||
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.os.ca.key", "", ErrRequiredSection))
|
||||
}
|
||||
|
||||
// test if o.CA fields are present ( x509 package handles the b64 decode
|
||||
// during yaml unmarshal, so we have the bytes if it was successful )
|
||||
var block *pem.Block
|
||||
block, _ = pem.Decode(o.CA.Crt)
|
||||
// nolint: gocritic
|
||||
if block == nil {
|
||||
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.os.ca.crt", o.CA.Crt, ErrInvalidCert))
|
||||
} else {
|
||||
if block.Type != "CERTIFICATE" {
|
||||
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.os.ca.crt", o.CA.Crt, ErrInvalidCertType))
|
||||
}
|
||||
}
|
||||
|
||||
block, _ = pem.Decode(o.CA.Key)
|
||||
// nolint: gocritic
|
||||
if block == nil {
|
||||
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.os.ca.key", o.CA.Key, ErrInvalidCert))
|
||||
} else {
|
||||
if !strings.HasSuffix(block.Type, "PRIVATE KEY") {
|
||||
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.os.ca.key", o.CA.Key, ErrInvalidCertType))
|
||||
}
|
||||
}
|
||||
|
||||
return result.ErrorOrNil()
|
||||
}
|
||||
}
|
43
pkg/userdata/os_security_test.go
Normal file
43
pkg/userdata/os_security_test.go
Normal file
@ -0,0 +1,43 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
// nolint: dupl
|
||||
package userdata
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/talos-systems/talos/pkg/crypto/x509"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
func (suite *validateSuite) TestValidateOSSecurity() {
|
||||
var err error
|
||||
|
||||
// Test for missing required sections
|
||||
os := &OSSecurity{}
|
||||
err = os.Validate(CheckOSCA())
|
||||
suite.Require().Error(err)
|
||||
// Embedding the check in suite.Assert().Equal(true, xerrors.Is had issues )
|
||||
if !xerrors.Is(err.(*multierror.Error).Errors[0], ErrRequiredSection) {
|
||||
suite.T().Errorf("%+v", err)
|
||||
|
||||
}
|
||||
|
||||
os.CA = &x509.PEMEncodedCertificateAndKey{}
|
||||
err = os.Validate(CheckOSCA())
|
||||
suite.Require().Error(err)
|
||||
suite.Assert().Equal(4, len(err.(*multierror.Error).Errors))
|
||||
|
||||
// Test for invalid certs
|
||||
os.CA.Crt = []byte("-----BEGIN Rubbish-----\n-----END Rubbish-----")
|
||||
os.CA.Key = []byte("-----BEGIN EC Fluffy KEY-----\n-----END EC Fluffy KEY-----")
|
||||
err = os.Validate(CheckOSCA())
|
||||
suite.Require().Error(err)
|
||||
|
||||
// Successful test
|
||||
os.CA.Crt = []byte("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----")
|
||||
os.CA.Key = []byte("-----BEGIN EC PRIVATE KEY-----\n-----END EC PRIVATE KEY-----")
|
||||
err = os.Validate(CheckOSCA())
|
||||
suite.Require().NoError(err)
|
||||
}
|
197
pkg/userdata/services.go
Normal file
197
pkg/userdata/services.go
Normal file
@ -0,0 +1,197 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package userdata
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// Env represents a set of environment variables.
|
||||
type Env = map[string]string
|
||||
|
||||
// Services represents the set of services available to configure.
|
||||
type Services struct {
|
||||
Init *Init `yaml:"init"`
|
||||
Kubelet *Kubelet `yaml:"kubelet"`
|
||||
Kubeadm *Kubeadm `yaml:"kubeadm"`
|
||||
Trustd *Trustd `yaml:"trustd"`
|
||||
Proxyd *Proxyd `yaml:"proxyd"`
|
||||
OSD *OSD `yaml:"osd"`
|
||||
CRT *CRT `yaml:"crt"`
|
||||
NTPd *NTPd `yaml:"ntp"`
|
||||
}
|
||||
|
||||
// Validate triggers the specified validation checks to run
|
||||
func (s *Services) Validate(checks ...ServiceCheck) error {
|
||||
var result *multierror.Error
|
||||
|
||||
for _, check := range checks {
|
||||
result = multierror.Append(result, check(s))
|
||||
}
|
||||
|
||||
return result.ErrorOrNil()
|
||||
}
|
||||
|
||||
// ServiceCheck defines the function type for checks
|
||||
type ServiceCheck func(*Services) error
|
||||
|
||||
// CheckServices ensures the minimum necessary services config has been provided
|
||||
func CheckServices() ServiceCheck {
|
||||
return func(s *Services) error {
|
||||
var result *multierror.Error
|
||||
|
||||
if s.Kubeadm == nil {
|
||||
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "services.kubeadm", "", ErrRequiredSection))
|
||||
}
|
||||
|
||||
if s.Trustd == nil {
|
||||
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "services.trustd", "", ErrRequiredSection))
|
||||
}
|
||||
|
||||
return result.ErrorOrNil()
|
||||
}
|
||||
}
|
||||
|
||||
// OSD describes the configuration of the osd service.
|
||||
type OSD struct {
|
||||
CommonServiceOptions `yaml:",inline"`
|
||||
}
|
||||
|
||||
// Proxyd describes the configuration of the proxyd service.
|
||||
type Proxyd struct {
|
||||
CommonServiceOptions `yaml:",inline"`
|
||||
}
|
||||
|
||||
// CRT describes the configuration of the container runtime service.
|
||||
type CRT struct {
|
||||
CommonServiceOptions `yaml:",inline"`
|
||||
}
|
||||
|
||||
// CommonServiceOptions represents the set of options common to all services.
|
||||
type CommonServiceOptions struct {
|
||||
Env Env `yaml:"env,omitempty"`
|
||||
}
|
||||
|
||||
// NTPd describes the configuration of the ntp service.
|
||||
type NTPd struct {
|
||||
CommonServiceOptions `yaml:",inline"`
|
||||
|
||||
Server string `yaml:"server,omitempty"`
|
||||
}
|
||||
|
||||
// Kubelet describes the configuration of the kubelet service.
|
||||
type Kubelet struct {
|
||||
CommonServiceOptions `yaml:",inline"`
|
||||
ExtraMounts []specs.Mount `yaml:"extraMounts"`
|
||||
}
|
||||
|
||||
// Trustd describes the configuration of the Root of Trust (RoT) service. The
|
||||
// username and password are used by master nodes, and worker nodes. The master
|
||||
// nodes use them to authenticate clients, while the workers use them to
|
||||
// authenticate as a client. The endpoints should only be specified in the
|
||||
// worker user data, and should include all master nodes participating as a RoT.
|
||||
type Trustd struct {
|
||||
CommonServiceOptions `yaml:",inline"`
|
||||
|
||||
Token string `yaml:"token"`
|
||||
Username string `yaml:"username"`
|
||||
Password string `yaml:"password"`
|
||||
Endpoints []string `yaml:"endpoints,omitempty"`
|
||||
CertSANs []string `yaml:"certSANs,omitempty"`
|
||||
BootstrapNode string `yaml:"bootstrapNode,omitempty"`
|
||||
}
|
||||
|
||||
// TrustdCheck defines the function type for checks
|
||||
type TrustdCheck func(*Trustd) error
|
||||
|
||||
// Validate triggers the specified validation checks to run
|
||||
func (t *Trustd) Validate(checks ...TrustdCheck) error {
|
||||
var result *multierror.Error
|
||||
|
||||
for _, check := range checks {
|
||||
result = multierror.Append(result, check(t))
|
||||
}
|
||||
|
||||
return result.ErrorOrNil()
|
||||
}
|
||||
|
||||
// CheckTrustdAuth ensures that a trustd token has been specified
|
||||
func CheckTrustdAuth() TrustdCheck {
|
||||
return func(t *Trustd) error {
|
||||
var result *multierror.Error
|
||||
|
||||
if t.Token == "" && (t.Username == "" || t.Password == "") {
|
||||
if t.Token == "" {
|
||||
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "services.trustd.token", t.Token, ErrRequiredSection))
|
||||
} else {
|
||||
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "services.trustd.username:password", t.Username+":"+t.Password, ErrRequiredSection))
|
||||
}
|
||||
}
|
||||
|
||||
return result.ErrorOrNil()
|
||||
}
|
||||
}
|
||||
|
||||
// CheckTrustdEndpoints ensures that the trustd endpoints have been specified
|
||||
func CheckTrustdEndpoints() TrustdCheck {
|
||||
return func(t *Trustd) error {
|
||||
var result *multierror.Error
|
||||
|
||||
if len(t.Endpoints) == 0 {
|
||||
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "services.trustd.endpoints", t.Endpoints, ErrRequiredSection))
|
||||
}
|
||||
|
||||
for idx, endpoint := range t.Endpoints {
|
||||
if ip := net.ParseIP(endpoint); ip == nil {
|
||||
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "services.trustd.endpoints["+strconv.Itoa(idx)+"]", endpoint, ErrInvalidAddress))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return result.ErrorOrNil()
|
||||
}
|
||||
}
|
||||
|
||||
// Init describes the configuration of the init service.
|
||||
type Init struct {
|
||||
CNI string `yaml:"cni,omitempty"`
|
||||
}
|
||||
|
||||
// InitCheck defines the function type for checks
|
||||
type InitCheck func(*Init) error
|
||||
|
||||
// Validate triggers the specified validation checks to run
|
||||
func (i *Init) Validate(checks ...InitCheck) error {
|
||||
var result *multierror.Error
|
||||
|
||||
for _, check := range checks {
|
||||
result = multierror.Append(result, check(i))
|
||||
}
|
||||
|
||||
return result.ErrorOrNil()
|
||||
}
|
||||
|
||||
// CheckInitCNI ensures that a valid cni driver has been specified
|
||||
func CheckInitCNI() InitCheck {
|
||||
return func(i *Init) error {
|
||||
var result *multierror.Error
|
||||
|
||||
switch i.CNI {
|
||||
case "calico":
|
||||
return result.ErrorOrNil()
|
||||
case "flannel":
|
||||
return result.ErrorOrNil()
|
||||
default:
|
||||
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "services.init.cni", i.CNI, ErrUnsupportedCNI))
|
||||
}
|
||||
|
||||
return result.ErrorOrNil()
|
||||
}
|
||||
}
|
67
pkg/userdata/services_test.go
Normal file
67
pkg/userdata/services_test.go
Normal file
@ -0,0 +1,67 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package userdata
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
func (suite *validateSuite) TestValidateServices() {
|
||||
var err error
|
||||
|
||||
// Test for missing required sections
|
||||
svc := &Services{}
|
||||
err = svc.Validate(CheckServices())
|
||||
suite.Require().Error(err)
|
||||
// services.kubeadm
|
||||
if !xerrors.Is(err.(*multierror.Error).Errors[0], ErrRequiredSection) {
|
||||
suite.T().Errorf("%+v", err)
|
||||
}
|
||||
// services.trustd
|
||||
if !xerrors.Is(err.(*multierror.Error).Errors[1], ErrRequiredSection) {
|
||||
suite.T().Errorf("%+v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *validateSuite) TestValidateTrustd() {
|
||||
var err error
|
||||
|
||||
svc := &Services{}
|
||||
svc.Trustd = &Trustd{}
|
||||
err = svc.Trustd.Validate(CheckTrustdAuth(), CheckTrustdEndpoints())
|
||||
suite.Require().Error(err)
|
||||
suite.Assert().Equal(2, len(err.(*multierror.Error).Errors))
|
||||
|
||||
svc.Trustd.Endpoints = []string{"1.2.3.4"}
|
||||
err = svc.Trustd.Validate(CheckTrustdEndpoints())
|
||||
suite.Require().NoError(err)
|
||||
|
||||
svc.Trustd.Token = "yolo"
|
||||
err = svc.Trustd.Validate(CheckTrustdAuth())
|
||||
suite.Require().NoError(err)
|
||||
|
||||
svc.Trustd.Token = ""
|
||||
svc.Trustd.Username = "bob"
|
||||
svc.Trustd.Password = "burger"
|
||||
err = svc.Trustd.Validate(CheckTrustdAuth())
|
||||
suite.Require().NoError(err)
|
||||
}
|
||||
|
||||
func (suite *validateSuite) TestValidateInit() {
|
||||
var err error
|
||||
|
||||
svc := &Services{}
|
||||
svc.Init = &Init{}
|
||||
err = svc.Init.Validate(CheckInitCNI())
|
||||
suite.Require().Error(err)
|
||||
if !xerrors.Is(err.(*multierror.Error).Errors[0], ErrUnsupportedCNI) {
|
||||
suite.T().Errorf("%+v", err)
|
||||
}
|
||||
|
||||
svc.Init.CNI = "calico"
|
||||
err = svc.Init.Validate(CheckInitCNI())
|
||||
suite.Require().NoError(err)
|
||||
}
|
@ -7,7 +7,6 @@ package userdata
|
||||
import (
|
||||
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()
|
||||
}
|
||||
|
@ -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
27
pkg/userdata/version.go
Normal file
@ -0,0 +1,27 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package userdata
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// Version represents the config file version
|
||||
type Version string
|
||||
|
||||
// Validate triggers the specified validation checks to run
|
||||
func (v Version) Validate() error {
|
||||
var result *multierror.Error
|
||||
if v == "" {
|
||||
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "version", "", ErrRequiredSection))
|
||||
}
|
||||
|
||||
if v != "1" {
|
||||
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "version", v, ErrInvalidVersion))
|
||||
}
|
||||
|
||||
return result.ErrorOrNil()
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user