talos/internal/integration/api/serviceaccount.go
Dmitry Sharshakov 653f838b09
feat: support multiple Docker cluster in talosctl cluster create
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>
2024-04-04 21:21:39 +04:00

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