diff --git a/transport/stunnel/client.go b/transport/stunnel/client.go new file mode 100644 index 0000000..e7bf234 --- /dev/null +++ b/transport/stunnel/client.go @@ -0,0 +1,261 @@ +package stunnel + +import ( + "bytes" + "context" + "strconv" + "text/template" + + "github.com/backube/pvc-transfer/transport" + "github.com/backube/pvc-transfer/transport/tls/certs" + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +const clientListenPort = 6443 + +const ( + stunnelClientConfTemplate = ` + pid = + sslVersion = TLSv1.3 + client = yes + syslog = no + output = /dev/stdout + [rsync] + debug = 7 + accept = {{ .listenPort }} + cert = /etc/stunnel/certs/tls.crt + key = /etc/stunnel/certs/tls.key + CAfile = /etc/stunnel/certs/ca.crt + verify = 2 +{{- if not (eq .proxyHost "") }} + protocol = connect + connect = {{ .proxyHost }} + protocolHost = {{ .hostname }}:{{ .listenPort }} +{{- if not (eq .proxyUsername "") }} + protocolUsername = {{ .proxyUsername }} +{{- end }} +{{- if not (eq .proxyPassword "") }} + protocolPassword = {{ .proxyPassword }} +{{- end }} +{{- else }} + connect = {{ .hostname }}:{{ .connectPort }} +{{- end }} +` +) + +type client struct { + logger logr.Logger + ingressPort int32 + listenPort int32 + containers []corev1.Container + volumes []corev1.Volume + options *transport.Options + serverHostname string + namespacedName types.NamespacedName +} + +func (sc *client) MarkForCleanup(ctx context.Context, c ctrlclient.Client, key, value string) error { + return markForCleanup(ctx, c, sc.namespacedName, key, value, "client") +} + +func (sc *client) NamespacedName() types.NamespacedName { + return sc.namespacedName +} + +func (sc *client) ConnectPort() int32 { + return sc.ingressPort +} + +func (sc *client) ListenPort() int32 { + return sc.listenPort +} + +func (sc *client) Containers() []corev1.Container { + return sc.containers +} + +func (sc *client) Volumes() []corev1.Volume { + return sc.volumes +} + +func (sc *client) Options() *transport.Options { + return sc.options +} + +func (sc *client) Type() transport.Type { + return TransportTypeStunnel +} + +func (sc *client) Credentials() types.NamespacedName { + return types.NamespacedName{ + Namespace: sc.namespacedName.Namespace, + Name: stunnelSecret, + } +} + +func (sc *client) Hostname() string { + return "localhost" +} + +func NewClient(ctx context.Context, c ctrlclient.Client, logger logr.Logger, + namespacedName types.NamespacedName, + hostname string, + ingressPort int32, + options *transport.Options) (transport.Transport, error) { + clientLogger := logger.WithValues("stunnelClient", namespacedName) + tc := &client{ + logger: clientLogger, + namespacedName: namespacedName, + options: options, + ingressPort: ingressPort, + serverHostname: hostname, + listenPort: clientListenPort, + } + + err := tc.reconcileConfig(ctx, c) + if err != nil { + return nil, err + } + + err = tc.reconcileSecret(ctx, c) + if err != nil { + return nil, err + } + + tc.containers = tc.clientContainers(tc.ListenPort()) + tc.volumes = tc.clientVolumes() + + return tc, nil +} + +func (sc *client) reconcileConfig(ctx context.Context, c ctrlclient.Client) error { + stunnelConfTemplate, err := template.New("config").Parse(stunnelClientConfTemplate) + if err != nil { + sc.logger.Error(err, "unable to parse stunnel client config template") + return err + } + + connections := map[string]string{ + "listenPort": strconv.Itoa(int(sc.ListenPort())), + "hostname": sc.serverHostname, + "connectPort": strconv.Itoa(int(sc.ConnectPort())), + "proxyHost": sc.Options().ProxyURL, + "proxyUsername": sc.Options().ProxyUsername, + "proxyPassword": sc.Options().ProxyPassword, + } + var stunnelConf bytes.Buffer + err = stunnelConfTemplate.Execute(&stunnelConf, connections) + if err != nil { + sc.logger.Error(err, "unable to execute stunnel client config template") + return err + } + + stunnelConfigMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: sc.NamespacedName().Namespace, + Name: getResourceName(sc.namespacedName, "client", stunnelConfig), + }, + } + _, err = controllerutil.CreateOrUpdate(ctx, c, stunnelConfigMap, func() error { + stunnelConfigMap.Labels = sc.options.Labels + stunnelConfigMap.OwnerReferences = sc.options.Owners + + stunnelConfigMap.Data = map[string]string{ + "stunnel.conf": stunnelConf.String(), + } + return err + }) + return err +} + +func (sc *client) reconcileSecret(ctx context.Context, c ctrlclient.Client) error { + valid, err := isSecretValid(ctx, c, sc.logger, sc.namespacedName, "client") + if err != nil { + sc.logger.Error(err, "error getting existing ssl certs from secret") + return err + } + if valid { + return nil + } + + crtBundle, err := certs.New() + if err != nil { + sc.logger.Error(err, "error generating ssl certificate bundle for stunnel client") + return err + } + + return reconcileCertificateSecrets(ctx, c, sc.namespacedName, sc.options, crtBundle) +} + +func (sc *client) clientContainers(listenPort int32) []corev1.Container { + return []corev1.Container{ + { + Name: Container, + Image: getImage(sc.options), + Command: []string{ + "/bin/stunnel", + "/etc/stunnel/stunnel.conf", + }, + Ports: []corev1.ContainerPort{ + { + Name: "stunnel", + Protocol: corev1.ProtocolTCP, + ContainerPort: listenPort, + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: getResourceName(sc.namespacedName, "client", stunnelConfig), + MountPath: "/etc/stunnel/stunnel.conf", + SubPath: "stunnel.conf", + }, + { + Name: getResourceName(sc.namespacedName, "client", stunnelSecret), + MountPath: "/etc/stunnel/certs", + }, + }, + }, + } +} + +func (sc *client) clientVolumes() []corev1.Volume { + return []corev1.Volume{ + { + Name: getResourceName(sc.namespacedName, "client", stunnelConfig), + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: getResourceName(sc.namespacedName, "client", stunnelConfig), + }, + }, + }, + }, + { + Name: getResourceName(sc.namespacedName, "client", stunnelSecret), + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: getResourceName(sc.namespacedName, "client", stunnelSecret), + Items: []corev1.KeyToPath{ + { + Key: "tls.crt", + Path: "tls.crt", + }, + { + Key: "tls.key", + Path: "tls.key", + }, + { + Key: "ca.crt", + Path: "ca.crt", + }, + }, + }, + }, + }, + } +} diff --git a/transport/stunnel/client_test.go b/transport/stunnel/client_test.go new file mode 100644 index 0000000..3001c79 --- /dev/null +++ b/transport/stunnel/client_test.go @@ -0,0 +1,158 @@ +package stunnel + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/backube/pvc-transfer/transport" + logrtesting "github.com/go-logr/logr/testing" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" +) + +func TestNewClient(t *testing.T) { + tests := []struct { + name string + namespacedName types.NamespacedName + hostname string + ingressPort int32 + labels map[string]string + ownerReferences []metav1.OwnerReference + wantErr bool + objects []ctrlclient.Object + }{ + { + name: "test with no stunnel client objects", + namespacedName: types.NamespacedName{Namespace: "bar", Name: "foo"}, + hostname: "example-test.com", + ingressPort: 8080, + labels: map[string]string{"test": "me"}, + ownerReferences: testOwnerReferences(), + wantErr: false, + objects: []ctrlclient.Object{}, + }, + { + name: "test stunnel client, valid secret exists but no configmap", + namespacedName: types.NamespacedName{Namespace: "bar", Name: "foo"}, + labels: map[string]string{"test": "me"}, + ownerReferences: testOwnerReferences(), + wantErr: false, + objects: []ctrlclient.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-client-stunnel-credentials", + Namespace: "bar", + }, + Data: map[string][]byte{"tls.key": []byte(`key`), "tls.crt": []byte(`crt`)}, + }, + }, + }, + { + name: "test stunnel client, invalid secret exists but no configmap", + namespacedName: types.NamespacedName{Namespace: "bar", Name: "foo"}, + labels: map[string]string{"test": "me"}, + ownerReferences: testOwnerReferences(), + wantErr: false, + objects: []ctrlclient.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-client-stunnel-credentials", + Namespace: "bar", + }, + Data: map[string][]byte{"tls.crt": []byte(`crt`)}, + }, + }, + }, + { + name: "test stunnel client, valid configmap but no secret", + namespacedName: types.NamespacedName{Namespace: "bar", Name: "foo"}, + labels: map[string]string{"test": "me"}, + ownerReferences: testOwnerReferences(), + wantErr: false, + objects: []ctrlclient.Object{ + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-client-stunnel-config", + Namespace: "bar", + }, + Data: map[string]string{"stunnel.conf": "foreground = yes"}, + }, + }, + }, + { + name: "test stunnel client, invalid configmap but no secret", + namespacedName: types.NamespacedName{Namespace: "bar", Name: "foo"}, + labels: map[string]string{"test": "me"}, + ownerReferences: testOwnerReferences(), + wantErr: false, + objects: []ctrlclient.Object{ + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-client-stunnel-config", + Namespace: "bar", + }, + Data: map[string]string{"stunnel.conf": "foreground = no"}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fakeClient := fakeClientWithObjects(tt.objects...) + ctx := context.WithValue(context.Background(), "test", tt.name) + fakeLogger := logrtesting.TestLogger{t} + stunnelClient, err := NewClient(ctx, fakeClient, fakeLogger, tt.namespacedName, tt.hostname, tt.ingressPort, &transport.Options{Labels: tt.labels, Owners: tt.ownerReferences}) + if (err != nil) != tt.wantErr { + t.Errorf("NewClient() error = %v, wantErr %v", err, tt.wantErr) + return + } + cm := &corev1.ConfigMap{} + err = fakeClient.Get(context.Background(), types.NamespacedName{ + Namespace: "bar", + Name: "foo-client-" + stunnelConfig, + }, cm) + if err != nil { + panic(fmt.Errorf("%#v should not be getting error from fake client", err)) + } + + configdata, ok := cm.Data["stunnel.conf"] + if !ok { + t.Error("unable to find stunnel config data in configmap") + } + if !strings.Contains(configdata, "pid =") { + t.Error("configmap data does not contain the right data") + } + + secret := &corev1.Secret{} + err = fakeClient.Get(context.Background(), types.NamespacedName{ + Namespace: "bar", + Name: "foo-client-" + stunnelSecret, + }, secret) + if err != nil { + panic(fmt.Errorf("%#v should not be getting error from fake client", err)) + } + + _, ok = secret.Data["tls.key"] + if !ok { + t.Error("unable to find tls.key in stunnel secret") + } + + _, ok = secret.Data["tls.crt"] + if !ok { + t.Error("unable to find tls.crt in stunnel secret") + } + + if len(stunnelClient.Volumes()) == 0 { + t.Error("stunnel client volumes not set properly") + } + + if len(stunnelClient.Containers()) == 0 { + t.Error("stunnel client containers not set properly") + } + }) + } +} diff --git a/transport/stunnel/server.go b/transport/stunnel/server.go index 958fc9e..24415bd 100644 --- a/transport/stunnel/server.go +++ b/transport/stunnel/server.go @@ -7,7 +7,6 @@ import ( "text/template" "github.com/backube/pvc-transfer/endpoint" - "github.com/backube/pvc-transfer/internal/utils" "github.com/backube/pvc-transfer/transport" "github.com/backube/pvc-transfer/transport/tls/certs" "github.com/go-logr/logr" @@ -130,46 +129,7 @@ func (s *server) Hostname() string { } func (s *server) MarkForCleanup(ctx context.Context, c ctrlclient.Client, key, value string) error { - cm := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: s.prefixedName(stunnelConfig), - Namespace: s.NamespacedName().Namespace, - }, - } - err := utils.UpdateWithLabel(ctx, c, cm, key, value) - if err != nil { - return err - } - - clientSecret := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: getResourceName(s.namespacedName, clientSecretNameSuffix()), - Namespace: s.NamespacedName().Namespace, - }, - } - err = utils.UpdateWithLabel(ctx, c, clientSecret, key, value) - if err != nil { - return err - } - - serverSecret := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: getResourceName(s.namespacedName, serverSecretNameSuffix()), - Namespace: s.NamespacedName().Namespace, - }, - } - err = utils.UpdateWithLabel(ctx, c, serverSecret, key, value) - if err != nil { - return err - } - - crtBundleSecret := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: getResourceName(s.namespacedName, caBundleSecretNameSuffix()), - Namespace: s.NamespacedName().Namespace, - }, - } - return utils.UpdateWithLabel(ctx, c, crtBundleSecret, key, value) + return markForCleanup(ctx, c, s.namespacedName, key, value, "server") } func (s *server) reconcileConfig(ctx context.Context, c ctrlclient.Client) error { @@ -194,7 +154,7 @@ func (s *server) reconcileConfig(ctx context.Context, c ctrlclient.Client) error stunnelConfigMap := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Name: s.prefixedName(stunnelConfig), + Name: getResourceName(s.namespacedName, "server", stunnelConfig), Namespace: s.NamespacedName().Namespace, }, } @@ -216,7 +176,7 @@ func (s *server) prefixedName(name string) string { } func (s *server) reconcileSecret(ctx context.Context, c ctrlclient.Client) error { - secretValid, err := isSecretValid(ctx, c, s.logger, s.namespacedName, serverSecretNameSuffix()) + secretValid, err := isSecretValid(ctx, c, s.logger, s.namespacedName, "server") if err != nil { s.logger.Error(err, "error getting existing ssl certs from secret") return err @@ -258,7 +218,7 @@ func (s *server) serverContainers() []corev1.Container { SubPath: "stunnel.conf", }, { - Name: getResourceName(s.namespacedName, serverSecretNameSuffix()), + Name: getResourceName(s.namespacedName, "server", stunnelSecret), MountPath: "/etc/stunnel/certs", }, }, @@ -279,10 +239,10 @@ func (s *server) serverVolumes() []corev1.Volume { }, }, { - Name: getResourceName(s.namespacedName, serverSecretNameSuffix()), + Name: getResourceName(s.namespacedName, "server", stunnelSecret), VolumeSource: corev1.VolumeSource{ Secret: &corev1.SecretVolumeSource{ - SecretName: getResourceName(s.namespacedName, serverSecretNameSuffix()), + SecretName: getResourceName(s.namespacedName, "server", stunnelSecret), Items: []corev1.KeyToPath{ { Key: "tls.crt", @@ -292,6 +252,10 @@ func (s *server) serverVolumes() []corev1.Volume { Key: "tls.key", Path: "tls.key", }, + { + Key: "ca.crt", + Path: "ca.crt", + }, }, }, }, diff --git a/transport/stunnel/server_test.go b/transport/stunnel/server_test.go index 0aed654..e1f574b 100644 --- a/transport/stunnel/server_test.go +++ b/transport/stunnel/server_test.go @@ -3,7 +3,6 @@ package stunnel import ( "context" "fmt" - "reflect" "strings" "testing" @@ -221,125 +220,3 @@ func TestNewServer(t *testing.T) { }) } } - -func Test_server_MarkForCleanup(t *testing.T) { - tests := []struct { - name string - namespacedName types.NamespacedName - labels map[string]string - wantErr bool - key string - value string - objects []ctrlclient.Object - }{ - { - name: "test with configmap and secret objects", - namespacedName: types.NamespacedName{Namespace: "bar", Name: "foo"}, - labels: map[string]string{"test": "me"}, - wantErr: false, - key: "cleanup-key", - value: "cleanup-value", - objects: []ctrlclient.Object{ - &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "foo-server-stunnel-credentials", - Namespace: "bar", - Labels: map[string]string{"test": "me"}, - }, - Data: map[string][]byte{"tls.key": []byte(`key`), "tls.crt": []byte(`crt`)}, - }, - &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "foo-client-stunnel-credentials", - Namespace: "bar", - Labels: map[string]string{"test": "me"}, - }, - Data: map[string][]byte{"tls.key": []byte(`key`), "tls.crt": []byte(`crt`)}, - }, - &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "foo-ca-bundle-stunnel-credentials", - Namespace: "bar", - Labels: map[string]string{"test": "me"}, - }, - Data: map[string][]byte{"tls.key": []byte(`key`), "tls.crt": []byte(`crt`)}, - }, - &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: "foo-server-stunnel-config", - Namespace: "bar", - Labels: map[string]string{"test": "me"}, - }, - Data: map[string]string{"stunnel.conf": "foreground = yes"}, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - s := &server{ - logger: logrtesting.TestLogger{T: t}, - options: &transport.Options{ - Labels: tt.labels, - Owners: testOwnerReferences(), - }, - namespacedName: tt.namespacedName, - } - ctx := context.WithValue(context.Background(), "test", tt.name) - fakeClient := fakeClientWithObjects(tt.objects...) - if err := s.MarkForCleanup(ctx, fakeClient, tt.key, tt.value); (err != nil) != tt.wantErr { - t.Errorf("MarkForCleanup() error = %v, wantErr %v", err, tt.wantErr) - } - - cm := &corev1.ConfigMap{} - err := fakeClient.Get(context.Background(), types.NamespacedName{ - Namespace: "bar", - Name: "foo-server-" + stunnelConfig, - }, cm) - if err != nil { - panic(fmt.Errorf("%#v should not be getting error from fake client", err)) - } - - tt.labels[tt.key] = tt.value - if !reflect.DeepEqual(tt.labels, cm.Labels) { - t.Errorf("labels on configmap = %#v, wanted %#v", cm.Labels, tt.labels) - } - - secretSecret := &corev1.Secret{} - err = fakeClient.Get(context.Background(), types.NamespacedName{ - Namespace: "bar", - Name: "foo-server-" + stunnelSecret, - }, secretSecret) - if err != nil { - panic(fmt.Errorf("%#v should not be getting error from fake client", err)) - } - if !reflect.DeepEqual(tt.labels, secretSecret.Labels) { - t.Errorf("labels on secretSecret = %#v, wanted %#v", secretSecret.Labels, tt.labels) - } - - clientSecret := &corev1.Secret{} - err = fakeClient.Get(context.Background(), types.NamespacedName{ - Namespace: "bar", - Name: "foo-client-" + stunnelSecret, - }, clientSecret) - if err != nil { - panic(fmt.Errorf("%#v should not be getting error from fake client", err)) - } - if !reflect.DeepEqual(tt.labels, clientSecret.Labels) { - t.Errorf("labels on secretSecret = %#v, wanted %#v", secretSecret.Labels, tt.labels) - } - - caBundleSecret := &corev1.Secret{} - err = fakeClient.Get(context.Background(), types.NamespacedName{ - Namespace: "bar", - Name: "foo-ca-bundle-" + stunnelSecret, - }, caBundleSecret) - if err != nil { - panic(fmt.Errorf("%#v should not be getting error from fake client", err)) - } - if !reflect.DeepEqual(tt.labels, clientSecret.Labels) { - t.Errorf("labels on secretSecret = %#v, wanted %#v", secretSecret.Labels, tt.labels) - } - }) - } -} diff --git a/transport/stunnel/stunnel.go b/transport/stunnel/stunnel.go index fb372c6..9d830d9 100644 --- a/transport/stunnel/stunnel.go +++ b/transport/stunnel/stunnel.go @@ -3,16 +3,18 @@ package stunnel import ( "bytes" "context" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "fmt" + "github.com/backube/pvc-transfer/internal/utils" "github.com/backube/pvc-transfer/transport" "github.com/backube/pvc-transfer/transport/tls/certs" "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) const ( @@ -26,18 +28,6 @@ const ( Container = "stunnel" ) -func serverSecretNameSuffix() string { - return "server-" + stunnelSecret -} - -func clientSecretNameSuffix() string { - return "client-" + stunnelSecret -} - -func caBundleSecretNameSuffix() string { - return "ca-bundle-" + stunnelSecret -} - func getImage(options *transport.Options) string { if options.Image == "" { return defaultStunnelImage @@ -46,15 +36,15 @@ func getImage(options *transport.Options) string { } } -func getResourceName(obj types.NamespacedName, suffix string) string { - return obj.Name + "-" + suffix +func getResourceName(obj types.NamespacedName, component, suffix string) string { + return fmt.Sprintf("%s-%s-%s", obj.Name, component, suffix) } -func isSecretValid(ctx context.Context, c ctrlclient.Client, logger logr.Logger, key types.NamespacedName, suffix string) (bool, error) { +func isSecretValid(ctx context.Context, c ctrlclient.Client, logger logr.Logger, key types.NamespacedName, component string) (bool, error) { secret := &corev1.Secret{} err := c.Get(ctx, types.NamespacedName{ Namespace: key.Namespace, - Name: getResourceName(key, suffix), + Name: getResourceName(key, component, stunnelSecret), }, secret) switch { case k8serrors.IsNotFound(err): @@ -67,7 +57,7 @@ func isSecretValid(ctx context.Context, c ctrlclient.Client, logger logr.Logger, if !ok { logger.Info("secret data missing key tls.key", "secret", types.NamespacedName{ Namespace: key.Namespace, - Name: getResourceName(key, suffix), + Name: getResourceName(key, component, stunnelSecret), }) return false, nil } @@ -76,7 +66,7 @@ func isSecretValid(ctx context.Context, c ctrlclient.Client, logger logr.Logger, if !ok { logger.Info("secret data missing key tls.crt", "secret", types.NamespacedName{ Namespace: key.Namespace, - Name: getResourceName(key, suffix), + Name: getResourceName(key, component, stunnelSecret), }) return false, nil } @@ -85,7 +75,7 @@ func isSecretValid(ctx context.Context, c ctrlclient.Client, logger logr.Logger, if !ok { logger.Info("secret data missing key ca.crt", "secret", types.NamespacedName{ Namespace: key.Namespace, - Name: getResourceName(key, suffix), + Name: getResourceName(key, component, stunnelSecret), }) return false, nil } @@ -100,7 +90,7 @@ func reconcileCertificateSecrets(ctx context.Context, crtBundle *certs.CertificateBundle) error { crtBundleSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: getResourceName(key, caBundleSecretNameSuffix()), + Name: getResourceName(key, "ca-bundle", stunnelSecret), Namespace: key.Namespace, }, } @@ -124,7 +114,7 @@ func reconcileCertificateSecrets(ctx context.Context, serverSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: getResourceName(key, serverSecretNameSuffix()), + Name: getResourceName(key, "server", stunnelSecret), Namespace: key.Namespace, }, } @@ -145,7 +135,7 @@ func reconcileCertificateSecrets(ctx context.Context, clientSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: getResourceName(key, clientSecretNameSuffix()), + Name: getResourceName(key, "client", stunnelSecret), Namespace: key.Namespace, }, } @@ -162,3 +152,58 @@ func reconcileCertificateSecrets(ctx context.Context, }) return err } + +func markForCleanup(ctx context.Context, c ctrlclient.Client, objKey types.NamespacedName, key, value, component string) error { + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: getResourceName(objKey, component, stunnelConfig), + Namespace: objKey.Namespace, + }, + } + err := utils.UpdateWithLabel(ctx, c, cm, key, value) + if err != nil { + return err + } + + clientSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: getResourceName(objKey, "client", stunnelSecret), + Namespace: objKey.Namespace, + }, + } + err = utils.UpdateWithLabel(ctx, c, clientSecret, key, value) + switch { + case k8serrors.IsNotFound(err): + break + case err != nil: + return err + } + + serverSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: getResourceName(objKey, "server", stunnelSecret), + Namespace: objKey.Namespace, + }, + } + err = utils.UpdateWithLabel(ctx, c, serverSecret, key, value) + switch { + case k8serrors.IsNotFound(err): + break + case err != nil: + return err + } + crtBundleSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: getResourceName(objKey, "ca-bundle", stunnelSecret), + Namespace: objKey.Namespace, + }, + } + err = utils.UpdateWithLabel(ctx, c, crtBundleSecret, key, value) + switch { + case k8serrors.IsNotFound(err): + break + case err != nil: + return err + } + return nil +} diff --git a/transport/stunnel/stunnel_test.go b/transport/stunnel/stunnel_test.go index f34796d..80de79d 100644 --- a/transport/stunnel/stunnel_test.go +++ b/transport/stunnel/stunnel_test.go @@ -2,6 +2,8 @@ package stunnel import ( "context" + "fmt" + "reflect" "testing" "github.com/backube/pvc-transfer/transport" @@ -92,7 +94,7 @@ func Test_getExistingCert(t *testing.T) { objects: []ctrlclient.Object{ &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: "foo-stunnel-credentials", + Name: "foo-foo-stunnel-credentials", Namespace: "bar", Labels: map[string]string{"test": "me"}, }, @@ -104,7 +106,7 @@ func Test_getExistingCert(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := &server{ - logger: logrtesting.TestLogger{t}, + logger: logrtesting.TestLogger{T: t}, namespacedName: tt.namespacedName, options: &transport.Options{ Labels: tt.labels, @@ -112,7 +114,7 @@ func Test_getExistingCert(t *testing.T) { }, } ctx := context.WithValue(context.Background(), "test", tt.name) - found, err := isSecretValid(ctx, fakeClientWithObjects(tt.objects...), s.logger, s.namespacedName, stunnelSecret) + found, err := isSecretValid(ctx, fakeClientWithObjects(tt.objects...), s.logger, s.namespacedName, "foo") if err != nil { t.Error("found unexpected error", err) } @@ -125,3 +127,154 @@ func Test_getExistingCert(t *testing.T) { }) } } + +func Test_mrkForCleanup(t *testing.T) { + tests := []struct { + name string + namespacedName types.NamespacedName + labels map[string]string + wantErr bool + key string + value string + objects []ctrlclient.Object + verifyObjects []ctrlclient.Object + }{ + { + name: "test with configmap and secret objects", + namespacedName: types.NamespacedName{Namespace: "bar", Name: "foo"}, + labels: map[string]string{"test": "me"}, + wantErr: false, + key: "cleanup-key", + value: "cleanup-value", + objects: []ctrlclient.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-server-stunnel-credentials", + Namespace: "bar", + Labels: map[string]string{"test": "me"}, + }, + Data: map[string][]byte{"tls.key": []byte(`key`), "tls.crt": []byte(`crt`)}, + }, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-client-stunnel-credentials", + Namespace: "bar", + Labels: map[string]string{"test": "me"}, + }, + Data: map[string][]byte{"tls.key": []byte(`key`), "tls.crt": []byte(`crt`)}, + }, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-ca-bundle-stunnel-credentials", + Namespace: "bar", + Labels: map[string]string{"test": "me"}, + }, + Data: map[string][]byte{"tls.key": []byte(`key`), "tls.crt": []byte(`crt`)}, + }, + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-foo-stunnel-config", + Namespace: "bar", + Labels: map[string]string{"test": "me"}, + }, + Data: map[string]string{"stunnel.conf": "foreground = yes"}, + }, + }, + verifyObjects: []ctrlclient.Object{ + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-foo-" + stunnelConfig, + }, + }, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-client-" + stunnelSecret, + }, + }, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-server-" + stunnelSecret, + }, + }, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-ca-bundle-" + stunnelSecret, + }, + }, + }, + }, + { + name: "test with configmap but no server secret", + namespacedName: types.NamespacedName{Namespace: "bar", Name: "foo"}, + labels: map[string]string{"test": "me"}, + wantErr: false, + key: "cleanup-key", + value: "cleanup-value", + objects: []ctrlclient.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-client-stunnel-credentials", + Namespace: "bar", + Labels: map[string]string{"test": "me"}, + }, + Data: map[string][]byte{"tls.key": []byte(`key`), "tls.crt": []byte(`crt`)}, + }, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-ca-bundle-stunnel-credentials", + Namespace: "bar", + Labels: map[string]string{"test": "me"}, + }, + Data: map[string][]byte{"tls.key": []byte(`key`), "tls.crt": []byte(`crt`)}, + }, + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-foo-stunnel-config", + Namespace: "bar", + Labels: map[string]string{"test": "me"}, + }, + Data: map[string]string{"stunnel.conf": "foreground = yes"}, + }, + }, + verifyObjects: []ctrlclient.Object{ + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-foo-" + stunnelConfig, + }, + }, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-client-" + stunnelSecret, + }, + }, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-ca-bundle-" + stunnelSecret, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.WithValue(context.Background(), "test", tt.name) + fakeClient := fakeClientWithObjects(tt.objects...) + if err := markForCleanup(ctx, fakeClient, tt.namespacedName, tt.key, tt.value, "foo"); (err != nil) != tt.wantErr { + t.Errorf("markForCleanup() error = %v, wantErr %v", err, tt.wantErr) + } + + tt.labels[tt.key] = tt.value + for _, obj := range tt.verifyObjects { + err := fakeClient.Get(context.Background(), types.NamespacedName{ + Namespace: tt.namespacedName.Namespace, + Name: obj.GetName()}, obj) + if err != nil { + panic(fmt.Errorf("%#v should not be getting error from fake client", err)) + } + if !reflect.DeepEqual(tt.labels, obj.GetLabels()) { + t.Errorf("labels on obj = %#v, wanted %#v", obj.GetLabels(), tt.labels) + } + } + }) + } +} diff --git a/transport/transport.go b/transport/transport.go index 12e83ad..6f46c06 100644 --- a/transport/transport.go +++ b/transport/transport.go @@ -53,10 +53,6 @@ type Options struct { ProxyUsername string // ProxyPassword password for connecting to the proxy ProxyPassword string - // NoVerifyCA allows you to override verification of TLS certs - NoVerifyCA bool - // CAVerifyLevel the level at which CA certs will be verify if NoVerifyCA is false - CAVerifyLevel string } type Type string