feat: implement KubeSpan identity controller
Fixes #4138 When KubeSpan is enabled, Talos automatically generates or loads KubeSpan identity which consists of Wireguard key pair. ULA address is calculated based on ClusterID and first NIC MAC address. Some code was borrowed from #3577. Example: ``` $ talosctl -n 172.20.0.2 get ksi NODE NAMESPACE TYPE ID VERSION ADDRESS PUBLICKEY 172.20.0.2 kubespan KubeSpanIdentity local 1 fd71:6e1d:86be:6302:e871:1bff:feb2:ccee/128 Oak2fBEWngBhwslBxDVgnRNHXs88OAp4kjroSX0uqUE= ``` Additional changes: * `--with-kubespan` flag for `talosctl cluster create` for quick testing * validate that cluster discovery (and KubeSpan) requires ClusterID and ClusterSecret. Signed-off-by: Andrey Smirnov <andrey.smirnov@talos-systems.com> Signed-off-by: Seán C McCord <ulexus@gmail.com> Co-authored-by: Seán C McCord <ulexus@gmail.com>
This commit is contained in:
parent
da0f6e7e1d
commit
caee24bf61
@ -104,6 +104,7 @@ var (
|
||||
encryptStatePartition bool
|
||||
encryptEphemeralPartition bool
|
||||
useVIP bool
|
||||
enableKubeSpan bool
|
||||
configPatch string
|
||||
configPatchControlPlane string
|
||||
configPatchWorker string
|
||||
@ -382,6 +383,15 @@ func create(ctx context.Context) (err error) {
|
||||
)
|
||||
}
|
||||
|
||||
if enableKubeSpan {
|
||||
genOptions = append(genOptions,
|
||||
generate.WithNetworkOptions(
|
||||
v1alpha1.WithKubeSpan(),
|
||||
),
|
||||
generate.WithClusterDiscovery(),
|
||||
)
|
||||
}
|
||||
|
||||
defaultInternalLB, defaultEndpoint := provisioner.GetLoadBalancers(request.Network)
|
||||
|
||||
if defaultInternalLB == "" {
|
||||
@ -818,6 +828,7 @@ func init() {
|
||||
createCmd.Flags().BoolVar(&encryptEphemeralPartition, "encrypt-ephemeral", false, "enable ephemeral partition encryption")
|
||||
createCmd.Flags().StringVar(&talosVersion, "talos-version", "", "the desired Talos version to generate config for (if not set, defaults to image version)")
|
||||
createCmd.Flags().BoolVar(&useVIP, "use-vip", false, "use a virtual IP for the controlplane endpoint instead of the loadbalancer")
|
||||
createCmd.Flags().BoolVar(&enableKubeSpan, "with-kubespan", false, "enable KubeSpan system")
|
||||
createCmd.Flags().StringVar(&configPatch, "config-patch", "", "patch generated machineconfigs (applied to all node types)")
|
||||
createCmd.Flags().StringVar(&configPatchControlPlane, "config-patch-control-plane", "", "patch generated machineconfigs (applied to 'init' and 'controlplane' types)")
|
||||
createCmd.Flags().StringVar(&configPatchWorker, "config-patch-worker", "", "patch generated machineconfigs (applied to 'worker' type)")
|
||||
|
1
go.mod
1
go.mod
@ -58,6 +58,7 @@ require (
|
||||
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43
|
||||
github.com/mdlayher/genetlink v1.0.0
|
||||
github.com/mdlayher/netlink v1.4.1
|
||||
github.com/mdlayher/netx v0.0.0-20200512211805-669a06fde734
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417
|
||||
github.com/packethost/packngo v0.19.0
|
||||
|
2
go.sum
2
go.sum
@ -851,6 +851,8 @@ github.com/mdlayher/netlink v1.3.0/go.mod h1:xK/BssKuwcRXHrtN04UBkwQ6dY9VviGGuri
|
||||
github.com/mdlayher/netlink v1.4.0/go.mod h1:dRJi5IABcZpBD2A3D0Mv/AiX8I9uDEu5oGkAVrekmf8=
|
||||
github.com/mdlayher/netlink v1.4.1 h1:I154BCU+mKlIf7BgcAJB2r7QjveNPty6uNY1g9ChVfI=
|
||||
github.com/mdlayher/netlink v1.4.1/go.mod h1:e4/KuJ+s8UhfUpO9z00/fDZZmhSrs+oxyqAS9cNgn6Q=
|
||||
github.com/mdlayher/netx v0.0.0-20200512211805-669a06fde734 h1:DzkgdcT/W7794xU5P7GdZvok/lJECZ8g4xS+vMNLREI=
|
||||
github.com/mdlayher/netx v0.0.0-20200512211805-669a06fde734/go.mod h1:iN5Y6R8oOaC0KMzLtw/dqCJ2ZOipmk+bncXKStCHr7Q=
|
||||
github.com/mdlayher/raw v0.0.0-20190313224157-43dbcdd7739d/go.mod h1:r1fbeITl2xL/zLbVnNHFyOzQJTgr/3fpf1lJX/cjzR8=
|
||||
github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=
|
||||
github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 h1:aFkJ6lx4FPip+S+Uw4aTegFMct9shDvP+79PsSxpm3w=
|
||||
|
@ -7,17 +7,15 @@ package cluster
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
|
||||
"github.com/AlekSi/pointer"
|
||||
"github.com/cosi-project/runtime/pkg/controller"
|
||||
"github.com/cosi-project/runtime/pkg/resource"
|
||||
"github.com/cosi-project/runtime/pkg/state"
|
||||
"go.uber.org/zap"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/talos-systems/talos/internal/app/machined/pkg/controllers"
|
||||
"github.com/talos-systems/talos/internal/app/machined/pkg/runtime"
|
||||
"github.com/talos-systems/talos/pkg/machinery/constants"
|
||||
"github.com/talos-systems/talos/pkg/resources/cluster"
|
||||
@ -60,53 +58,6 @@ func (ctrl *NodeIdentityController) Outputs() []controller.Output {
|
||||
}
|
||||
}
|
||||
|
||||
func loadOrNewFromState(statePath, path string, empty interface{}, generate func(interface{}) error) error {
|
||||
fullPath := filepath.Join(statePath, path)
|
||||
|
||||
f, err := os.OpenFile(fullPath, os.O_RDONLY, 0)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("error reading state file: %w", err)
|
||||
}
|
||||
|
||||
// file doesn't exist yet, generate new value and save it
|
||||
if f == nil {
|
||||
if err = generate(empty); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err = os.OpenFile(fullPath, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0o600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating state file: %w", err)
|
||||
}
|
||||
|
||||
defer f.Close() //nolint:errcheck
|
||||
|
||||
encoder := yaml.NewEncoder(f)
|
||||
if err = encoder.Encode(empty); err != nil {
|
||||
return fmt.Errorf("error marshaling: %w", err)
|
||||
}
|
||||
|
||||
if err = encoder.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return f.Close()
|
||||
}
|
||||
|
||||
// read existing cached value
|
||||
defer f.Close() //nolint:errcheck
|
||||
|
||||
if err = yaml.NewDecoder(f).Decode(empty); err != nil {
|
||||
return fmt.Errorf("error unmarshaling: %w", err)
|
||||
}
|
||||
|
||||
if reflect.ValueOf(empty).Elem().IsZero() {
|
||||
return fmt.Errorf("value is still zero after unmarshaling")
|
||||
}
|
||||
|
||||
return f.Close()
|
||||
}
|
||||
|
||||
// Run implements controller.Controller interface.
|
||||
//
|
||||
//nolint:gocyclo
|
||||
@ -136,7 +87,7 @@ func (ctrl *NodeIdentityController) Run(ctx context.Context, r controller.Runtim
|
||||
|
||||
var localIdentity cluster.IdentitySpec
|
||||
|
||||
if err := loadOrNewFromState(ctrl.StatePath, constants.NodeIdentityFilename, &localIdentity, func(v interface{}) error {
|
||||
if err := controllers.LoadOrNewFromFile(filepath.Join(ctrl.StatePath, constants.NodeIdentityFilename), &localIdentity, func(v interface{}) error {
|
||||
return v.(*cluster.IdentitySpec).Generate()
|
||||
}); err != nil {
|
||||
return fmt.Errorf("error caching node identity: %w", err)
|
||||
|
103
internal/app/machined/pkg/controllers/kubespan/config.go
Normal file
103
internal/app/machined/pkg/controllers/kubespan/config.go
Normal file
@ -0,0 +1,103 @@
|
||||
// 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 kubespan
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/AlekSi/pointer"
|
||||
"github.com/cosi-project/runtime/pkg/controller"
|
||||
"github.com/cosi-project/runtime/pkg/resource"
|
||||
"github.com/cosi-project/runtime/pkg/state"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/talos-systems/talos/pkg/resources/config"
|
||||
"github.com/talos-systems/talos/pkg/resources/kubespan"
|
||||
)
|
||||
|
||||
// ConfigController watches v1alpha1.Config, updates KubeSpan config.
|
||||
type ConfigController struct{}
|
||||
|
||||
// Name implements controller.Controller interface.
|
||||
func (ctrl *ConfigController) Name() string {
|
||||
return "kubespan.ConfigController"
|
||||
}
|
||||
|
||||
// Inputs implements controller.Controller interface.
|
||||
func (ctrl *ConfigController) Inputs() []controller.Input {
|
||||
return []controller.Input{
|
||||
{
|
||||
Namespace: config.NamespaceName,
|
||||
Type: config.MachineConfigType,
|
||||
ID: pointer.ToString(config.V1Alpha1ID),
|
||||
Kind: controller.InputWeak,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Outputs implements controller.Controller interface.
|
||||
func (ctrl *ConfigController) Outputs() []controller.Output {
|
||||
return []controller.Output{
|
||||
{
|
||||
Type: kubespan.ConfigType,
|
||||
Kind: controller.OutputExclusive,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Run implements controller.Controller interface.
|
||||
//
|
||||
//nolint:gocyclo
|
||||
func (ctrl *ConfigController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-r.EventCh():
|
||||
cfg, err := r.Get(ctx, resource.NewMetadata(config.NamespaceName, config.MachineConfigType, config.V1Alpha1ID, resource.VersionUndefined))
|
||||
if err != nil {
|
||||
if !state.IsNotFoundError(err) {
|
||||
return fmt.Errorf("error getting config: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
touchedIDs := make(map[resource.ID]struct{})
|
||||
|
||||
if cfg != nil {
|
||||
c := cfg.(*config.MachineConfig).Config()
|
||||
|
||||
if err = r.Modify(ctx, kubespan.NewConfig(config.NamespaceName, kubespan.ConfigID), func(res resource.Resource) error {
|
||||
res.(*kubespan.Config).TypedSpec().Enabled = c.Machine().Network().KubeSpan().Enabled()
|
||||
res.(*kubespan.Config).TypedSpec().ClusterID = c.Cluster().ID()
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
touchedIDs[kubespan.ConfigID] = struct{}{}
|
||||
}
|
||||
|
||||
// list keys for cleanup
|
||||
list, err := r.List(ctx, resource.NewMetadata(config.NamespaceName, kubespan.ConfigType, "", resource.VersionUndefined))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error listing resources: %w", err)
|
||||
}
|
||||
|
||||
for _, res := range list.Items {
|
||||
if res.Metadata().Owner() != ctrl.Name() {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := touchedIDs[res.Metadata().ID()]; !ok {
|
||||
if err = r.Destroy(ctx, res.Metadata()); err != nil {
|
||||
return fmt.Errorf("error cleaning up specs: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
// 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 kubespan_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cosi-project/runtime/pkg/resource"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/talos-systems/go-retry/retry"
|
||||
|
||||
kubespanctrl "github.com/talos-systems/talos/internal/app/machined/pkg/controllers/kubespan"
|
||||
"github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1"
|
||||
"github.com/talos-systems/talos/pkg/resources/config"
|
||||
"github.com/talos-systems/talos/pkg/resources/kubespan"
|
||||
)
|
||||
|
||||
type ConfigSuite struct {
|
||||
KubeSpanSuite
|
||||
}
|
||||
|
||||
func (suite *ConfigSuite) TestReconcileConfig() {
|
||||
suite.Require().NoError(suite.runtime.RegisterController(&kubespanctrl.ConfigController{}))
|
||||
|
||||
suite.startRuntime()
|
||||
|
||||
cfg := config.NewMachineConfig(&v1alpha1.Config{
|
||||
ConfigVersion: "v1alpha1",
|
||||
MachineConfig: &v1alpha1.MachineConfig{
|
||||
MachineNetwork: &v1alpha1.NetworkConfig{
|
||||
NetworkKubeSpan: v1alpha1.NetworkKubeSpan{
|
||||
KubeSpanEnabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
ClusterConfig: &v1alpha1.ClusterConfig{
|
||||
ClusterID: "8XuV9TZHW08DOk3bVxQjH9ih_TBKjnh-j44tsCLSBzo=",
|
||||
},
|
||||
})
|
||||
|
||||
suite.Require().NoError(suite.state.Create(suite.ctx, cfg))
|
||||
|
||||
specMD := resource.NewMetadata(config.NamespaceName, kubespan.ConfigType, kubespan.ConfigID, resource.VersionUndefined)
|
||||
|
||||
suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry(
|
||||
suite.assertResource(
|
||||
specMD,
|
||||
func(res resource.Resource) error {
|
||||
spec := res.(*kubespan.Config).TypedSpec()
|
||||
|
||||
suite.Assert().True(spec.Enabled)
|
||||
suite.Assert().Equal("8XuV9TZHW08DOk3bVxQjH9ih_TBKjnh-j44tsCLSBzo=", spec.ClusterID)
|
||||
|
||||
return nil
|
||||
},
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
func (suite *ConfigSuite) TestReconcileDisabled() {
|
||||
suite.Require().NoError(suite.runtime.RegisterController(&kubespanctrl.ConfigController{}))
|
||||
|
||||
suite.startRuntime()
|
||||
|
||||
cfg := config.NewMachineConfig(&v1alpha1.Config{
|
||||
ConfigVersion: "v1alpha1",
|
||||
MachineConfig: &v1alpha1.MachineConfig{},
|
||||
ClusterConfig: &v1alpha1.ClusterConfig{},
|
||||
})
|
||||
|
||||
suite.Require().NoError(suite.state.Create(suite.ctx, cfg))
|
||||
|
||||
specMD := resource.NewMetadata(config.NamespaceName, kubespan.ConfigType, kubespan.ConfigID, resource.VersionUndefined)
|
||||
|
||||
suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry(
|
||||
suite.assertResource(
|
||||
specMD,
|
||||
func(res resource.Resource) error {
|
||||
spec := res.(*kubespan.Config).TypedSpec()
|
||||
|
||||
suite.Assert().False(spec.Enabled)
|
||||
|
||||
return nil
|
||||
},
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
func TestConfigSuite(t *testing.T) {
|
||||
suite.Run(t, new(ConfigSuite))
|
||||
}
|
152
internal/app/machined/pkg/controllers/kubespan/identity.go
Normal file
152
internal/app/machined/pkg/controllers/kubespan/identity.go
Normal file
@ -0,0 +1,152 @@
|
||||
// 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 kubespan
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/AlekSi/pointer"
|
||||
"github.com/cosi-project/runtime/pkg/controller"
|
||||
"github.com/cosi-project/runtime/pkg/resource"
|
||||
"github.com/cosi-project/runtime/pkg/state"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/talos-systems/talos/internal/app/machined/pkg/controllers"
|
||||
"github.com/talos-systems/talos/pkg/machinery/constants"
|
||||
"github.com/talos-systems/talos/pkg/resources/config"
|
||||
"github.com/talos-systems/talos/pkg/resources/kubespan"
|
||||
"github.com/talos-systems/talos/pkg/resources/network"
|
||||
runtimeres "github.com/talos-systems/talos/pkg/resources/runtime"
|
||||
"github.com/talos-systems/talos/pkg/resources/v1alpha1"
|
||||
)
|
||||
|
||||
// IdentityController watches KubeSpan configuration, updates KubeSpan Identity.
|
||||
type IdentityController struct {
|
||||
StatePath string
|
||||
}
|
||||
|
||||
// Name implements controller.Controller interface.
|
||||
func (ctrl *IdentityController) Name() string {
|
||||
return "kubespan.IdentityController"
|
||||
}
|
||||
|
||||
// Inputs implements controller.Controller interface.
|
||||
func (ctrl *IdentityController) Inputs() []controller.Input {
|
||||
return []controller.Input{
|
||||
{
|
||||
Namespace: config.NamespaceName,
|
||||
Type: kubespan.ConfigType,
|
||||
ID: pointer.ToString(kubespan.ConfigID),
|
||||
Kind: controller.InputWeak,
|
||||
},
|
||||
{
|
||||
Namespace: network.NamespaceName,
|
||||
Type: network.HardwareAddrType,
|
||||
ID: pointer.ToString(network.FirstHardwareAddr),
|
||||
Kind: controller.InputWeak,
|
||||
},
|
||||
{
|
||||
Namespace: v1alpha1.NamespaceName,
|
||||
Type: runtimeres.MountStatusType,
|
||||
ID: pointer.ToString(constants.StatePartitionLabel),
|
||||
Kind: controller.InputWeak,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Outputs implements controller.Controller interface.
|
||||
func (ctrl *IdentityController) Outputs() []controller.Output {
|
||||
return []controller.Output{
|
||||
{
|
||||
Type: kubespan.IdentityType,
|
||||
Kind: controller.OutputExclusive,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Run implements controller.Controller interface.
|
||||
//
|
||||
//nolint:gocyclo,cyclop
|
||||
func (ctrl *IdentityController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error {
|
||||
if ctrl.StatePath == "" {
|
||||
ctrl.StatePath = constants.StateMountPoint
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-r.EventCh():
|
||||
if _, err := r.Get(ctx, resource.NewMetadata(v1alpha1.NamespaceName, runtimeres.MountStatusType, constants.StatePartitionLabel, resource.VersionUndefined)); err != nil {
|
||||
if state.IsNotFoundError(err) {
|
||||
// wait for STATE to be mounted
|
||||
continue
|
||||
}
|
||||
|
||||
return fmt.Errorf("error reading mount status: %w", err)
|
||||
}
|
||||
|
||||
cfg, err := r.Get(ctx, resource.NewMetadata(config.NamespaceName, kubespan.ConfigType, kubespan.ConfigID, resource.VersionUndefined))
|
||||
if err != nil && !state.IsNotFoundError(err) {
|
||||
return fmt.Errorf("error getting kubespan configuration: %w", err)
|
||||
}
|
||||
|
||||
firstMAC, err := r.Get(ctx, resource.NewMetadata(network.NamespaceName, network.HardwareAddrType, network.FirstHardwareAddr, resource.VersionUndefined))
|
||||
if err != nil && !state.IsNotFoundError(err) {
|
||||
return fmt.Errorf("error getting first MAC address: %w", err)
|
||||
}
|
||||
|
||||
touchedIDs := make(map[resource.ID]struct{})
|
||||
|
||||
if cfg != nil && firstMAC != nil && cfg.(*kubespan.Config).TypedSpec().Enabled {
|
||||
var localIdentity kubespan.IdentitySpec
|
||||
|
||||
if err = controllers.LoadOrNewFromFile(filepath.Join(ctrl.StatePath, constants.KubeSpanIdentityFilename), &localIdentity, func(v interface{}) error {
|
||||
return v.(*kubespan.IdentitySpec).GenerateKey()
|
||||
}); err != nil {
|
||||
return fmt.Errorf("error caching kubespan identity: %w", err)
|
||||
}
|
||||
|
||||
kubespanCfg := cfg.(*kubespan.Config).TypedSpec()
|
||||
mac := firstMAC.(*network.HardwareAddr).TypedSpec()
|
||||
|
||||
if err = localIdentity.UpdateAddress(kubespanCfg.ClusterID, net.HardwareAddr(mac.HardwareAddr)); err != nil {
|
||||
return fmt.Errorf("error updating KubeSpan address: %w", err)
|
||||
}
|
||||
|
||||
if err = r.Modify(ctx, kubespan.NewIdentity(kubespan.NamespaceName, kubespan.LocalIdentity), func(res resource.Resource) error {
|
||||
*res.(*kubespan.Identity).TypedSpec() = localIdentity
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
touchedIDs[kubespan.LocalIdentity] = struct{}{}
|
||||
}
|
||||
|
||||
// list keys for cleanup
|
||||
list, err := r.List(ctx, resource.NewMetadata(kubespan.NamespaceName, kubespan.IdentityType, "", resource.VersionUndefined))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error listing resources: %w", err)
|
||||
}
|
||||
|
||||
for _, res := range list.Items {
|
||||
if res.Metadata().Owner() != ctrl.Name() {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := touchedIDs[res.Metadata().ID()]; !ok {
|
||||
if err = r.Destroy(ctx, res.Metadata()); err != nil {
|
||||
return fmt.Errorf("error cleaning up specs: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
141
internal/app/machined/pkg/controllers/kubespan/identity_test.go
Normal file
141
internal/app/machined/pkg/controllers/kubespan/identity_test.go
Normal file
@ -0,0 +1,141 @@
|
||||
// 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 kubespan_test
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cosi-project/runtime/pkg/resource"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/talos-systems/go-retry/retry"
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
|
||||
kubespanctrl "github.com/talos-systems/talos/internal/app/machined/pkg/controllers/kubespan"
|
||||
"github.com/talos-systems/talos/pkg/machinery/constants"
|
||||
"github.com/talos-systems/talos/pkg/machinery/nethelpers"
|
||||
"github.com/talos-systems/talos/pkg/resources/config"
|
||||
"github.com/talos-systems/talos/pkg/resources/kubespan"
|
||||
"github.com/talos-systems/talos/pkg/resources/network"
|
||||
runtimeres "github.com/talos-systems/talos/pkg/resources/runtime"
|
||||
"github.com/talos-systems/talos/pkg/resources/v1alpha1"
|
||||
)
|
||||
|
||||
type IdentitySuite struct {
|
||||
KubeSpanSuite
|
||||
|
||||
statePath string
|
||||
}
|
||||
|
||||
func (suite *IdentitySuite) TestGenerate() {
|
||||
suite.statePath = suite.T().TempDir()
|
||||
|
||||
suite.Require().NoError(suite.runtime.RegisterController(&kubespanctrl.IdentityController{
|
||||
StatePath: suite.statePath,
|
||||
}))
|
||||
|
||||
suite.startRuntime()
|
||||
|
||||
stateMount := runtimeres.NewMountStatus(v1alpha1.NamespaceName, constants.StatePartitionLabel)
|
||||
|
||||
suite.Assert().NoError(suite.state.Create(suite.ctx, stateMount))
|
||||
|
||||
cfg := kubespan.NewConfig(config.NamespaceName, kubespan.ConfigID)
|
||||
cfg.TypedSpec().Enabled = true
|
||||
cfg.TypedSpec().ClusterID = "8XuV9TZHW08DOk3bVxQjH9ih_TBKjnh-j44tsCLSBzo="
|
||||
|
||||
suite.Require().NoError(suite.state.Create(suite.ctx, cfg))
|
||||
|
||||
firstMac := network.NewHardwareAddr(network.NamespaceName, network.FirstHardwareAddr)
|
||||
mac, err := net.ParseMAC("ea:71:1b:b2:cc:ee")
|
||||
suite.Require().NoError(err)
|
||||
|
||||
firstMac.TypedSpec().HardwareAddr = nethelpers.HardwareAddr(mac)
|
||||
|
||||
suite.Require().NoError(suite.state.Create(suite.ctx, firstMac))
|
||||
|
||||
specMD := resource.NewMetadata(kubespan.NamespaceName, kubespan.IdentityType, kubespan.LocalIdentity, resource.VersionUndefined)
|
||||
|
||||
suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry(
|
||||
suite.assertResource(
|
||||
specMD,
|
||||
func(res resource.Resource) error {
|
||||
spec := res.(*kubespan.Identity).TypedSpec()
|
||||
|
||||
_, err := wgtypes.ParseKey(spec.PrivateKey)
|
||||
suite.Assert().NoError(err)
|
||||
|
||||
_, err = wgtypes.ParseKey(spec.PublicKey)
|
||||
suite.Assert().NoError(err)
|
||||
|
||||
suite.Assert().Equal("fd7f:175a:b97c:5602:e871:1bff:feb2:ccee/128", spec.Address.String())
|
||||
suite.Assert().Equal("fd7f:175a:b97c:5602::/64", spec.Subnet.String())
|
||||
|
||||
return nil
|
||||
},
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
func (suite *IdentitySuite) TestLoad() {
|
||||
// using verbatim data here to make sure nodeId representation is supported in future version fo Talos
|
||||
const identityYaml = `address: ""
|
||||
subnet: ""
|
||||
privateKey: sF45u5ePau58WeeCUY3T8D9foEKaQ8Opx4cGC8g4XE4=
|
||||
publicKey: Oak2fBEWngBhwslBxDVgnRNHXs88OAp4kjroSX0uqUE=
|
||||
`
|
||||
|
||||
suite.statePath = suite.T().TempDir()
|
||||
|
||||
suite.Require().NoError(suite.runtime.RegisterController(&kubespanctrl.IdentityController{
|
||||
StatePath: suite.statePath,
|
||||
}))
|
||||
|
||||
suite.startRuntime()
|
||||
|
||||
suite.Require().NoError(os.WriteFile(filepath.Join(suite.statePath, constants.KubeSpanIdentityFilename), []byte(identityYaml), 0o600))
|
||||
|
||||
stateMount := runtimeres.NewMountStatus(v1alpha1.NamespaceName, constants.StatePartitionLabel)
|
||||
|
||||
suite.Assert().NoError(suite.state.Create(suite.ctx, stateMount))
|
||||
|
||||
cfg := kubespan.NewConfig(config.NamespaceName, kubespan.ConfigID)
|
||||
cfg.TypedSpec().Enabled = true
|
||||
cfg.TypedSpec().ClusterID = "8XuV9TZHW08DOk3bVxQjH9ih_TBKjnh-j44tsCLSBzo="
|
||||
|
||||
suite.Require().NoError(suite.state.Create(suite.ctx, cfg))
|
||||
|
||||
firstMac := network.NewHardwareAddr(network.NamespaceName, network.FirstHardwareAddr)
|
||||
mac, err := net.ParseMAC("ea:71:1b:b2:cc:ee")
|
||||
suite.Require().NoError(err)
|
||||
|
||||
firstMac.TypedSpec().HardwareAddr = nethelpers.HardwareAddr(mac)
|
||||
|
||||
suite.Require().NoError(suite.state.Create(suite.ctx, firstMac))
|
||||
|
||||
specMD := resource.NewMetadata(kubespan.NamespaceName, kubespan.IdentityType, kubespan.LocalIdentity, resource.VersionUndefined)
|
||||
|
||||
suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry(
|
||||
suite.assertResource(
|
||||
specMD,
|
||||
func(res resource.Resource) error {
|
||||
spec := res.(*kubespan.Identity).TypedSpec()
|
||||
|
||||
suite.Assert().Equal("sF45u5ePau58WeeCUY3T8D9foEKaQ8Opx4cGC8g4XE4=", spec.PrivateKey)
|
||||
suite.Assert().Equal("Oak2fBEWngBhwslBxDVgnRNHXs88OAp4kjroSX0uqUE=", spec.PublicKey)
|
||||
suite.Assert().Equal("fd7f:175a:b97c:5602:e871:1bff:feb2:ccee/128", spec.Address.String())
|
||||
suite.Assert().Equal("fd7f:175a:b97c:5602::/64", spec.Subnet.String())
|
||||
|
||||
return nil
|
||||
},
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
func TestIdentitySuite(t *testing.T) {
|
||||
suite.Run(t, new(IdentitySuite))
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
// 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 kubespan provides controllers which manage Talos KubeSpan feature.
|
||||
package kubespan
|
@ -0,0 +1,93 @@
|
||||
// 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 kubespan_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cosi-project/runtime/pkg/controller/runtime"
|
||||
"github.com/cosi-project/runtime/pkg/resource"
|
||||
"github.com/cosi-project/runtime/pkg/state"
|
||||
"github.com/cosi-project/runtime/pkg/state/impl/inmem"
|
||||
"github.com/cosi-project/runtime/pkg/state/impl/namespaced"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/talos-systems/go-retry/retry"
|
||||
|
||||
"github.com/talos-systems/talos/pkg/logging"
|
||||
"github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1"
|
||||
"github.com/talos-systems/talos/pkg/resources/config"
|
||||
)
|
||||
|
||||
type KubeSpanSuite struct {
|
||||
suite.Suite
|
||||
|
||||
state state.State
|
||||
|
||||
runtime *runtime.Runtime
|
||||
wg sync.WaitGroup
|
||||
|
||||
ctx context.Context
|
||||
ctxCancel context.CancelFunc
|
||||
}
|
||||
|
||||
func (suite *KubeSpanSuite) SetupTest() {
|
||||
suite.ctx, suite.ctxCancel = context.WithTimeout(context.Background(), 3*time.Minute)
|
||||
|
||||
suite.state = state.WrapCore(namespaced.NewState(inmem.Build))
|
||||
|
||||
var err error
|
||||
|
||||
logger := logging.Wrap(log.Writer())
|
||||
|
||||
suite.runtime, err = runtime.NewRuntime(suite.state, logger)
|
||||
suite.Require().NoError(err)
|
||||
}
|
||||
|
||||
func (suite *KubeSpanSuite) startRuntime() {
|
||||
suite.wg.Add(1)
|
||||
|
||||
go func() {
|
||||
defer suite.wg.Done()
|
||||
|
||||
suite.Assert().NoError(suite.runtime.Run(suite.ctx))
|
||||
}()
|
||||
}
|
||||
|
||||
func (suite *KubeSpanSuite) assertResource(md resource.Metadata, check func(res resource.Resource) error) func() error {
|
||||
return func() error {
|
||||
r, err := suite.state.Get(suite.ctx, md)
|
||||
if err != nil {
|
||||
if state.IsNotFoundError(err) {
|
||||
return retry.ExpectedError(err)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return check(r)
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *KubeSpanSuite) TearDownTest() {
|
||||
suite.T().Log("tear down")
|
||||
|
||||
suite.ctxCancel()
|
||||
|
||||
suite.wg.Wait()
|
||||
|
||||
// trigger updates in resources to stop watch loops
|
||||
err := suite.state.Create(context.Background(), config.NewMachineConfig(&v1alpha1.Config{
|
||||
ConfigVersion: "v1alpha1",
|
||||
MachineConfig: &v1alpha1.MachineConfig{},
|
||||
}))
|
||||
if state.IsConflictError(err) {
|
||||
err = suite.state.Destroy(context.Background(), config.NewMachineConfig(nil).Metadata())
|
||||
}
|
||||
|
||||
suite.Assert().NoError(err)
|
||||
}
|
60
internal/app/machined/pkg/controllers/utils.go
Normal file
60
internal/app/machined/pkg/controllers/utils.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 controllers provides common methods for controller operations.
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
|
||||
yaml "gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// LoadOrNewFromFile either loads value from file.yaml or generates new values and saves as file.yaml.
|
||||
func LoadOrNewFromFile(path string, empty interface{}, generate func(interface{}) error) error {
|
||||
f, err := os.OpenFile(path, os.O_RDONLY, 0)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("error reading state file: %w", err)
|
||||
}
|
||||
|
||||
// file doesn't exist yet, generate new value and save it
|
||||
if f == nil {
|
||||
if err = generate(empty); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err = os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0o600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating state file: %w", err)
|
||||
}
|
||||
|
||||
defer f.Close() //nolint:errcheck
|
||||
|
||||
encoder := yaml.NewEncoder(f)
|
||||
if err = encoder.Encode(empty); err != nil {
|
||||
return fmt.Errorf("error marshaling: %w", err)
|
||||
}
|
||||
|
||||
if err = encoder.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return f.Close()
|
||||
}
|
||||
|
||||
// read existing cached value
|
||||
defer f.Close() //nolint:errcheck
|
||||
|
||||
if err = yaml.NewDecoder(f).Decode(empty); err != nil {
|
||||
return fmt.Errorf("error unmarshaling: %w", err)
|
||||
}
|
||||
|
||||
if reflect.ValueOf(empty).Elem().IsZero() {
|
||||
return fmt.Errorf("value is still zero after unmarshaling")
|
||||
}
|
||||
|
||||
return f.Close()
|
||||
}
|
@ -19,6 +19,7 @@ import (
|
||||
"github.com/talos-systems/talos/internal/app/machined/pkg/controllers/config"
|
||||
"github.com/talos-systems/talos/internal/app/machined/pkg/controllers/files"
|
||||
"github.com/talos-systems/talos/internal/app/machined/pkg/controllers/k8s"
|
||||
"github.com/talos-systems/talos/internal/app/machined/pkg/controllers/kubespan"
|
||||
"github.com/talos-systems/talos/internal/app/machined/pkg/controllers/network"
|
||||
"github.com/talos-systems/talos/internal/app/machined/pkg/controllers/perf"
|
||||
runtimecontrollers "github.com/talos-systems/talos/internal/app/machined/pkg/controllers/runtime"
|
||||
@ -75,7 +76,9 @@ func (ctrl *Controller) Run(ctx context.Context) error {
|
||||
&time.SyncController{
|
||||
V1Alpha1Mode: ctrl.v1alpha1Runtime.State().Platform().Mode(),
|
||||
},
|
||||
&cluster.NodeIdentityController{},
|
||||
&cluster.NodeIdentityController{
|
||||
V1Alpha1Mode: ctrl.v1alpha1Runtime.State().Platform().Mode(),
|
||||
},
|
||||
&config.MachineTypeController{},
|
||||
&config.K8sControlPlaneController{},
|
||||
&files.EtcFileController{
|
||||
@ -90,6 +93,8 @@ func (ctrl *Controller) Run(ctx context.Context) error {
|
||||
&k8s.ManifestApplyController{},
|
||||
&k8s.NodenameController{},
|
||||
&k8s.RenderSecretsStaticPodController{},
|
||||
&kubespan.ConfigController{},
|
||||
&kubespan.IdentityController{},
|
||||
&network.AddressConfigController{
|
||||
Cmdline: procfs.ProcCmdline(),
|
||||
V1Alpha1Mode: ctrl.v1alpha1Runtime.State().Platform().Mode(),
|
||||
|
@ -18,6 +18,7 @@ import (
|
||||
"github.com/talos-systems/talos/pkg/resources/config"
|
||||
"github.com/talos-systems/talos/pkg/resources/files"
|
||||
"github.com/talos-systems/talos/pkg/resources/k8s"
|
||||
"github.com/talos-systems/talos/pkg/resources/kubespan"
|
||||
"github.com/talos-systems/talos/pkg/resources/network"
|
||||
"github.com/talos-systems/talos/pkg/resources/perf"
|
||||
"github.com/talos-systems/talos/pkg/resources/runtime"
|
||||
@ -62,6 +63,7 @@ func NewState() (*State, error) {
|
||||
{config.NamespaceName, "Talos node configuration."},
|
||||
{files.NamespaceName, "Files and file-like resources."},
|
||||
{k8s.ControlPlaneNamespaceName, "Kubernetes control plane resources."},
|
||||
{kubespan.NamespaceName, "KubeSpan resources."},
|
||||
{network.NamespaceName, "Networking resources."},
|
||||
{network.ConfigNamespaceName, "Networking configuration resources."},
|
||||
{secrets.NamespaceName, "Resources with secret material."},
|
||||
@ -88,6 +90,8 @@ func NewState() (*State, error) {
|
||||
&k8s.StaticPod{},
|
||||
&k8s.StaticPodStatus{},
|
||||
&k8s.SecretsStatus{},
|
||||
&kubespan.Config{},
|
||||
&kubespan.Identity{},
|
||||
&network.AddressStatus{},
|
||||
&network.AddressSpec{},
|
||||
&network.HardwareAddr{},
|
||||
|
@ -85,6 +85,7 @@ type Input struct {
|
||||
Debug bool
|
||||
Persist bool
|
||||
AllowSchedulingOnMasters bool
|
||||
DiscoveryEnabled bool
|
||||
}
|
||||
|
||||
// GetAPIServerEndpoint returns the formatted host:port of the API server endpoint.
|
||||
@ -497,6 +498,7 @@ func NewInput(clustername, endpoint, kubernetesVersion string, secrets *SecretsB
|
||||
AllowSchedulingOnMasters: options.AllowSchedulingOnMasters,
|
||||
MachineDisks: options.MachineDisks,
|
||||
SystemDiskEncryptionConfig: options.SystemDiskEncryptionConfig,
|
||||
DiscoveryEnabled: options.DiscoveryEnabled,
|
||||
}
|
||||
|
||||
return input, nil
|
||||
|
@ -101,6 +101,9 @@ func initUd(in *Input) (*v1alpha1.Config, error) {
|
||||
ClusterAESCBCEncryptionSecret: in.Secrets.AESCBCEncryptionSecret,
|
||||
ExtraManifests: []string{},
|
||||
ClusterInlineManifests: v1alpha1.ClusterInlineManifests{},
|
||||
ClusterDiscoveryConfig: v1alpha1.ClusterDiscoveryConfig{
|
||||
DiscoveryEnabled: in.DiscoveryEnabled,
|
||||
},
|
||||
}
|
||||
|
||||
config.MachineConfig = machine
|
||||
|
@ -203,6 +203,15 @@ func WithRoles(roles role.Set) GenOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithClusterDiscovery enables cluster discovery feature.
|
||||
func WithClusterDiscovery() GenOption {
|
||||
return func(o *GenOptions) error {
|
||||
o.DiscoveryEnabled = true
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// GenOptions describes generate parameters.
|
||||
type GenOptions struct {
|
||||
EndpointList []string
|
||||
@ -222,6 +231,7 @@ type GenOptions struct {
|
||||
VersionContract *config.VersionContract
|
||||
SystemDiskEncryptionConfig *v1alpha1.SystemDiskEncryptionConfig
|
||||
Roles role.Set
|
||||
DiscoveryEnabled bool
|
||||
}
|
||||
|
||||
// DefaultGenOptions returns default options.
|
||||
|
@ -77,6 +77,9 @@ func workerUd(in *Input) (*v1alpha1.Config, error) {
|
||||
ServiceSubnet: in.ServiceNet,
|
||||
CNI: in.CNIConfig,
|
||||
},
|
||||
ClusterDiscoveryConfig: v1alpha1.ClusterDiscoveryConfig{
|
||||
DiscoveryEnabled: in.DiscoveryEnabled,
|
||||
},
|
||||
}
|
||||
|
||||
config.MachineConfig = machine
|
||||
|
@ -120,3 +120,12 @@ func WithNetworkInterfaceVirtualIP(iface, cidr string) NetworkConfigOption {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithKubeSpan configures a KubeSpan interface.
|
||||
func WithKubeSpan() NetworkConfigOption {
|
||||
return func(_ machine.Type, cfg *NetworkConfig) error {
|
||||
cfg.NetworkKubeSpan.KubeSpanEnabled = true
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -176,8 +176,18 @@ func (c *Config) Validate(mode config.RuntimeMode, options ...config.ValidationO
|
||||
}
|
||||
}
|
||||
|
||||
if c.Machine().Network().KubeSpan().Enabled() && !c.Cluster().Discovery().Enabled() {
|
||||
result = multierror.Append(result, fmt.Errorf(".cluster.discovery should be enabled when .machine.network.kubespan is enabled"))
|
||||
if c.Machine().Network().KubeSpan().Enabled() {
|
||||
if !c.Cluster().Discovery().Enabled() {
|
||||
result = multierror.Append(result, fmt.Errorf(".cluster.discovery should be enabled when .machine.network.kubespan is enabled"))
|
||||
}
|
||||
|
||||
if c.Cluster().ID() == "" {
|
||||
result = multierror.Append(result, fmt.Errorf(".cluster.id should be set when .machine.network.kubespan is enabled"))
|
||||
}
|
||||
|
||||
if c.Cluster().Secret() == "" {
|
||||
result = multierror.Append(result, fmt.Errorf(".cluster.secret should be set when .machine.network.kubespan is enabled"))
|
||||
}
|
||||
}
|
||||
|
||||
if opts.Strict {
|
||||
@ -215,7 +225,7 @@ func (c *ClusterConfig) Validate() error {
|
||||
result = multierror.Append(result, ecp.Validate())
|
||||
}
|
||||
|
||||
result = multierror.Append(result, c.ClusterInlineManifests.Validate(), c.ClusterDiscoveryConfig.Validate())
|
||||
result = multierror.Append(result, c.ClusterInlineManifests.Validate(), c.ClusterDiscoveryConfig.Validate(c))
|
||||
|
||||
return result.ErrorOrNil()
|
||||
}
|
||||
@ -296,14 +306,26 @@ func (manifests ClusterInlineManifests) Validate() error {
|
||||
}
|
||||
|
||||
// Validate the discovery config.
|
||||
func (c ClusterDiscoveryConfig) Validate() error {
|
||||
func (c ClusterDiscoveryConfig) Validate(clusterCfg *ClusterConfig) error {
|
||||
var result *multierror.Error
|
||||
|
||||
if !c.Enabled() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if c.Registries().Service().Enabled() {
|
||||
_, err := url.ParseRequestURI(c.Registries().Service().Endpoint())
|
||||
if err != nil {
|
||||
result = multierror.Append(result, fmt.Errorf("cluster discovery service registry endpoint is invalid: %w", err))
|
||||
}
|
||||
|
||||
if clusterCfg.ID() == "" {
|
||||
result = multierror.Append(result, fmt.Errorf("cluster discovery service requires .cluster.id"))
|
||||
}
|
||||
|
||||
if clusterCfg.Secret() == "" {
|
||||
result = multierror.Append(result, fmt.Errorf("cluster discovery service requires .cluster.secret"))
|
||||
}
|
||||
}
|
||||
|
||||
return result.ErrorOrNil()
|
||||
|
@ -744,7 +744,9 @@ func TestValidate(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "1 error occurred:\n\t* .cluster.discovery should be enabled when .machine.network.kubespan is enabled\n\n",
|
||||
expectedError: "3 errors occurred:\n\t* .cluster.discovery should be enabled when .machine.network.kubespan is enabled\n" +
|
||||
"\t* .cluster.id should be set when .machine.network.kubespan is enabled\n" +
|
||||
"\t* .cluster.secret should be set when .machine.network.kubespan is enabled\n\n",
|
||||
},
|
||||
{
|
||||
name: "DiscoveryServiceEndpoint",
|
||||
@ -754,6 +756,8 @@ func TestValidate(t *testing.T) {
|
||||
MachineType: "controlplane",
|
||||
},
|
||||
ClusterConfig: &v1alpha1.ClusterConfig{
|
||||
ClusterID: "foo",
|
||||
ClusterSecret: "bar",
|
||||
ControlPlane: &v1alpha1.ControlPlaneConfig{
|
||||
Endpoint: &v1alpha1.Endpoint{
|
||||
endpointURL,
|
||||
@ -771,6 +775,26 @@ func TestValidate(t *testing.T) {
|
||||
},
|
||||
expectedError: "1 error occurred:\n\t* cluster discovery service registry endpoint is invalid: parse \"foo\": invalid URI for request\n\n",
|
||||
},
|
||||
{
|
||||
name: "DiscoveryServiceClusterIDSecret",
|
||||
config: &v1alpha1.Config{
|
||||
ConfigVersion: "v1alpha1",
|
||||
MachineConfig: &v1alpha1.MachineConfig{
|
||||
MachineType: "controlplane",
|
||||
},
|
||||
ClusterConfig: &v1alpha1.ClusterConfig{
|
||||
ControlPlane: &v1alpha1.ControlPlaneConfig{
|
||||
Endpoint: &v1alpha1.Endpoint{
|
||||
endpointURL,
|
||||
},
|
||||
},
|
||||
ClusterDiscoveryConfig: v1alpha1.ClusterDiscoveryConfig{
|
||||
DiscoveryEnabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "2 errors occurred:\n\t* cluster discovery service requires .cluster.id\n\t* cluster discovery service requires .cluster.secret\n\n",
|
||||
},
|
||||
} {
|
||||
test := test
|
||||
|
||||
|
@ -467,6 +467,9 @@ const (
|
||||
|
||||
// DefaultDiscoveryServiceEndpoint is the default endpoint for Talos discovery service.
|
||||
DefaultDiscoveryServiceEndpoint = "https://discovery.talos.dev/"
|
||||
|
||||
// KubeSpanIdentityFilename is the filename to cache KubeSpan identity across reboots.
|
||||
KubeSpanIdentityFilename = "kubespan-identity.yaml"
|
||||
)
|
||||
|
||||
// See https://linux.die.net/man/3/klogctl
|
||||
|
84
pkg/resources/kubespan/config.go
Normal file
84
pkg/resources/kubespan/config.go
Normal file
@ -0,0 +1,84 @@
|
||||
// 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 kubespan
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cosi-project/runtime/pkg/resource"
|
||||
"github.com/cosi-project/runtime/pkg/resource/meta"
|
||||
|
||||
"github.com/talos-systems/talos/pkg/resources/config"
|
||||
)
|
||||
|
||||
// ConfigType is type of Config resource.
|
||||
const ConfigType = resource.Type("KubeSpanConfigs.kubespan.talos.dev")
|
||||
|
||||
// ConfigID the singleton config resource ID.
|
||||
const ConfigID = resource.ID("kubespan")
|
||||
|
||||
// LocalConfig is the resource ID for the local node Config.
|
||||
const LocalConfig = resource.ID("local")
|
||||
|
||||
// Config resource holds KubeSpan configuration.
|
||||
type Config struct {
|
||||
md resource.Metadata
|
||||
spec ConfigSpec
|
||||
}
|
||||
|
||||
// ConfigSpec describes KubeSpan configuration..
|
||||
type ConfigSpec struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
ClusterID string `yaml:"clusterId"`
|
||||
}
|
||||
|
||||
// NewConfig initializes a Config resource.
|
||||
func NewConfig(namespace resource.Namespace, id resource.ID) *Config {
|
||||
r := &Config{
|
||||
md: resource.NewMetadata(namespace, ConfigType, id, resource.VersionUndefined),
|
||||
spec: ConfigSpec{},
|
||||
}
|
||||
|
||||
r.md.BumpVersion()
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// Metadata implements resource.Resource.
|
||||
func (r *Config) Metadata() *resource.Metadata {
|
||||
return &r.md
|
||||
}
|
||||
|
||||
// Spec implements resource.Resource.
|
||||
func (r *Config) Spec() interface{} {
|
||||
return r.spec
|
||||
}
|
||||
|
||||
func (r *Config) String() string {
|
||||
return fmt.Sprintf("kubespan.Config(%q)", r.md.ID())
|
||||
}
|
||||
|
||||
// DeepCopy implements resource.Resource.
|
||||
func (r *Config) DeepCopy() resource.Resource {
|
||||
return &Config{
|
||||
md: r.md,
|
||||
spec: r.spec,
|
||||
}
|
||||
}
|
||||
|
||||
// ResourceDefinition implements meta.ResourceDefinitionProvider interface.
|
||||
func (r *Config) ResourceDefinition() meta.ResourceDefinitionSpec {
|
||||
return meta.ResourceDefinitionSpec{
|
||||
Type: ConfigType,
|
||||
Aliases: []resource.Type{},
|
||||
DefaultNamespace: config.NamespaceName,
|
||||
PrintColumns: []meta.PrintColumn{},
|
||||
}
|
||||
}
|
||||
|
||||
// TypedSpec allows to access the Spec with the proper type.
|
||||
func (r *Config) TypedSpec() *ConfigSpec {
|
||||
return &r.spec
|
||||
}
|
125
pkg/resources/kubespan/identity.go
Normal file
125
pkg/resources/kubespan/identity.go
Normal file
@ -0,0 +1,125 @@
|
||||
// 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 kubespan
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/cosi-project/runtime/pkg/resource"
|
||||
"github.com/cosi-project/runtime/pkg/resource/meta"
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
"inet.af/netaddr"
|
||||
|
||||
"github.com/talos-systems/talos/pkg/resources/network"
|
||||
)
|
||||
|
||||
// IdentityType is type of Identity resource.
|
||||
const IdentityType = resource.Type("KubeSpanIdentities.kubespan.talos.dev")
|
||||
|
||||
// LocalIdentity is the resource ID for the local node KubeSpan identity.
|
||||
const LocalIdentity = resource.ID("local")
|
||||
|
||||
// Identity resource holds node identity (as a member of the cluster).
|
||||
type Identity struct {
|
||||
md resource.Metadata
|
||||
spec IdentitySpec
|
||||
}
|
||||
|
||||
// IdentitySpec describes KubeSpan keys and address.
|
||||
//
|
||||
// Note: IdentitySpec is persisted on disk in the STATE partition,
|
||||
// so YAML serialization should be kept backwards compatible.
|
||||
type IdentitySpec struct {
|
||||
// Address of the node on the Wireguard network.
|
||||
Address netaddr.IPPrefix `yaml:"address"`
|
||||
Subnet netaddr.IPPrefix `yaml:"subnet"`
|
||||
// Public and private Wireguard keys.
|
||||
PrivateKey string `yaml:"privateKey"`
|
||||
PublicKey string `yaml:"publicKey"`
|
||||
}
|
||||
|
||||
// NewIdentity initializes a Identity resource.
|
||||
func NewIdentity(namespace resource.Namespace, id resource.ID) *Identity {
|
||||
r := &Identity{
|
||||
md: resource.NewMetadata(namespace, IdentityType, id, resource.VersionUndefined),
|
||||
spec: IdentitySpec{},
|
||||
}
|
||||
|
||||
r.md.BumpVersion()
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// Metadata implements resource.Resource.
|
||||
func (r *Identity) Metadata() *resource.Metadata {
|
||||
return &r.md
|
||||
}
|
||||
|
||||
// Spec implements resource.Resource.
|
||||
func (r *Identity) Spec() interface{} {
|
||||
return r.spec
|
||||
}
|
||||
|
||||
func (r *Identity) String() string {
|
||||
return fmt.Sprintf("kubespan.Identity(%q)", r.md.ID())
|
||||
}
|
||||
|
||||
// DeepCopy implements resource.Resource.
|
||||
func (r *Identity) DeepCopy() resource.Resource {
|
||||
return &Identity{
|
||||
md: r.md,
|
||||
spec: r.spec,
|
||||
}
|
||||
}
|
||||
|
||||
// ResourceDefinition implements meta.ResourceDefinitionProvider interface.
|
||||
func (r *Identity) ResourceDefinition() meta.ResourceDefinitionSpec {
|
||||
return meta.ResourceDefinitionSpec{
|
||||
Type: IdentityType,
|
||||
Aliases: []resource.Type{},
|
||||
DefaultNamespace: NamespaceName,
|
||||
PrintColumns: []meta.PrintColumn{
|
||||
{
|
||||
Name: "Address",
|
||||
JSONPath: `{.address}`,
|
||||
},
|
||||
{
|
||||
Name: "PublicKey",
|
||||
JSONPath: `{.publicKey}`,
|
||||
},
|
||||
},
|
||||
Sensitivity: meta.Sensitive,
|
||||
}
|
||||
}
|
||||
|
||||
// TypedSpec allows to access the Spec with the proper type.
|
||||
func (r *Identity) TypedSpec() *IdentitySpec {
|
||||
return &r.spec
|
||||
}
|
||||
|
||||
// GenerateKey generates new Wireguard key.
|
||||
func (spec *IdentitySpec) GenerateKey() error {
|
||||
key, err := wgtypes.GeneratePrivateKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
spec.PrivateKey = key.String()
|
||||
spec.PublicKey = key.PublicKey().String()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateAddress re-calculates node address based on input data.
|
||||
func (spec *IdentitySpec) UpdateAddress(clusterID string, mac net.HardwareAddr) error {
|
||||
spec.Subnet = network.ULAPrefix(clusterID, network.ULAKubeSpan)
|
||||
|
||||
var err error
|
||||
|
||||
spec.Address, err = wgEUI64(spec.Subnet, mac)
|
||||
|
||||
return err
|
||||
}
|
33
pkg/resources/kubespan/identity_test.go
Normal file
33
pkg/resources/kubespan/identity_test.go
Normal file
@ -0,0 +1,33 @@
|
||||
// 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 kubespan_test
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/talos-systems/talos/pkg/resources/kubespan"
|
||||
)
|
||||
|
||||
func TestIdentityGenerateKey(t *testing.T) {
|
||||
var spec kubespan.IdentitySpec
|
||||
|
||||
assert.NoError(t, spec.GenerateKey())
|
||||
}
|
||||
|
||||
func TestIdentityUpdateAddress(t *testing.T) {
|
||||
var spec kubespan.IdentitySpec
|
||||
|
||||
mac, err := net.ParseMAC("2e:1a:b6:53:81:69")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NoError(t, spec.UpdateAddress("8XuV9TZHW08DOk3bVxQjH9ih_TBKjnh-j44tsCLSBzo=", mac))
|
||||
|
||||
assert.Equal(t, "fd7f:175a:b97c:5602:2c1a:b6ff:fe53:8169/128", spec.Address.String())
|
||||
assert.Equal(t, "fd7f:175a:b97c:5602::/64", spec.Subnet.String())
|
||||
}
|
10
pkg/resources/kubespan/kubespan.go
Normal file
10
pkg/resources/kubespan/kubespan.go
Normal file
@ -0,0 +1,10 @@
|
||||
// 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 kubespan
|
||||
|
||||
import "github.com/cosi-project/runtime/pkg/resource"
|
||||
|
||||
// NamespaceName contains resources related to KubeSpan.
|
||||
const NamespaceName resource.Namespace = "kubespan"
|
33
pkg/resources/kubespan/kubespan_test.go
Normal file
33
pkg/resources/kubespan/kubespan_test.go
Normal file
@ -0,0 +1,33 @@
|
||||
// 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 kubespan_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/cosi-project/runtime/pkg/resource"
|
||||
"github.com/cosi-project/runtime/pkg/state"
|
||||
"github.com/cosi-project/runtime/pkg/state/impl/inmem"
|
||||
"github.com/cosi-project/runtime/pkg/state/impl/namespaced"
|
||||
"github.com/cosi-project/runtime/pkg/state/registry"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/talos-systems/talos/pkg/resources/kubespan"
|
||||
)
|
||||
|
||||
func TestRegisterResource(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
|
||||
resources := state.WrapCore(namespaced.NewState(inmem.Build))
|
||||
resourceRegistry := registry.NewResourceRegistry(resources)
|
||||
|
||||
for _, resource := range []resource.Resource{
|
||||
&kubespan.Config{},
|
||||
&kubespan.Identity{},
|
||||
} {
|
||||
assert.NoError(t, resourceRegistry.Register(ctx, resource))
|
||||
}
|
||||
}
|
31
pkg/resources/kubespan/utils.go
Normal file
31
pkg/resources/kubespan/utils.go
Normal file
@ -0,0 +1,31 @@
|
||||
// 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 kubespan
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/mdlayher/netx/eui64"
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
func wgEUI64(prefix netaddr.IPPrefix, mac net.HardwareAddr) (out netaddr.IPPrefix, err error) {
|
||||
if prefix.IsZero() {
|
||||
return out, fmt.Errorf("cannot calculate IP from zero prefix")
|
||||
}
|
||||
|
||||
stdIP, err := eui64.ParseMAC(prefix.IPNet().IP, mac)
|
||||
if err != nil {
|
||||
return out, fmt.Errorf("failed to parse MAC into EUI-64 address: %w", err)
|
||||
}
|
||||
|
||||
ip, ok := netaddr.FromStdIP(stdIP)
|
||||
if !ok {
|
||||
return out, fmt.Errorf("failed to parse intermediate standard IP %q: %w", stdIP.String(), err)
|
||||
}
|
||||
|
||||
return netaddr.IPPrefixFrom(ip, ip.BitLen()), nil
|
||||
}
|
45
pkg/resources/network/ula.go
Normal file
45
pkg/resources/network/ula.go
Normal file
@ -0,0 +1,45 @@
|
||||
// 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 network
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
// ULAPurpose is the Unique Local Addressing key for the Talos-specific purpose of the prefix.
|
||||
type ULAPurpose byte
|
||||
|
||||
const (
|
||||
// ULAUnknown indicates an unknown ULA Purpose.
|
||||
ULAUnknown = 0x00
|
||||
|
||||
// ULABootstrap is the Unique Local Addressing space key for the Talos Self-Bootstrapping protocol.
|
||||
ULABootstrap = 0x01
|
||||
|
||||
// ULAKubeSpan is the Unique Local Addressing space key for the Talos KubeSpan feature.
|
||||
ULAKubeSpan = 0x02
|
||||
)
|
||||
|
||||
// ULAPrefix calculates and returns a Talos-specific Unique Local Address prefix for the given purpose.
|
||||
// This implements a Talos-specific implementation of RFC4193.
|
||||
// The Talos implementation uses a combination of a 48-bit cluster-unique portion with an 8-bit purpose portion.
|
||||
func ULAPrefix(clusterID string, purpose ULAPurpose) netaddr.IPPrefix {
|
||||
var prefixData [16]byte
|
||||
|
||||
hash := sha256.Sum256([]byte(clusterID))
|
||||
|
||||
// Take the last 16 bytes of the clusterID's hash.
|
||||
copy(prefixData[:], hash[sha256.Size-16:])
|
||||
|
||||
// Apply the ULA prefix as per RFC4193
|
||||
prefixData[0] = 0xfd
|
||||
|
||||
// Apply the Talos-specific ULA Purpose suffix
|
||||
prefixData[7] = byte(purpose)
|
||||
|
||||
return netaddr.IPPrefixFrom(netaddr.IPFrom16(prefixData), 64).Masked()
|
||||
}
|
17
pkg/resources/network/ula_test.go
Normal file
17
pkg/resources/network/ula_test.go
Normal file
@ -0,0 +1,17 @@
|
||||
// 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 network_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/talos-systems/talos/pkg/resources/network"
|
||||
)
|
||||
|
||||
func TestULAPrefix(t *testing.T) {
|
||||
assert.Equal(t, "fd7f:175a:b97c:5602::/64", network.ULAPrefix("8XuV9TZHW08DOk3bVxQjH9ih_TBKjnh-j44tsCLSBzo=", network.ULAKubeSpan).String())
|
||||
}
|
@ -139,6 +139,7 @@ talosctl cluster create [flags]
|
||||
--with-bootloader enable bootloader to load kernel and initramfs from disk image after install (default true)
|
||||
--with-debug enable debug in Talos config to send service logs to the console
|
||||
--with-init-node create the cluster with an init node
|
||||
--with-kubespan enable KubeSpan system
|
||||
--with-uefi enable UEFI on x86_64 architecture (always enabled for arm64)
|
||||
--workers int the number of workers to create (default 1)
|
||||
```
|
||||
|
Loading…
x
Reference in New Issue
Block a user