diff --git a/charts/cass-operator-chart/templates/customresourcedefinition.yaml b/charts/cass-operator-chart/templates/customresourcedefinition.yaml index be3b5964c..4b4a507d1 100644 --- a/charts/cass-operator-chart/templates/customresourcedefinition.yaml +++ b/charts/cass-operator-chart/templates/customresourcedefinition.yaml @@ -92,6 +92,20 @@ spec: - serverSecretName type: object type: object + networking: + properties: + nodePort: + properties: + broadcast: + type: integer + broadcastSSL: + type: integer + cql: + type: integer + cqlSSL: + type: integer + type: object + type: object nodeSelector: additionalProperties: type: string diff --git a/docs/user/README.md b/docs/user/README.md index 1c9bd2c21..5da30eb83 100644 --- a/docs/user/README.md +++ b/docs/user/README.md @@ -363,6 +363,24 @@ spec: serverImage: private-docker-registry.example.com/dse-img/dse:5f6e7d8c ``` +## Configuring a NodePort service + +A NodePort service may be requested by setting the following fields: + + networking: + nodePort: + cql: 30001 + broadcast: 30002 + +The SSL versions of the ports may be requested: + + networking: + nodePort: + cqlSSL: 30010 + broadcastSSL: 30020 + +If any of the nodePort fields have been configured then a NodePort service will be created that routes from the specified external port to the identically numbered internal port. Cassandra will be configured to listen on the specified ports. + # Using Your Cluster ## Connecting from inside the Kubernetes cluster diff --git a/mage/ginkgo/lib.go b/mage/ginkgo/lib.go index 331d40a9b..9deceb9a7 100644 --- a/mage/ginkgo/lib.go +++ b/mage/ginkgo/lib.go @@ -214,7 +214,6 @@ func (ns *NsWrapper) WaitForOutputContainsAndLog(description string, kcmd kubect Expect(execErr).ToNot(HaveOccurred()) } - func (ns *NsWrapper) WaitForDatacenterCondition(dcName string, conditionType string, value string) { step := fmt.Sprintf("checking that dc condition %s has value %s", conditionType, value) json := fmt.Sprintf("jsonpath={.status.conditions[?(.type=='%s')].status}", conditionType) @@ -223,7 +222,6 @@ func (ns *NsWrapper) WaitForDatacenterCondition(dcName string, conditionType str ns.WaitForOutputAndLog(step, k, value, 600) } - func (ns *NsWrapper) WaitForDatacenterToHaveNoPods(dcName string) { step := "checking that no dc pods remain" json := "jsonpath={.items}" @@ -369,3 +367,27 @@ func (ns NsWrapper) HelmInstall(chartPath string) { err := helm_util.Install(chartPath, "cass-operator", ns.Namespace, overrides) mageutil.PanicOnError(err) } + +// Note that the actual value will be cast to a string before the comparison with the expectedValue +func (ns NsWrapper) ExpectKeyValue(m map[string]interface{}, key string, expectedValue string) { + actualValue, ok := m[key].(string) + if !ok { + // Note: floats will end up as strings with six decimal points + // example: "12.000000" + tryFloat64, ok := m[key].(float64) + if !ok { + msg := fmt.Sprintf("Actual value for key %s is not expected type", key) + err := fmt.Errorf(msg) + Expect(err).ToNot(HaveOccurred()) + } + actualValue = fmt.Sprintf("%f", tryFloat64) + } + Expect(actualValue).To(Equal(expectedValue), "Expected %s %s to be %s", key, m[key], expectedValue) +} + +// Compare all key/values from an expected map to an actual map +func (ns NsWrapper) ExpectKeyValues(actual map[string]interface{}, expected map[string]string) { + for key := range expected { + ns.ExpectKeyValue(actual, key, expected[key]) + } +} diff --git a/operator/deploy/crds/cassandra.datastax.com_cassandradatacenters_crd.yaml b/operator/deploy/crds/cassandra.datastax.com_cassandradatacenters_crd.yaml index 41e50410e..dacacdda5 100644 --- a/operator/deploy/crds/cassandra.datastax.com_cassandradatacenters_crd.yaml +++ b/operator/deploy/crds/cassandra.datastax.com_cassandradatacenters_crd.yaml @@ -92,6 +92,20 @@ spec: - serverSecretName type: object type: object + networking: + properties: + nodePort: + properties: + broadcast: + type: integer + broadcastSSL: + type: integer + cql: + type: integer + cqlSSL: + type: integer + type: object + type: object nodeSelector: additionalProperties: type: string diff --git a/operator/pkg/apis/cassandra/v1beta1/cassandradatacenter_types.go b/operator/pkg/apis/cassandra/v1beta1/cassandradatacenter_types.go index e6e53aa72..0d7b653e6 100644 --- a/operator/pkg/apis/cassandra/v1beta1/cassandradatacenter_types.go +++ b/operator/pkg/apis/cassandra/v1beta1/cassandradatacenter_types.go @@ -40,6 +40,10 @@ const ( // Progress states for status ProgressUpdating ProgressState = "Updating" ProgressReady ProgressState = "Ready" + + // Default port numbers + DefaultCqlPort = 9042 + DefaultBroadcastPort = 7000 ) // This type exists so there's no chance of pushing random strings to our progress status @@ -221,11 +225,29 @@ type CassandraDatacenterSpec struct { // Cassandra users to bootstrap Users []CassandraUser `json:"users,omitempty"` + Networking *NetworkingConfig `json:"networking,omitempty"` + AdditionalSeeds []string `json:"additionalSeeds,omitempty"` Reaper *ReaperConfig `json:"reaper,omitempty"` } +type NetworkingConfig struct { + NodePort *NodePortConfig `json:"nodePort,omitempty"` +} + +type NodePortConfig struct { + Cql int `json:"cql,omitempty"` + CqlSSL int `json:"cqlSSL,omitempty"` + Broadcast int `json:"broadcast,omitempty"` + BroadcastSSL int `json:"broadcastSSL,omitempty"` +} + +// Is the NodePort service enabled? +func (dc *CassandraDatacenter) IsNodePortEnabled() bool { + return dc.Spec.Networking != nil && dc.Spec.Networking.NodePort != nil +} + type DseWorkloads struct { AnalyticsEnabled bool `json:"analyticsEnabled,omitempty"` GraphEnabled bool `json:"graphEnabled,omitempty"` @@ -491,6 +513,10 @@ func (dc *CassandraDatacenter) GetDatacenterServiceName() string { return dc.Spec.ClusterName + "-" + dc.Name + "-service" } +func (dc *CassandraDatacenter) GetNodePortServiceName() string { + return dc.Spec.ClusterName + "-" + dc.Name + "-node-port-service" +} + func (dc *CassandraDatacenter) ShouldGenerateSuperuserSecret() bool { return len(dc.Spec.SuperuserSecretName) == 0 } @@ -533,13 +559,28 @@ func (dc *CassandraDatacenter) GetConfigAsJSON() (string, error) { } } + cql := 0 + cqlSSL := 0 + broadcast := 0 + broadcastSSL := 0 + if dc.IsNodePortEnabled() { + cql = dc.Spec.Networking.NodePort.Cql + cqlSSL = dc.Spec.Networking.NodePort.CqlSSL + broadcast = dc.Spec.Networking.NodePort.Broadcast + broadcastSSL = dc.Spec.Networking.NodePort.BroadcastSSL + } + modelValues := serverconfig.GetModelValues( seeds, dc.Spec.ClusterName, dc.Name, graphEnabled, solrEnabled, - sparkEnabled) + sparkEnabled, + cql, + cqlSSL, + broadcast, + broadcastSSL) var modelBytes []byte @@ -569,13 +610,53 @@ func (dc *CassandraDatacenter) GetConfigAsJSON() (string, error) { return modelParsed.String(), nil } +// Gets the defined CQL port for NodePort. +// 0 will be returned if NodePort is not configured. +// The SSL port will be returned if it is defined, +// otherwise the normal CQL port will be used. +func (dc *CassandraDatacenter) GetNodePortCqlPort() int { + if !dc.IsNodePortEnabled() { + return 0 + } + + if dc.Spec.Networking.NodePort.CqlSSL != 0 { + return dc.Spec.Networking.NodePort.CqlSSL + } else if dc.Spec.Networking.NodePort.Cql != 0 { + return dc.Spec.Networking.NodePort.Cql + } else { + return DefaultCqlPort + } +} + +// Gets the defined broadcast/intranode port for NodePort. +// 0 will be returned if NodePort is not configured. +// The SSL port will be returned if it is defined, +// otherwise the normal broadcast port will be used. +func (dc *CassandraDatacenter) GetNodePortBroadcastPort() int { + if !dc.IsNodePortEnabled() { + return 0 + } + + if dc.Spec.Networking.NodePort.BroadcastSSL != 0 { + return dc.Spec.Networking.NodePort.BroadcastSSL + } else if dc.Spec.Networking.NodePort.Broadcast != 0 { + return dc.Spec.Networking.NodePort.Broadcast + } else { + return DefaultBroadcastPort + } +} + // GetContainerPorts will return the container ports for the pods in a statefulset based on the provided config func (dc *CassandraDatacenter) GetContainerPorts() ([]corev1.ContainerPort, error) { + + cqlPort := DefaultCqlPort + broadcastPort := DefaultBroadcastPort + ports := []corev1.ContainerPort{ { // Note: Port Names cannot be more than 15 characters Name: "native", - ContainerPort: 9042, + ContainerPort: int32(cqlPort), }, { Name: "inter-node-msg", @@ -583,7 +664,7 @@ func (dc *CassandraDatacenter) GetContainerPorts() ([]corev1.ContainerPort, erro }, { Name: "intra-node", - ContainerPort: 7000, + ContainerPort: int32(broadcastPort), }, { Name: "tls-intra-node", diff --git a/operator/pkg/apis/cassandra/v1beta1/cassandradatacenter_types_test.go b/operator/pkg/apis/cassandra/v1beta1/cassandradatacenter_types_test.go index feb4314af..c6d171e72 100644 --- a/operator/pkg/apis/cassandra/v1beta1/cassandradatacenter_types_test.go +++ b/operator/pkg/apis/cassandra/v1beta1/cassandradatacenter_types_test.go @@ -236,13 +236,13 @@ func TestCassandraDatacenter_GetContainerPorts(t *testing.T) { want: []corev1.ContainerPort{ { Name: "native", - ContainerPort: 9042, + ContainerPort: DefaultCqlPort, }, { Name: "inter-node-msg", ContainerPort: 8609, }, { Name: "intra-node", - ContainerPort: 7000, + ContainerPort: DefaultBroadcastPort, }, { Name: "tls-intra-node", ContainerPort: 7001, @@ -263,13 +263,13 @@ func TestCassandraDatacenter_GetContainerPorts(t *testing.T) { want: []corev1.ContainerPort{ { Name: "native", - ContainerPort: 9042, + ContainerPort: DefaultCqlPort, }, { Name: "inter-node-msg", ContainerPort: 8609, }, { Name: "intra-node", - ContainerPort: 7000, + ContainerPort: DefaultBroadcastPort, }, { Name: "tls-intra-node", ContainerPort: 7001, @@ -292,13 +292,13 @@ func TestCassandraDatacenter_GetContainerPorts(t *testing.T) { want: []corev1.ContainerPort{ { Name: "native", - ContainerPort: 9042, + ContainerPort: DefaultCqlPort, }, { Name: "inter-node-msg", ContainerPort: 8609, }, { Name: "intra-node", - ContainerPort: 7000, + ContainerPort: DefaultBroadcastPort, }, { Name: "tls-intra-node", ContainerPort: 7001, diff --git a/operator/pkg/apis/cassandra/v1beta1/zz_generated.deepcopy.go b/operator/pkg/apis/cassandra/v1beta1/zz_generated.deepcopy.go index d964ee1d9..ad85a57ff 100644 --- a/operator/pkg/apis/cassandra/v1beta1/zz_generated.deepcopy.go +++ b/operator/pkg/apis/cassandra/v1beta1/zz_generated.deepcopy.go @@ -120,6 +120,11 @@ func (in *CassandraDatacenterSpec) DeepCopyInto(out *CassandraDatacenterSpec) { *out = make([]CassandraUser, len(*in)) copy(*out, *in) } + if in.Networking != nil { + in, out := &in.Networking, &out.Networking + *out = new(NetworkingConfig) + (*in).DeepCopyInto(*out) + } if in.AdditionalSeeds != nil { in, out := &in.AdditionalSeeds, &out.AdditionalSeeds *out = make([]string, len(*in)) @@ -327,6 +332,43 @@ func (in *ManagementApiAuthManualConfig) DeepCopy() *ManagementApiAuthManualConf return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NetworkingConfig) DeepCopyInto(out *NetworkingConfig) { + *out = *in + if in.NodePort != nil { + in, out := &in.NodePort, &out.NodePort + *out = new(NodePortConfig) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkingConfig. +func (in *NetworkingConfig) DeepCopy() *NetworkingConfig { + if in == nil { + return nil + } + out := new(NetworkingConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NodePortConfig) DeepCopyInto(out *NodePortConfig) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodePortConfig. +func (in *NodePortConfig) DeepCopy() *NodePortConfig { + if in == nil { + return nil + } + out := new(NodePortConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Rack) DeepCopyInto(out *Rack) { *out = *in diff --git a/operator/pkg/reconciliation/constructor.go b/operator/pkg/reconciliation/constructor.go index c6a87ab56..b80290932 100644 --- a/operator/pkg/reconciliation/constructor.go +++ b/operator/pkg/reconciliation/constructor.go @@ -32,10 +32,15 @@ func newServiceForCassandraDatacenter(dc *api.CassandraDatacenter) *corev1.Servi svcName := dc.GetDatacenterServiceName() service := makeGenericHeadlessService(dc) service.ObjectMeta.Name = svcName + + cqlPort := api.DefaultCqlPort + if dc.IsNodePortEnabled() { + cqlPort = dc.GetNodePortCqlPort() + } + service.Spec.Ports = []corev1.ServicePort{ - // Note: Port Names cannot be more than 15 characters { - Name: "native", Port: 9042, TargetPort: intstr.FromInt(9042), + Name: "native", Port: int32(cqlPort), TargetPort: intstr.FromInt(cqlPort), }, { Name: "mgmt-api", Port: 8080, TargetPort: intstr.FromInt(8080), @@ -122,6 +127,39 @@ func newEndpointsForAdditionalSeeds(dc *api.CassandraDatacenter) *corev1.Endpoin return &endpoints } +// newNodePortServiceForCassandraDatacenter creates a headless service owned by the CassandraDatacenter, +// that preserves the client source IPs +func newNodePortServiceForCassandraDatacenter(dc *api.CassandraDatacenter) *corev1.Service { + service := makeGenericHeadlessService(dc) + service.ObjectMeta.Name = dc.GetNodePortServiceName() + + service.Spec.Type = "NodePort" + // Note: ClusterIp = "None" is not valid for NodePort + service.Spec.ClusterIP = "" + service.Spec.ExternalTrafficPolicy = corev1.ServiceExternalTrafficPolicyTypeLocal + + cqlPort := dc.GetNodePortCqlPort() + broadcastPort := dc.GetNodePortBroadcastPort() + + service.Spec.Ports = []corev1.ServicePort{ + // Note: Port Names cannot be more than 15 characters + { + Name: "broadcast", + Port: int32(broadcastPort), + NodePort: int32(broadcastPort), + TargetPort: intstr.FromInt(broadcastPort), + }, + { + Name: "native", + Port: int32(cqlPort), + NodePort: int32(cqlPort), + TargetPort: intstr.FromInt(cqlPort), + }, + } + + return service +} + // newAllPodsServiceForCassandraDatacenter creates a headless service owned by the CassandraDatacenter, // which covers all server pods in the datacenter, whether they are ready or not func newAllPodsServiceForCassandraDatacenter(dc *api.CassandraDatacenter) *corev1.Service { @@ -504,6 +542,12 @@ func buildInitContainers(dc *api.CassandraDatacenter, rackName string) ([]corev1 } serverCfg.VolumeMounts = []corev1.VolumeMount{serverCfgMount} + // Convert the bool to a string for the env var setting + useHostIpForBroadcast := "false" + if dc.IsNodePortEnabled() { + useHostIpForBroadcast = "true" + } + configData, err := dc.GetConfigAsJSON() if err != nil { return nil, err @@ -512,6 +556,8 @@ func buildInitContainers(dc *api.CassandraDatacenter, rackName string) ([]corev1 serverCfg.Env = []corev1.EnvVar{ {Name: "CONFIG_FILE_DATA", Value: configData}, {Name: "POD_IP", ValueFrom: selectorFromFieldPath("status.podIP")}, + {Name: "HOST_IP", ValueFrom: selectorFromFieldPath("status.hostIP")}, + {Name: "USE_HOST_IP_FOR_BROADCAST", Value: useHostIpForBroadcast}, {Name: "RACK_NAME", Value: rackName}, {Name: "PRODUCT_VERSION", Value: serverVersion}, {Name: "PRODUCT_NAME", Value: dc.Spec.ServerType}, diff --git a/operator/pkg/reconciliation/reconcile_services.go b/operator/pkg/reconciliation/reconcile_services.go index 5e7db3d3a..67f213f7f 100644 --- a/operator/pkg/reconciliation/reconcile_services.go +++ b/operator/pkg/reconciliation/reconcile_services.go @@ -66,6 +66,11 @@ func (rc *ReconciliationContext) CheckHeadlessServices() result.ReconcileResult services = append(services, additionalSeedService) } + if dc.IsNodePortEnabled() { + nodePortService := newNodePortServiceForCassandraDatacenter(dc) + services = append(services, nodePortService) + } + createNeeded := []*corev1.Service{} for idx := range services { @@ -102,6 +107,11 @@ func (rc *ReconciliationContext) CheckHeadlessServices() result.ReconcileResult desiredSvc.Labels = utils.MergeMap(map[string]string{}, currentService.Labels, desiredSvc.Labels) desiredSvc.Annotations = utils.MergeMap(map[string]string{}, currentService.Annotations, desiredSvc.Annotations) + // ClusterIP may have been updated for the NodePort service + // so we need to preserve it. Copying should not break any of + // the other services either. + desiredSvc.Spec.ClusterIP = currentService.Spec.ClusterIP + logger.Info("Updating service", "service", currentService, "desired", desiredSvc) diff --git a/operator/pkg/serverconfig/configgen.go b/operator/pkg/serverconfig/configgen.go index 2a97a5420..9de10d34c 100644 --- a/operator/pkg/serverconfig/configgen.go +++ b/operator/pkg/serverconfig/configgen.go @@ -17,7 +17,11 @@ func GetModelValues( dcName string, graphEnabled int, solrEnabled int, - sparkEnabled int) NodeConfig { + sparkEnabled int, + cqlPort int, + cqlSSLPort int, + broadcastPort int, + broadcastSSLPort int) NodeConfig { seedsString := strings.Join(seeds, ",") @@ -32,7 +36,21 @@ func GetModelValues( "graph-enabled": graphEnabled, "solr-enabled": solrEnabled, "spark-enabled": sparkEnabled, - }} + }, + "cassandra-yaml": NodeConfig{}, + } + + if cqlSSLPort != 0 { + modelValues["cassandra-yaml"].(NodeConfig)["native_transport_port_ssl"] = cqlSSLPort + } else if cqlPort != 0 { + modelValues["cassandra-yaml"].(NodeConfig)["native_transport_port"] = cqlPort + } + + if broadcastSSLPort != 0 { + modelValues["cassandra-yaml"].(NodeConfig)["ssl_storage_port"] = broadcastSSLPort + } else if broadcastPort != 0 { + modelValues["cassandra-yaml"].(NodeConfig)["storage_port"] = broadcastPort + } return modelValues } diff --git a/operator/pkg/serverconfig/configgen_test.go b/operator/pkg/serverconfig/configgen_test.go index 27656511a..6bbd8deff 100644 --- a/operator/pkg/serverconfig/configgen_test.go +++ b/operator/pkg/serverconfig/configgen_test.go @@ -10,12 +10,16 @@ import ( func TestGetModelValues(t *testing.T) { type args struct { - seeds []string - clusterName string - dcName string - graphEnabled int - solrEnabled int - sparkEnabled int + seeds []string + clusterName string + dcName string + graphEnabled int + solrEnabled int + sparkEnabled int + cqlPort int + cqlSSLPort int + broadcastPort int + broadcastSSLPort int } tests := []struct { name string @@ -25,12 +29,16 @@ func TestGetModelValues(t *testing.T) { { name: "Happy Path", args: args{ - seeds: []string{"seed0", "seed1", "seed2"}, - clusterName: "cluster-name", - dcName: "dc-name", - graphEnabled: 1, - solrEnabled: 0, - sparkEnabled: 0, + seeds: []string{"seed0", "seed1", "seed2"}, + clusterName: "cluster-name", + dcName: "dc-name", + graphEnabled: 1, + solrEnabled: 0, + sparkEnabled: 0, + cqlPort: 9042, + cqlSSLPort: 0, + broadcastPort: 7000, + broadcastSSLPort: 7000, }, want: NodeConfig{ "cluster-info": NodeConfig{ @@ -42,17 +50,26 @@ func TestGetModelValues(t *testing.T) { "name": "dc-name", "solr-enabled": 0, "spark-enabled": 0, - }}, + }, + "cassandra-yaml": NodeConfig{ + "native_transport_port": 9042, + "ssl_storage_port": 7000, + }, + }, }, { name: "Empty seeds", args: args{ - seeds: []string{}, - clusterName: "cluster-name", - dcName: "dc-name", - graphEnabled: 0, - solrEnabled: 1, - sparkEnabled: 0, + seeds: []string{}, + clusterName: "cluster-name", + dcName: "dc-name", + graphEnabled: 0, + solrEnabled: 1, + sparkEnabled: 0, + cqlPort: 9042, + cqlSSLPort: 9142, + broadcastPort: 7000, + broadcastSSLPort: 0, }, want: NodeConfig{ "cluster-info": NodeConfig{ @@ -64,17 +81,26 @@ func TestGetModelValues(t *testing.T) { "name": "dc-name", "solr-enabled": 1, "spark-enabled": 0, - }}, + }, + "cassandra-yaml": NodeConfig{ + "native_transport_port_ssl": 9142, + "storage_port": 7000, + }, + }, }, { name: "Missing cluster name", args: args{ - seeds: []string{"seed0", "seed1", "seed2"}, - clusterName: "", - dcName: "dc-name", - graphEnabled: 1, - solrEnabled: 1, - sparkEnabled: 1, + seeds: []string{"seed0", "seed1", "seed2"}, + clusterName: "", + dcName: "dc-name", + graphEnabled: 1, + solrEnabled: 1, + sparkEnabled: 1, + cqlPort: 9042, + cqlSSLPort: 0, + broadcastPort: 7200, + broadcastSSLPort: 7300, }, want: NodeConfig{ "cluster-info": NodeConfig{ @@ -86,17 +112,26 @@ func TestGetModelValues(t *testing.T) { "name": "dc-name", "solr-enabled": 1, "spark-enabled": 1, - }}, + }, + "cassandra-yaml": NodeConfig{ + "native_transport_port": 9042, + "ssl_storage_port": 7300, + }, + }, }, { name: "Missing dc name", args: args{ - seeds: []string{"seed0", "seed1", "seed2"}, - clusterName: "cluster-name", - dcName: "", - graphEnabled: 0, - solrEnabled: 0, - sparkEnabled: 1, + seeds: []string{"seed0", "seed1", "seed2"}, + clusterName: "cluster-name", + dcName: "", + graphEnabled: 0, + solrEnabled: 0, + sparkEnabled: 1, + cqlPort: 9142, + cqlSSLPort: 0, + broadcastPort: 7000, + broadcastSSLPort: 0, }, want: NodeConfig{ "cluster-info": NodeConfig{ @@ -108,17 +143,26 @@ func TestGetModelValues(t *testing.T) { "name": "", "solr-enabled": 0, "spark-enabled": 1, - }}, + }, + "cassandra-yaml": NodeConfig{ + "native_transport_port": 9142, + "storage_port": 7000, + }, + }, }, { name: "Empty args", args: args{ - seeds: nil, - clusterName: "", - dcName: "", - graphEnabled: 0, - solrEnabled: 0, - sparkEnabled: 0, + seeds: nil, + clusterName: "", + dcName: "", + graphEnabled: 0, + solrEnabled: 0, + sparkEnabled: 0, + cqlPort: 0, + cqlSSLPort: 0, + broadcastPort: 0, + broadcastSSLPort: 0, }, want: NodeConfig{ "cluster-info": NodeConfig{ @@ -130,12 +174,24 @@ func TestGetModelValues(t *testing.T) { "name": "", "solr-enabled": 0, "spark-enabled": 0, - }}, + }, + "cassandra-yaml": NodeConfig{}, + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := GetModelValues(tt.args.seeds, tt.args.clusterName, tt.args.dcName, tt.args.graphEnabled, tt.args.solrEnabled, tt.args.sparkEnabled); !reflect.DeepEqual(got, tt.want) { + if got := GetModelValues( + tt.args.seeds, + tt.args.clusterName, + tt.args.dcName, + tt.args.graphEnabled, + tt.args.solrEnabled, + tt.args.sparkEnabled, + tt.args.cqlPort, + tt.args.cqlSSLPort, + tt.args.broadcastPort, + tt.args.broadcastSSLPort); !reflect.DeepEqual(got, tt.want) { t.Errorf("GetModelValues() = %v, want %v", got, tt.want) } }) diff --git a/tests/nodeport_service/nodeport_service_suite_test.go b/tests/nodeport_service/nodeport_service_suite_test.go new file mode 100644 index 000000000..7c12eba9e --- /dev/null +++ b/tests/nodeport_service/nodeport_service_suite_test.go @@ -0,0 +1,102 @@ +// Copyright DataStax, Inc. +// Please see the included license file for details. + +package nodeport_service + +import ( + "encoding/json" + "fmt" + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + // corev1 "k8s.io/api/core/v1" + + ginkgo_util "github.com/datastax/cass-operator/mage/ginkgo" + "github.com/datastax/cass-operator/mage/kubectl" +) + +var ( + testName = "NodePort Service" + namespace = "test-node-port-service" + dcName = "dc1" + dcYaml = "../testdata/nodeport-service-dc.yaml" + operatorYaml = "../testdata/operator.yaml" + nodePortServiceResource = "services/cluster1-dc1-node-port-service" + ns = ginkgo_util.NewWrapper(testName, namespace) +) + +func TestLifecycle(t *testing.T) { + AfterSuite(func() { + logPath := fmt.Sprintf("%s/aftersuite", ns.LogDir) + kubectl.DumpAllLogs(logPath).ExecV() + + fmt.Printf("\n\tPost-run logs dumped at: %s\n\n", logPath) + ns.Terminate() + }) + + RegisterFailHandler(Fail) + RunSpecs(t, testName) +} + +func checkNodePortService() { + // Check the service + + k := kubectl.Get(nodePortServiceResource).FormatOutput("json") + output := ns.OutputPanic(k) + data := map[string]interface{}{} + err := json.Unmarshal([]byte(output), &data) + Expect(err).ToNot(HaveOccurred()) + + err = json.Unmarshal([]byte(output), &data) + + spec := data["spec"].(map[string]interface{}) + policy := spec["externalTrafficPolicy"].(string) + Expect(policy).To(Equal("Local"), "Expected externalTrafficPolicy %s to be Local", policy) + + portData := spec["ports"].([]interface{}) + port0 := portData[0].(map[string]interface{}) + port1 := portData[1].(map[string]interface{}) + + // for some reason, k8s is giving the port numbers back as floats + ns.ExpectKeyValues(port0, map[string]string{ + "name": "broadcast", + "nodePort": "30002.000000", + "port": "30002.000000", + "targetPort": "30002.000000", + }) + + ns.ExpectKeyValues(port1, map[string]string{ + "name": "native", + "nodePort": "30001.000000", + "port": "30001.000000", + "targetPort": "30001.000000", + }) +} + +var _ = Describe(testName, func() { + Context("when in a new cluster", func() { + Specify("the operator can properly create a nodeport service", func() { + var step string + var k kubectl.KCmd + + By("creating a namespace") + err := kubectl.CreateNamespace(namespace).ExecV() + Expect(err).ToNot(HaveOccurred()) + + step = "setting up cass-operator resources via helm chart" + ns.HelmInstall("../../charts/cass-operator-chart") + + ns.WaitForOperatorReady() + + step = "creating a datacenter resource with a nodeport service" + k = kubectl.ApplyFiles(dcYaml) + ns.ExecAndLog(step, k) + + ns.WaitForDatacenterReady(dcName) + + checkNodePortService() + }) + }) +}) diff --git a/tests/testdata/nodeport-service-dc.yaml b/tests/testdata/nodeport-service-dc.yaml new file mode 100644 index 000000000..62ae57361 --- /dev/null +++ b/tests/testdata/nodeport-service-dc.yaml @@ -0,0 +1,33 @@ +apiVersion: cassandra.datastax.com/v1beta1 +kind: CassandraDatacenter +metadata: + name: dc1 +spec: + clusterName: cluster1 + serverType: dse + serverVersion: "6.8.1" + managementApiAuth: + insecure: {} + networking: + nodePort: + cql: 30001 + broadcast: 30002 + size: 2 + storageConfig: + cassandraDataVolumeClaimSpec: + storageClassName: server-storage + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi + racks: + - name: r1 + - name: r2 + config: + jvm-server-options: + initial_heap_size: "800m" + max_heap_size: "800m" + cassandra-yaml: + file_cache_size_in_mb: 100 + memtable_space_in_mb: 100