From 37386fd4bfdf698a2a3ed11fc0b27f420dfa98ef Mon Sep 17 00:00:00 2001 From: Puru <5674762+tuladhar@users.noreply.github.com> Date: Mon, 19 Aug 2024 16:42:41 +0545 Subject: [PATCH] Add teleport-tbot feature flag (#143) --- CHANGELOG.md | 5 + .../templates/deployment.yaml | 3 + helm/teleport-operator/values.schema.json | 8 ++ helm/teleport-operator/values.yaml | 3 + internal/controller/cluster_controller.go | 41 +++++- internal/pkg/key/key.go | 18 +++ internal/pkg/teleport/app.go | 128 ++++++++++++++++++ internal/pkg/teleport/configmap.go | 78 +++++++++++ internal/pkg/teleport/secret.go | 41 ++++++ main.go | 17 ++- 10 files changed, 332 insertions(+), 10 deletions(-) create mode 100644 internal/pkg/teleport/app.go diff --git a/CHANGELOG.md b/CHANGELOG.md index f00f39f0..b77a2744 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Added tbot feature flag, enabled with `--tbot` flag +- If tbot feature flag is set, creates configmap and append to tbot app extra config for generating kubeconfig. + ## [0.9.3] - 2024-05-08 ### Changed diff --git a/helm/teleport-operator/templates/deployment.yaml b/helm/teleport-operator/templates/deployment.yaml index ee646223..82e19d84 100644 --- a/helm/teleport-operator/templates/deployment.yaml +++ b/helm/teleport-operator/templates/deployment.yaml @@ -36,6 +36,9 @@ spec: image: "{{ .Values.registry.domain }}/{{ .Values.image.name }}:{{ .Chart.Version }}" args: - "--namespace={{ include "resource.default.namespace" . }}" + {{- if .Values.tbot.enabled }} + - "--tbot" + {{- end }} ports: - name: metrics protocol: TCP diff --git a/helm/teleport-operator/values.schema.json b/helm/teleport-operator/values.schema.json index e9485182..00e67450 100644 --- a/helm/teleport-operator/values.schema.json +++ b/helm/teleport-operator/values.schema.json @@ -155,6 +155,14 @@ } } } + }, + "tbot": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + } + } } } } diff --git a/helm/teleport-operator/values.yaml b/helm/teleport-operator/values.yaml index 1cb3c5e1..4332a193 100644 --- a/helm/teleport-operator/values.yaml +++ b/helm/teleport-operator/values.yaml @@ -62,3 +62,6 @@ affinity: matchExpressions: - key: "node-role.kubernetes.io/control-plane" operator: "Exists" + +tbot: + enabled: false diff --git a/internal/controller/cluster_controller.go b/internal/controller/cluster_controller.go index dea34550..450ffd8a 100644 --- a/internal/controller/cluster_controller.go +++ b/internal/controller/cluster_controller.go @@ -39,11 +39,12 @@ 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 - Namespace string + Client client.Client + Log logr.Logger + Scheme *runtime.Scheme + Teleport *teleport.Teleport + IsBotEnabled bool + Namespace string } //+kubebuilder:rbac:groups=cluster.x-k8s.io.giantswarm.io,resources=clusters,verbs=get;list;watch;create;update;patch;delete @@ -118,6 +119,20 @@ func (r *ClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct return ctrl.Result{}, microerror.Mask(err) } + if r.IsBotEnabled { + if err := r.Teleport.DeleteBotAppExtraConfig(ctx, log, r.Client, cluster.Name); err != nil { + return ctrl.Result{}, microerror.Mask(err) + } + + if err := r.Teleport.DeleteTbotConfigMap(ctx, log, r.Client, cluster.Name, key.TeleportBotNamespace); err != nil { + return ctrl.Result{}, microerror.Mask(err) + } + + if err := r.Teleport.DeleteKubeconfigSecret(ctx, log, r.Client, cluster.Name, key.TeleportBotNamespace); err != nil { + return ctrl.Result{}, microerror.Mask(err) + } + } + // Remove finalizer from the Cluster CR if controllerutil.ContainsFinalizer(cluster, key.TeleportOperatorFinalizer) { if err := teleport.RemoveFinalizer(ctx, log, cluster, r.Client); err != nil { @@ -208,6 +223,22 @@ func (r *ClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct } } + if r.IsBotEnabled { + secret, err := r.Teleport.GetKubeconfigSecret(ctx, r.Client, cluster.Name, key.TeleportBotNamespace) + if err != nil { + return ctrl.Result{}, microerror.Mask(err) + } + if secret == nil { + if err := r.Teleport.EnsureTbotConfigMap(ctx, log, r.Client, cluster.Name, key.TeleportBotNamespace, registerName); err != nil { + return ctrl.Result{}, microerror.Mask(err) + } + + if err := r.Teleport.EnsureBotAppExtraConfig(ctx, log, r.Client, cluster.Name); err != nil { + return ctrl.Result{}, microerror.Mask(err) + } + } + } + // We need to requeue to check the teleport token validity // and update secret for the cluster, if it expires return ctrl.Result{RequeueAfter: 1 * time.Minute}, nil diff --git a/internal/pkg/key/key.go b/internal/pkg/key/key.go index d6aab5e6..9b18ff8f 100644 --- a/internal/pkg/key/key.go +++ b/internal/pkg/key/key.go @@ -13,6 +13,8 @@ const ( TeleportOperatorLabelValue = "teleport-operator" TeleportOperatorConfigName = "teleport-operator" TeleportBotSecretName = "identity-output" + TeleportBotNamespace = "giantswarm" + TeleportBotAppName = "teleport-tbot" TeleportKubeTokenValidity = 720 * time.Hour TeleportNodeTokenValidity = 720 * time.Hour @@ -30,10 +32,18 @@ func GetConfigmapName(clusterName string, appName string) string { return fmt.Sprintf("%s-%s-config", clusterName, appName) } +func GetTbotConfigmapName(clusterName string) string { + return fmt.Sprintf("teleport-tbot-%s-config", clusterName) +} + func GetSecretName(clusterName string) string { return fmt.Sprintf("%s-teleport-join-token", clusterName) } +func GetKubeconfigSecretName(clusterName string) string { + return fmt.Sprintf("teleport-%s-kubeconfig", clusterName) +} + func GetRegisterName(managementClusterName, clusterName string) string { return fmt.Sprintf("%s-%s", managementClusterName, clusterName) } @@ -59,3 +69,11 @@ kubeClusterName: "%s" return fmt.Sprintf(dataTpl, authToken, proxyAddr, kubeClusterName) } + +func GetTbotConfigmapDataFromTemplate(kubeClusterName string, clusterName string) string { + dataTpl := `outputs: + %s: "%s" +` + + return fmt.Sprintf(dataTpl, kubeClusterName, clusterName) +} diff --git a/internal/pkg/teleport/app.go b/internal/pkg/teleport/app.go new file mode 100644 index 00000000..25da4a29 --- /dev/null +++ b/internal/pkg/teleport/app.go @@ -0,0 +1,128 @@ +package teleport + +import ( + "context" + "reflect" + + "github.com/giantswarm/apiextensions-application/api/v1alpha1" + "github.com/giantswarm/microerror" + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/api/errors" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/giantswarm/teleport-operator/internal/pkg/key" +) + +func (t *Teleport) getBotExtraConfig(clusterName string) v1alpha1.AppExtraConfig { + return v1alpha1.AppExtraConfig{ + Kind: "configMap", + Name: key.GetTbotConfigmapName(clusterName), + Namespace: key.TeleportBotNamespace, + Priority: 25} +} + +func (t *Teleport) appendBotExtraConfig(appExtraConfigs []v1alpha1.AppExtraConfig, extraConfig v1alpha1.AppExtraConfig) []v1alpha1.AppExtraConfig { + extraConfigs := []v1alpha1.AppExtraConfig{} + if appExtraConfigs == nil { + return append(extraConfigs, extraConfig) + } + shouldAppend := true + for _, config := range appExtraConfigs { + if reflect.DeepEqual(config, extraConfig) { + shouldAppend = false + break + } + } + if shouldAppend { + extraConfigs = append(appExtraConfigs, extraConfig) + } else { + extraConfigs = appExtraConfigs + } + return extraConfigs +} + +func (t *Teleport) removeBotExtraConfig(appExtraConfigs []v1alpha1.AppExtraConfig, extraConfig v1alpha1.AppExtraConfig) []v1alpha1.AppExtraConfig { + extraConfigs := []v1alpha1.AppExtraConfig{} + for _, config := range appExtraConfigs { + if reflect.DeepEqual(config, extraConfig) { + continue + } + extraConfigs = append(extraConfigs, config) + } + return extraConfigs +} + +func (t *Teleport) getBotApp(ctx context.Context, ctrlClient client.Client, clusterName string) (*v1alpha1.App, error) { + app := &v1alpha1.App{} + key := client.ObjectKey{Name: key.TeleportBotAppName, Namespace: key.TeleportBotNamespace} + if err := ctrlClient.Get(ctx, key, app); err != nil { + return app, err + } + return app, nil +} + +func (t *Teleport) EnsureBotAppExtraConfig(ctx context.Context, log logr.Logger, ctrlClient client.Client, clusterName string) error { + app, err := t.getBotApp(ctx, ctrlClient, clusterName) + if err != nil { + if errors.IsNotFound(err) { + log.Error(err, "tbot: App not found", "app", app) + } + return err + } + + appExtraConfigs := []v1alpha1.AppExtraConfig{} + if app.Spec.ExtraConfigs != nil { + appExtraConfigs = app.Spec.ExtraConfigs + } + + extraConfig := t.getBotExtraConfig(clusterName) + app.Spec.ExtraConfigs = t.appendBotExtraConfig(appExtraConfigs, extraConfig) + + if reflect.DeepEqual(appExtraConfigs, app.Spec.ExtraConfigs) { + return nil + } + + log.Info("tbot: Updating app", "app", app) + if err := ctrlClient.Update(ctx, app); err != nil { + if errors.IsConflict(err) { + log.Error(err, "tbot: Conflict detected during app update", "app", app) + } + + return microerror.Mask(err) + } + + return nil +} + +func (t *Teleport) DeleteBotAppExtraConfig(ctx context.Context, log logr.Logger, ctrlClient client.Client, clusterName string) error { + app, err := t.getBotApp(ctx, ctrlClient, clusterName) + if err != nil { + if errors.IsNotFound(err) { + log.Error(err, "tbot: App not found", "app", app) + } + return err + } + + if app.Spec.ExtraConfigs == nil { + return nil + } + appExtraConfigs := app.Spec.ExtraConfigs + + extraConfig := t.getBotExtraConfig(clusterName) + app.Spec.ExtraConfigs = t.removeBotExtraConfig(appExtraConfigs, extraConfig) + + if reflect.DeepEqual(appExtraConfigs, app.Spec.ExtraConfigs) { + return nil + } + + log.Info("tbot: Updating app", "app", app) + if err := ctrlClient.Update(ctx, app); err != nil { + if errors.IsConflict(err) { + log.Error(err, "tbot: Conflict detected during app update", "app", app) + } + + return microerror.Mask(err) + } + + return nil +} diff --git a/internal/pkg/teleport/configmap.go b/internal/pkg/teleport/configmap.go index 79551c58..b67a6c5b 100644 --- a/internal/pkg/teleport/configmap.go +++ b/internal/pkg/teleport/configmap.go @@ -32,6 +32,23 @@ func (t *Teleport) GetConfigMap(ctx context.Context, log logr.Logger, ctrlClient return configMap, nil } +func (t *Teleport) GetTbotConfigMap(ctx context.Context, ctrlClient client.Client, clusterName string) (*corev1.ConfigMap, error) { + var ( + configMapName = key.GetTbotConfigmapName(clusterName) + configMap = &corev1.ConfigMap{} + ) + + key := client.ObjectKey{Name: configMapName, Namespace: key.TeleportBotNamespace} + if err := ctrlClient.Get(ctx, key, configMap); err != nil { + if apierrors.IsNotFound(err) { + return nil, nil + } + return nil, microerror.Mask(fmt.Errorf("bot: Failed to get configmap: %w", err)) + } + + return configMap, nil +} + func (t *Teleport) GetTokenFromConfigMap(ctx context.Context, configMap *corev1.ConfigMap) (string, error) { valuesBytes, ok := configMap.Data["values"] if !ok { @@ -83,6 +100,43 @@ func (t *Teleport) CreateConfigMap(ctx context.Context, log logr.Logger, ctrlCli return nil } +func (t *Teleport) CreateTbotConfigMap(ctx context.Context, ctrlClient client.Client, clusterName string, registerName string) (*corev1.ConfigMap, error) { + configMapName := key.GetTbotConfigmapName(clusterName) + data := map[string]string{ + "values": t.getTbotConfigMapData(registerName, clusterName), + } + cm := corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: configMapName, + Namespace: key.TeleportBotNamespace, + }, + Data: data, + } + + if err := ctrlClient.Create(ctx, &cm); err != nil { + return nil, microerror.Mask(err) + } + + return &cm, nil +} + +func (t *Teleport) EnsureTbotConfigMap(ctx context.Context, log logr.Logger, ctrlClient client.Client, clusterName string, namespace string, registerName string) error { + cm, err := t.GetTbotConfigMap(ctx, ctrlClient, clusterName) + if err != nil { + return microerror.Mask(err) + } + + if cm == nil { + cm, err = t.CreateTbotConfigMap(ctx, ctrlClient, clusterName, registerName) + if err != nil { + return microerror.Mask(err) + } + log.Info("tbot: Created configmap", "configmap", cm) + } + + return nil +} + func (t *Teleport) UpdateConfigMap(ctx context.Context, log logr.Logger, ctrlClient client.Client, configMap *corev1.ConfigMap, token string) error { valuesBytes, ok := configMap.Data["values"] if !ok { @@ -131,6 +185,26 @@ func (t *Teleport) DeleteConfigMap(ctx context.Context, log logr.Logger, ctrlCli return nil } +func (t *Teleport) DeleteTbotConfigMap(ctx context.Context, log logr.Logger, ctrlClient client.Client, clusterName string, namespace string) error { + configMapName := key.GetTbotConfigmapName(clusterName) + cm := corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: configMapName, + Namespace: namespace, + }, + } + + if err := ctrlClient.Delete(ctx, &cm); err != nil { + if apierrors.IsNotFound(err) { + return nil + } + return microerror.Mask(err) + } + + log.Info("tbot: Deleted configmap", "configMap", configMapName) + return nil +} + func (t *Teleport) getConfigMapData(registerName string, token string) string { var ( authToken = token @@ -141,3 +215,7 @@ func (t *Teleport) getConfigMapData(registerName string, token string) string { return key.GetConfigmapDataFromTemplate(authToken, proxyAddr, kubeClusterName, teleportVersionOverride) } + +func (t *Teleport) getTbotConfigMapData(registerName string, clusterName string) string { + return key.GetTbotConfigmapDataFromTemplate(registerName, clusterName) +} diff --git a/internal/pkg/teleport/secret.go b/internal/pkg/teleport/secret.go index fe5d9094..b7f8f447 100644 --- a/internal/pkg/teleport/secret.go +++ b/internal/pkg/teleport/secret.go @@ -99,3 +99,44 @@ func (t *Teleport) DeleteSecret(ctx context.Context, log logr.Logger, ctrlClient log.Info("Deleted secret", "secretName", secretName) return nil } + +func (t *Teleport) DeleteKubeconfigSecret(ctx context.Context, log logr.Logger, ctrlClient client.Client, clusterName string, clusterNamespace string) error { + secretName := key.GetKubeconfigSecretName(clusterName) //#nosec G101 + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: clusterNamespace, + }, + } + if err := ctrlClient.Delete(ctx, secret); err != nil { + if apierrors.IsNotFound(err) { + return nil + } + return microerror.Mask(fmt.Errorf("failed to delete Secret: %w", err)) + } + log.Info("tbot: Deleted secret", "secretName", secretName) + return nil +} + +func (t *Teleport) GetKubeconfigSecret(ctx context.Context, ctrlClient client.Client, clusterName string, clusterNamespace string) (*corev1.Secret, error) { + var ( + secretName = key.GetKubeconfigSecretName(clusterName) //#nosec G101 + secretNamespace = clusterNamespace + secretNamespacedName = types.NamespacedName{Name: secretName, Namespace: secretNamespace} + secret = &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: secretNamespace, + }, + } + ) + + if err := ctrlClient.Get(ctx, secretNamespacedName, secret); err != nil { + if apierrors.IsNotFound(err) { + return nil, nil + } + return nil, microerror.Mask(fmt.Errorf("failed to get Secret: %w", err)) + } + + return secret, nil +} diff --git a/main.go b/main.go index 39a23b3f..cc91a8a2 100644 --- a/main.go +++ b/main.go @@ -62,6 +62,7 @@ func init() { func main() { var metricsAddr string var enableLeaderElection bool + var enableTeleportBot bool var probeAddr string var namespace string flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") @@ -69,6 +70,9 @@ func main() { flag.BoolVar(&enableLeaderElection, "leader-elect", false, "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") + flag.BoolVar(&enableTeleportBot, "tbot", false, + "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") opts := zap.Options{ Development: true, @@ -119,11 +123,12 @@ func main() { tele := teleport.New(namespace, config, token.NewGenerator()) if err = (&controller.ClusterReconciler{ - Client: mgr.GetClient(), - Log: ctrl.Log.WithName("controllers").WithName("Cluster"), - Scheme: mgr.GetScheme(), - Teleport: tele, - Namespace: namespace, + Client: mgr.GetClient(), + Log: ctrl.Log.WithName("controllers").WithName("Cluster"), + Scheme: mgr.GetScheme(), + Teleport: tele, + IsBotEnabled: enableTeleportBot, + Namespace: namespace, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Cluster") os.Exit(1) @@ -139,6 +144,8 @@ func main() { os.Exit(1) } + setupLog.Info("is teleport bot enabled?", "enabled", enableTeleportBot) + setupLog.Info("starting manager") if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { setupLog.Error(err, "problem running manager")