From c83b18c4f5c200fa1cec1c727627e0ac0299f70d Mon Sep 17 00:00:00 2001 From: Michael Burman Date: Tue, 20 Aug 2024 16:27:35 +0300 Subject: [PATCH 1/4] Add nodetool command for pod access with support for automated TLS --- Makefile | 4 +- cmd/kubectl-k8ssandra/k8ssandra/k8ssandra.go | 3 +- cmd/kubectl-k8ssandra/nodetool/nodetool.go | 140 +++++++++++++++++++ pkg/cassdcutil/config.go | 68 +++++++++ pkg/cassdcutil/config_test.go | 35 +++++ pkg/cassdcutil/fetcher.go | 55 ++++++++ pkg/cassdcutil/manage.go | 22 +-- pkg/cassdcutil/secrets.go | 42 ++++++ 8 files changed, 345 insertions(+), 24 deletions(-) create mode 100644 cmd/kubectl-k8ssandra/nodetool/nodetool.go create mode 100644 pkg/cassdcutil/config.go create mode 100644 pkg/cassdcutil/config_test.go create mode 100644 pkg/cassdcutil/fetcher.go create mode 100644 pkg/cassdcutil/secrets.go diff --git a/Makefile b/Makefile index a748bd0..837fb88 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ ENVTEST_K8S_VERSION = 1.28.x GO_FLAGS ?= -v .PHONY: all -all: build +all: test build ##@ General @@ -59,7 +59,7 @@ lint: golangci-lint ## Run golangci-lint against code $(GOLANGCI_LINT) run ./... .PHONY: build -build: test ## Build kubectl-k8ssandra +build: ## Build kubectl-k8ssandra CGO_ENABLED=0 go build -o kubectl-k8ssandra cmd/kubectl-k8ssandra/main.go .PHONY: docker-build diff --git a/cmd/kubectl-k8ssandra/k8ssandra/k8ssandra.go b/cmd/kubectl-k8ssandra/k8ssandra/k8ssandra.go index 4631978..4f1559e 100644 --- a/cmd/kubectl-k8ssandra/k8ssandra/k8ssandra.go +++ b/cmd/kubectl-k8ssandra/k8ssandra/k8ssandra.go @@ -7,9 +7,9 @@ import ( // "github.com/k8ssandra/k8ssandra-client/cmd/kubectl-k8ssandra/edit" // "github.com/k8ssandra/k8ssandra-client/cmd/kubectl-k8ssandra/list" // "github.com/k8ssandra/k8ssandra-client/cmd/kubectl-k8ssandra/migrate" - // "github.com/k8ssandra/k8ssandra-client/cmd/kubectl-k8ssandra/nodetool" "github.com/k8ssandra/k8ssandra-client/cmd/kubectl-k8ssandra/config" "github.com/k8ssandra/k8ssandra-client/cmd/kubectl-k8ssandra/helm" + "github.com/k8ssandra/k8ssandra-client/cmd/kubectl-k8ssandra/nodetool" "github.com/k8ssandra/k8ssandra-client/cmd/kubectl-k8ssandra/operate" "github.com/k8ssandra/k8ssandra-client/cmd/kubectl-k8ssandra/register" "github.com/k8ssandra/k8ssandra-client/cmd/kubectl-k8ssandra/users" @@ -54,6 +54,7 @@ func NewCmd(streams genericclioptions.IOStreams) *cobra.Command { // cmd.AddCommand(migrate.NewInstallCmd(streams)) cmd.AddCommand(config.NewCmd(streams)) cmd.AddCommand(helm.NewHelmCmd(streams)) + cmd.AddCommand(nodetool.NewCmd(streams)) register.SetupRegisterClusterCmd(cmd, streams) // cmd.Flags().BoolVar(&o.listNamespaces, "list", o.listNamespaces, "if true, print the list of all namespaces in the current KUBECONFIG") diff --git a/cmd/kubectl-k8ssandra/nodetool/nodetool.go b/cmd/kubectl-k8ssandra/nodetool/nodetool.go new file mode 100644 index 0000000..00f42de --- /dev/null +++ b/cmd/kubectl-k8ssandra/nodetool/nodetool.go @@ -0,0 +1,140 @@ +package nodetool + +import ( + "context" + "fmt" + + "github.com/k8ssandra/k8ssandra-client/pkg/cassdcutil" + "github.com/k8ssandra/k8ssandra-client/pkg/kubernetes" + "github.com/k8ssandra/k8ssandra-client/pkg/util" + "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/kubectl/pkg/cmd/exec" +) + +var ( + cqlshExample = ` + # launch a interactive cqlsh shell on node + %[1]s nodetool [] +` + errNotEnoughParameters = fmt.Errorf("not enough parameters to run nodetool") +) + +type options struct { + configFlags *genericclioptions.ConfigFlags + genericclioptions.IOStreams + execOptions *exec.ExecOptions + cassManager *cassdcutil.CassManager + params []string +} + +func newOptions(streams genericclioptions.IOStreams) *options { + return &options{ + configFlags: genericclioptions.NewConfigFlags(true), + IOStreams: streams, + } +} + +// NewCmd provides a cobra command wrapping cqlShOptions +func NewCmd(streams genericclioptions.IOStreams) *cobra.Command { + o := newOptions(streams) + + cmd := &cobra.Command{ + Use: "nodetool [pod] [flags]", + Short: "nodetool launched on pod", + Example: fmt.Sprintf(cqlshExample, "kubectl k8ssandra"), + SilenceUsage: true, + RunE: func(c *cobra.Command, args []string) error { + if err := o.Complete(c, args); err != nil { + return err + } + if err := o.Validate(); err != nil { + return err + } + if err := o.Run(); err != nil { + return err + } + + return nil + }, + } + + o.configFlags.AddFlags(cmd.Flags()) + return cmd +} + +// Complete parses the arguments and necessary flags to options +func (c *options) Complete(cmd *cobra.Command, args []string) error { + var err error + + if len(args) < 2 { + return errNotEnoughParameters + } + + execOptions, err := util.GetExecOptions(c.IOStreams, c.configFlags) + if err != nil { + return err + } + c.execOptions = execOptions + execOptions.PodName = args[0] + + restConfig, err := c.configFlags.ToRESTConfig() + if err != nil { + return err + } + + kubeClient, err := kubernetes.GetClientInNamespace(restConfig, execOptions.Namespace) + if err != nil { + return err + } + + c.cassManager = cassdcutil.NewManager(kubeClient) + + c.params = args[1:] + + return nil +} + +// Validate ensures that all required arguments and flag values are provided +func (c *options) Validate() error { + // We could validate here if a nodetool command requires flags, but lets let nodetool throw that error + + return nil +} + +// Run triggers the nodetool command on target pod +func (c *options) Run() error { + ctx := context.Background() + + dc, err := c.cassManager.PodDatacenter(ctx, c.execOptions.PodName, c.execOptions.Namespace) + if err != nil { + return err + } + + cassSecret, err := c.cassManager.CassandraAuthDetails(ctx, dc) + if err != nil { + return err + } + c.execOptions.Command = []string{"nodetool"} + + c.execOptions.Command = append(c.execOptions.Command, nodetoolAuthParameters(cassSecret)...) + + c.execOptions.Command = append(c.execOptions.Command, c.params...) + + return c.execOptions.Run() +} + +func nodetoolAuthParameters(authDetails *cassdcutil.CassandraAuth) []string { + auth := []string{"--username", authDetails.Username, "--password", authDetails.Password} + + if authDetails.KeystorePath != "" { + auth = append(auth, "-Dcom.sun.management.jmxremote.ssl.need.client.auth=true") + auth = append(auth, "-Dcom.sun.management.jmxremote.registry.ssl=true") + auth = append(auth, "-Djavax.net.ssl.keyStore="+authDetails.KeystorePath) + auth = append(auth, "-Djavax.net.ssl.keyStorePassword="+authDetails.KeystorePassword) + auth = append(auth, "-Djavax.net.ssl.trustStore="+authDetails.TruststorePath) + auth = append(auth, "-Djavax.net.ssl.trustStorePassword="+authDetails.TruststorePassword) + } + + return auth +} diff --git a/pkg/cassdcutil/config.go b/pkg/cassdcutil/config.go new file mode 100644 index 0000000..dc158d1 --- /dev/null +++ b/pkg/cassdcutil/config.go @@ -0,0 +1,68 @@ +package cassdcutil + +import ( + "github.com/Jeffail/gabs/v2" + cassdcapi "github.com/k8ssandra/cass-operator/apis/cassandra/v1beta1" +) + +func ClientEncryptionEnabled(dc *cassdcapi.CassandraDatacenter) bool { + config, err := gabs.ParseJSON(dc.Spec.Config) + if err != nil { + return false + } + + if config.Exists("cassandra-yaml", "client_encryption_options") { + if config.Path("cassandra-yaml.client_encryption_options.enabled").Data().(bool) { + return true + } + } + + return false +} + +func SubSectionOfCassYaml(dc *cassdcapi.CassandraDatacenter, section string) map[string]*gabs.Container { + config, err := gabs.ParseJSON(dc.Spec.Config) + if err != nil { + return nil + } + + cassYaml := config.Path("cassandra-yaml") + if cassYaml == nil { + return make(map[string]*gabs.Container) + } + + return cassYaml.Path(section).ChildrenMap() +} + +/* +func (dc *CassandraDatacenter) LegacyInternodeEnabled() bool { + config, err := gabs.ParseJSON(dc.Spec.Config) + if err != nil { + return false + } + + hasOldKeyStore := func(gobContainer map[string]*gabs.Container) bool { + if gobContainer == nil { + return false + } + + if keystorePath, found := gobContainer["keystore"]; found { + if strings.TrimSpace(keystorePath.Data().(string)) == "/etc/encryption/node-keystore.jks" { + return true + } + } + return false + } + + if config.Exists("cassandra-yaml", "client_encryption_options") || config.Exists("cassandra-yaml", "server_encryption_options") { + serverContainer := config.Path("cassandra-yaml.server_encryption_options").ChildrenMap() + clientContainer := config.Path("cassandra-yaml.client_encryption_options").ChildrenMap() + + if hasOldKeyStore(clientContainer) || hasOldKeyStore(serverContainer) { + return true + } + } + + return false +} +*/ diff --git a/pkg/cassdcutil/config_test.go b/pkg/cassdcutil/config_test.go new file mode 100644 index 0000000..24653d5 --- /dev/null +++ b/pkg/cassdcutil/config_test.go @@ -0,0 +1,35 @@ +package cassdcutil + +import ( + "encoding/json" + "testing" + + cassdcapi "github.com/k8ssandra/cass-operator/apis/cassandra/v1beta1" + "github.com/stretchr/testify/assert" +) + +var clientEncryptionEnabled = ` +{ + "cassandra-yaml": { + "client_encryption_options": { + "enabled": true, + "optional": false, + "keystore": "/etc/encryption/node-keystore.jks", + "keystore_password": "dc2", + "truststore": "/etc/encryption/node-keystore.jks", + "truststore_password": "dc2" + } + } +} +` + +func TestClientEncryptionEnabled(t *testing.T) { + dc := &cassdcapi.CassandraDatacenter{ + Spec: cassdcapi.CassandraDatacenterSpec{ + Config: json.RawMessage(clientEncryptionEnabled), + }, + } + + assert := assert.New(t) + assert.True(ClientEncryptionEnabled(dc)) +} diff --git a/pkg/cassdcutil/fetcher.go b/pkg/cassdcutil/fetcher.go new file mode 100644 index 0000000..9335278 --- /dev/null +++ b/pkg/cassdcutil/fetcher.go @@ -0,0 +1,55 @@ +package cassdcutil + +import ( + "context" + "fmt" + + cassdcapi "github.com/k8ssandra/cass-operator/apis/cassandra/v1beta1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// CassandraDatacenter fetches the CassandraDatacenter by its name and namespace +func (c *CassManager) CassandraDatacenter(ctx context.Context, name, namespace string) (*cassdcapi.CassandraDatacenter, error) { + cassdcKey := types.NamespacedName{Namespace: namespace, Name: name} + cassdc := &cassdcapi.CassandraDatacenter{} + + if err := c.client.Get(ctx, cassdcKey, cassdc); err != nil { + return nil, err + } + + return cassdc, nil +} + +// PodDatacenter returns the CassandraDatacenter instance of the pod if it's managed by cass-operator +func (c *CassManager) PodDatacenter(ctx context.Context, podName, namespace string) (*cassdcapi.CassandraDatacenter, error) { + key := types.NamespacedName{Namespace: namespace, Name: podName} + pod := &corev1.Pod{} + err := c.client.Get(ctx, key, pod) + if err != nil { + return nil, err + } + + if dc, found := pod.Labels[cassdcapi.DatacenterLabel]; !found { + return nil, fmt.Errorf("target pod not managed by cass-operator, no datacenter label") + } else { + // Get CassandraDatacenter for the dc + cassDcKey := types.NamespacedName{Namespace: namespace, Name: dc} + cassdc := &cassdcapi.CassandraDatacenter{} + err = c.client.Get(ctx, cassDcKey, cassdc) + if err != nil { + return nil, err + } + + return cassdc, nil + } +} + +// CassandraDatacenterPods returns the pods of the CassandraDatacenter +func (c *CassManager) CassandraDatacenterPods(ctx context.Context, cassdc *cassdcapi.CassandraDatacenter) (*corev1.PodList, error) { + // What if same namespace has two datacenters with the same name? Can that happen? + podList := &corev1.PodList{} + err := c.client.List(ctx, podList, client.InNamespace(cassdc.Namespace), client.MatchingLabels(map[string]string{cassdcapi.DatacenterLabel: cassdc.Name})) + return podList, err +} diff --git a/pkg/cassdcutil/manage.go b/pkg/cassdcutil/manage.go index 21ab353..642415d 100644 --- a/pkg/cassdcutil/manage.go +++ b/pkg/cassdcutil/manage.go @@ -7,7 +7,6 @@ import ( cassdcapi "github.com/k8ssandra/cass-operator/apis/cassandra/v1beta1" "github.com/k8ssandra/k8ssandra-client/pkg/tasks" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/types" waitutil "k8s.io/apimachinery/pkg/util/wait" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -22,26 +21,6 @@ func NewManager(client client.Client) *CassManager { } } -// CassandraDatacenter fetches the CassandraDatacenter by its name and namespace -func (c *CassManager) CassandraDatacenter(ctx context.Context, name, namespace string) (*cassdcapi.CassandraDatacenter, error) { - cassdcKey := types.NamespacedName{Namespace: namespace, Name: name} - cassdc := &cassdcapi.CassandraDatacenter{} - - if err := c.client.Get(ctx, cassdcKey, cassdc); err != nil { - return nil, err - } - - return cassdc, nil -} - -// CassandraDatacenterPods returns the pods of the CassandraDatacenter -func (c *CassManager) CassandraDatacenterPods(ctx context.Context, cassdc *cassdcapi.CassandraDatacenter) (*corev1.PodList, error) { - // What if same namespace has two datacenters with the same name? Can that happen? - podList := &corev1.PodList{} - err := c.client.List(ctx, podList, client.InNamespace(cassdc.Namespace), client.MatchingLabels(map[string]string{cassdcapi.DatacenterLabel: cassdc.Name})) - return podList, err -} - // ModifyStoppedState either stops or starts the cluster and does nothing if the state is already as requested func (c *CassManager) ModifyStoppedState(ctx context.Context, name, namespace string, stop, wait bool) error { cassdc, err := c.CassandraDatacenter(ctx, name, namespace) @@ -96,6 +75,7 @@ func (c *CassManager) RefreshStatus(ctx context.Context, cassdc *cassdcapi.Cassa return cassdc.Status.GetConditionStatus(status) == wanted, nil } +// RestartDc creates a task to restart the cluster and waits for completion if wait is set to true func (c *CassManager) RestartDc(ctx context.Context, name, namespace, rack string, wait bool) error { cassdc, err := c.CassandraDatacenter(ctx, name, namespace) if err != nil { diff --git a/pkg/cassdcutil/secrets.go b/pkg/cassdcutil/secrets.go new file mode 100644 index 0000000..b0b6c69 --- /dev/null +++ b/pkg/cassdcutil/secrets.go @@ -0,0 +1,42 @@ +package cassdcutil + +import ( + "context" + + corev1 "k8s.io/api/core/v1" + + cassdcapi "github.com/k8ssandra/cass-operator/apis/cassandra/v1beta1" +) + +type CassandraAuth struct { + Username string + Password string + + KeystorePath string + KeystorePassword string + TruststorePath string + TruststorePassword string +} + +// CassandraAuthDetails fetches the Cassandra superuser secrets for the given CassandraDatacenter. +func (c *CassManager) CassandraAuthDetails(ctx context.Context, cassdc *cassdcapi.CassandraDatacenter) (*CassandraAuth, error) { + secret := &corev1.Secret{} + if err := c.client.Get(ctx, cassdc.GetSuperuserSecretNamespacedName(), secret); err != nil { + return nil, err + } + + auth := &CassandraAuth{ + Username: string(secret.Data["username"]), + Password: string(secret.Data["password"]), + } + + if ClientEncryptionEnabled(cassdc) { + encryptionOptions := SubSectionOfCassYaml(cassdc, "client_encryption_options") + auth.KeystorePath = encryptionOptions["keystore"].Data().(string) + auth.KeystorePassword = encryptionOptions["keystore_password"].Data().(string) + auth.TruststorePath = encryptionOptions["truststore"].Data().(string) + auth.TruststorePassword = encryptionOptions["truststore_password"].Data().(string) + } + + return auth, nil +} From f5428ec24fe98d998b2dc8aa53cad61f2b7d552b Mon Sep 17 00:00:00 2001 From: Michael Burman Date: Tue, 20 Aug 2024 16:55:35 +0300 Subject: [PATCH 2/4] Add some tests --- pkg/cassdcutil/config.go | 40 ++------------------- pkg/cassdcutil/config_test.go | 66 ++++++++++++++++++++++++++++++++++ pkg/cassdcutil/secrets.go | 9 ++--- pkg/cassdcutil/secrets_test.go | 57 +++++++++++++++++++++++++++++ 4 files changed, 131 insertions(+), 41 deletions(-) create mode 100644 pkg/cassdcutil/secrets_test.go diff --git a/pkg/cassdcutil/config.go b/pkg/cassdcutil/config.go index dc158d1..33bf347 100644 --- a/pkg/cassdcutil/config.go +++ b/pkg/cassdcutil/config.go @@ -23,46 +23,12 @@ func ClientEncryptionEnabled(dc *cassdcapi.CassandraDatacenter) bool { func SubSectionOfCassYaml(dc *cassdcapi.CassandraDatacenter, section string) map[string]*gabs.Container { config, err := gabs.ParseJSON(dc.Spec.Config) if err != nil { - return nil - } - - cassYaml := config.Path("cassandra-yaml") - if cassYaml == nil { return make(map[string]*gabs.Container) } - return cassYaml.Path(section).ChildrenMap() -} - -/* -func (dc *CassandraDatacenter) LegacyInternodeEnabled() bool { - config, err := gabs.ParseJSON(dc.Spec.Config) - if err != nil { - return false - } - - hasOldKeyStore := func(gobContainer map[string]*gabs.Container) bool { - if gobContainer == nil { - return false - } - - if keystorePath, found := gobContainer["keystore"]; found { - if strings.TrimSpace(keystorePath.Data().(string)) == "/etc/encryption/node-keystore.jks" { - return true - } - } - return false - } - - if config.Exists("cassandra-yaml", "client_encryption_options") || config.Exists("cassandra-yaml", "server_encryption_options") { - serverContainer := config.Path("cassandra-yaml.server_encryption_options").ChildrenMap() - clientContainer := config.Path("cassandra-yaml.client_encryption_options").ChildrenMap() - - if hasOldKeyStore(clientContainer) || hasOldKeyStore(serverContainer) { - return true - } + if !config.Exists("cassandra-yaml") { + return make(map[string]*gabs.Container) } - return false + return config.Path("cassandra-yaml").Path(section).ChildrenMap() } -*/ diff --git a/pkg/cassdcutil/config_test.go b/pkg/cassdcutil/config_test.go index 24653d5..cba41e6 100644 --- a/pkg/cassdcutil/config_test.go +++ b/pkg/cassdcutil/config_test.go @@ -33,3 +33,69 @@ func TestClientEncryptionEnabled(t *testing.T) { assert := assert.New(t) assert.True(ClientEncryptionEnabled(dc)) } + +func TestEmptySubSection(t *testing.T) { + dc := &cassdcapi.CassandraDatacenter{ + Spec: cassdcapi.CassandraDatacenterSpec{}, + } + + assert := assert.New(t) + section := SubSectionOfCassYaml(dc, "client_encryption_options") + assert.NotNil(section) + assert.Equal(0, len(section)) + + dc.Spec.Config = json.RawMessage(``) + section = SubSectionOfCassYaml(dc, "client_encryption_options") + assert.NotNil(section) + assert.Equal(0, len(section)) +} + +func TestSubSectionNotMatch(t *testing.T) { + dc := &cassdcapi.CassandraDatacenter{ + Spec: cassdcapi.CassandraDatacenterSpec{ + Config: json.RawMessage(clientEncryptionEnabled), + }, + } + + assert := assert.New(t) + section := SubSectionOfCassYaml(dc, "server_encryption_options") + assert.NotNil(section) + assert.Equal(0, len(section)) +} + +func TestSubSectionPart(t *testing.T) { + dc := &cassdcapi.CassandraDatacenter{ + Spec: cassdcapi.CassandraDatacenterSpec{ + Config: json.RawMessage(clientEncryptionEnabled), + }, + } + + assert := assert.New(t) + section := SubSectionOfCassYaml(dc, "client_encryption_options") + assert.NotNil(section) + assert.Equal(6, len(section)) + + enabled, ok := section["enabled"].Data().(bool) + assert.True(ok) + assert.True(enabled) + + keystore, ok := section["keystore"].Data().(string) + assert.True(ok) + assert.Equal("/etc/encryption/node-keystore.jks", keystore) + + keystorePassword, ok := section["keystore_password"].Data().(string) + assert.True(ok) + assert.Equal("dc2", keystorePassword) + + truststore, ok := section["truststore"].Data().(string) + assert.True(ok) + assert.Equal("/etc/encryption/node-keystore.jks", truststore) + + truststorePassword, ok := section["truststore_password"].Data().(string) + assert.True(ok) + assert.Equal("dc2", truststorePassword) + + optional, ok := section["optional"].Data().(bool) + assert.True(ok) + assert.False(optional) +} diff --git a/pkg/cassdcutil/secrets.go b/pkg/cassdcutil/secrets.go index b0b6c69..3c3cb22 100644 --- a/pkg/cassdcutil/secrets.go +++ b/pkg/cassdcutil/secrets.go @@ -2,6 +2,7 @@ package cassdcutil import ( "context" + "strings" corev1 "k8s.io/api/core/v1" @@ -32,10 +33,10 @@ func (c *CassManager) CassandraAuthDetails(ctx context.Context, cassdc *cassdcap if ClientEncryptionEnabled(cassdc) { encryptionOptions := SubSectionOfCassYaml(cassdc, "client_encryption_options") - auth.KeystorePath = encryptionOptions["keystore"].Data().(string) - auth.KeystorePassword = encryptionOptions["keystore_password"].Data().(string) - auth.TruststorePath = encryptionOptions["truststore"].Data().(string) - auth.TruststorePassword = encryptionOptions["truststore_password"].Data().(string) + auth.KeystorePath = strings.TrimSpace(encryptionOptions["keystore"].Data().(string)) + auth.KeystorePassword = strings.TrimSpace(encryptionOptions["keystore_password"].Data().(string)) + auth.TruststorePath = strings.TrimSpace(encryptionOptions["truststore"].Data().(string)) + auth.TruststorePassword = strings.TrimSpace(encryptionOptions["truststore_password"].Data().(string)) } return auth, nil diff --git a/pkg/cassdcutil/secrets_test.go b/pkg/cassdcutil/secrets_test.go new file mode 100644 index 0000000..bb27510 --- /dev/null +++ b/pkg/cassdcutil/secrets_test.go @@ -0,0 +1,57 @@ +package cassdcutil + +import ( + "context" + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + cassdcapi "github.com/k8ssandra/cass-operator/apis/cassandra/v1beta1" +) + +func TestCassandraAuthDetails(t *testing.T) { + scheme := runtime.NewScheme() + clientgoscheme.AddToScheme(scheme) + cassdcapi.AddToScheme(scheme) + assert := assert.New(t) + + cassdc := &cassdcapi.CassandraDatacenter{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-dc", + }, + Spec: cassdcapi.CassandraDatacenterSpec{ + ClusterName: "test-cluster", + SuperuserSecretName: "test-secret", + Config: json.RawMessage(clientEncryptionEnabled), + }, + } + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret", + }, + Data: map[string][]byte{ + "username": []byte("test-cluster-superuser"), + "password": []byte("cryptic-password"), + }, + } + + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(cassdc, secret).Build() + cassManager := &CassManager{client: client} + + authDetails, err := cassManager.CassandraAuthDetails(context.TODO(), cassdc) + assert.NoError(err) + assert.NotNil(authDetails) + + assert.Equal("test-cluster-superuser", authDetails.Username) + assert.Equal("cryptic-password", authDetails.Password) + assert.Equal("/etc/encryption/node-keystore.jks", authDetails.KeystorePath) + assert.Equal("dc2", authDetails.KeystorePassword) + assert.Equal("/etc/encryption/node-keystore.jks", authDetails.TruststorePath) + assert.Equal("dc2", authDetails.TruststorePassword) +} From b74da3015cc8ad918debaa9510e33a7cb36729ba Mon Sep 17 00:00:00 2001 From: Michael Burman Date: Tue, 20 Aug 2024 16:58:27 +0300 Subject: [PATCH 3/4] Fix test assert placement --- pkg/cassdcutil/secrets_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cassdcutil/secrets_test.go b/pkg/cassdcutil/secrets_test.go index bb27510..9604aa1 100644 --- a/pkg/cassdcutil/secrets_test.go +++ b/pkg/cassdcutil/secrets_test.go @@ -17,9 +17,9 @@ import ( func TestCassandraAuthDetails(t *testing.T) { scheme := runtime.NewScheme() - clientgoscheme.AddToScheme(scheme) - cassdcapi.AddToScheme(scheme) assert := assert.New(t) + assert.NoError(clientgoscheme.AddToScheme(scheme)) + assert.NoError(cassdcapi.AddToScheme(scheme)) cassdc := &cassdcapi.CassandraDatacenter{ ObjectMeta: metav1.ObjectMeta{ From aae19a08e1c9ad7291a431c0ab4bc512862597ed Mon Sep 17 00:00:00 2001 From: Michael Burman Date: Tue, 27 Aug 2024 11:18:31 +0300 Subject: [PATCH 4/4] Use OwnerReferences to track the correct Datacenter --- pkg/cassdcutil/fetcher.go | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/pkg/cassdcutil/fetcher.go b/pkg/cassdcutil/fetcher.go index 9335278..683be92 100644 --- a/pkg/cassdcutil/fetcher.go +++ b/pkg/cassdcutil/fetcher.go @@ -5,6 +5,7 @@ import ( "fmt" cassdcapi "github.com/k8ssandra/cass-operator/apis/cassandra/v1beta1" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" @@ -23,6 +24,7 @@ func (c *CassManager) CassandraDatacenter(ctx context.Context, name, namespace s } // PodDatacenter returns the CassandraDatacenter instance of the pod if it's managed by cass-operator +// We use the OwnerReference method because the pod labels are incorrect if datacenter name override is used func (c *CassManager) PodDatacenter(ctx context.Context, podName, namespace string) (*cassdcapi.CassandraDatacenter, error) { key := types.NamespacedName{Namespace: namespace, Name: podName} pod := &corev1.Pod{} @@ -31,19 +33,28 @@ func (c *CassManager) PodDatacenter(ctx context.Context, podName, namespace stri return nil, err } - if dc, found := pod.Labels[cassdcapi.DatacenterLabel]; !found { - return nil, fmt.Errorf("target pod not managed by cass-operator, no datacenter label") - } else { - // Get CassandraDatacenter for the dc - cassDcKey := types.NamespacedName{Namespace: namespace, Name: dc} - cassdc := &cassdcapi.CassandraDatacenter{} - err = c.client.Get(ctx, cassDcKey, cassdc) - if err != nil { - return nil, err - } - - return cassdc, nil + if len(pod.OwnerReferences) < 1 { + return nil, fmt.Errorf("target pod not managed by cass-operator, no owner reference") } + + statefulSet := &appsv1.StatefulSet{} + err = c.client.Get(ctx, types.NamespacedName{Namespace: namespace, Name: pod.OwnerReferences[0].Name}, statefulSet) + if err != nil { + return nil, err + } + + if len(statefulSet.OwnerReferences) < 1 { + return nil, fmt.Errorf("target statefulset not managed by cass-operator, no owner reference") + } + + cassDcKey := types.NamespacedName{Namespace: namespace, Name: statefulSet.OwnerReferences[0].Name} + cassdc := &cassdcapi.CassandraDatacenter{} + err = c.client.Get(ctx, cassDcKey, cassdc) + if err != nil { + return nil, err + } + + return cassdc, nil } // CassandraDatacenterPods returns the pods of the CassandraDatacenter