diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fae9a7f..e21506c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed +- Changed the logic of setting roles by parsing `"%s-teleport-kube-agent-user-values"` configmaps to check if apps are enabled. +- Deprecated `MC-Namespace` and `tokenRoles` + ## [0.11.2] - 2024-10-11 ### Fixed diff --git a/go.mod b/go.mod index d40eff57..548f7505 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/onsi/gomega v1.34.1 github.com/pkg/errors v0.9.1 google.golang.org/grpc v1.67.1 + gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.25.0 k8s.io/apimachinery v0.25.0 @@ -107,7 +108,6 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect k8s.io/apiextensions-apiserver v0.25.0 // indirect k8s.io/component-base v0.25.0 // indirect k8s.io/klog/v2 v2.70.1 // indirect diff --git a/helm/teleport-operator/templates/deployment.yaml b/helm/teleport-operator/templates/deployment.yaml index 48194453..bb865482 100644 --- a/helm/teleport-operator/templates/deployment.yaml +++ b/helm/teleport-operator/templates/deployment.yaml @@ -36,8 +36,6 @@ spec: image: "{{ .Values.registry.domain }}/{{ .Values.image.name }}:{{ .Chart.Version }}" args: - "--namespace={{ include "resource.default.namespace" . }}" - - "--token-roles={{ .Values.teleportOperator.roles | join "," }}" - - "--mc-namespace={{ .Values.teleportOperator.mcNamespace }}" {{- if .Values.tbot.enabled }} - "--tbot" {{- end }} diff --git a/helm/teleport-operator/values.schema.json b/helm/teleport-operator/values.schema.json index 67551da4..00e67450 100644 --- a/helm/teleport-operator/values.schema.json +++ b/helm/teleport-operator/values.schema.json @@ -63,19 +63,6 @@ } } }, - "teleportOperator": { - "type": "object", - "properties": { - "roles": { - "type": "array", - "items": { - "type": "string" - }, - "description": "List of roles for the Teleport operator. Example: ['kube', 'app']" - } - }, - "required": ["roles"] - }, "pod": { "type": "object", "properties": { diff --git a/helm/teleport-operator/values.yaml b/helm/teleport-operator/values.yaml index be3da1d4..d0f96315 100644 --- a/helm/teleport-operator/values.yaml +++ b/helm/teleport-operator/values.yaml @@ -19,10 +19,6 @@ teleport: teleportClusterName: test.teleport.giantswarm.io teleportVersion: 16.1.7 -teleportOperator: - roles: - - kube - mcNamespace: "org-giantswarm" pod: user: diff --git a/internal/controller/cluster_controller.go b/internal/controller/cluster_controller.go index e7cfe1b7..f7975725 100644 --- a/internal/controller/cluster_controller.go +++ b/internal/controller/cluster_controller.go @@ -39,14 +39,13 @@ const identityExpirationPeriod = 20 * time.Minute // ClusterReconciler reconciles a Cluster object type ClusterReconciler struct { - Client client.Client - Log logr.Logger - Scheme *runtime.Scheme - Teleport *teleport.Teleport - IsBotEnabled bool - Namespace string - MCNamespace string - TokenRoles []string + Client client.Client + Log logr.Logger + Scheme *runtime.Scheme + Teleport *teleport.Teleport + IsBotEnabled bool + Namespace string + lastAssignedRoles []string } //+kubebuilder:rbac:groups=cluster.x-k8s.io.giantswarm.io,resources=clusters,verbs=get;list;watch;create;update;patch;delete @@ -73,13 +72,19 @@ func (r *ClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct return ctrl.Result{}, microerror.Mask(err) } - roles := []string{key.RoleKube} - if r.MCNamespace != "" && cluster.Namespace == r.MCNamespace { - roles = r.TokenRoles - } - log.Info("Reconciling cluster", "cluster", cluster) + appsEnabled, err := r.Teleport.AreTeleportAppsEnabled(ctx, cluster.Name, cluster.Namespace) + if err != nil { + log.Error(err, "Failed to check if Teleport apps are enabled") + return ctrl.Result{}, microerror.Mask(err) + } + + roles := []string{key.RoleKube} + if appsEnabled { + roles = append(roles, key.RoleApp) + } + r.lastAssignedRoles = roles if r.Teleport.Identity != nil { log.Info("Teleport identity", "last-read-minutes-ago", r.Teleport.Identity.Age(), "hash", r.Teleport.Identity.Hash()) } diff --git a/internal/controller/cluster_controller_test.go b/internal/controller/cluster_controller_test.go index f78da51d..af8493db 100644 --- a/internal/controller/cluster_controller_test.go +++ b/internal/controller/cluster_controller_test.go @@ -3,12 +3,15 @@ package controller import ( "context" "errors" + "fmt" + "reflect" "testing" "time" appv1alpha1 "github.com/giantswarm/apiextensions-application/api/v1alpha1" teleportTypes "github.com/gravitational/teleport/api/types" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/kubernetes/scheme" @@ -24,38 +27,68 @@ import ( func Test_ClusterController(t *testing.T) { testCases := []struct { - name string - namespace string - token string - tokens []teleportTypes.ProvisionToken - config *config.Config - identity *config.IdentityConfig - identitySecret *corev1.Secret - cluster *capi.Cluster - secret *corev1.Secret - configMap *corev1.ConfigMap - newTeleportClient func(ctx context.Context, proxyAddr, identityFile string) (teleport.Client, error) - expectedCluster *capi.Cluster - expectedSecret *corev1.Secret - expectedConfigMap *corev1.ConfigMap - expectedError error - mcNamespace string - tokenRoles []string + name string + namespace string + token string + tokens []teleportTypes.ProvisionToken + config *config.Config + identity *config.IdentityConfig + identitySecret *corev1.Secret + cluster *capi.Cluster + secret *corev1.Secret + configMap *corev1.ConfigMap + userValuesConfigMap *corev1.ConfigMap + newTeleportClient func(ctx context.Context, proxyAddr, identityFile string) (teleport.Client, error) + expectedCluster *capi.Cluster + expectedSecret *corev1.Secret + expectedConfigMap *corev1.ConfigMap + expectedError error + expectedRoles []string }{ { - name: "case 0: Register cluster and create Secret, ConfigMap and App resources in case they do not exist", - namespace: test.NamespaceName, - token: test.TokenName, - config: newConfig(), - identity: newIdentity(test.LastReadValue), - cluster: test.NewCluster(test.ClusterName, test.NamespaceName, []string{key.TeleportOperatorFinalizer}, time.Time{}), + name: "case 0: Register cluster with apps enabled", + namespace: test.NamespaceName, + token: test.TokenName, + config: newConfig(), + identity: newIdentity(test.LastReadValue), + cluster: test.NewCluster(test.ClusterName, test.NamespaceName, []string{key.TeleportOperatorFinalizer}, time.Time{}), + userValuesConfigMap: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-teleport-kube-agent-user-values", test.ClusterName), + Namespace: test.NamespaceName, + }, + Data: map[string]string{ + "values": "apps:\n- name: test-app\n uri: http://test-app", + }, + }, + expectedCluster: test.NewCluster(test.ClusterName, test.NamespaceName, []string{key.TeleportOperatorFinalizer}, time.Time{}), + expectedSecret: test.NewSecret(test.ClusterName, test.NamespaceName, test.TokenName), + expectedConfigMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{key.RoleKube, key.RoleApp}), + expectedRoles: []string{key.RoleKube, key.RoleApp}, + }, + { + name: "case 1: Register cluster without apps", + namespace: test.NamespaceName, + token: test.TokenName, + config: newConfig(), + identity: newIdentity(test.LastReadValue), + cluster: test.NewCluster(test.ClusterName, test.NamespaceName, []string{key.TeleportOperatorFinalizer}, time.Time{}), + userValuesConfigMap: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-teleport-kube-agent-user-values", test.ClusterName), + Namespace: test.NamespaceName, + }, + Data: map[string]string{ + "values": "someOtherConfig: true", + }, + }, expectedCluster: test.NewCluster(test.ClusterName, test.NamespaceName, []string{key.TeleportOperatorFinalizer}, time.Time{}), expectedSecret: test.NewSecret(test.ClusterName, test.NamespaceName, test.TokenName), expectedConfigMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{key.RoleKube}), - tokenRoles: []string{key.RoleKube, key.RoleApp}, + expectedRoles: []string{key.RoleKube}, }, { - name: "case 1: Register cluster in MC namespace and create Secret, ConfigMap with TokenRoles", + name: "case 2: Register cluster without user values ConfigMap", namespace: test.NamespaceName, token: test.TokenName, config: newConfig(), @@ -63,37 +96,45 @@ func Test_ClusterController(t *testing.T) { cluster: test.NewCluster(test.ClusterName, test.NamespaceName, []string{key.TeleportOperatorFinalizer}, time.Time{}), expectedCluster: test.NewCluster(test.ClusterName, test.NamespaceName, []string{key.TeleportOperatorFinalizer}, time.Time{}), expectedSecret: test.NewSecret(test.ClusterName, test.NamespaceName, test.TokenName), - expectedConfigMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{key.RoleKube, key.RoleApp}), - mcNamespace: test.NamespaceName, - tokenRoles: []string{key.RoleKube, key.RoleApp}, + expectedConfigMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{key.RoleKube}), + expectedRoles: []string{key.RoleKube}, }, { - name: "case 2: Update Secret and ConfigMap resources in case join token changes", - namespace: test.NamespaceName, - token: test.NewTokenName, - config: newConfig(), - identity: newIdentity(test.LastReadValue), - cluster: test.NewCluster(test.ClusterName, test.NamespaceName, []string{key.TeleportOperatorFinalizer}, time.Time{}), - secret: test.NewSecret(test.ClusterName, test.NamespaceName, test.TokenName), - configMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{key.RoleKube}), + name: "case 3: Update Secret and ConfigMap resources in case join token changes", + namespace: test.NamespaceName, + token: test.NewTokenName, + config: newConfig(), + identity: newIdentity(test.LastReadValue), + cluster: test.NewCluster(test.ClusterName, test.NamespaceName, []string{key.TeleportOperatorFinalizer}, time.Time{}), + secret: test.NewSecret(test.ClusterName, test.NamespaceName, test.TokenName), + configMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{key.RoleKube}), + userValuesConfigMap: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-teleport-kube-agent-user-values", test.ClusterName), + Namespace: test.NamespaceName, + }, + Data: map[string]string{ + "values": "apps:\n- name: test-app\n uri: http://test-app", + }, + }, expectedCluster: test.NewCluster(test.ClusterName, test.NamespaceName, []string{key.TeleportOperatorFinalizer}, time.Time{}), expectedSecret: test.NewSecret(test.ClusterName, test.NamespaceName, test.NewTokenName), - expectedConfigMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.NewTokenName, []string{key.RoleKube}), - tokenRoles: []string{key.RoleKube, key.RoleApp}, + expectedConfigMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.NewTokenName, []string{key.RoleKube, key.RoleApp}), + expectedRoles: []string{key.RoleKube, key.RoleApp}, }, { - name: "case 3: Deregister cluster and delete resources in case the cluster is deleted", - namespace: test.NamespaceName, - token: test.TokenName, - config: newConfig(), - identity: newIdentity(test.LastReadValue), - cluster: test.NewCluster(test.ClusterName, test.NamespaceName, []string{key.TeleportOperatorFinalizer}, time.Now()), - secret: test.NewSecret(test.ClusterName, test.NamespaceName, test.TokenName), - configMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{key.RoleKube}), - tokenRoles: []string{key.RoleKube, key.RoleApp}, + name: "case 4: Deregister cluster and delete resources in case the cluster is deleted", + namespace: test.NamespaceName, + token: test.TokenName, + config: newConfig(), + identity: newIdentity(test.LastReadValue), + cluster: test.NewCluster(test.ClusterName, test.NamespaceName, []string{key.TeleportOperatorFinalizer}, time.Now()), + secret: test.NewSecret(test.ClusterName, test.NamespaceName, test.TokenName), + configMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{key.RoleKube}), + expectedRoles: []string{key.RoleKube}, }, { - name: "case 4: Reconnect to Teleport when credentials are rotated", + name: "case 5: Reconnect to Teleport when credentials are rotated", namespace: test.NamespaceName, token: test.NewTokenName, config: newConfig(), @@ -102,16 +143,25 @@ func Test_ClusterController(t *testing.T) { secret: test.NewSecret(test.ClusterName, test.NamespaceName, test.TokenName), identitySecret: test.NewIdentitySecret(test.NamespaceName, test.IdentityFileValue), configMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.TokenName, []string{key.RoleKube}), + userValuesConfigMap: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-teleport-kube-agent-user-values", test.ClusterName), + Namespace: test.NamespaceName, + }, + Data: map[string]string{ + "values": "apps:\n- name: test-app\n uri: http://test-app", + }, + }, newTeleportClient: func(ctx context.Context, proxyAddr, identityFile string) (teleport.Client, error) { return test.NewTeleportClient(test.FakeTeleportClientConfig{Tokens: nil}), nil }, expectedCluster: test.NewCluster(test.ClusterName, test.NamespaceName, []string{key.TeleportOperatorFinalizer}, time.Time{}), expectedSecret: test.NewSecret(test.ClusterName, test.NamespaceName, test.NewTokenName), - expectedConfigMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.NewTokenName, []string{key.RoleKube}), - tokenRoles: []string{key.RoleKube, key.RoleApp}, + expectedConfigMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.NewTokenName, []string{key.RoleKube, key.RoleApp}), + expectedRoles: []string{key.RoleKube, key.RoleApp}, }, { - name: "case 5: Return an error in case reconnection to Teleport fails after the credentials are rotated", + name: "case 6: Return an error in case reconnection to Teleport fails after the credentials are rotated", namespace: test.NamespaceName, token: test.TokenName, config: newConfig(), @@ -123,7 +173,7 @@ func Test_ClusterController(t *testing.T) { return nil, errors.New("simulated error") }, expectedError: errors.New("secrets \"identity-output\" not found"), - tokenRoles: []string{key.RoleKube, key.RoleApp}, + expectedRoles: []string{key.RoleKube}, }, } @@ -142,6 +192,9 @@ func Test_ClusterController(t *testing.T) { if tc.identitySecret != nil { runtimeObjects = append(runtimeObjects, tc.identitySecret) } + if tc.userValuesConfigMap != nil { + runtimeObjects = append(runtimeObjects, tc.userValuesConfigMap) + } newTeleportClient := teleport.NewClient if tc.newTeleportClient != nil { @@ -166,13 +219,12 @@ func Test_ClusterController(t *testing.T) { Namespace: tc.namespace, Teleport: teleport.New(tc.namespace, tc.config, test.NewMockTokenGenerator(tc.token)), IsBotEnabled: false, - TokenRoles: tc.tokenRoles, - MCNamespace: tc.mcNamespace, } controller.Teleport.TeleportClient = test.NewTeleportClient(test.FakeTeleportClientConfig{ Tokens: tc.tokens, }) controller.Teleport.Identity = tc.identity + controller.Teleport.Client = ctrlClient req := ctrl.Request{ NamespacedName: types.NamespacedName{ @@ -245,10 +297,21 @@ func Test_ClusterController(t *testing.T) { if err != nil { t.Fatalf("unexpected error %v", err) } + + // Check if the roles were set correctly + // This assumes you've added a method to expose the last assigned roles for testing + assignedRoles := controller.GetLastAssignedRoles() + if !reflect.DeepEqual(assignedRoles, tc.expectedRoles) { + t.Errorf("Expected roles %v, but got %v", tc.expectedRoles, assignedRoles) + } }) } } +func (r *ClusterReconciler) GetLastAssignedRoles() []string { + return r.lastAssignedRoles +} + func newConfig() *config.Config { return &config.Config{ AppCatalog: test.AppCatalog, diff --git a/internal/pkg/teleport/teleport.go b/internal/pkg/teleport/teleport.go index af771af8..59049788 100644 --- a/internal/pkg/teleport/teleport.go +++ b/internal/pkg/teleport/teleport.go @@ -1,6 +1,15 @@ package teleport import ( + "context" + "fmt" + + "github.com/giantswarm/microerror" + "gopkg.in/yaml.v2" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/giantswarm/teleport-operator/internal/pkg/config" "github.com/giantswarm/teleport-operator/internal/pkg/token" ) @@ -11,6 +20,7 @@ type Teleport struct { TeleportClient Client Namespace string TokenGenerator token.Generator + Client client.Client } func New(namespace string, cfg *config.Config, tokenGenerator token.Generator) *Teleport { @@ -20,3 +30,32 @@ func New(namespace string, cfg *config.Config, tokenGenerator token.Generator) * TokenGenerator: tokenGenerator, } } + +func (t *Teleport) AreTeleportAppsEnabled(ctx context.Context, clusterName, namespace string) (bool, error) { + configMap := &corev1.ConfigMap{} + err := t.Client.Get(ctx, types.NamespacedName{ + Name: fmt.Sprintf("%s-teleport-kube-agent-user-values", clusterName), + Namespace: namespace, + }, configMap) + + if err != nil { + if client.IgnoreNotFound(err) != nil { + return false, microerror.Mask(err) + } + return false, nil // ConfigMap not found, return false without error + } + + valuesYaml, ok := configMap.Data["values"] + if !ok { + return false, nil // No values key, apps are not enabled + } + + var values map[string]interface{} + err = yaml.Unmarshal([]byte(valuesYaml), &values) + if err != nil { + return false, microerror.Mask(err) + } + + apps, ok := values["apps"].([]interface{}) + return ok && len(apps) > 0, nil +} diff --git a/main.go b/main.go index 706573cc..0f482ff2 100644 --- a/main.go +++ b/main.go @@ -39,7 +39,6 @@ import ( "github.com/giantswarm/teleport-operator/internal/controller" "github.com/giantswarm/teleport-operator/internal/pkg/config" - "github.com/giantswarm/teleport-operator/internal/pkg/key" "github.com/giantswarm/teleport-operator/internal/pkg/teleport" "github.com/giantswarm/teleport-operator/internal/pkg/token" //+kubebuilder:scaffold:imports @@ -64,8 +63,6 @@ func main() { var enableTeleportBot bool var probeAddr string var namespace string - var tokenRolesStr string - var mcNamespace string flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") @@ -76,8 +73,6 @@ func main() { "Enable teleport bot for teleport-operator. "+ "Enabling this will ensure teleport bot configmap is created and app.spec.extraConfig is updated.") flag.StringVar(&namespace, "namespace", "", "Namespace where operator is deployed") - flag.StringVar(&tokenRolesStr, "token-roles", "kube", "Comma-separated list of roles for the token (kube, app, node)") - flag.StringVar(&mcNamespace, "mc-namespace", "", "Namespace for management cluster with additional roles") opts := zap.Options{ Development: true, @@ -85,18 +80,8 @@ func main() { opts.BindFlags(flag.CommandLine) flag.Parse() - if mcNamespace == "" { - mcNamespace = os.Getenv("MC_NAMESPACE") - } - ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) - tokenRoles, err := key.ParseRoles(tokenRolesStr) - if err != nil { - setupLog.Error(err, "Failed to parse token roles") - os.Exit(1) - } - ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) restConfig := ctrl.GetConfigOrDie() @@ -138,6 +123,7 @@ func main() { } tele := teleport.New(namespace, config, token.NewGenerator()) + tele.Client = mgr.GetClient() if err = (&controller.ClusterReconciler{ Client: mgr.GetClient(), Log: ctrl.Log.WithName("controllers").WithName("Cluster"), @@ -145,8 +131,6 @@ func main() { Teleport: tele, IsBotEnabled: enableTeleportBot, Namespace: namespace, - TokenRoles: tokenRoles, - MCNamespace: mcNamespace, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Cluster") os.Exit(1) @@ -163,7 +147,6 @@ func main() { } setupLog.Info("is teleport bot enabled?", "enabled", enableTeleportBot) - setupLog.Info("management cluster namespace", "namespace", mcNamespace) setupLog.Info("starting manager") if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { diff --git a/teleport-operator b/teleport-operator new file mode 100755 index 00000000..7a024d06 Binary files /dev/null and b/teleport-operator differ