diff --git a/Makefile b/Makefile index 0e29ad50b..4cbb3ae30 100644 --- a/Makefile +++ b/Makefile @@ -1650,6 +1650,11 @@ kind: ## Run a default KinD cluster kind create cluster --name $(KIND_CLUSTER) --wait 10m --config $(SCRIPTS_DIR)/kind-config.yaml --image $(KIND_IMAGE) $(SCRIPTS_DIR)/kind-label-node.sh +.PHONY: kind-dual +kind-dual: ## Run a KinD cluster configured for a dual stack IPv4 and IPv6 network + kind create cluster --name $(KIND_CLUSTER) --wait 10m --config $(SCRIPTS_DIR)/kind-config-dual.yaml --image $(KIND_IMAGE) + $(SCRIPTS_DIR)/kind-label-node.sh + # ---------------------------------------------------------------------------------------------------------------------- # Start a Kind cluster # ---------------------------------------------------------------------------------------------------------------------- diff --git a/api/v1/coherence_types.go b/api/v1/coherence_types.go index 8516274aa..873c90360 100644 --- a/api/v1/coherence_types.go +++ b/api/v1/coherence_types.go @@ -562,7 +562,7 @@ func (in *CoherenceSpec) GetManagementPort() int32 { } } -// GetPersistenceSpec returns the Coherence persistence spcification. +// GetPersistenceSpec returns the Coherence persistence specification. func (in *CoherenceSpec) GetPersistenceSpec() *PersistenceSpec { if in == nil { return nil @@ -570,6 +570,14 @@ func (in *CoherenceSpec) GetPersistenceSpec() *PersistenceSpec { return in.Persistence } +// GetWkaIPFamily returns the IP Family of the headless Service used for Coherence WKA. +func (in *CoherenceSpec) GetWkaIPFamily() corev1.IPFamily { + if in == nil || in.WKA == nil || in.WKA.IPFamily == nil { + return corev1.IPFamilyUnknown + } + return *in.WKA.IPFamily +} + // ----- CoherenceWKASpec struct -------------------------------------------- // CoherenceWKASpec configures Coherence well-known-addressing to use an @@ -599,6 +607,11 @@ type CoherenceWKASpec struct { // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ // +optional Annotations map[string]string `json:"annotations,omitempty" protobuf:"bytes,12,rep,name=annotations"` + + // IPFamily is the IP family to use for the WKA service (and also the StatefulSet headless service). + // Valid values are "IPv4" or "IPv6". + // +optional + IPFamily *corev1.IPFamily `json:"ipFamily,omitempty"` } // ----- CoherenceTracingSpec struct ---------------------------------------- diff --git a/api/v1/coherencejobresource_types.go b/api/v1/coherencejobresource_types.go index 5aa2c1d9b..2da6a1c62 100644 --- a/api/v1/coherencejobresource_types.go +++ b/api/v1/coherencejobresource_types.go @@ -334,6 +334,19 @@ func (in *CoherenceJob) IsBeforeVersion(version string) bool { return true } +// GetWkaIPFamily returns the IP Family of the headless Service used for Coherence WKA. +func (in *CoherenceJob) GetWkaIPFamily() corev1.IPFamily { + if in == nil { + return corev1.IPFamilyUnknown + } + return in.Spec.GetWkaIPFamily() +} + +// GetHeadlessServiceIPFamily always returns an empty array as this is not applicable to Jobs. +func (in *CoherenceJob) GetHeadlessServiceIPFamily() []corev1.IPFamily { + return nil +} + // ----- CoherenceJobList type ---------------------------------------------- // CoherenceJobResourceSpec defines the specification of a CoherenceJob resource. @@ -487,6 +500,14 @@ func (in *CoherenceJobResourceSpec) GetReplicas() int32 { return *in.CoherenceResourceSpec.Replicas } +// GetWkaIPFamily returns the IP Family of the headless Service used for Coherence WKA. +func (in *CoherenceJobResourceSpec) GetWkaIPFamily() corev1.IPFamily { + if in == nil || in.Coherence == nil { + return corev1.IPFamilyUnknown + } + return in.Coherence.GetWkaIPFamily() +} + // UpdateJob updates a JobSpec from the fields in this spec func (in *CoherenceJobResourceSpec) UpdateJob(spec *batchv1.JobSpec) { if in == nil { diff --git a/api/v1/coherenceresource.go b/api/v1/coherenceresource.go index 936cc7142..1476bfe88 100644 --- a/api/v1/coherenceresource.go +++ b/api/v1/coherenceresource.go @@ -21,8 +21,12 @@ type CoherenceResource interface { GetCoherenceClusterName() string // GetWkaServiceName returns the name of the headless Service used for Coherence WKA. GetWkaServiceName() string + // GetWkaIPFamily returns the IP Family of the headless Service used for Coherence WKA. + GetWkaIPFamily() corev1.IPFamily // GetHeadlessServiceName returns the name of the headless Service used for the StatefulSet. GetHeadlessServiceName() string + // GetHeadlessServiceIPFamily always returns an empty array as this is not applicable to Jobs. + GetHeadlessServiceIPFamily() []corev1.IPFamily // GetReplicas returns the number of replicas required for a deployment. // The Replicas field is a pointer and may be nil so this method will // return either the actual Replicas value or the default (DefaultReplicas const) diff --git a/api/v1/coherenceresource_types.go b/api/v1/coherenceresource_types.go index bca656445..314eae1ca 100644 --- a/api/v1/coherenceresource_types.go +++ b/api/v1/coherenceresource_types.go @@ -150,6 +150,11 @@ func (in *Coherence) GetWkaServiceName() string { return in.Name + WKAServiceNameSuffix } +// GetWkaIPFamily returns the IP Family of the headless Service used for Coherence WKA. +func (in *Coherence) GetWkaIPFamily() corev1.IPFamily { + return in.Spec.GetWkaIPFamily() +} + // GetHeadlessServiceName returns the name of the headless Service used for the StatefulSet. func (in *Coherence) GetHeadlessServiceName() string { if in == nil { @@ -158,6 +163,14 @@ func (in *Coherence) GetHeadlessServiceName() string { return in.Name + HeadlessServiceNameSuffix } +// GetHeadlessServiceIPFamily returns the IP Family of the headless Service used for the StatefulSet. +func (in *Coherence) GetHeadlessServiceIPFamily() []corev1.IPFamily { + if in == nil { + return nil + } + return in.Spec.HeadlessServiceIpFamilies +} + // GetReplicas returns the number of replicas required for a deployment. // The Replicas field is a pointer and may be nil so this method will // return either the actual Replicas value or the default (DefaultReplicas const) @@ -527,6 +540,10 @@ type CoherenceStatefulSetResourceSpec struct { // one of the node labels used to set the Coherence site or rack value. // +optional RollingUpdateLabel *string `json:"rollingUpdateLabel,omitempty"` + // HeadlessServiceIpFamilies is the optional array of IP families that can be configured for + // the headless service used for the StatefulSet. + // +optional + HeadlessServiceIpFamilies []corev1.IPFamily `json:"headlessServiceIpFamilies,omitempty"` } // RollingUpdateStrategyType is a string enumeration type that enumerates diff --git a/api/v1/coherenceresourcespec_types.go b/api/v1/coherenceresourcespec_types.go index bbbb24d6b..f302eaee9 100644 --- a/api/v1/coherenceresourcespec_types.go +++ b/api/v1/coherenceresourcespec_types.go @@ -331,6 +331,14 @@ func (in *CoherenceResourceSpec) SetReplicas(replicas int32) { } } +// GetWkaIPFamily returns the IP Family of the headless Service used for Coherence WKA. +func (in *CoherenceResourceSpec) GetWkaIPFamily() corev1.IPFamily { + if in == nil || in.Coherence == nil { + return corev1.IPFamilyUnknown + } + return in.Coherence.GetWkaIPFamily() +} + // GetRestartPolicy returns the name of the application image to use func (in *CoherenceResourceSpec) GetRestartPolicy() *corev1.RestartPolicy { if in == nil { @@ -553,6 +561,12 @@ func (in *CoherenceResourceSpec) CreateWKAService(deployment CoherenceResource) }, } + ip := deployment.GetWkaIPFamily() + if ip != corev1.IPFamilyUnknown { + svc.Spec.IPFamilyPolicy = ptr.To(corev1.IPFamilyPolicySingleStack) + svc.Spec.IPFamilies = []corev1.IPFamily{ip} + } + return Resource{ Kind: ResourceTypeService, Name: svc.GetName(), @@ -592,6 +606,15 @@ func (in *CoherenceResourceSpec) CreateHeadlessService(deployment CoherenceResou }, } + ipFamilies := deployment.GetHeadlessServiceIPFamily() + if len(ipFamilies) == 1 { + svc.Spec.IPFamilyPolicy = ptr.To(corev1.IPFamilyPolicySingleStack) + svc.Spec.IPFamilies = ipFamilies + } else if len(ipFamilies) > 1 { + svc.Spec.IPFamilyPolicy = ptr.To(corev1.IPFamilyPolicyPreferDualStack) + svc.Spec.IPFamilies = ipFamilies + } + return Resource{ Kind: ResourceTypeService, Name: svc.GetName(), diff --git a/api/v1/create_job_wka_services_test.go b/api/v1/create_job_wka_services_test.go index 4c937f052..f813392b1 100644 --- a/api/v1/create_job_wka_services_test.go +++ b/api/v1/create_job_wka_services_test.go @@ -12,10 +12,11 @@ import ( coh "github.com/oracle/coherence-operator/api/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" "testing" ) -func TestCreateWKAServiceForMinimalJonDeployment(t *testing.T) { +func TestCreateWKAServiceForMinimalJsonDeployment(t *testing.T) { // Create the test deployment deployment := &coh.CoherenceJob{ ObjectMeta: metav1.ObjectMeta{ @@ -315,6 +316,60 @@ func TestCreateWKAServiceForJobWithAdditionalAnnotations(t *testing.T) { assertWKAServiceForJob(t, deployment, expected) } +func TestCreateJobWKAServiceWithIPFamily(t *testing.T) { + // Create the test deployment + deployment := &coh.CoherenceJob{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-ns", + Name: "test", + }, + Spec: coh.CoherenceJobResourceSpec{ + CoherenceResourceSpec: coh.CoherenceResourceSpec{ + Coherence: &coh.CoherenceSpec{ + WKA: &coh.CoherenceWKASpec{ + IPFamily: ptr.To(corev1.IPv4Protocol), + }, + }, + }, + Cluster: "test-cluster", + }, + } + + // create the expected WKA service + labels := deployment.CreateCommonLabels() + labels[coh.LabelCoherenceCluster] = "test-cluster" + labels[coh.LabelComponent] = coh.LabelComponentWKA + + // The selector for the service (match all Pods with the same cluster label) + selector := make(map[string]string) + selector[coh.LabelCoherenceCluster] = "test-cluster" + selector[coh.LabelComponent] = coh.LabelComponentCoherencePod + selector[coh.LabelCoherenceWKAMember] = "true" + + expected := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-ns", + Name: "test-wka", + Labels: labels, + Annotations: map[string]string{ + "service.alpha.kubernetes.io/tolerate-unready-endpoints": "true", + }, + }, + Spec: corev1.ServiceSpec{ + ClusterIP: corev1.ClusterIPNone, + // Pods must be part of the WKA service even if not ready + PublishNotReadyAddresses: true, + Ports: getDefaultServicePorts(), + Selector: selector, + IPFamilyPolicy: ptr.To(corev1.IPFamilyPolicySingleStack), + IPFamilies: []corev1.IPFamily{corev1.IPv4Protocol}, + }, + } + + // assert that the Services are as expected + assertWKAServiceForJob(t, deployment, expected) +} + func assertWKAServiceForJob(t *testing.T, deployment *coh.CoherenceJob, expected *corev1.Service) { g := NewGomegaWithT(t) diff --git a/api/v1/create_statefulset_headless_services_test.go b/api/v1/create_statefulset_headless_services_test.go new file mode 100644 index 000000000..b18edd700 --- /dev/null +++ b/api/v1/create_statefulset_headless_services_test.go @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2020, 2024, Oracle and/or its affiliates. + * Licensed under the Universal Permissive License v 1.0 as shown at + * http://oss.oracle.com/licenses/upl. + */ + +package v1_test + +import ( + "github.com/go-test/deep" + . "github.com/onsi/gomega" + coh "github.com/oracle/coherence-operator/api/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + "testing" +) + +func TestCreateHeadlessServiceForMinimalDeployment(t *testing.T) { + // Create the test deployment + deployment := &coh.Coherence{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-ns", + Name: "test", + }, + } + + // create the expected WKA service + labels := deployment.CreateCommonLabels() + labels[coh.LabelCoherenceCluster] = "test" + labels[coh.LabelComponent] = coh.LabelComponentCoherenceHeadless + + // The selector for the service (match all Pods with the same cluster label) + selector := make(map[string]string) + selector[coh.LabelCoherenceDeployment] = "test" + selector[coh.LabelCoherenceCluster] = "test" + selector[coh.LabelCoherenceRole] = "test" + selector[coh.LabelComponent] = coh.LabelComponentCoherencePod + + expected := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-ns", + Name: "test-sts", + Labels: labels, + }, + Spec: corev1.ServiceSpec{ + ClusterIP: corev1.ClusterIPNone, + PublishNotReadyAddresses: true, + Ports: getDefaultServicePorts(), + Selector: selector, + }, + } + + // assert that the Services are as expected + assertHeadlessService(t, deployment, expected) +} + +func TestCreateHeadlessServiceWithSingleIPFamily(t *testing.T) { + // Create the test deployment + deployment := &coh.Coherence{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-ns", + Name: "test", + }, + Spec: coh.CoherenceStatefulSetResourceSpec{ + HeadlessServiceIpFamilies: []corev1.IPFamily{ + corev1.IPv6Protocol, + }, + }, + } + + // create the expected WKA service + labels := deployment.CreateCommonLabels() + labels[coh.LabelCoherenceCluster] = "test" + labels[coh.LabelComponent] = coh.LabelComponentCoherenceHeadless + + // The selector for the service (match all Pods with the same cluster label) + selector := make(map[string]string) + selector[coh.LabelCoherenceDeployment] = "test" + selector[coh.LabelCoherenceCluster] = "test" + selector[coh.LabelCoherenceRole] = "test" + selector[coh.LabelComponent] = coh.LabelComponentCoherencePod + + expected := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-ns", + Name: "test-sts", + Labels: labels, + }, + Spec: corev1.ServiceSpec{ + ClusterIP: corev1.ClusterIPNone, + PublishNotReadyAddresses: true, + Ports: getDefaultServicePorts(), + Selector: selector, + IPFamilyPolicy: ptr.To(corev1.IPFamilyPolicySingleStack), + IPFamilies: []corev1.IPFamily{corev1.IPv6Protocol}, + }, + } + + // assert that the Services are as expected + assertHeadlessService(t, deployment, expected) +} + +func TestCreateHeadlessServiceWithDualStackIPFamily(t *testing.T) { + // Create the test deployment + deployment := &coh.Coherence{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-ns", + Name: "test", + }, + Spec: coh.CoherenceStatefulSetResourceSpec{ + HeadlessServiceIpFamilies: []corev1.IPFamily{ + corev1.IPv4Protocol, + corev1.IPv6Protocol, + }, + }, + } + + // create the expected WKA service + labels := deployment.CreateCommonLabels() + labels[coh.LabelCoherenceCluster] = "test" + labels[coh.LabelComponent] = coh.LabelComponentCoherenceHeadless + + // The selector for the service (match all Pods with the same cluster label) + selector := make(map[string]string) + selector[coh.LabelCoherenceDeployment] = "test" + selector[coh.LabelCoherenceCluster] = "test" + selector[coh.LabelCoherenceRole] = "test" + selector[coh.LabelComponent] = coh.LabelComponentCoherencePod + + expected := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-ns", + Name: "test-sts", + Labels: labels, + }, + Spec: corev1.ServiceSpec{ + ClusterIP: corev1.ClusterIPNone, + PublishNotReadyAddresses: true, + Ports: getDefaultServicePorts(), + Selector: selector, + IPFamilyPolicy: ptr.To(corev1.IPFamilyPolicyPreferDualStack), + IPFamilies: []corev1.IPFamily{corev1.IPv4Protocol, corev1.IPv6Protocol}, + }, + } + + // assert that the Services are as expected + assertHeadlessService(t, deployment, expected) +} + +func assertHeadlessService(t *testing.T, deployment *coh.Coherence, expected *corev1.Service) { + g := NewGomegaWithT(t) + + resActual := deployment.Spec.CreateHeadlessService(deployment) + resExpected := coh.Resource{ + Kind: coh.ResourceTypeService, + Name: expected.GetName(), + Spec: expected, + } + + diffs := deep.Equal(resActual, resExpected) + g.Expect(diffs).To(BeNil()) +} diff --git a/api/v1/create_wka_services_test.go b/api/v1/create_wka_services_test.go index 3e7dd306c..4c3ac6eb7 100644 --- a/api/v1/create_wka_services_test.go +++ b/api/v1/create_wka_services_test.go @@ -317,6 +317,59 @@ func TestCreateWKAServiceForDeploymentWithAdditionalAnnotations(t *testing.T) { assertWKAService(t, deployment, expected) } +func TestCreateWKAServiceWithIPFamily(t *testing.T) { + // Create the test deployment + deployment := &coh.Coherence{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-ns", + Name: "test", + }, + Spec: coh.CoherenceStatefulSetResourceSpec{ + CoherenceResourceSpec: coh.CoherenceResourceSpec{ + Coherence: &coh.CoherenceSpec{ + WKA: &coh.CoherenceWKASpec{ + IPFamily: ptr.To(corev1.IPv4Protocol), + }, + }, + }, + }, + } + + // create the expected WKA service + labels := deployment.CreateCommonLabels() + labels[coh.LabelCoherenceCluster] = "test" + labels[coh.LabelComponent] = coh.LabelComponentWKA + + // The selector for the service (match all Pods with the same cluster label) + selector := make(map[string]string) + selector[coh.LabelCoherenceCluster] = "test" + selector[coh.LabelComponent] = coh.LabelComponentCoherencePod + selector[coh.LabelCoherenceWKAMember] = "true" + + expected := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-ns", + Name: "test-wka", + Labels: labels, + Annotations: map[string]string{ + "service.alpha.kubernetes.io/tolerate-unready-endpoints": "true", + }, + }, + Spec: corev1.ServiceSpec{ + ClusterIP: corev1.ClusterIPNone, + // Pods must be part of the WKA service even if not ready + PublishNotReadyAddresses: true, + Ports: getDefaultServicePorts(), + Selector: selector, + IPFamilyPolicy: ptr.To(corev1.IPFamilyPolicySingleStack), + IPFamilies: []corev1.IPFamily{corev1.IPv4Protocol}, + }, + } + + // assert that the Services are as expected + assertWKAService(t, deployment, expected) +} + func assertWKAService(t *testing.T, deployment *coh.Coherence, expected *corev1.Service) { g := NewGomegaWithT(t) diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index ddf8ab6ac..ec177e7aa 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -917,6 +917,11 @@ func (in *CoherenceStatefulSetResourceSpec) DeepCopyInto(out *CoherenceStatefulS *out = new(string) **out = **in } + if in.HeadlessServiceIpFamilies != nil { + in, out := &in.HeadlessServiceIpFamilies, &out.HeadlessServiceIpFamilies + *out = make([]corev1.IPFamily, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CoherenceStatefulSetResourceSpec. @@ -971,6 +976,11 @@ func (in *CoherenceWKASpec) DeepCopyInto(out *CoherenceWKASpec) { (*out)[key] = val } } + if in.IPFamily != nil { + in, out := &in.IPFamily, &out.IPFamily + *out = new(corev1.IPFamily) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CoherenceWKASpec. diff --git a/docs/about/01_overview.adoc b/docs/about/01_overview.adoc index 0c0c65bac..637d688db 100644 --- a/docs/about/01_overview.adoc +++ b/docs/about/01_overview.adoc @@ -90,7 +90,10 @@ Configuring Coherence behaviour. -- Configure the behaviour of the JVM. -- +==== +[PILLARS] +==== [CARD] .Expose Ports & Services [icon=control_camera,link=docs/ports/010_overview.adoc] @@ -98,6 +101,13 @@ Configure the behaviour of the JVM. Configure services to expose ports provided by the application. -- +[CARD] +.Networking +[icon=share,link=docs/networking/010_overview.adoc] +-- +Configure networking settings. +-- + ==== [PILLARS] diff --git a/docs/about/02_introduction.adoc b/docs/about/02_introduction.adoc index cba510958..573423169 100644 --- a/docs/about/02_introduction.adoc +++ b/docs/about/02_introduction.adoc @@ -70,6 +70,9 @@ The Coherence CRD is designed to make the more commonly used configuration param to configure. The Coherence CRD is simple to use, in fact none of its fields are mandatory, so an application can be deployed with nothing more than a name, and a container image. +=== Dual-Stack Kubernetes Clusters +The Operator supports running Coherence on dual-stack IPv4 and IPv6 Kubernetes clusters. + === Consistency By using the Operator to manage Coherence clusters all clusters are configured and managed the same way making it easier for DevOps to manage multiple clusters and applications. diff --git a/docs/about/04_coherence_spec.adoc b/docs/about/04_coherence_spec.adoc index ef5c84942..1ae189a16 100644 --- a/docs/about/04_coherence_spec.adoc +++ b/docs/about/04_coherence_spec.adoc @@ -277,6 +277,7 @@ m| namespace | The optional namespace of the existing Coherence deployment to us m| addresses | A list of addresses to be used for WKA. If this field is set, the WKA property for the Coherence cluster will be set using this value and the other WKA fields will be ignored. m| []string | false m| labels | Labels is a map of optional additional labels to apply to the WKA Service. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ m| map[string]string | false m| annotations | Annotations is a map of optional additional labels to apply to the WKA Service. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ m| map[string]string | false +m| ipFamily | IPFamily is the IP family to use for the WKA service (and also the StatefulSet headless service). Valid values are "IPv4" or "IPv6". m| *https://pkg.go.dev/k8s.io/api/core/v1#IPFamily | false |=== <