Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Teleport Matchine ID & Tests #37

Merged
merged 17 commits into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .nancy-ignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
#pkg:golang/k8s.io/[email protected]
CVE-2020-8561 until=2024-01-08 # k8s.io/[email protected]
CVE-2023-47108 until=2024-01-08 # go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/[email protected]
32 changes: 10 additions & 22 deletions helm/teleport-operator/templates/configmap.yaml
Original file line number Diff line number Diff line change
@@ -1,27 +1,15 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: tbot-config
namespace: {{ include "resource.default.namespace" . }}
name: {{ include "resource.default.name" . }}
namespace: {{ include "resource.default.namespace" . }}
labels:
{{- include "labels.common" . | nindent 4 }}
{{- include "labels.common" . | nindent 4 }}
type: Opaque
data:
tbot.yaml: |
version: v2
onboarding:
join_method: kubernetes
# ensure token is set to the name of the join token you created earlier
token: {{ .Values.teleport.managementClusterName }}
storage:
# a memory destination is used for the bots own state since the kubernetes
# join method does not require persistence.
type: memory
# ensure this is configured to the address of your Teleport Proxy or
# Auth Server. Prefer the address of the Teleport Proxy.
auth_server: {{ .Values.teleport.proxyAddr }}
# outputs will be filled in during the completion of an access guide.
outputs:
- type: identity
destination:
type: kubernetes_secret
name: identity-output
appCatalog: {{ .Values.teleport.appCatalog | quote }}
appName: {{ .Values.teleport.appName | quote }}
appVersion: {{ .Values.teleport.appVersion | quote }}
managementClusterName: {{ .Values.teleport.managementClusterName | quote }}
proxyAddr: {{ .Values.teleport.proxyAddr | quote }}
teleportVersion: {{ .Values.teleport.teleportVersion | quote }}
16 changes: 0 additions & 16 deletions helm/teleport-operator/templates/secret.yaml

This file was deleted.

27 changes: 27 additions & 0 deletions helm/teleport-operator/templates/tbot-configmap.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: tbot-config
namespace: {{ include "resource.default.namespace" . }}
labels:
{{- include "labels.common" . | nindent 4 }}
data:
tbot.yaml: |
version: v2
onboarding:
join_method: kubernetes
# ensure token is set to the name of the join token you created earlier
token: {{ .Values.teleport.managementClusterName }}-bot
storage:
# a memory destination is used for the bots own state since the kubernetes
# join method does not require persistence.
type: memory
# ensure this is configured to the address of your Teleport Proxy or
# Auth Server. Prefer the address of the Teleport Proxy.
auth_server: {{ .Values.teleport.proxyAddr }}
# outputs will be filled in during the completion of an access guide.
outputs:
- type: identity
destination:
type: kubernetes_secret
name: identity-output
36 changes: 17 additions & 19 deletions internal/controller/cluster_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ package controller

import (
"context"
"crypto/sha512"
"encoding/hex"
"time"

"github.com/giantswarm/microerror"
Expand All @@ -29,13 +27,16 @@ import (
capi "sigs.k8s.io/cluster-api/api/v1beta1"
ctrl "sigs.k8s.io/controller-runtime"

"github.com/giantswarm/teleport-operator/internal/pkg/config"
"github.com/giantswarm/teleport-operator/internal/pkg/key"
"github.com/giantswarm/teleport-operator/internal/pkg/teleport"

"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)

const identityExpirationPeriod = 20 * time.Minute

// ClusterReconciler reconciles a Cluster object
type ClusterReconciler struct {
Client client.Client
Expand Down Expand Up @@ -71,35 +72,32 @@ func (r *ClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
}
log.Info("Reconciling cluster", "cluster", cluster)

now := time.Now()
diff := now.Sub(r.Teleport.SecretConfig.LastRead)
seconds := diff.Seconds()
minutes := seconds / 60
hasher := sha512.New()
hasher.Write([]byte(r.Teleport.SecretConfig.IdentityFile))
sum := hasher.Sum(nil)
hashString := hex.EncodeToString(sum)

log.Info("Teleport identity", "last-read-minutes-ago", minutes, "hash", hashString)
if r.Teleport.Identity != nil {
log.Info("Teleport identity", "last-read-minutes-ago", r.Teleport.Identity.Age(), "hash", r.Teleport.Identity.Hash())
}

if time.Since(r.Teleport.SecretConfig.LastRead) > 20*time.Minute {
if r.Teleport.Identity == nil || time.Since(r.Teleport.Identity.LastRead) > identityExpirationPeriod {
log.Info("Retrieving new identity", "secretName", key.TeleportBotSecretName)

newSecretConfig, err := teleport.GetConfigFromSecret(ctx, r.Client, r.Namespace)
newIdentityConfig, err := config.GetIdentityConfigFromSecret(ctx, r.Client, r.Namespace)
if err != nil {
return ctrl.Result{}, microerror.Mask(err)
}
r.Teleport.SecretConfig = newSecretConfig

if r.Teleport.TeleportClient, err = teleport.NewClient(ctx, newSecretConfig.ProxyAddr, newSecretConfig.IdentityFile); err != nil {
if r.Teleport.TeleportClient, err = teleport.NewClient(ctx, r.Teleport.Config.ProxyAddr, newIdentityConfig.IdentityFile); err != nil {
return ctrl.Result{}, microerror.Mask(err)
}
log.Info("Re-connected to teleport cluster with new identity", "proxyAddr", newSecretConfig.ProxyAddr)
if r.Teleport.Identity == nil {
log.Info("Connected to teleport cluster", "proxyAddr", r.Teleport.Config.ProxyAddr)
} else {
log.Info("Re-connected to teleport cluster with new identity", "proxyAddr", r.Teleport.Config.ProxyAddr)
}
r.Teleport.Identity = newIdentityConfig
}

registerName := cluster.Name
if cluster.Name != r.Teleport.SecretConfig.ManagementClusterName {
registerName = key.GetRegisterName(r.Teleport.SecretConfig.ManagementClusterName, cluster.Name)
if cluster.Name != r.Teleport.Config.ManagementClusterName {
registerName = key.GetRegisterName(r.Teleport.Config.ManagementClusterName, cluster.Name)
}

// Check if the cluster instance is marked to be deleted, which is indicated by the deletion timestamp being set.
Expand Down
121 changes: 100 additions & 21 deletions internal/controller/cluster_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package controller

import (
"context"
"errors"
"testing"
"time"

Expand All @@ -15,6 +16,7 @@ import (
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"

"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/test"
Expand All @@ -26,19 +28,24 @@ func Test_ClusterController(t *testing.T) {
namespace string
token string
tokens []teleportTypes.ProvisionToken
secretConfig *teleport.SecretConfig
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
}{
{
name: "case 0: Register cluster and create Secret, ConfigMap and App resources in case they do not exist",
namespace: test.NamespaceName,
token: test.TokenName,
secretConfig: newSecretConfig(),
config: newConfig(),
identity: newIdentity(test.LastReadValue),
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),
Expand All @@ -48,7 +55,8 @@ func Test_ClusterController(t *testing.T) {
name: "case 1: Register cluster and update Secret, ConfigMap and App resources in case they exist",
namespace: test.NamespaceName,
token: test.TokenName,
secretConfig: newSecretConfig(),
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),
Expand All @@ -60,7 +68,8 @@ func Test_ClusterController(t *testing.T) {
name: "case 2: Update Secret and ConfigMap resources in case join token changes",
namespace: test.NamespaceName,
token: test.NewTokenName,
secretConfig: newSecretConfig(),
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),
Expand All @@ -69,13 +78,45 @@ func Test_ClusterController(t *testing.T) {
expectedConfigMap: test.NewConfigMap(test.ClusterName, test.AppName, test.NamespaceName, test.NewTokenName),
},
{
name: "case 3: Deregister cluster and delete resources in case the cluster is deleted",
namespace: test.NamespaceName,
token: test.TokenName,
secretConfig: newSecretConfig(),
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),
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),
},
{
name: "case 4: Reconnect to Teleport when credentials are rotated",
namespace: test.NamespaceName,
token: test.NewTokenName,
config: newConfig(),
identity: newIdentity(time.Now().Add(-identityExpirationPeriod - time.Second)),
cluster: test.NewCluster(test.ClusterName, test.NamespaceName, []string{key.TeleportOperatorFinalizer}, time.Time{}),
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),
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),
},
{
name: "case 5: Return an error in case reconnection to Teleport fails after the credentials are rotated",
namespace: test.NamespaceName,
token: test.TokenName,
config: newConfig(),
identity: newIdentity(time.Now().Add(-identityExpirationPeriod - time.Second)),
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),
newTeleportClient: func(ctx context.Context, proxyAddr, identityFile string) (teleport.Client, error) {
return nil, errors.New("simulated error")
},
expectedError: errors.New("secrets \"identity-output\" not found"),
},
}

Expand All @@ -91,6 +132,17 @@ func Test_ClusterController(t *testing.T) {
if tc.configMap != nil {
runtimeObjects = append(runtimeObjects, tc.configMap)
}
if tc.identitySecret != nil {
runtimeObjects = append(runtimeObjects, tc.identitySecret)
}

newTeleportClient := teleport.NewClient
if tc.newTeleportClient != nil {
teleport.NewClient = tc.newTeleportClient
}
defer func() {
teleport.NewClient = newTeleportClient
}()

ctrlClient, err := test.NewFakeK8sClient(runtimeObjects)
if err != nil {
Expand All @@ -105,11 +157,12 @@ func Test_ClusterController(t *testing.T) {
Log: log,
Scheme: scheme.Scheme,
Namespace: tc.namespace,
Teleport: teleport.New(tc.namespace, tc.secretConfig, test.NewMockTokenGenerator(tc.token)),
Teleport: teleport.New(tc.namespace, tc.config, test.NewMockTokenGenerator(tc.token)),
}
controller.Teleport.TeleportClient = test.NewTeleportClient(test.FakeTeleportClientConfig{
Tokens: tc.tokens,
})
controller.Teleport.Identity = tc.identity

req := ctrl.Request{
NamespacedName: types.NamespacedName{
Expand All @@ -120,7 +173,14 @@ func Test_ClusterController(t *testing.T) {

_, err = controller.Reconcile(ctx, req)
if err != nil {
if tc.expectedError != nil && err.Error() != tc.expectedError.Error() {
t.Fatalf("unexpected error: expected %v, actual %v", tc.expectedError, err)
} else if tc.expectedError != nil {
return
}
t.Fatalf("unexpected error %v", err)
} else if tc.expectedError != nil {
t.Fatalf("did not receive an expected error %v", tc.expectedError)
}

clusterList := &capi.ClusterList{}
Expand All @@ -142,12 +202,17 @@ func Test_ClusterController(t *testing.T) {
if err != nil {
t.Fatalf("unexpected error %v", err)
}
expectedSecretListLength := 1
if tc.identitySecret != nil {
expectedSecretListLength = 2
}
if tc.expectedSecret != nil {
if len(secretList.Items) == 0 {
if len(secretList.Items) < expectedSecretListLength {
t.Fatalf("unexpected result: secret list is empty\n%v", secretList)
}
test.CheckSecret(t, tc.expectedSecret, &secretList.Items[0])
} else if len(secretList.Items) > 0 {
actualSecret := findSecretInList(secretList, tc.expectedSecret.Name)
test.CheckSecret(t, tc.expectedSecret, actualSecret)
} else if len(secretList.Items) > expectedSecretListLength-1 {
t.Fatalf("unexpected result: secret list is not empty\n%v", secretList)
}

Expand All @@ -158,11 +223,11 @@ func Test_ClusterController(t *testing.T) {
}
if tc.expectedConfigMap != nil {
if len(configMapList.Items) == 0 {
t.Fatalf("unexpected result: secret list is empty\n%v", secretList)
t.Fatalf("unexpected result: config map list is empty\n%v", configMapList)
}
test.CheckConfigMap(t, tc.expectedConfigMap, &configMapList.Items[0])
} else if len(configMapList.Items) > 0 {
t.Fatalf("unexpected result: config map list is not empty\n%v", secretList)
t.Fatalf("unexpected result: config map list is not empty\n%v", configMapList)
}

appList := &appv1alpha1.AppList{}
Expand All @@ -174,15 +239,29 @@ func Test_ClusterController(t *testing.T) {
}
}

func newSecretConfig() *teleport.SecretConfig {
return &teleport.SecretConfig{
func newConfig() *config.Config {
return &config.Config{
AppCatalog: test.AppCatalog,
AppName: test.AppName,
AppVersion: test.AppVersion,
IdentityFile: test.IdentityFileValue,
LastRead: test.LastReadValue,
ManagementClusterName: test.ManagementClusterName,
ProxyAddr: test.ProxyAddr,
TeleportVersion: test.TeleportVersion,
}
}

func newIdentity(lastRead time.Time) *config.IdentityConfig {
return &config.IdentityConfig{
IdentityFile: test.IdentityFileValue,
LastRead: lastRead,
}
}

func findSecretInList(secretList *corev1.SecretList, name string) *corev1.Secret {
for _, secret := range secretList.Items {
if secret.Name == name {
return &secret
}
}
return nil
}
Loading