feat: introduce support for Talos API access from Kubernetes
This is a first step: providing a service to access Talos API. Signed-off-by: Andrey Smirnov <andrey.smirnov@talos-systems.com>
This commit is contained in:
parent
34d3a41643
commit
3addea83b9
@ -280,6 +280,8 @@ func (ctrl *K8sControlPlaneController) manageManifestsConfig(ctx context.Context
|
||||
FlannelCNIImage: images.FlannelCNI,
|
||||
|
||||
PodSecurityPolicyEnabled: !cfgProvider.Cluster().APIServer().DisablePodSecurityPolicy(),
|
||||
|
||||
TalosAPIServiceEnabled: cfgProvider.Machine().Features().KubernetesTalosAPIAccess().Enabled(),
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -19,6 +19,7 @@ import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
k8sadapter "github.com/talos-systems/talos/internal/app/machined/pkg/adapters/k8s"
|
||||
"github.com/talos-systems/talos/pkg/machinery/constants"
|
||||
"github.com/talos-systems/talos/pkg/machinery/resources/k8s"
|
||||
"github.com/talos-systems/talos/pkg/machinery/resources/secrets"
|
||||
)
|
||||
@ -159,9 +160,19 @@ func (ctrl *ManifestController) render(cfg k8s.BootstrapManifestsConfigSpec, scr
|
||||
k8s.BootstrapManifestsConfigSpec
|
||||
|
||||
Secrets *secrets.KubernetesRootSpec
|
||||
|
||||
KubernetesTalosAPIServiceName string
|
||||
KubernetesTalosAPIServiceNamespace string
|
||||
|
||||
ApidPort int
|
||||
}{
|
||||
BootstrapManifestsConfigSpec: cfg,
|
||||
Secrets: scrt,
|
||||
|
||||
KubernetesTalosAPIServiceName: constants.KubernetesTalosAPIServiceName,
|
||||
KubernetesTalosAPIServiceNamespace: constants.KubernetesTalosAPIServiceNamespace,
|
||||
|
||||
ApidPort: constants.ApidPort,
|
||||
}
|
||||
|
||||
type manifestDesc struct {
|
||||
@ -211,6 +222,14 @@ func (ctrl *ManifestController) render(cfg k8s.BootstrapManifestsConfigSpec, scr
|
||||
)
|
||||
}
|
||||
|
||||
if cfg.TalosAPIServiceEnabled {
|
||||
defaultManifests = append(defaultManifests,
|
||||
[]manifestDesc{
|
||||
{"12-talos-api-service", talosAPIService},
|
||||
}...,
|
||||
)
|
||||
}
|
||||
|
||||
manifests := make([]renderedManifest, len(defaultManifests))
|
||||
|
||||
for i := range defaultManifests {
|
||||
|
@ -700,3 +700,21 @@ spec:
|
||||
- min: 1
|
||||
max: 65536
|
||||
`)
|
||||
|
||||
// talosAPIService is the service to access Talos API from Kubernetes.
|
||||
// Service exposes the Endpoints which are managed by controllers.
|
||||
var talosAPIService = []byte(`apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
component: apid
|
||||
provider: talos
|
||||
name: {{ .KubernetesTalosAPIServiceName }}
|
||||
namespace: {{ .KubernetesTalosAPIServiceNamespace }}
|
||||
spec:
|
||||
ports:
|
||||
- name: apid
|
||||
port: {{ .ApidPort }}
|
||||
protocol: TCP
|
||||
targetPort: {{ .ApidPort }}
|
||||
`)
|
||||
|
113
internal/app/machined/pkg/controllers/kubeaccess/config.go
Normal file
113
internal/app/machined/pkg/controllers/kubeaccess/config.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 kubeaccess
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/cosi-project/runtime/pkg/controller"
|
||||
"github.com/cosi-project/runtime/pkg/resource"
|
||||
"github.com/cosi-project/runtime/pkg/state"
|
||||
"github.com/siderolabs/go-pointer"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/talos-systems/talos/pkg/machinery/resources/config"
|
||||
"github.com/talos-systems/talos/pkg/machinery/resources/kubeaccess"
|
||||
)
|
||||
|
||||
// ConfigController watches v1alpha1.Config, updates Talos API access config.
|
||||
type ConfigController struct{}
|
||||
|
||||
// Name implements controller.Controller interface.
|
||||
func (ctrl *ConfigController) Name() string {
|
||||
return "kubeaccess.ConfigController"
|
||||
}
|
||||
|
||||
// Inputs implements controller.Controller interface.
|
||||
func (ctrl *ConfigController) Inputs() []controller.Input {
|
||||
return []controller.Input{
|
||||
{
|
||||
Namespace: config.NamespaceName,
|
||||
Type: config.MachineTypeType,
|
||||
ID: pointer.To(config.MachineTypeID),
|
||||
Kind: controller.InputWeak,
|
||||
},
|
||||
{
|
||||
Namespace: config.NamespaceName,
|
||||
Type: config.MachineConfigType,
|
||||
ID: pointer.To(config.V1Alpha1ID),
|
||||
Kind: controller.InputWeak,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Outputs implements controller.Controller interface.
|
||||
func (ctrl *ConfigController) Outputs() []controller.Output {
|
||||
return []controller.Output{
|
||||
{
|
||||
Type: kubeaccess.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():
|
||||
}
|
||||
|
||||
machineType, err := r.Get(ctx, resource.NewMetadata(config.NamespaceName, config.MachineTypeType, config.MachineTypeID, resource.VersionUndefined))
|
||||
if err != nil {
|
||||
if state.IsNotFoundError(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
return fmt.Errorf("error getting machine type: %w", err)
|
||||
}
|
||||
|
||||
if !machineType.(*config.MachineType).MachineType().IsControlPlane() {
|
||||
if err = r.Destroy(ctx, kubeaccess.NewConfig(config.NamespaceName, kubeaccess.ConfigID).Metadata()); err != nil {
|
||||
if !state.IsNotFoundError(err) {
|
||||
return fmt.Errorf("error destroying kubeaccess config: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// not a control plane node, nothing to do
|
||||
continue
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
if err = r.Modify(ctx, kubeaccess.NewConfig(config.NamespaceName, kubeaccess.ConfigID), func(res resource.Resource) error {
|
||||
spec := res.(*kubeaccess.Config).TypedSpec()
|
||||
|
||||
*spec = kubeaccess.ConfigSpec{}
|
||||
|
||||
if cfg != nil {
|
||||
c := cfg.(*config.MachineConfig).Config()
|
||||
|
||||
spec.Enabled = c.Machine().Features().KubernetesTalosAPIAccess().Enabled()
|
||||
spec.AllowedAPIRoles = c.Machine().Features().KubernetesTalosAPIAccess().AllowedRoles()
|
||||
spec.AllowedKubernetesNamespaces = c.Machine().Features().KubernetesTalosAPIAccess().AllowedKubernetesNamespaces()
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
139
internal/app/machined/pkg/controllers/kubeaccess/config_test.go
Normal file
139
internal/app/machined/pkg/controllers/kubeaccess/config_test.go
Normal file
@ -0,0 +1,139 @@
|
||||
// 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 kubeaccess_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cosi-project/runtime/pkg/resource"
|
||||
"github.com/siderolabs/go-pointer"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/talos-systems/go-retry/retry"
|
||||
|
||||
kubeaccessctrl "github.com/talos-systems/talos/internal/app/machined/pkg/controllers/kubeaccess"
|
||||
"github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1"
|
||||
"github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1/machine"
|
||||
"github.com/talos-systems/talos/pkg/machinery/resources/config"
|
||||
"github.com/talos-systems/talos/pkg/machinery/resources/kubeaccess"
|
||||
)
|
||||
|
||||
type ConfigSuite struct {
|
||||
KubeaccessSuite
|
||||
}
|
||||
|
||||
func (suite *ConfigSuite) TestReconcileConfig() {
|
||||
suite.Require().NoError(suite.runtime.RegisterController(&kubeaccessctrl.ConfigController{}))
|
||||
|
||||
suite.startRuntime()
|
||||
|
||||
machineType := config.NewMachineType()
|
||||
machineType.SetMachineType(machine.TypeControlPlane)
|
||||
|
||||
suite.Require().NoError(suite.state.Create(suite.ctx, machineType))
|
||||
|
||||
cfg := config.NewMachineConfig(&v1alpha1.Config{
|
||||
ConfigVersion: "v1alpha1",
|
||||
MachineConfig: &v1alpha1.MachineConfig{
|
||||
MachineFeatures: &v1alpha1.FeaturesConfig{
|
||||
KubernetesTalosAPIAccessConfig: &v1alpha1.KubernetesTalosAPIAccessConfig{
|
||||
AccessEnabled: pointer.To(true),
|
||||
AccessAllowedRoles: []string{"os:admin"},
|
||||
AccessAllowedKubernetesNamespaces: []string{"kube-system"},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
suite.Require().NoError(suite.state.Create(suite.ctx, cfg))
|
||||
|
||||
specMD := resource.NewMetadata(config.NamespaceName, kubeaccess.ConfigType, kubeaccess.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.(*kubeaccess.Config).TypedSpec()
|
||||
|
||||
suite.Assert().True(spec.Enabled)
|
||||
suite.Assert().Equal([]string{"os:admin"}, spec.AllowedAPIRoles)
|
||||
suite.Assert().Equal([]string{"kube-system"}, spec.AllowedKubernetesNamespaces)
|
||||
|
||||
return nil
|
||||
},
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
func (suite *ConfigSuite) TestReconcileDisabled() {
|
||||
suite.Require().NoError(suite.runtime.RegisterController(&kubeaccessctrl.ConfigController{}))
|
||||
|
||||
suite.startRuntime()
|
||||
|
||||
machineType := config.NewMachineType()
|
||||
machineType.SetMachineType(machine.TypeInit)
|
||||
|
||||
suite.Require().NoError(suite.state.Create(suite.ctx, machineType))
|
||||
|
||||
cfg := config.NewMachineConfig(&v1alpha1.Config{
|
||||
ConfigVersion: "v1alpha1",
|
||||
MachineConfig: &v1alpha1.MachineConfig{},
|
||||
})
|
||||
|
||||
suite.Require().NoError(suite.state.Create(suite.ctx, cfg))
|
||||
|
||||
specMD := resource.NewMetadata(config.NamespaceName, kubeaccess.ConfigType, kubeaccess.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.(*kubeaccess.Config).TypedSpec()
|
||||
|
||||
suite.Assert().False(spec.Enabled)
|
||||
suite.Assert().Empty(spec.AllowedAPIRoles)
|
||||
suite.Assert().Empty(spec.AllowedKubernetesNamespaces)
|
||||
|
||||
return nil
|
||||
},
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
func (suite *ConfigSuite) TestReconcileWorker() {
|
||||
suite.Require().NoError(suite.runtime.RegisterController(&kubeaccessctrl.ConfigController{}))
|
||||
|
||||
suite.startRuntime()
|
||||
|
||||
machineType := config.NewMachineType()
|
||||
machineType.SetMachineType(machine.TypeWorker)
|
||||
|
||||
suite.Require().NoError(suite.state.Create(suite.ctx, machineType))
|
||||
|
||||
cfg := config.NewMachineConfig(&v1alpha1.Config{
|
||||
ConfigVersion: "v1alpha1",
|
||||
MachineConfig: &v1alpha1.MachineConfig{
|
||||
MachineFeatures: &v1alpha1.FeaturesConfig{
|
||||
KubernetesTalosAPIAccessConfig: &v1alpha1.KubernetesTalosAPIAccessConfig{
|
||||
AccessEnabled: pointer.To(true),
|
||||
AccessAllowedRoles: []string{"os:admin"},
|
||||
AccessAllowedKubernetesNamespaces: []string{"kube-system"},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
suite.Require().NoError(suite.state.Create(suite.ctx, cfg))
|
||||
|
||||
// worker should have feature disabled even if it is enabled in the config
|
||||
specMD := resource.NewMetadata(config.NamespaceName, kubeaccess.ConfigType, kubeaccess.ConfigID, resource.VersionUndefined)
|
||||
|
||||
suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry(
|
||||
suite.assertNoResource(specMD)))
|
||||
}
|
||||
|
||||
func TestConfigSuite(t *testing.T) {
|
||||
suite.Run(t, new(ConfigSuite))
|
||||
}
|
249
internal/app/machined/pkg/controllers/kubeaccess/endpoint.go
Normal file
249
internal/app/machined/pkg/controllers/kubeaccess/endpoint.go
Normal file
@ -0,0 +1,249 @@
|
||||
// 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 kubeaccess
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/cosi-project/runtime/pkg/controller"
|
||||
"github.com/cosi-project/runtime/pkg/resource"
|
||||
"github.com/cosi-project/runtime/pkg/state"
|
||||
"github.com/siderolabs/go-pointer"
|
||||
"go.uber.org/zap"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
|
||||
"github.com/talos-systems/talos/pkg/kubernetes"
|
||||
"github.com/talos-systems/talos/pkg/machinery/constants"
|
||||
"github.com/talos-systems/talos/pkg/machinery/resources/config"
|
||||
"github.com/talos-systems/talos/pkg/machinery/resources/k8s"
|
||||
"github.com/talos-systems/talos/pkg/machinery/resources/kubeaccess"
|
||||
"github.com/talos-systems/talos/pkg/machinery/resources/secrets"
|
||||
)
|
||||
|
||||
// EndpointController manages Kubernetes endpoints resource for Talos API endpoints.
|
||||
type EndpointController struct{}
|
||||
|
||||
// Name implements controller.Controller interface.
|
||||
func (ctrl *EndpointController) Name() string {
|
||||
return "kubeaccess.EndpointController"
|
||||
}
|
||||
|
||||
// Inputs implements controller.Controller interface.
|
||||
func (ctrl *EndpointController) Inputs() []controller.Input {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Outputs implements controller.Controller interface.
|
||||
func (ctrl *EndpointController) Outputs() []controller.Output {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run implements controller.Controller interface.
|
||||
//
|
||||
//nolint:gocyclo
|
||||
func (ctrl *EndpointController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error {
|
||||
for {
|
||||
if err := r.UpdateInputs([]controller.Input{
|
||||
{
|
||||
Namespace: config.NamespaceName,
|
||||
Type: kubeaccess.ConfigType,
|
||||
ID: pointer.To(kubeaccess.ConfigID),
|
||||
Kind: controller.InputWeak,
|
||||
},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-r.EventCh():
|
||||
}
|
||||
|
||||
kubeaccessConfig, err := r.Get(ctx, kubeaccess.NewConfig(config.NamespaceName, kubeaccess.ConfigID).Metadata())
|
||||
if err != nil {
|
||||
if !state.IsNotFoundError(err) {
|
||||
return fmt.Errorf("error fetching kubeaccess config: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if kubeaccessConfig == nil || !kubeaccessConfig.(*kubeaccess.Config).TypedSpec().Enabled {
|
||||
// disabled, nothing to do
|
||||
continue
|
||||
}
|
||||
|
||||
if err = ctrl.reconcile(ctx, r, logger); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func (ctrl *EndpointController) reconcile(ctx context.Context, r controller.Runtime, logger *zap.Logger) error {
|
||||
if err := r.UpdateInputs([]controller.Input{
|
||||
{
|
||||
Namespace: config.NamespaceName,
|
||||
Type: kubeaccess.ConfigType,
|
||||
ID: pointer.To(kubeaccess.ConfigID),
|
||||
Kind: controller.InputWeak,
|
||||
},
|
||||
{
|
||||
Namespace: secrets.NamespaceName,
|
||||
Type: secrets.KubernetesType,
|
||||
ID: pointer.To(secrets.KubernetesID),
|
||||
Kind: controller.InputWeak,
|
||||
},
|
||||
{
|
||||
Namespace: k8s.ControlPlaneNamespaceName,
|
||||
Type: k8s.EndpointType,
|
||||
Kind: controller.InputWeak,
|
||||
},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.QueueReconcile()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-r.EventCh():
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
}
|
||||
|
||||
kubeaccessConfig, err := r.Get(ctx, kubeaccess.NewConfig(config.NamespaceName, kubeaccess.ConfigID).Metadata())
|
||||
if err != nil {
|
||||
if !state.IsNotFoundError(err) {
|
||||
return fmt.Errorf("error fetching kubeaccess config: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if kubeaccessConfig == nil || !kubeaccessConfig.(*kubeaccess.Config).TypedSpec().Enabled {
|
||||
// disabled, bail out
|
||||
return nil
|
||||
}
|
||||
|
||||
endpointResources, err := r.List(ctx, resource.NewMetadata(k8s.ControlPlaneNamespaceName, k8s.EndpointType, "", resource.VersionUndefined))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting endpoints resources: %w", err)
|
||||
}
|
||||
|
||||
var endpointAddrs k8s.EndpointList
|
||||
|
||||
// merge all endpoints into a single list
|
||||
for _, res := range endpointResources.Items {
|
||||
endpointAddrs = endpointAddrs.Merge(res.(*k8s.Endpoint))
|
||||
}
|
||||
|
||||
if len(endpointAddrs) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
secretsResources, err := r.Get(ctx, resource.NewMetadata(secrets.NamespaceName, secrets.KubernetesType, secrets.KubernetesID, resource.VersionUndefined))
|
||||
if err != nil {
|
||||
if state.IsNotFoundError(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
secrets := secretsResources.(*secrets.Kubernetes).TypedSpec()
|
||||
|
||||
kubeconfig, err := clientcmd.BuildConfigFromKubeconfigGetter("", func() (*clientcmdapi.Config, error) {
|
||||
return clientcmd.Load([]byte(secrets.LocalhostAdminKubeconfig))
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error loading kubeconfig: %w", err)
|
||||
}
|
||||
|
||||
if err = ctrl.updateTalosEndpoints(ctx, logger, kubeconfig, endpointAddrs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func (ctrl *EndpointController) updateTalosEndpoints(ctx context.Context, logger *zap.Logger, kubeconfig *rest.Config, endpointAddrs k8s.EndpointList) error {
|
||||
client, err := kubernetes.NewForConfig(kubeconfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error building Kubernetes client: %w", err)
|
||||
}
|
||||
|
||||
defer client.Close() //nolint:errcheck
|
||||
|
||||
for {
|
||||
oldEndpoints, err := client.CoreV1().Endpoints(constants.KubernetesTalosAPIServiceNamespace).Get(ctx, constants.KubernetesTalosAPIServiceName, metav1.GetOptions{})
|
||||
if err != nil && !apierrors.IsNotFound(err) {
|
||||
return fmt.Errorf("error getting endpoints: %w", err)
|
||||
}
|
||||
|
||||
var newEndpoints *corev1.Endpoints
|
||||
|
||||
if apierrors.IsNotFound(err) {
|
||||
newEndpoints = &corev1.Endpoints{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: constants.KubernetesTalosAPIServiceName,
|
||||
Namespace: constants.KubernetesTalosAPIServiceNamespace,
|
||||
Labels: map[string]string{
|
||||
"provider": constants.KubernetesTalosProvider,
|
||||
"component": "apid",
|
||||
},
|
||||
},
|
||||
}
|
||||
} else {
|
||||
newEndpoints = oldEndpoints.DeepCopy()
|
||||
}
|
||||
|
||||
newEndpoints.Subsets = []corev1.EndpointSubset{
|
||||
{
|
||||
Ports: []corev1.EndpointPort{
|
||||
{
|
||||
Name: "apid",
|
||||
Port: constants.ApidPort,
|
||||
Protocol: "TCP",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, addr := range endpointAddrs {
|
||||
newEndpoints.Subsets[0].Addresses = append(newEndpoints.Subsets[0].Addresses,
|
||||
corev1.EndpointAddress{
|
||||
IP: addr.String(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if oldEndpoints != nil && reflect.DeepEqual(oldEndpoints.Subsets, newEndpoints.Subsets) {
|
||||
// no change, bail out
|
||||
return nil
|
||||
}
|
||||
|
||||
if oldEndpoints == nil {
|
||||
_, err = client.CoreV1().Endpoints(constants.KubernetesTalosAPIServiceNamespace).Create(ctx, newEndpoints, metav1.CreateOptions{})
|
||||
} else {
|
||||
_, err = client.CoreV1().Endpoints(constants.KubernetesTalosAPIServiceNamespace).Update(ctx, newEndpoints, metav1.UpdateOptions{})
|
||||
}
|
||||
|
||||
switch {
|
||||
case err == nil:
|
||||
logger.Info("updated Talos API endpoints in Kubernetes", zap.Strings("endpoints", endpointAddrs.Strings()))
|
||||
|
||||
return nil
|
||||
case apierrors.IsConflict(err) || apierrors.IsAlreadyExists(err):
|
||||
// retry
|
||||
default:
|
||||
return fmt.Errorf("error updating Kubernetes Talos API endpoints: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
@ -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 kubeaccess provides controllers which manage Talos API access from Kubernetes workloads.
|
||||
package kubeaccess
|
@ -0,0 +1,112 @@
|
||||
// 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 kubeaccess_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/machinery/resources/config"
|
||||
)
|
||||
|
||||
type KubeaccessSuite struct {
|
||||
suite.Suite
|
||||
|
||||
state state.State
|
||||
|
||||
runtime *runtime.Runtime
|
||||
wg sync.WaitGroup
|
||||
|
||||
ctx context.Context //nolint:containedctx
|
||||
ctxCancel context.CancelFunc
|
||||
}
|
||||
|
||||
func (suite *KubeaccessSuite) 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 *KubeaccessSuite) startRuntime() {
|
||||
suite.wg.Add(1)
|
||||
|
||||
go func() {
|
||||
defer suite.wg.Done()
|
||||
|
||||
suite.Assert().NoError(suite.runtime.Run(suite.ctx))
|
||||
}()
|
||||
}
|
||||
|
||||
func (suite *KubeaccessSuite) 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 *KubeaccessSuite) assertNoResource(md resource.Metadata) func() error {
|
||||
return func() error {
|
||||
_, err := suite.state.Get(suite.ctx, md)
|
||||
if err == nil {
|
||||
return retry.ExpectedErrorf("resource %s still exists", md)
|
||||
}
|
||||
|
||||
if state.IsNotFoundError(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *KubeaccessSuite) 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)
|
||||
}
|
@ -18,6 +18,7 @@ import (
|
||||
|
||||
talosconfig "github.com/talos-systems/talos/pkg/machinery/config"
|
||||
"github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1/machine"
|
||||
"github.com/talos-systems/talos/pkg/machinery/constants"
|
||||
"github.com/talos-systems/talos/pkg/machinery/resources/config"
|
||||
"github.com/talos-systems/talos/pkg/machinery/resources/secrets"
|
||||
)
|
||||
@ -146,6 +147,14 @@ func (ctrl *RootController) updateOSSecrets(cfgProvider talosconfig.Provider, os
|
||||
}
|
||||
}
|
||||
|
||||
if cfgProvider.Machine().Features().KubernetesTalosAPIAccess().Enabled() {
|
||||
// add Kubernetes Talos service name to the list of SANs
|
||||
osSecrets.CertSANDNSNames = append(osSecrets.CertSANDNSNames,
|
||||
constants.KubernetesTalosAPIServiceName,
|
||||
constants.KubernetesTalosAPIServiceName+"."+constants.KubernetesTalosAPIServiceNamespace,
|
||||
)
|
||||
}
|
||||
|
||||
osSecrets.Token = cfgProvider.Machine().Security().Token()
|
||||
|
||||
return nil
|
||||
|
@ -132,6 +132,7 @@ func (r *Runtime) CanApplyImmediate(cfg config.Provider) error {
|
||||
// * .machine.kernel
|
||||
// * .machine.registries (note that auth is not applied immediately, containerd limitation)
|
||||
// * .machine.pods
|
||||
// * .machine.features.kubernetesTalosAPIAccess
|
||||
newConfig.ConfigDebug = currentConfig.ConfigDebug
|
||||
newConfig.ClusterConfig = currentConfig.ClusterConfig
|
||||
|
||||
@ -148,6 +149,10 @@ func (r *Runtime) CanApplyImmediate(cfg config.Provider) error {
|
||||
newConfig.MachineConfig.MachineKernel = currentConfig.MachineConfig.MachineKernel
|
||||
newConfig.MachineConfig.MachineRegistries = currentConfig.MachineConfig.MachineRegistries
|
||||
newConfig.MachineConfig.MachinePods = currentConfig.MachineConfig.MachinePods
|
||||
|
||||
if newConfig.MachineConfig.MachineFeatures != nil && currentConfig.MachineConfig.MachineFeatures != nil {
|
||||
newConfig.MachineConfig.MachineFeatures.KubernetesTalosAPIAccessConfig = currentConfig.MachineConfig.MachineFeatures.KubernetesTalosAPIAccessConfig
|
||||
}
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(currentConfig, newConfig) {
|
||||
|
@ -25,6 +25,7 @@ import (
|
||||
"github.com/talos-systems/talos/internal/app/machined/pkg/controllers/files"
|
||||
"github.com/talos-systems/talos/internal/app/machined/pkg/controllers/hardware"
|
||||
"github.com/talos-systems/talos/internal/app/machined/pkg/controllers/k8s"
|
||||
"github.com/talos-systems/talos/internal/app/machined/pkg/controllers/kubeaccess"
|
||||
"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"
|
||||
@ -134,6 +135,8 @@ func (ctrl *Controller) Run(ctx context.Context, drainer *runtime.Drainer) error
|
||||
&k8s.RenderConfigsStaticPodController{},
|
||||
&k8s.RenderSecretsStaticPodController{},
|
||||
&k8s.StaticPodConfigController{},
|
||||
&kubeaccess.ConfigController{},
|
||||
&kubeaccess.EndpointController{},
|
||||
&kubespan.ConfigController{},
|
||||
&kubespan.EndpointController{},
|
||||
&kubespan.IdentityController{},
|
||||
|
@ -20,6 +20,7 @@ import (
|
||||
"github.com/talos-systems/talos/pkg/machinery/resources/files"
|
||||
"github.com/talos-systems/talos/pkg/machinery/resources/hardware"
|
||||
"github.com/talos-systems/talos/pkg/machinery/resources/k8s"
|
||||
"github.com/talos-systems/talos/pkg/machinery/resources/kubeaccess"
|
||||
"github.com/talos-systems/talos/pkg/machinery/resources/kubespan"
|
||||
"github.com/talos-systems/talos/pkg/machinery/resources/network"
|
||||
"github.com/talos-systems/talos/pkg/machinery/resources/perf"
|
||||
@ -114,6 +115,7 @@ func NewState() (*State, error) {
|
||||
&k8s.StaticPod{},
|
||||
&k8s.StaticPodStatus{},
|
||||
&k8s.SecretsStatus{},
|
||||
&kubeaccess.Config{},
|
||||
&kubespan.Config{},
|
||||
&kubespan.Endpoint{},
|
||||
&kubespan.Identity{},
|
||||
|
@ -515,6 +515,14 @@ type SystemDiskEncryption interface {
|
||||
type Features interface {
|
||||
RBACEnabled() bool
|
||||
StableHostnameEnabled() bool
|
||||
KubernetesTalosAPIAccess() KubernetesTalosAPIAccess
|
||||
}
|
||||
|
||||
// KubernetesTalosAPIAccess describes the Kubernetes Talos API access features.
|
||||
type KubernetesTalosAPIAccess interface {
|
||||
Enabled() bool
|
||||
AllowedRoles() []string
|
||||
AllowedKubernetesNamespaces() []string
|
||||
}
|
||||
|
||||
// VolumeMount describes extra volume mount for the static pods.
|
||||
|
@ -59,3 +59,8 @@ func (t *Type) UnmarshalText(text []byte) error {
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// IsControlPlane returns true if the type is a control plane node.
|
||||
func (t Type) IsControlPlane() bool {
|
||||
return t == TypeControlPlane || t == TypeInit
|
||||
}
|
||||
|
@ -4,7 +4,11 @@
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import "github.com/siderolabs/go-pointer"
|
||||
import (
|
||||
"github.com/siderolabs/go-pointer"
|
||||
|
||||
"github.com/talos-systems/talos/pkg/machinery/config"
|
||||
)
|
||||
|
||||
// RBACEnabled implements config.Features interface.
|
||||
func (f *FeaturesConfig) RBACEnabled() bool {
|
||||
@ -19,3 +23,8 @@ func (f *FeaturesConfig) RBACEnabled() bool {
|
||||
func (f *FeaturesConfig) StableHostnameEnabled() bool {
|
||||
return pointer.SafeDeref(f.StableHostname)
|
||||
}
|
||||
|
||||
// KubernetesTalosAPIAccess implements config.Features interface.
|
||||
func (f *FeaturesConfig) KubernetesTalosAPIAccess() config.KubernetesTalosAPIAccess {
|
||||
return f.KubernetesTalosAPIAccessConfig
|
||||
}
|
||||
|
@ -0,0 +1,34 @@
|
||||
// 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 v1alpha1
|
||||
|
||||
import "github.com/siderolabs/go-pointer"
|
||||
|
||||
// Enabled implements config.KubernetesTalosAPIAccess.
|
||||
func (c *KubernetesTalosAPIAccessConfig) Enabled() bool {
|
||||
if c == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return pointer.SafeDeref(c.AccessEnabled)
|
||||
}
|
||||
|
||||
// AllowedRoles implements config.KubernetesTalosAPIAccess.
|
||||
func (c *KubernetesTalosAPIAccessConfig) AllowedRoles() []string {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.AccessAllowedRoles
|
||||
}
|
||||
|
||||
// AllowedKubernetesNamespaces implements config.KubernetesTalosAPIAccess.
|
||||
func (c *KubernetesTalosAPIAccessConfig) AllowedKubernetesNamespaces() []string {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.AccessAllowedKubernetesNamespaces
|
||||
}
|
@ -588,6 +588,16 @@ metadata:
|
||||
ExtensionImage: "ghcr.io/siderolabs/gvisor:20220117.0-v1.0.0",
|
||||
},
|
||||
}
|
||||
|
||||
kubernetesTalosAPIAccessConfigExample = &KubernetesTalosAPIAccessConfig{
|
||||
AccessEnabled: pointer.To(true),
|
||||
AccessAllowedRoles: []string{
|
||||
"os:reader",
|
||||
},
|
||||
AccessAllowedKubernetesNamespaces: []string{
|
||||
"kube-system",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Config defines the v1alpha1 configuration file.
|
||||
@ -2274,7 +2284,7 @@ type SystemDiskEncryptionConfig struct {
|
||||
EphemeralPartition *EncryptionConfig `yaml:"ephemeral,omitempty"`
|
||||
}
|
||||
|
||||
// FeaturesConfig describe individual Talos features that can be switched on or off.
|
||||
// FeaturesConfig describes individual Talos features that can be switched on or off.
|
||||
type FeaturesConfig struct {
|
||||
// description: |
|
||||
// Enable role-based access control (RBAC).
|
||||
@ -2282,6 +2292,28 @@ type FeaturesConfig struct {
|
||||
// description: |
|
||||
// Enable stable default hostname.
|
||||
StableHostname *bool `yaml:"stableHostname,omitempty"`
|
||||
// description: |
|
||||
// Configure Talos API access from Kubernetes pods.
|
||||
//
|
||||
// This feature is disabled if the feature config is not specified.
|
||||
// examples:
|
||||
// - value: kubernetesTalosAPIAccessConfigExample
|
||||
KubernetesTalosAPIAccessConfig *KubernetesTalosAPIAccessConfig `yaml:"kubernetesTalosAPIAccess,omitempty"`
|
||||
}
|
||||
|
||||
// KubernetesTalosAPIAccessConfig describes the configuration for the Talos API access from Kubernetes pods.
|
||||
type KubernetesTalosAPIAccessConfig struct {
|
||||
// description: |
|
||||
// Enable Talos API access from Kubernetes pods.
|
||||
AccessEnabled *bool `yaml:"enabled,omitempty"`
|
||||
// description: |
|
||||
// The list of Talos API roles which can be granted for access from Kubernetes pods.
|
||||
//
|
||||
// Empty list means that no roles can be granted, so access is blocked.
|
||||
AccessAllowedRoles []string `yaml:"allowedRoles,omitempty"`
|
||||
// description: |
|
||||
// The list of Kubernetes namespaces Talos API access is available from.
|
||||
AccessAllowedKubernetesNamespaces []string `yaml:"allowedKubernetesNamespaces,omitempty"`
|
||||
}
|
||||
|
||||
// VolumeMountConfig struct describes extra volume mount for the static pods.
|
||||
|
@ -69,6 +69,7 @@ var (
|
||||
RegistryTLSConfigDoc encoder.Doc
|
||||
SystemDiskEncryptionConfigDoc encoder.Doc
|
||||
FeaturesConfigDoc encoder.Doc
|
||||
KubernetesTalosAPIAccessConfigDoc encoder.Doc
|
||||
VolumeMountConfigDoc encoder.Doc
|
||||
ClusterInlineManifestDoc encoder.Doc
|
||||
NetworkKubeSpanDoc encoder.Doc
|
||||
@ -2240,8 +2241,8 @@ func init() {
|
||||
SystemDiskEncryptionConfigDoc.Fields[1].Comments[encoder.LineComment] = "Ephemeral partition encryption."
|
||||
|
||||
FeaturesConfigDoc.Type = "FeaturesConfig"
|
||||
FeaturesConfigDoc.Comments[encoder.LineComment] = "FeaturesConfig describe individual Talos features that can be switched on or off."
|
||||
FeaturesConfigDoc.Description = "FeaturesConfig describe individual Talos features that can be switched on or off."
|
||||
FeaturesConfigDoc.Comments[encoder.LineComment] = "FeaturesConfig describes individual Talos features that can be switched on or off."
|
||||
FeaturesConfigDoc.Description = "FeaturesConfig describes individual Talos features that can be switched on or off."
|
||||
|
||||
FeaturesConfigDoc.AddExample("", machineFeaturesExample)
|
||||
FeaturesConfigDoc.AppearsIn = []encoder.Appearance{
|
||||
@ -2250,7 +2251,7 @@ func init() {
|
||||
FieldName: "features",
|
||||
},
|
||||
}
|
||||
FeaturesConfigDoc.Fields = make([]encoder.Doc, 2)
|
||||
FeaturesConfigDoc.Fields = make([]encoder.Doc, 3)
|
||||
FeaturesConfigDoc.Fields[0].Name = "rbac"
|
||||
FeaturesConfigDoc.Fields[0].Type = "bool"
|
||||
FeaturesConfigDoc.Fields[0].Note = ""
|
||||
@ -2261,6 +2262,41 @@ func init() {
|
||||
FeaturesConfigDoc.Fields[1].Note = ""
|
||||
FeaturesConfigDoc.Fields[1].Description = "Enable stable default hostname."
|
||||
FeaturesConfigDoc.Fields[1].Comments[encoder.LineComment] = "Enable stable default hostname."
|
||||
FeaturesConfigDoc.Fields[2].Name = "kubernetesTalosAPIAccess"
|
||||
FeaturesConfigDoc.Fields[2].Type = "KubernetesTalosAPIAccessConfig"
|
||||
FeaturesConfigDoc.Fields[2].Note = ""
|
||||
FeaturesConfigDoc.Fields[2].Description = "Configure Talos API access from Kubernetes pods.\n\nThis feature is disabled if the feature config is not specified."
|
||||
FeaturesConfigDoc.Fields[2].Comments[encoder.LineComment] = "Configure Talos API access from Kubernetes pods."
|
||||
|
||||
FeaturesConfigDoc.Fields[2].AddExample("", kubernetesTalosAPIAccessConfigExample)
|
||||
|
||||
KubernetesTalosAPIAccessConfigDoc.Type = "KubernetesTalosAPIAccessConfig"
|
||||
KubernetesTalosAPIAccessConfigDoc.Comments[encoder.LineComment] = "KubernetesTalosAPIAccessConfig describes the configuration for the Talos API access from Kubernetes pods."
|
||||
KubernetesTalosAPIAccessConfigDoc.Description = "KubernetesTalosAPIAccessConfig describes the configuration for the Talos API access from Kubernetes pods."
|
||||
|
||||
KubernetesTalosAPIAccessConfigDoc.AddExample("", kubernetesTalosAPIAccessConfigExample)
|
||||
KubernetesTalosAPIAccessConfigDoc.AppearsIn = []encoder.Appearance{
|
||||
{
|
||||
TypeName: "FeaturesConfig",
|
||||
FieldName: "kubernetesTalosAPIAccess",
|
||||
},
|
||||
}
|
||||
KubernetesTalosAPIAccessConfigDoc.Fields = make([]encoder.Doc, 3)
|
||||
KubernetesTalosAPIAccessConfigDoc.Fields[0].Name = "enabled"
|
||||
KubernetesTalosAPIAccessConfigDoc.Fields[0].Type = "bool"
|
||||
KubernetesTalosAPIAccessConfigDoc.Fields[0].Note = ""
|
||||
KubernetesTalosAPIAccessConfigDoc.Fields[0].Description = "Enable Talos API access from Kubernetes pods."
|
||||
KubernetesTalosAPIAccessConfigDoc.Fields[0].Comments[encoder.LineComment] = "Enable Talos API access from Kubernetes pods."
|
||||
KubernetesTalosAPIAccessConfigDoc.Fields[1].Name = "allowedRoles"
|
||||
KubernetesTalosAPIAccessConfigDoc.Fields[1].Type = "[]string"
|
||||
KubernetesTalosAPIAccessConfigDoc.Fields[1].Note = ""
|
||||
KubernetesTalosAPIAccessConfigDoc.Fields[1].Description = "The list of Talos API roles which can be granted for access from Kubernetes pods.\n\nEmpty list means that no roles can be granted, so access is blocked."
|
||||
KubernetesTalosAPIAccessConfigDoc.Fields[1].Comments[encoder.LineComment] = "The list of Talos API roles which can be granted for access from Kubernetes pods."
|
||||
KubernetesTalosAPIAccessConfigDoc.Fields[2].Name = "allowedKubernetesNamespaces"
|
||||
KubernetesTalosAPIAccessConfigDoc.Fields[2].Type = "[]string"
|
||||
KubernetesTalosAPIAccessConfigDoc.Fields[2].Note = ""
|
||||
KubernetesTalosAPIAccessConfigDoc.Fields[2].Description = "The list of Kubernetes namespaces Talos API access is available from."
|
||||
KubernetesTalosAPIAccessConfigDoc.Fields[2].Comments[encoder.LineComment] = "The list of Kubernetes namespaces Talos API access is available from."
|
||||
|
||||
VolumeMountConfigDoc.Type = "VolumeMountConfig"
|
||||
VolumeMountConfigDoc.Comments[encoder.LineComment] = "VolumeMountConfig struct describes extra volume mount for the static pods."
|
||||
@ -2781,6 +2817,10 @@ func (_ FeaturesConfig) Doc() *encoder.Doc {
|
||||
return &FeaturesConfigDoc
|
||||
}
|
||||
|
||||
func (_ KubernetesTalosAPIAccessConfig) Doc() *encoder.Doc {
|
||||
return &KubernetesTalosAPIAccessConfigDoc
|
||||
}
|
||||
|
||||
func (_ VolumeMountConfig) Doc() *encoder.Doc {
|
||||
return &VolumeMountConfigDoc
|
||||
}
|
||||
@ -2894,6 +2934,7 @@ func GetConfigurationDoc() *encoder.FileDoc {
|
||||
&RegistryTLSConfigDoc,
|
||||
&SystemDiskEncryptionConfigDoc,
|
||||
&FeaturesConfigDoc,
|
||||
&KubernetesTalosAPIAccessConfigDoc,
|
||||
&VolumeMountConfigDoc,
|
||||
&ClusterInlineManifestDoc,
|
||||
&NetworkKubeSpanDoc,
|
||||
|
@ -254,6 +254,14 @@ func (c *Config) Validate(mode config.RuntimeMode, options ...config.ValidationO
|
||||
}
|
||||
}
|
||||
|
||||
if c.Machine().Features().KubernetesTalosAPIAccess().Enabled() && !c.Machine().Features().RBACEnabled() {
|
||||
result = multierror.Append(result, fmt.Errorf("feature API RBAC should be enabled when Kubernetes Talos API Access feature is enabled"))
|
||||
}
|
||||
|
||||
if c.Machine().Features().KubernetesTalosAPIAccess().Enabled() && !c.Machine().Type().IsControlPlane() {
|
||||
result = multierror.Append(result, fmt.Errorf("feature Kubernetes Talos API Access can only be enabled on control plane machines"))
|
||||
}
|
||||
|
||||
if opts.Strict {
|
||||
for _, w := range warnings {
|
||||
result = multierror.Append(result, fmt.Errorf("warning: %s", w))
|
||||
|
@ -1138,6 +1138,51 @@ func TestValidate(t *testing.T) {
|
||||
},
|
||||
expectedError: "1 error occurred:\n\t* [networking.os.device.deviceSelector]: config section should contain at least one field\n\n",
|
||||
},
|
||||
{
|
||||
name: "TalosAPIAccessRBAC",
|
||||
config: &v1alpha1.Config{
|
||||
ConfigVersion: "v1alpha1",
|
||||
MachineConfig: &v1alpha1.MachineConfig{
|
||||
MachineType: "controlplane",
|
||||
MachineFeatures: &v1alpha1.FeaturesConfig{
|
||||
KubernetesTalosAPIAccessConfig: &v1alpha1.KubernetesTalosAPIAccessConfig{
|
||||
AccessEnabled: pointer.To(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
ClusterConfig: &v1alpha1.ClusterConfig{
|
||||
ControlPlane: &v1alpha1.ControlPlaneConfig{
|
||||
Endpoint: &v1alpha1.Endpoint{
|
||||
endpointURL,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "1 error occurred:\n\t* feature API RBAC should be enabled when Kubernetes Talos API Access feature is enabled\n\n",
|
||||
},
|
||||
{
|
||||
name: "TalosAPIAccessWorker",
|
||||
config: &v1alpha1.Config{
|
||||
ConfigVersion: "v1alpha1",
|
||||
MachineConfig: &v1alpha1.MachineConfig{
|
||||
MachineType: "worker",
|
||||
MachineFeatures: &v1alpha1.FeaturesConfig{
|
||||
RBAC: pointer.To(true),
|
||||
KubernetesTalosAPIAccessConfig: &v1alpha1.KubernetesTalosAPIAccessConfig{
|
||||
AccessEnabled: pointer.To(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
ClusterConfig: &v1alpha1.ClusterConfig{
|
||||
ControlPlane: &v1alpha1.ControlPlaneConfig{
|
||||
Endpoint: &v1alpha1.Endpoint{
|
||||
endpointURL,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "1 error occurred:\n\t* feature Kubernetes Talos API Access can only be enabled on control plane machines\n\n",
|
||||
},
|
||||
} {
|
||||
test := test
|
||||
|
||||
|
@ -927,6 +927,11 @@ func (in *FeaturesConfig) DeepCopyInto(out *FeaturesConfig) {
|
||||
*out = new(bool)
|
||||
**out = **in
|
||||
}
|
||||
if in.KubernetesTalosAPIAccessConfig != nil {
|
||||
in, out := &in.KubernetesTalosAPIAccessConfig, &out.KubernetesTalosAPIAccessConfig
|
||||
*out = new(KubernetesTalosAPIAccessConfig)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -1142,6 +1147,37 @@ func (in *KubeletNodeIPConfig) DeepCopy() *KubeletNodeIPConfig {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *KubernetesTalosAPIAccessConfig) DeepCopyInto(out *KubernetesTalosAPIAccessConfig) {
|
||||
*out = *in
|
||||
if in.AccessEnabled != nil {
|
||||
in, out := &in.AccessEnabled, &out.AccessEnabled
|
||||
*out = new(bool)
|
||||
**out = **in
|
||||
}
|
||||
if in.AccessAllowedRoles != nil {
|
||||
in, out := &in.AccessAllowedRoles, &out.AccessAllowedRoles
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.AccessAllowedKubernetesNamespaces != nil {
|
||||
in, out := &in.AccessAllowedKubernetesNamespaces, &out.AccessAllowedKubernetesNamespaces
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesTalosAPIAccessConfig.
|
||||
func (in *KubernetesTalosAPIAccessConfig) DeepCopy() *KubernetesTalosAPIAccessConfig {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(KubernetesTalosAPIAccessConfig)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *LoggingConfig) DeepCopyInto(out *LoggingConfig) {
|
||||
*out = *in
|
||||
|
@ -689,6 +689,15 @@ const (
|
||||
|
||||
// GoVersion is the version of Go compiler this release was built with.
|
||||
GoVersion = "go1.18.4"
|
||||
|
||||
// KubernetesTalosAPIServiceName is the name of the Kubernetes service to access Talos API.
|
||||
KubernetesTalosAPIServiceName = "talos"
|
||||
|
||||
// KubernetesTalosAPIServiceNamespace is the namespace of the Kubernetes service to access Talos API.
|
||||
KubernetesTalosAPIServiceNamespace = "default"
|
||||
|
||||
// KubernetesTalosProvider is the name of the Talos provider as a Kubernetes label.
|
||||
KubernetesTalosProvider = "talos.dev"
|
||||
)
|
||||
|
||||
// See https://linux.die.net/man/3/klogctl
|
||||
|
@ -44,6 +44,8 @@ type BootstrapManifestsConfigSpec struct {
|
||||
FlannelCNIImage string `yaml:"flannelCNIImage"`
|
||||
|
||||
PodSecurityPolicyEnabled bool `yaml:"podSecurityPolicyEnabled"`
|
||||
|
||||
TalosAPIServiceEnabled bool `yaml:"talosAPIServiceEnabled"`
|
||||
}
|
||||
|
||||
// NewBootstrapManifestsConfig returns new BootstrapManifestsConfig resource.
|
||||
|
68
pkg/machinery/resources/kubeaccess/config.go
Normal file
68
pkg/machinery/resources/kubeaccess/config.go
Normal file
@ -0,0 +1,68 @@
|
||||
// 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 kubeaccess
|
||||
|
||||
import (
|
||||
"github.com/cosi-project/runtime/pkg/resource"
|
||||
"github.com/cosi-project/runtime/pkg/resource/meta"
|
||||
"github.com/cosi-project/runtime/pkg/resource/typed"
|
||||
|
||||
"github.com/talos-systems/talos/pkg/machinery/resources/config"
|
||||
)
|
||||
|
||||
// ConfigType is type of Config resource.
|
||||
const ConfigType = resource.Type("KubernetesAccessConfigs.cluster.talos.dev")
|
||||
|
||||
// ConfigID the singleton config resource ID.
|
||||
const ConfigID = resource.ID("config")
|
||||
|
||||
// Config resource holds KubeSpan configuration.
|
||||
type Config = typed.Resource[ConfigSpec, ConfigRD]
|
||||
|
||||
// ConfigSpec describes KubeSpan configuration..
|
||||
type ConfigSpec struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
AllowedAPIRoles []string `yaml:"allowedAPIRoles"`
|
||||
AllowedKubernetesNamespaces []string `yaml:"allowedKubernetesNamespaces"`
|
||||
}
|
||||
|
||||
// DeepCopy generates a deep copy of ConfigSpec.
|
||||
func (cs ConfigSpec) DeepCopy() ConfigSpec {
|
||||
cp := cs
|
||||
|
||||
if cs.AllowedAPIRoles != nil {
|
||||
cp.AllowedAPIRoles = make([]string, len(cs.AllowedAPIRoles))
|
||||
copy(cp.AllowedAPIRoles, cs.AllowedAPIRoles)
|
||||
}
|
||||
|
||||
if cs.AllowedKubernetesNamespaces != nil {
|
||||
cp.AllowedKubernetesNamespaces = make([]string, len(cs.AllowedKubernetesNamespaces))
|
||||
copy(cp.AllowedKubernetesNamespaces, cs.AllowedKubernetesNamespaces)
|
||||
}
|
||||
|
||||
return cp
|
||||
}
|
||||
|
||||
// NewConfig initializes a Config resource.
|
||||
func NewConfig(namespace resource.Namespace, id resource.ID) *Config {
|
||||
return typed.NewResource[ConfigSpec, ConfigRD](
|
||||
resource.NewMetadata(namespace, ConfigType, id, resource.VersionUndefined),
|
||||
ConfigSpec{},
|
||||
)
|
||||
}
|
||||
|
||||
// ConfigRD provides auxiliary methods for Config.
|
||||
type ConfigRD struct{}
|
||||
|
||||
// ResourceDefinition implements typed.ResourceDefinition interface.
|
||||
func (c ConfigRD) ResourceDefinition(resource.Metadata, ConfigSpec) meta.ResourceDefinitionSpec {
|
||||
return meta.ResourceDefinitionSpec{
|
||||
Type: ConfigType,
|
||||
Aliases: []resource.Type{},
|
||||
DefaultNamespace: config.NamespaceName,
|
||||
PrintColumns: []meta.PrintColumn{},
|
||||
Sensitivity: meta.NonSensitive,
|
||||
}
|
||||
}
|
6
pkg/machinery/resources/kubeaccess/kubeaccess.go
Normal file
6
pkg/machinery/resources/kubeaccess/kubeaccess.go
Normal file
@ -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 kubeaccess provides resources related to the Talos API access from Kubernetes workloads.
|
||||
package kubeaccess
|
32
pkg/machinery/resources/kubeaccess/kubeaccess_test.go
Normal file
32
pkg/machinery/resources/kubeaccess/kubeaccess_test.go
Normal file
@ -0,0 +1,32 @@
|
||||
// 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 kubeaccess_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/machinery/resources/kubeaccess"
|
||||
)
|
||||
|
||||
func TestRegisterResource(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
|
||||
resources := state.WrapCore(namespaced.NewState(inmem.Build))
|
||||
resourceRegistry := registry.NewResourceRegistry(resources)
|
||||
|
||||
for _, resource := range []resource.Resource{
|
||||
&kubeaccess.Config{},
|
||||
} {
|
||||
assert.NoError(t, resourceRegistry.Register(ctx, resource))
|
||||
}
|
||||
}
|
@ -363,6 +363,16 @@ systemDiskEncryption:
|
||||
|`features` |<a href="#featuresconfig">FeaturesConfig</a> |Features describe individual Talos features that can be switched on or off. <details><summary>Show example(s)</summary>{{< highlight yaml >}}
|
||||
features:
|
||||
rbac: true # Enable role-based access control (RBAC).
|
||||
|
||||
# # Configure Talos API access from Kubernetes pods.
|
||||
# kubernetesTalosAPIAccess:
|
||||
# enabled: true # Enable Talos API access from Kubernetes pods.
|
||||
# # The list of Talos API roles which can be granted for access from Kubernetes pods.
|
||||
# allowedRoles:
|
||||
# - os:reader
|
||||
# # The list of Kubernetes namespaces Talos API access is available from.
|
||||
# allowedKubernetesNamespaces:
|
||||
# - kube-system
|
||||
{{< /highlight >}}</details> | |
|
||||
|`udev` |<a href="#udevconfig">UdevConfig</a> |Configures the udev system. <details><summary>Show example(s)</summary>{{< highlight yaml >}}
|
||||
udev:
|
||||
@ -2513,7 +2523,7 @@ ephemeral:
|
||||
|
||||
---
|
||||
## FeaturesConfig
|
||||
FeaturesConfig describe individual Talos features that can be switched on or off.
|
||||
FeaturesConfig describes individual Talos features that can be switched on or off.
|
||||
|
||||
Appears in:
|
||||
|
||||
@ -2523,6 +2533,16 @@ Appears in:
|
||||
|
||||
{{< highlight yaml >}}
|
||||
rbac: true # Enable role-based access control (RBAC).
|
||||
|
||||
# # Configure Talos API access from Kubernetes pods.
|
||||
# kubernetesTalosAPIAccess:
|
||||
# enabled: true # Enable Talos API access from Kubernetes pods.
|
||||
# # The list of Talos API roles which can be granted for access from Kubernetes pods.
|
||||
# allowedRoles:
|
||||
# - os:reader
|
||||
# # The list of Kubernetes namespaces Talos API access is available from.
|
||||
# allowedKubernetesNamespaces:
|
||||
# - kube-system
|
||||
{{< /highlight >}}
|
||||
|
||||
|
||||
@ -2530,6 +2550,45 @@ rbac: true # Enable role-based access control (RBAC).
|
||||
|-------|------|-------------|----------|
|
||||
|`rbac` |bool |Enable role-based access control (RBAC). | |
|
||||
|`stableHostname` |bool |Enable stable default hostname. | |
|
||||
|`kubernetesTalosAPIAccess` |<a href="#kubernetestalosapiaccessconfig">KubernetesTalosAPIAccessConfig</a> |<details><summary>Configure Talos API access from Kubernetes pods.</summary><br />This feature is disabled if the feature config is not specified.</details> <details><summary>Show example(s)</summary>{{< highlight yaml >}}
|
||||
kubernetesTalosAPIAccess:
|
||||
enabled: true # Enable Talos API access from Kubernetes pods.
|
||||
# The list of Talos API roles which can be granted for access from Kubernetes pods.
|
||||
allowedRoles:
|
||||
- os:reader
|
||||
# The list of Kubernetes namespaces Talos API access is available from.
|
||||
allowedKubernetesNamespaces:
|
||||
- kube-system
|
||||
{{< /highlight >}}</details> | |
|
||||
|
||||
|
||||
|
||||
---
|
||||
## KubernetesTalosAPIAccessConfig
|
||||
KubernetesTalosAPIAccessConfig describes the configuration for the Talos API access from Kubernetes pods.
|
||||
|
||||
Appears in:
|
||||
|
||||
- <code><a href="#featuresconfig">FeaturesConfig</a>.kubernetesTalosAPIAccess</code>
|
||||
|
||||
|
||||
|
||||
{{< highlight yaml >}}
|
||||
enabled: true # Enable Talos API access from Kubernetes pods.
|
||||
# The list of Talos API roles which can be granted for access from Kubernetes pods.
|
||||
allowedRoles:
|
||||
- os:reader
|
||||
# The list of Kubernetes namespaces Talos API access is available from.
|
||||
allowedKubernetesNamespaces:
|
||||
- kube-system
|
||||
{{< /highlight >}}
|
||||
|
||||
|
||||
| Field | Type | Description | Value(s) |
|
||||
|-------|------|-------------|----------|
|
||||
|`enabled` |bool |Enable Talos API access from Kubernetes pods. | |
|
||||
|`allowedRoles` |[]string |<details><summary>The list of Talos API roles which can be granted for access from Kubernetes pods.</summary><br />Empty list means that no roles can be granted, so access is blocked.</details> | |
|
||||
|`allowedKubernetesNamespaces` |[]string |The list of Kubernetes namespaces Talos API access is available from. | |
|
||||
|
||||
|
||||
|
||||
|
@ -47,6 +47,7 @@ The list of config changes allowed to be applied immediately in Talos {{< releas
|
||||
* `.machine.pods`
|
||||
* `.machine.kernel`
|
||||
* `.machine.registries` (CRI containerd plugin will not pick up the registry authentication settings without a reboot)
|
||||
* `.machine.features.kubernetesTalosAPIAccess`
|
||||
|
||||
### `talosctl apply-config`
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user