mirror of
https://github.com/siderolabs/talos.git
synced 2025-08-08 23:57:06 +02:00
Dynamically map Kubernetes and Talos API ports to an available port on the host, so every cluster gets its own unique set of parts. As part of the changes, refactor the provision library and interfaces, dropping old weird interfaces replacing with (hopefully) much more descriprive names. Signed-off-by: Dmitry Sharshakov <dmitry.sharshakov@siderolabs.com> Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
291 lines
9.0 KiB
Go
291 lines
9.0 KiB
Go
// 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/.
|
|
|
|
//go:build integration_api
|
|
|
|
package api
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/siderolabs/go-pointer"
|
|
"github.com/siderolabs/go-retry/retry"
|
|
corev1 "k8s.io/api/core/v1"
|
|
eventsv1 "k8s.io/api/events/v1"
|
|
kubeerrors "k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
|
|
"github.com/siderolabs/talos/internal/integration/base"
|
|
machineapi "github.com/siderolabs/talos/pkg/machinery/api/machine"
|
|
"github.com/siderolabs/talos/pkg/machinery/client"
|
|
"github.com/siderolabs/talos/pkg/machinery/client/config"
|
|
"github.com/siderolabs/talos/pkg/machinery/config/machine"
|
|
"github.com/siderolabs/talos/pkg/machinery/config/types/v1alpha1"
|
|
"github.com/siderolabs/talos/pkg/machinery/constants"
|
|
)
|
|
|
|
var (
|
|
serviceAccountGVR = schema.GroupVersionResource{
|
|
Group: constants.ServiceAccountResourceGroup,
|
|
Version: constants.ServiceAccountResourceVersion,
|
|
Resource: constants.ServiceAccountResourcePlural,
|
|
}
|
|
secretGVR = schema.GroupVersionResource{
|
|
Group: "",
|
|
Version: "v1",
|
|
Resource: "secrets",
|
|
}
|
|
)
|
|
|
|
// ServiceAccountSuite verifies Talos ServiceAccount.
|
|
type ServiceAccountSuite struct {
|
|
base.K8sSuite
|
|
|
|
ctx context.Context //nolint:containedctx
|
|
ctxCancel context.CancelFunc
|
|
}
|
|
|
|
// SuiteName ...
|
|
func (suite *ServiceAccountSuite) SuiteName() string {
|
|
return "api.ServiceAccountSuite"
|
|
}
|
|
|
|
// SetupTest ...
|
|
func (suite *ServiceAccountSuite) SetupTest() {
|
|
// make sure API calls have timeout
|
|
suite.ctx, suite.ctxCancel = context.WithTimeout(context.Background(), 5*time.Minute)
|
|
|
|
suite.ClearConnectionRefused(suite.ctx, suite.DiscoverNodeInternalIPsByType(suite.ctx, machine.TypeWorker)...)
|
|
suite.AssertClusterHealthy(suite.ctx)
|
|
}
|
|
|
|
// TearDownTest ...
|
|
func (suite *ServiceAccountSuite) TearDownTest() {
|
|
if suite.ctxCancel != nil {
|
|
suite.ctxCancel()
|
|
}
|
|
}
|
|
|
|
// TestValid tests Kubernetes service accounts.
|
|
func (suite *ServiceAccountSuite) TestValid() {
|
|
name := "test-valid"
|
|
|
|
err := suite.configureAPIAccess(true, []string{"os:reader"}, []string{"kube-system"})
|
|
suite.Assert().NoError(err)
|
|
|
|
_, err = suite.getCRD()
|
|
suite.Assert().NoError(err)
|
|
|
|
sa, err := suite.createServiceAccount("kube-system", name, []string{"os:reader"})
|
|
suite.Assert().NoError(err)
|
|
|
|
defer suite.DeleteResource(suite.ctx, serviceAccountGVR, "default", name) //nolint:errcheck
|
|
|
|
err = suite.WaitForEventExists(suite.ctx, "kube-system", func(event eventsv1.Event) bool {
|
|
return event.Regarding.UID == sa.GetUID() &&
|
|
event.Type == corev1.EventTypeNormal &&
|
|
event.Reason == "Synced"
|
|
})
|
|
suite.Assert().NoError(err)
|
|
|
|
secret, err := suite.waitForSecret("kube-system", name)
|
|
suite.Assert().NoError(err)
|
|
|
|
talosConfig := secret.Data["config"]
|
|
|
|
conf, err := config.FromBytes(talosConfig)
|
|
suite.Assert().NoError(err)
|
|
|
|
expectedServiceName := fmt.Sprintf(
|
|
"%s.%s",
|
|
constants.KubernetesTalosAPIServiceName,
|
|
constants.KubernetesTalosAPIServiceNamespace,
|
|
)
|
|
suite.Assert().Equal([]string{expectedServiceName}, conf.Contexts[conf.Context].Endpoints)
|
|
|
|
err = suite.DeleteResource(suite.ctx, serviceAccountGVR, "kube-system", name)
|
|
suite.Require().NoError(err)
|
|
|
|
err = suite.EnsureResourceIsDeleted(suite.ctx, 30*time.Second, secretGVR, "kube-system", name)
|
|
suite.Assert().NoError(err)
|
|
}
|
|
|
|
// TestNotAllowedNamespace tests Kubernetes service accounts in not allowed namespaces.
|
|
func (suite *ServiceAccountSuite) TestNotAllowedNamespace() {
|
|
name := "test-allowed-ns"
|
|
|
|
err := suite.configureAPIAccess(true, []string{"os:reader"}, []string{"kube-system"})
|
|
suite.Require().NoError(err)
|
|
|
|
sa, err := suite.createServiceAccount("default", name, []string{"os:reader"})
|
|
suite.Require().NoError(err)
|
|
|
|
defer suite.DeleteResource(suite.ctx, serviceAccountGVR, "default", name) //nolint:errcheck
|
|
|
|
err = suite.WaitForEventExists(suite.ctx, "default", func(event eventsv1.Event) bool {
|
|
return event.Regarding.UID == sa.GetUID() &&
|
|
event.Type == corev1.EventTypeWarning &&
|
|
event.Reason == "ErrNamespaceNotAllowed"
|
|
})
|
|
suite.Require().NoError(err)
|
|
}
|
|
|
|
// TestNotAllowedRoles tests Kubernetes service accounts with not allowed roles.
|
|
func (suite *ServiceAccountSuite) TestNotAllowedRoles() {
|
|
name := "test-not-allowed-roles"
|
|
|
|
err := suite.configureAPIAccess(true, []string{"os:reader"}, []string{"kube-system"})
|
|
suite.Assert().NoError(err)
|
|
|
|
sa, err := suite.createServiceAccount("kube-system", name, []string{"os:admin"})
|
|
suite.Assert().NoError(err)
|
|
suite.Assert().NotNil(sa)
|
|
|
|
defer suite.DeleteResource(suite.ctx, serviceAccountGVR, "kube-system", name) //nolint:errcheck
|
|
|
|
err = suite.WaitForEventExists(suite.ctx, "kube-system", func(event eventsv1.Event) bool {
|
|
return event.Regarding.UID == sa.GetUID() &&
|
|
event.Type == corev1.EventTypeWarning &&
|
|
event.Reason == "ErrRolesNotAllowed"
|
|
})
|
|
suite.Assert().NoError(err)
|
|
}
|
|
|
|
// TestFeatureNotEnabled tests Kubernetes service accounts when API access feature is not enabled.
|
|
func (suite *ServiceAccountSuite) TestFeatureNotEnabled() {
|
|
name := "test-feature-not-enabled"
|
|
|
|
err := suite.configureAPIAccess(false, []string{"os:reader"}, []string{"kube-system"})
|
|
suite.Assert().NoError(err)
|
|
|
|
sa, err := suite.createServiceAccount("kube-system", name, []string{"os:reader"})
|
|
if kubeerrors.IsNotFound(err) {
|
|
// CRD is not created because the feature was never enabled, all good
|
|
return
|
|
}
|
|
|
|
suite.Assert().NoError(err)
|
|
suite.Assert().NotNil(sa)
|
|
|
|
defer suite.DeleteResource(suite.ctx, serviceAccountGVR, "kube-system", name) //nolint:errcheck
|
|
|
|
err = suite.WaitForEventExists(suite.ctx, "kube-system", func(event eventsv1.Event) bool {
|
|
return event.Regarding.UID == sa.GetUID() &&
|
|
event.Type == corev1.EventTypeWarning &&
|
|
event.Reason == "ErrAccessNotEnabled"
|
|
})
|
|
|
|
suite.Assert().NoError(err)
|
|
}
|
|
|
|
func (suite *ServiceAccountSuite) waitForSecret(ns, name string) (*corev1.Secret, error) {
|
|
var (
|
|
secret *corev1.Secret
|
|
err error
|
|
)
|
|
|
|
err = retry.Constant(1*time.Minute).RetryWithContext(suite.ctx, func(ctx context.Context) error {
|
|
secret, err = suite.Clientset.CoreV1().Secrets(ns).Get(suite.ctx, name, metav1.GetOptions{})
|
|
if kubeerrors.IsNotFound(err) {
|
|
return retry.ExpectedError(err)
|
|
}
|
|
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return secret, nil
|
|
}
|
|
|
|
func (suite *ServiceAccountSuite) getCRD() (*unstructured.Unstructured, error) {
|
|
crdName := fmt.Sprintf("%s.%s", constants.ServiceAccountResourcePlural, constants.ServiceAccountResourceGroup)
|
|
|
|
return suite.DynamicClient.Resource(schema.GroupVersionResource{
|
|
Group: "apiextensions.k8s.io",
|
|
Version: "v1",
|
|
Resource: "customresourcedefinitions",
|
|
}).Get(suite.ctx, crdName, metav1.GetOptions{})
|
|
}
|
|
|
|
func (suite *ServiceAccountSuite) createServiceAccount(ns string, name string, roles []string) (*unstructured.Unstructured, error) {
|
|
return suite.DynamicClient.Resource(serviceAccountGVR).Namespace(ns).Create(suite.ctx, &unstructured.Unstructured{
|
|
Object: map[string]interface{}{
|
|
"apiVersion": fmt.Sprintf("%s/%s", constants.ServiceAccountResourceGroup, constants.ServiceAccountResourceVersion),
|
|
"kind": constants.ServiceAccountResourceKind,
|
|
"metadata": map[string]interface{}{
|
|
"name": name,
|
|
},
|
|
"spec": map[string]interface{}{
|
|
"roles": roles,
|
|
},
|
|
},
|
|
}, metav1.CreateOptions{})
|
|
}
|
|
|
|
// configureAPIAccess configures the API access feature on all control plane nodes.
|
|
func (suite *ServiceAccountSuite) configureAPIAccess(
|
|
enabled bool,
|
|
allowedRoles []string,
|
|
allowedNamespaces []string,
|
|
) error {
|
|
controlPlaneIPs := suite.DiscoverNodeInternalIPsByType(suite.ctx, machine.TypeControlPlane)
|
|
|
|
for _, ip := range controlPlaneIPs {
|
|
nodeCtx := client.WithNode(suite.ctx, ip)
|
|
|
|
nodeConfig, err := suite.ReadConfigFromNode(nodeCtx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
bytes := suite.PatchV1Alpha1Config(nodeConfig, func(nodeConfigRaw *v1alpha1.Config) {
|
|
accessConfig := v1alpha1.KubernetesTalosAPIAccessConfig{
|
|
AccessEnabled: pointer.To(enabled),
|
|
AccessAllowedRoles: allowedRoles,
|
|
AccessAllowedKubernetesNamespaces: allowedNamespaces,
|
|
}
|
|
|
|
nodeConfigRaw.MachineConfig.MachineFeatures.KubernetesTalosAPIAccessConfig = &accessConfig
|
|
})
|
|
|
|
_, err = suite.Client.ApplyConfiguration(nodeCtx, &machineapi.ApplyConfigurationRequest{
|
|
Data: bytes,
|
|
Mode: machineapi.ApplyConfigurationRequest_NO_REBOOT,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if enabled { // wait for CRD and the Talos endpoint to be created
|
|
return retry.Constant(30*time.Second).RetryWithContext(suite.ctx, func(ctx context.Context) error {
|
|
_, err := suite.getCRD()
|
|
if err != nil {
|
|
return retry.ExpectedError(err)
|
|
}
|
|
|
|
_, err = suite.Clientset.CoreV1().
|
|
Services(constants.KubernetesTalosAPIServiceNamespace).
|
|
Get(suite.ctx, constants.KubernetesTalosAPIServiceName, metav1.GetOptions{})
|
|
if err != nil {
|
|
return retry.ExpectedError(err)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func init() {
|
|
allSuites = append(allSuites, new(ServiceAccountSuite))
|
|
}
|