talos/internal/integration/api/serviceaccount.go
Andrey Smirnov 96aa9638f7
chore: rename talos-systems/talos to siderolabs/talos
There's a cyclic dependency on siderolink library which imports talos
machinery back. We will fix that after we get talos pushed under a new
name.

Signed-off-by: Andrey Smirnov <andrey.smirnov@talos-systems.com>
2022-11-03 16:50:32 +04:00

303 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/types/v1alpha1"
"github.com/siderolabs/talos/pkg/machinery/config/types/v1alpha1/machine"
"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)
}
// TearDownTest ...
func (suite *ServiceAccountSuite) TearDownTest() {
if suite.ctxCancel != nil {
suite.ctxCancel()
}
}
// TestValid tests Kubernetes service accounts.
//
//nolint:dupl
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.
//
//nolint:dupl
func (suite *ServiceAccountSuite) TestNotAllowedNamespace() {
name := "test-allowed-ns"
err := suite.configureAPIAccess(true, []string{"os:reader"}, []string{"kube-system"})
suite.Assert().NoError(err)
sa, err := suite.createServiceAccount("default", name, []string{"os:reader"})
suite.Assert().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.Assert().NoError(err)
}
// TestNotAllowedRoles tests Kubernetes service accounts with not allowed roles.
//
//nolint:dupl
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)
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.
//
//nolint:dupl
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)
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.WithNodes(suite.ctx, ip)
nodeConfig, err := suite.ReadConfigFromNode(nodeCtx)
if err != nil {
return err
}
nodeConfigRaw, ok := nodeConfig.Raw().(*v1alpha1.Config)
if !ok {
return fmt.Errorf("unexpected node config type %T", nodeConfig.Raw())
}
accessConfig := v1alpha1.KubernetesTalosAPIAccessConfig{
AccessEnabled: pointer.To(enabled),
AccessAllowedRoles: allowedRoles,
AccessAllowedKubernetesNamespaces: allowedNamespaces,
}
nodeConfigRaw.MachineConfig.MachineFeatures.KubernetesTalosAPIAccessConfig = &accessConfig
bytes, err := nodeConfigRaw.Bytes()
if err != nil {
return err
}
_, 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))
}