diff --git a/Makefile b/Makefile index 226a53c53..f3af26123 100644 --- a/Makefile +++ b/Makefile @@ -71,17 +71,22 @@ endif # By default we target amd64 as this is by far the most common local build environment # We actually build images for amd64 and arm64 # ---------------------------------------------------------------------------------------------------------------------- -IMAGE_ARCH ?= amd64 -ARCH ?= amd64 +UNAME_S = $(shell uname -s) +UNAME_M = $(shell uname -m) +ifeq (x86_64, $(UNAME_M)) + IMAGE_ARCH = amd64 + ARCH = amd64 +else + IMAGE_ARCH = $(UNAME_M) + ARCH = $(UNAME_M) +endif + OS ?= linux -UNAME_S := $(shell uname -s) GOPROXY ?= https://proxy.golang.org # ---------------------------------------------------------------------------------------------------------------------- # Set the location of the Operator SDK executable # ---------------------------------------------------------------------------------------------------------------------- -UNAME_S = $(shell uname -s) -UNAME_M = $(shell uname -m) OPERATOR_SDK_VERSION := v1.9.0 # ---------------------------------------------------------------------------------------------------------------------- diff --git a/api/v1/coherence_types.go b/api/v1/coherence_types.go index 001f23cf0..22fb4b75d 100644 --- a/api/v1/coherence_types.go +++ b/api/v1/coherence_types.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023, Oracle and/or its affiliates. + * 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. */ @@ -864,13 +864,14 @@ func (in *PersistentStorageSpec) CreatePersistentVolumeClaim(deployment *Coheren in.PersistentVolumeClaim.DeepCopyInto(&spec) } - labels := deployment.CreateCommonLabels() + labels := deployment.CreateGlobalLabels() labels[LabelComponent] = LabelComponentPVC return &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ - Name: name, - Labels: labels, + Name: name, + Labels: labels, + Annotations: deployment.CreateGlobalAnnotations(), }, Spec: spec, } @@ -1099,7 +1100,7 @@ func (in *NamedPortSpec) CreateService(deployment CoherenceResource) *corev1.Ser name, _ := in.GetServiceName(deployment) // The labels for the service - svcLabels := deployment.CreateCommonLabels() + svcLabels := deployment.CreateGlobalLabels() svcLabels[LabelComponent] = LabelComponentPortService svcLabels[LabelPort] = in.Name if in.Service != nil { @@ -1109,9 +1110,15 @@ func (in *NamedPortSpec) CreateService(deployment CoherenceResource) *corev1.Ser } // The service annotations - var ann map[string]string + ann := deployment.CreateGlobalAnnotations() if in.Service != nil && in.Service.Annotations != nil { - ann = in.Service.Annotations + if ann == nil { + ann = in.Service.Annotations + } else { + for k, v := range in.Service.Annotations { + ann[k] = v + } + } } // Create the Service serviceSpec @@ -1181,7 +1188,7 @@ func (in *NamedPortSpec) CreateServiceMonitor(deployment CoherenceResource) *mon } // The labels for the ServiceMonitor - labels := deployment.CreateCommonLabels() + labels := deployment.CreateGlobalLabels() labels[LabelComponent] = LabelComponentPortServiceMonitor for k, v := range in.ServiceMonitor.Labels { labels[k] = v @@ -1206,9 +1213,10 @@ func (in *NamedPortSpec) CreateServiceMonitor(deployment CoherenceResource) *mon return &monitoringv1.ServiceMonitor{ ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: deployment.GetNamespace(), - Labels: labels, + Name: name, + Namespace: deployment.GetNamespace(), + Labels: labels, + Annotations: deployment.CreateGlobalAnnotations(), }, Spec: spec, } @@ -2979,6 +2987,25 @@ func (in *PersistentVolumeClaimObjectMeta) toObjectMeta() metav1.ObjectMeta { } } +// ----- GlobalSpec --------------------------------------------------------- + +// GlobalSpec is attributes that will be applied to all resources managed by the Operator. +type GlobalSpec struct { + // Map of string keys and values that can be used to organize and categorize + // (scope and select) objects. May match selectors of replication controllers + // and services. + // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ + // +optional + Labels map[string]string `json:"labels,omitempty" protobuf:"bytes,11,rep,name=labels"` + + // Annotations is an unstructured key value map stored with a resource that may be + // set by external tools to store and retrieve arbitrary metadata. They are not + // queryable and should be preserved when modifying objects. + // 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"` +} + // ----- helper methods ----------------------------------------------------- // Int32PtrToStringWithDefault converts an int32 pointer to a string using the default if the pointer is nil. diff --git a/api/v1/coherencejobresource_types.go b/api/v1/coherencejobresource_types.go index 0e04bbbc3..3d04c08f4 100644 --- a/api/v1/coherencejobresource_types.go +++ b/api/v1/coherencejobresource_types.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023, Oracle and/or its affiliates. + * 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. */ @@ -7,6 +7,7 @@ package v1 import ( + "github.com/oracle/coherence-operator/pkg/operator" "golang.org/x/mod/semver" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" @@ -68,6 +69,13 @@ func (in *CoherenceJob) GetEnvVarFrom() []corev1.EnvFromSource { return in.Spec.EnvFrom } +func (in *CoherenceJob) GetGlobalSpec() *GlobalSpec { + if in == nil { + return nil + } + return in.Spec.Global +} + // GetSpec returns this resource's CoherenceResourceSpec func (in *CoherenceJob) GetSpec() *CoherenceResourceSpec { return &in.Spec.CoherenceResourceSpec @@ -193,6 +201,25 @@ func (in *CoherenceJob) FindPortServiceName(name string) (string, bool) { return in.Spec.FindPortServiceName(name, in) } +// CreateGlobalLabels creates the common label set for all resources. +func (in *CoherenceJob) CreateGlobalLabels() map[string]string { + labels := operator.GetGlobalLabelsNoError() + if labels == nil { + labels = make(map[string]string) + } + + globalSpec := in.GetGlobalSpec() + if globalSpec != nil { + for k, v := range globalSpec.Labels { + labels[k] = v + } + } + for k, v := range in.CreateCommonLabels() { + labels[k] = v + } + return labels +} + // CreateCommonLabels creates the deployment's common label set. func (in *CoherenceJob) CreateCommonLabels() map[string]string { labels := make(map[string]string) @@ -211,6 +238,21 @@ func (in *CoherenceJob) CreateCommonLabels() map[string]string { return labels } +// CreateGlobalAnnotations creates the common annotation set for all resources. +func (in *CoherenceJob) CreateGlobalAnnotations() map[string]string { + annotations := operator.GetGlobalAnnotationsNoError() + globalSpec := in.GetGlobalSpec() + if globalSpec != nil && globalSpec.Annotations != nil { + if annotations == nil { + annotations = make(map[string]string) + } + for k, v := range globalSpec.Annotations { + annotations[k] = v + } + } + return annotations +} + // CreateAnnotations returns the annotations to apply to this cluster's // deployment (StatefulSet). func (in *CoherenceJob) CreateAnnotations() map[string]string { @@ -409,6 +451,8 @@ type CoherenceJobResourceSpec struct { // Cannot be updated. // +optional EnvFrom []corev1.EnvFromSource `json:"envFrom,omitempty"` + // Global contains attributes that will be applied to all resources managed by the Coherence Operator. + Global *GlobalSpec `json:"global,omitempty"` } // GetRestartPolicy returns the name of the application image to use @@ -458,7 +502,7 @@ func (in *CoherenceJobResourceSpec) IsSyncCompletions() bool { } // CreateJobResource creates the deployment's Job resource. -func (in *CoherenceJobResourceSpec) CreateJobResource(deployment CoherenceResource) Resource { +func (in *CoherenceJobResourceSpec) CreateJobResource(deployment *CoherenceJob) Resource { job := in.CreateJob(deployment) return Resource{ @@ -469,13 +513,14 @@ func (in *CoherenceJobResourceSpec) CreateJobResource(deployment CoherenceResour } // CreateJob creates the deployment's Job. -func (in *CoherenceJobResourceSpec) CreateJob(deployment CoherenceResource) batchv1.Job { +func (in *CoherenceJobResourceSpec) CreateJob(deployment *CoherenceJob) batchv1.Job { + ann := deployment.CreateGlobalAnnotations() job := batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Namespace: deployment.GetNamespace(), Name: deployment.GetName(), - Labels: deployment.CreateCommonLabels(), - Annotations: deployment.CreateAnnotations(), + Labels: deployment.CreateGlobalLabels(), + Annotations: ann, }, } diff --git a/api/v1/coherenceresource.go b/api/v1/coherenceresource.go index 988aab094..9626da465 100644 --- a/api/v1/coherenceresource.go +++ b/api/v1/coherenceresource.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023, Oracle and/or its affiliates. + * 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. */ @@ -43,6 +43,10 @@ type CoherenceResource interface { FindPortServiceName(name string) (string, bool) // CreateCommonLabels creates the deployment's common label set. CreateCommonLabels() map[string]string + // CreateGlobalLabels creates the common label set for all resources. + CreateGlobalLabels() map[string]string + // CreateGlobalAnnotations creates the common annotation set for all resources. + CreateGlobalAnnotations() map[string]string // CreateAnnotations returns the annotations to apply to this cluster's // deployment (StatefulSet). CreateAnnotations() map[string]string @@ -92,4 +96,6 @@ type CoherenceResource interface { IsForceExit() bool // GetEnvVarFrom returns the array of EnvVarSource configurations GetEnvVarFrom() []corev1.EnvFromSource + // GetGlobalSpec returns the attributes to be applied to all resources + GetGlobalSpec() *GlobalSpec } diff --git a/api/v1/coherenceresource_types.go b/api/v1/coherenceresource_types.go index 09b137c35..ddce1345f 100644 --- a/api/v1/coherenceresource_types.go +++ b/api/v1/coherenceresource_types.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023, Oracle and/or its affiliates. + * 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. */ @@ -8,6 +8,7 @@ package v1 import ( "fmt" + "github.com/oracle/coherence-operator/pkg/operator" "golang.org/x/mod/semver" appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" @@ -186,6 +187,13 @@ func (in *Coherence) GetEnvVarFrom() []corev1.EnvFromSource { return in.Spec.EnvFrom } +func (in *Coherence) GetGlobalSpec() *GlobalSpec { + if in == nil { + return nil + } + return in.Spec.Global +} + // FindFullyQualifiedPortServiceNames returns a map of the exposed ports of this resource mapped to their Service's // fully qualified domain name. func (in *Coherence) FindFullyQualifiedPortServiceNames() map[string]string { @@ -226,6 +234,24 @@ func (in *Coherence) FindPortServiceName(name string) (string, bool) { return in.Spec.FindPortServiceName(name, in) } +// CreateGlobalLabels creates the common label set for all resources. +func (in *Coherence) CreateGlobalLabels() map[string]string { + labels := operator.GetGlobalLabelsNoError() + if labels == nil { + labels = make(map[string]string) + } + globalSpec := in.GetGlobalSpec() + if globalSpec != nil { + for k, v := range globalSpec.Labels { + labels[k] = v + } + } + for k, v := range in.CreateCommonLabels() { + labels[k] = v + } + return labels +} + // CreateCommonLabels creates the deployment's common label set. func (in *Coherence) CreateCommonLabels() map[string]string { labels := make(map[string]string) @@ -244,17 +270,37 @@ func (in *Coherence) CreateCommonLabels() map[string]string { return labels } +// CreateGlobalAnnotations creates the common annotation set for all resources. +func (in *Coherence) CreateGlobalAnnotations() map[string]string { + annotations := operator.GetGlobalAnnotationsNoError() + globalSpec := in.GetGlobalSpec() + if globalSpec != nil && globalSpec.Annotations != nil { + if annotations == nil { + annotations = make(map[string]string) + } + for k, v := range globalSpec.Annotations { + annotations[k] = v + } + } + return annotations +} + // CreateAnnotations returns the annotations to apply to this cluster's // deployment (StatefulSet). func (in *Coherence) CreateAnnotations() map[string]string { - var annotations map[string]string + annotations := in.CreateGlobalAnnotations() + if in.Spec.StatefulSetAnnotations != nil { - annotations = make(map[string]string) + if annotations == nil { + annotations = make(map[string]string) + } for k, v := range in.Spec.StatefulSetAnnotations { annotations[k] = v } } else if in.Annotations != nil { - annotations = make(map[string]string) + if annotations == nil { + annotations = make(map[string]string) + } for k, v := range in.Annotations { annotations[k] = v } @@ -449,6 +495,8 @@ type CoherenceStatefulSetResourceSpec struct { // Cannot be updated. // +optional EnvFrom []corev1.EnvFromSource `json:"envFrom,omitempty"` + // Global contains attributes that will be applied to all resources managed by the Coherence Operator. + Global *GlobalSpec `json:"global,omitempty"` } // CreateStatefulSetResource creates the deployment's StatefulSet resource. @@ -468,7 +516,7 @@ func (in *CoherenceStatefulSetResourceSpec) CreateStatefulSet(deployment *Cohere ObjectMeta: metav1.ObjectMeta{ Namespace: deployment.GetNamespace(), Name: deployment.GetName(), - Labels: deployment.CreateCommonLabels(), + Labels: deployment.CreateGlobalLabels(), Annotations: deployment.CreateAnnotations(), }, } diff --git a/api/v1/coherenceresourcespec_types.go b/api/v1/coherenceresourcespec_types.go index efd420ff5..249cd3539 100644 --- a/api/v1/coherenceresourcespec_types.go +++ b/api/v1/coherenceresourcespec_types.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023, Oracle and/or its affiliates. + * 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. */ @@ -508,7 +508,7 @@ func (in *CoherenceResourceSpec) CreatePodSelectorLabels(deployment CoherenceRes // CreateWKAService creates the headless WKA Service func (in *CoherenceResourceSpec) CreateWKAService(deployment CoherenceResource) Resource { - labels := deployment.CreateCommonLabels() + labels := deployment.CreateGlobalLabels() labels[LabelComponent] = LabelComponentWKA // The selector for the service (match all Pods with the same cluster label) @@ -517,14 +517,18 @@ func (in *CoherenceResourceSpec) CreateWKAService(deployment CoherenceResource) selector[LabelComponent] = LabelComponentCoherencePod selector[LabelCoherenceWKAMember] = "true" + ann := deployment.CreateGlobalAnnotations() + if ann == nil { + ann = make(map[string]string) + } + ann["service.alpha.kubernetes.io/tolerate-unready-endpoints"] = "true" + svc := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ - Namespace: deployment.GetNamespace(), - Name: deployment.GetWkaServiceName(), - Labels: labels, - Annotations: map[string]string{ - "service.alpha.kubernetes.io/tolerate-unready-endpoints": "true", - }, + Namespace: deployment.GetNamespace(), + Name: deployment.GetWkaServiceName(), + Labels: labels, + Annotations: ann, }, Spec: corev1.ServiceSpec{ ClusterIP: corev1.ClusterIPNone, @@ -545,7 +549,7 @@ func (in *CoherenceResourceSpec) CreateWKAService(deployment CoherenceResource) // CreateHeadlessService creates the headless Service for the deployment's StatefulSet. func (in *CoherenceResourceSpec) CreateHeadlessService(deployment CoherenceResource) Resource { // The labels for the service - svcLabels := deployment.CreateCommonLabels() + svcLabels := deployment.CreateGlobalLabels() svcLabels[LabelComponent] = LabelComponentCoherenceHeadless // The selector for the service @@ -561,9 +565,10 @@ func (in *CoherenceResourceSpec) CreateHeadlessService(deployment CoherenceResou // Create the Service svc := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ - Namespace: deployment.GetNamespace(), - Name: deployment.GetHeadlessServiceName(), - Labels: svcLabels, + Namespace: deployment.GetNamespace(), + Name: deployment.GetHeadlessServiceName(), + Labels: svcLabels, + Annotations: deployment.CreateGlobalAnnotations(), }, Spec: corev1.ServiceSpec{ ClusterIP: "None", @@ -618,7 +623,36 @@ func (in *CoherenceResourceSpec) createDefaultServicePorts() []corev1.ServicePor func (in *CoherenceResourceSpec) CreatePodTemplateSpec(deployment CoherenceResource) corev1.PodTemplateSpec { // Create the PodSpec labels - podLabels := in.CreatePodSelectorLabels(deployment) + selectorLabels := in.CreatePodSelectorLabels(deployment) + globalLabels := deployment.CreateGlobalLabels() + + podLabels := make(map[string]string) + for k, v := range globalLabels { + podLabels[k] = v + } + for k, v := range selectorLabels { + podLabels[k] = v + } + + var annotations map[string]string + globalAnnotations := deployment.CreateGlobalAnnotations() + if globalAnnotations != nil { + if annotations == nil { + annotations = make(map[string]string) + } + for k, v := range globalAnnotations { + annotations[k] = v + } + } + if in.Annotations != nil { + if annotations == nil { + annotations = make(map[string]string) + } + for k, v := range in.Annotations { + annotations[k] = v + } + } + // Add the WKA member label podLabels[LabelCoherenceWKAMember] = strconv.FormatBool(in.Coherence.IsWKAMember()) // Add any labels specified for the deployment @@ -639,7 +673,7 @@ func (in *CoherenceResourceSpec) CreatePodTemplateSpec(deployment CoherenceResou podTemplate := corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: podLabels, - Annotations: in.Annotations, + Annotations: annotations, }, Spec: corev1.PodSpec{ Affinity: in.EnsurePodAffinity(deployment), diff --git a/api/v1/create_job_coherencespec_test.go b/api/v1/create_job_coherencespec_test.go index 49e0e8dc1..49e301a16 100644 --- a/api/v1/create_job_coherencespec_test.go +++ b/api/v1/create_job_coherencespec_test.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Oracle and/or its affiliates. + * 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. */ @@ -430,3 +430,53 @@ func TestCreateJobWithCoherenceSpecWithMultipleWkaAddresses(t *testing.T) { // assert that the Job is as expected assertJobCreation(t, deployment, jobExpected) } + +func TestCreateJobWithGlobalLabels(t *testing.T) { + m := make(map[string]string) + m["one"] = "value-one" + m["two"] = "value-two" + + spec := coh.CoherenceJobResourceSpec{ + Global: &coh.GlobalSpec{ + Labels: m, + }, + } + + // Create the test deployment + deployment := createTestCoherenceJobDeployment(spec) + // Create expected Job + jobExpected := createMinimalExpectedJob(deployment) + labelsExpected := jobExpected.Labels + labelsExpected["one"] = "value-one" + labelsExpected["two"] = "value-two" + + // assert that the Job is as expected + assertJobCreation(t, deployment, jobExpected) +} + +func TestCreateJobWithGlobalAnnotations(t *testing.T) { + m := make(map[string]string) + m["one"] = "value-one" + m["two"] = "value-two" + + spec := coh.CoherenceJobResourceSpec{ + Global: &coh.GlobalSpec{ + Annotations: m, + }, + } + + // Create the test deployment + deployment := createTestCoherenceJobDeployment(spec) + // Create expected Job + jobExpected := createMinimalExpectedJob(deployment) + annExpected := jobExpected.Annotations + if annExpected == nil { + annExpected = make(map[string]string) + } + annExpected["one"] = "value-one" + annExpected["two"] = "value-two" + jobExpected.Annotations = annExpected + + // assert that the Job is as expected + assertJobCreation(t, deployment, jobExpected) +} diff --git a/api/v1/create_services_for_ports_test.go b/api/v1/create_services_for_ports_test.go index 4ef377ae9..1f3b220e9 100644 --- a/api/v1/create_services_for_ports_test.go +++ b/api/v1/create_services_for_ports_test.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023, Oracle and/or its affiliates. + * 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. */ @@ -33,9 +33,7 @@ func TestCreateServicesWithAdditionalPortsEmpty(t *testing.T) { } func TestCreateServicesWithPortsWithOneAdditionalPortWithServiceEnabledFalse(t *testing.T) { - protocol := corev1.ProtocolUDP - spec := coh.CoherenceResourceSpec{ Ports: []coh.NamedPortSpec{ { @@ -60,9 +58,7 @@ func TestCreateServicesWithPortsWithOneAdditionalPortWithServiceEnabledFalse(t * } func TestCreateServicesWithPortsWithOneAdditionalPort(t *testing.T) { - protocol := corev1.ProtocolUDP - spec := coh.CoherenceResourceSpec{ Ports: []coh.NamedPortSpec{ { @@ -503,6 +499,141 @@ func TestCreateServicesWithPortsWithTwoAdditionalPorts(t *testing.T) { assertService(t, deployment, &svcExpectedOne, &svcExpectedTwo) } +func TestCreateServiceWithGlobalLabels(t *testing.T) { + m := make(map[string]string) + m["one"] = "value-one" + m["two"] = "value-two" + + protocol := corev1.ProtocolUDP + + spec := coh.CoherenceStatefulSetResourceSpec{ + Global: &coh.GlobalSpec{ + Labels: m, + }, + CoherenceResourceSpec: coh.CoherenceResourceSpec{ + Ports: []coh.NamedPortSpec{ + { + Name: "test-port-one", + Port: 9876, + Protocol: &protocol, + NodePort: int32Ptr(2020), + HostPort: int32Ptr(1234), + HostIP: stringPtr("10.10.1.0"), + Service: &coh.ServiceSpec{ + Enabled: boolPtr(true), + }, + }, + }, + }, + } + + // Create the test deployment + deployment := createTestCoherenceDeployment(spec) + + // Create the expected labels + labels := deployment.CreateCommonLabels() + labels[coh.LabelComponent] = coh.LabelComponentPortService + labels[coh.LabelPort] = "test-port-one" + labels["one"] = "value-one" + labels["two"] = "value-two" + + // Create the expected service selector labels + selectorLabels := deployment.CreateCommonLabels() + selectorLabels[coh.LabelComponent] = coh.LabelComponentCoherencePod + + // Create expected Service + svcExpected := corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-test-port-one", deployment.Name), + Labels: labels, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Name: "test-port-one", + Protocol: protocol, + Port: 9876, + TargetPort: intstr.FromInt32(9876), + NodePort: 2020, + }, + }, + Selector: selectorLabels, + }, + } + + // assert that the Services are as expected + assertService(t, deployment, &svcExpected) +} + +func TestCreateServiceWithGlobalAnnotations(t *testing.T) { + m := make(map[string]string) + m["one"] = "value-one" + m["two"] = "value-two" + + protocol := corev1.ProtocolUDP + + spec := coh.CoherenceStatefulSetResourceSpec{ + Global: &coh.GlobalSpec{ + Annotations: m, + }, + CoherenceResourceSpec: coh.CoherenceResourceSpec{ + Ports: []coh.NamedPortSpec{ + { + Name: "test-port-one", + Port: 9876, + Protocol: &protocol, + NodePort: int32Ptr(2020), + HostPort: int32Ptr(1234), + HostIP: stringPtr("10.10.1.0"), + Service: &coh.ServiceSpec{ + Enabled: boolPtr(true), + }, + }, + }, + }, + } + + // Create the test deployment + deployment := createTestCoherenceDeployment(spec) + + // Create the expected labels + labels := deployment.CreateCommonLabels() + labels[coh.LabelComponent] = coh.LabelComponentPortService + labels[coh.LabelPort] = "test-port-one" + + ann := make(map[string]string) + ann["one"] = "value-one" + ann["two"] = "value-two" + + // Create the expected service selector labels + selectorLabels := deployment.CreateCommonLabels() + selectorLabels[coh.LabelComponent] = coh.LabelComponentCoherencePod + + // Create expected Service + svcExpected := corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-test-port-one", deployment.Name), + Labels: labels, + Annotations: ann, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Name: "test-port-one", + Protocol: protocol, + Port: 9876, + TargetPort: intstr.FromInt32(9876), + NodePort: 2020, + }, + }, + Selector: selectorLabels, + }, + } + + // assert that the Services are as expected + assertService(t, deployment, &svcExpected) +} + func assertService(t *testing.T, deployment *coh.Coherence, servicesExpected ...metav1.Object) { g := NewGomegaWithT(t) diff --git a/api/v1/create_statefulset_test.go b/api/v1/create_statefulset_test.go index 9c1f2a659..7500a3b2d 100644 --- a/api/v1/create_statefulset_test.go +++ b/api/v1/create_statefulset_test.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023, Oracle and/or its affiliates. + * 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. */ @@ -662,3 +662,53 @@ func TestCreateStatefulSetWithTopologySpreadConstraints(t *testing.T) { // assert that the StatefulSet is as expected assertStatefulSetCreation(t, deployment, stsExpected) } + +func TestCreateStatefulSetWithGlobalLabels(t *testing.T) { + m := make(map[string]string) + m["one"] = "value-one" + m["two"] = "value-two" + + spec := coh.CoherenceStatefulSetResourceSpec{ + Global: &coh.GlobalSpec{ + Labels: m, + }, + } + + // Create the test deployment + deployment := createTestCoherenceDeployment(spec) + // Create expected StatefulSet + jobExpected := createMinimalExpectedStatefulSet(deployment) + labelsExpected := jobExpected.Labels + labelsExpected["one"] = "value-one" + labelsExpected["two"] = "value-two" + + // assert that the Job is as expected + assertStatefulSetCreation(t, deployment, jobExpected) +} + +func TestCreateStatefulSetWithGlobalAnnotations(t *testing.T) { + m := make(map[string]string) + m["one"] = "value-one" + m["two"] = "value-two" + + spec := coh.CoherenceStatefulSetResourceSpec{ + Global: &coh.GlobalSpec{ + Annotations: m, + }, + } + + // Create the test deployment + deployment := createTestCoherenceDeployment(spec) + // Create expected Job + jobExpected := createMinimalExpectedStatefulSet(deployment) + annExpected := jobExpected.Annotations + if annExpected == nil { + annExpected = make(map[string]string) + } + annExpected["one"] = "value-one" + annExpected["two"] = "value-two" + jobExpected.Annotations = annExpected + + // assert that the Job is as expected + assertStatefulSetCreation(t, deployment, jobExpected) +} diff --git a/api/v1/hasher_test.go b/api/v1/hasher_test.go index e84381685..0088f5279 100644 --- a/api/v1/hasher_test.go +++ b/api/v1/hasher_test.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023, Oracle and/or its affiliates. + * 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. */ @@ -28,5 +28,8 @@ func TestHash(t *testing.T) { coh.EnsureHashLabel(deployment) + // If this test fails you have probably added a new field to CoherenceResourceSpec + // This will break backwards compatibility. This field needs to be added to + // both CoherenceStatefulSetResourceSpec and CoherenceJobResourceSpec instead g.Expect(deployment.GetLabels()["coherence-hash"]).To(Equal("5cb9fd9f96")) } diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index db03cd762..9b47746ef 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -347,6 +347,11 @@ func (in *CoherenceJobResourceSpec) DeepCopyInto(out *CoherenceJobResourceSpec) (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.Global != nil { + in, out := &in.Global, &out.Global + *out = new(GlobalSpec) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CoherenceJobResourceSpec. @@ -888,6 +893,11 @@ func (in *CoherenceStatefulSetResourceSpec) DeepCopyInto(out *CoherenceStatefulS (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.Global != nil { + in, out := &in.Global, &out.Global + *out = new(GlobalSpec) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CoherenceStatefulSetResourceSpec. @@ -1023,6 +1033,35 @@ func (in *ConfigMapVolumeSpec) DeepCopy() *ConfigMapVolumeSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GlobalSpec) DeepCopyInto(out *GlobalSpec) { + *out = *in + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GlobalSpec. +func (in *GlobalSpec) DeepCopy() *GlobalSpec { + if in == nil { + return nil + } + out := new(GlobalSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ImageSpec) DeepCopyInto(out *ImageSpec) { *out = *in diff --git a/controllers/coherence_controller.go b/controllers/coherence_controller.go index 99be2fc2c..cb28a4341 100644 --- a/controllers/coherence_controller.go +++ b/controllers/coherence_controller.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023, Oracle and/or its affiliates. + * 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. */ @@ -25,6 +25,7 @@ import ( "github.com/spf13/viper" coreV1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" @@ -192,7 +193,7 @@ func (in *CoherenceReconciler) Reconcile(ctx context.Context, request ctrl.Reque } // ensure that the Operator configuration Secret exists - if err = in.ensureOperatorSecret(ctx, request.Namespace, in.GetClient(), in.Log); err != nil { + if err = in.ensureOperatorSecret(ctx, deployment, in.GetClient(), in.Log); err != nil { err = errors.Wrap(err, "ensuring Operator configuration secret") return in.HandleErrAndRequeue(ctx, err, nil, fmt.Sprintf(reconcileFailedMessage, request.Name, request.Namespace, err), in.Log) } @@ -475,8 +476,16 @@ func (in *CoherenceReconciler) finalizeDeployment(ctx context.Context, c *coh.Co } // ensureOperatorSecret ensures that the Operator configuration secret exists in the namespace. -func (in *CoherenceReconciler) ensureOperatorSecret(ctx context.Context, namespace string, c client.Client, log logr.Logger) error { - s := &coreV1.Secret{} +func (in *CoherenceReconciler) ensureOperatorSecret(ctx context.Context, deployment *coh.Coherence, c client.Client, log logr.Logger) error { + namespace := deployment.Namespace + s := &coreV1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: coh.OperatorConfigName, + Namespace: namespace, + Labels: deployment.CreateGlobalLabels(), + Annotations: deployment.CreateGlobalAnnotations(), + }, + } err := c.Get(ctx, types.NamespacedName{Name: coh.OperatorConfigName, Namespace: namespace}, s) if err != nil && !apierrors.IsNotFound(err) { @@ -485,9 +494,6 @@ func (in *CoherenceReconciler) ensureOperatorSecret(ctx context.Context, namespa restHostAndPort := rest.GetServerHostAndPort() - s.SetNamespace(namespace) - s.SetName(coh.OperatorConfigName) - oldValue := s.Data[coh.OperatorConfigKeyHost] if oldValue == nil || string(oldValue) != restHostAndPort { // data is different so create/update diff --git a/controllers/coherencejob_controller.go b/controllers/coherencejob_controller.go index d7d09a486..af4f1978b 100644 --- a/controllers/coherencejob_controller.go +++ b/controllers/coherencejob_controller.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023, Oracle and/or its affiliates. + * 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. */ @@ -20,6 +20,7 @@ import ( "github.com/pkg/errors" coreV1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" @@ -53,7 +54,7 @@ func (in *CoherenceJobReconciler) Reconcile(ctx context.Context, request ctrl.Re return in.ReconcileDeployment(ctx, request, deployment) } -func (in *CoherenceJobReconciler) ReconcileDeployment(ctx context.Context, request ctrl.Request, deployment coh.CoherenceResource) (ctrl.Result, error) { +func (in *CoherenceJobReconciler) ReconcileDeployment(ctx context.Context, request ctrl.Request, deployment *coh.CoherenceJob) (ctrl.Result, error) { var err error log := in.Log.WithValues("namespace", request.Namespace, "name", request.Name) @@ -144,7 +145,7 @@ func (in *CoherenceJobReconciler) ReconcileDeployment(ctx context.Context, reque } // ensure that the Operator configuration Secret exists - if err = in.ensureOperatorSecret(ctx, request.Namespace, in.GetClient(), in.Log); err != nil { + if err = in.ensureOperatorSecret(ctx, deployment, in.GetClient(), in.Log); err != nil { err = errors.Wrap(err, "ensuring Operator configuration secret") return in.HandleErrAndRequeue(ctx, err, nil, fmt.Sprintf(reconcileFailedMessage, request.Name, request.Namespace, err), in.Log) } @@ -329,8 +330,16 @@ func (in *CoherenceJobReconciler) ensureVersionAnnotationApplied(ctx context.Con } // ensureOperatorSecret ensures that the Operator configuration secret exists in the namespace. -func (in *CoherenceJobReconciler) ensureOperatorSecret(ctx context.Context, namespace string, c client.Client, log logr.Logger) error { - s := &coreV1.Secret{} +func (in *CoherenceJobReconciler) ensureOperatorSecret(ctx context.Context, deployment *coh.CoherenceJob, c client.Client, log logr.Logger) error { + namespace := deployment.Namespace + s := &coreV1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: coh.OperatorConfigName, + Namespace: namespace, + Labels: deployment.CreateGlobalLabels(), + Annotations: deployment.CreateGlobalAnnotations(), + }, + } err := c.Get(ctx, types.NamespacedName{Name: coh.OperatorConfigName, Namespace: namespace}, s) if err != nil && !apierrors.IsNotFound(err) { @@ -339,9 +348,6 @@ func (in *CoherenceJobReconciler) ensureOperatorSecret(ctx context.Context, name restHostAndPort := rest.GetServerHostAndPort() - s.SetNamespace(namespace) - s.SetName(coh.OperatorConfigName) - oldValue := s.Data[coh.OperatorConfigKeyHost] if oldValue == nil || string(oldValue) != restHostAndPort { // data is different so create/update diff --git a/controllers/webhook/webhook.go b/controllers/webhook/webhook.go index bfe8dcb56..b0e8167b4 100644 --- a/controllers/webhook/webhook.go +++ b/controllers/webhook/webhook.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023, Oracle and/or its affiliates. + * 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. */ @@ -109,8 +109,10 @@ func baseWebhookSecret(ns string) *corev1.Secret { Kind: "Secret", }, ObjectMeta: metav1.ObjectMeta{ - Name: viper.GetString(operator.FlagWebhookSecret), - Namespace: ns, + Name: viper.GetString(operator.FlagWebhookSecret), + Namespace: ns, + Labels: operator.GetGlobalLabelsNoError(), + Annotations: operator.GetGlobalAnnotationsNoError(), }, Type: "kubernetes.io/tls", } @@ -303,13 +305,18 @@ func createMutatingWebhookConfiguration(ns string) admissionv1.MutatingWebhookCo noSideEffects := admissionv1.SideEffectClassNone path := coh.MutatingWebHookPath clientConfig := createWebhookClientConfig(ns, path) + labels := operator.GetGlobalLabelsNoError() + ann := operator.GetGlobalAnnotationsNoError() + if ann == nil { + ann = make(map[string]string) + } + ann[certTypeAnnotation] = viper.GetString(operator.FlagCertType) return admissionv1.MutatingWebhookConfiguration{ ObjectMeta: metav1.ObjectMeta{ - Name: viper.GetString(operator.FlagMutatingWebhookName), - Annotations: map[string]string{ - certTypeAnnotation: viper.GetString(operator.FlagCertType), - }, + Name: viper.GetString(operator.FlagMutatingWebhookName), + Labels: labels, + Annotations: ann, }, TypeMeta: metav1.TypeMeta{ Kind: "MutatingWebhookConfiguration", @@ -346,13 +353,18 @@ func createValidatingWebhookConfiguration(ns string) admissionv1.ValidatingWebho noSideEffects := admissionv1.SideEffectClassNone path := coh.ValidatingWebHookPath clientConfig := createWebhookClientConfig(ns, path) + labels := operator.GetGlobalLabelsNoError() + ann := operator.GetGlobalAnnotationsNoError() + if ann == nil { + ann = make(map[string]string) + } + ann[certTypeAnnotation] = viper.GetString(operator.FlagCertType) return admissionv1.ValidatingWebhookConfiguration{ ObjectMeta: metav1.ObjectMeta{ - Name: viper.GetString(operator.FlagValidatingWebhookName), - Annotations: map[string]string{ - certTypeAnnotation: viper.GetString(operator.FlagCertType), - }, + Name: viper.GetString(operator.FlagValidatingWebhookName), + Labels: labels, + Annotations: ann, }, TypeMeta: metav1.TypeMeta{ Kind: "ValidatingWebhookConfiguration", @@ -406,13 +418,17 @@ func createWebhookClientConfig(ns, path string) admissionv1.WebhookClientConfig func issuer(ns string, group string, apiVersion string) *unstructured.Unstructured { apiString := fmt.Sprintf("%s/%s", group, apiVersion) certIssuer := viper.GetString(operator.FlagCertIssuer) + labels := operator.GetGlobalLabelsNoError() + ann := operator.GetGlobalAnnotationsNoError() return &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": apiString, "kind": "Issuer", "metadata": map[string]interface{}{ - "name": certIssuer, - "namespace": ns, + "name": certIssuer, + "namespace": ns, + "labels": labels, + "annotations": ann, }, "spec": map[string]interface{}{ "selfSigned": map[string]interface{}{}, @@ -426,13 +442,17 @@ func certificate(ns string, group string, apiVersion string) *unstructured.Unstr name := viper.GetString(operator.FlagWebhookService) certIssuer := viper.GetString(operator.FlagCertIssuer) dns := operator.GetWebhookServiceDNSNames() + labels := operator.GetGlobalLabelsNoError() + ann := operator.GetGlobalAnnotationsNoError() return &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": apiString, "kind": "Certificate", "metadata": map[string]interface{}{ - "name": certManagerCertName, - "namespace": ns, + "name": certManagerCertName, + "namespace": ns, + "labels": labels, + "annotations": ann, }, "spec": map[string]interface{}{ "commonName": fmt.Sprintf("%s.%s.svc", name, ns), diff --git a/docs/about/04_coherence_spec.adoc b/docs/about/04_coherence_spec.adoc index 9730f8c38..27c015809 100644 --- a/docs/about/04_coherence_spec.adoc +++ b/docs/about/04_coherence_spec.adoc @@ -34,6 +34,7 @@ TIP: This document was generated from comments in the Go structs in the pkg/api/ * <> * <> * <> +* <> * <> * <> * <> @@ -300,6 +301,19 @@ m| optional | Specify whether the ConfigMap or its keys must be defined m| * <> +=== GlobalSpec + +GlobalSpec is attributes that will be applied to all resources managed by the Operator. + +[cols="1,10,1,1"options="header"] +|=== +| Field | Description | Type | Required +m| labels | Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ m| map[string]string | false +m| annotations | Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ m| map[string]string | false +|=== + +<
> + === ImageSpec ImageSpec defines the settings for a Docker image @@ -906,6 +920,7 @@ m| haBeforeUpdate | Whether to perform a StatusHA test on the cluster before per m| allowUnsafeDelete | AllowUnsafeDelete controls whether the Operator will add a finalizer to the Coherence resource so that it can intercept deletion of the resource and initiate a controlled shutdown of the Coherence cluster. The default value is `false`. The primary use for setting this flag to `true` is in CI/CD environments so that cleanup jobs can delete a whole namespace without requiring the Operator to have removed finalizers from any Coherence resources deployed into that namespace. It is not recommended to set this flag to `true` in a production environment, especially when using Coherence persistence features. m| *bool | false m| actions | Actions to execute once all the Pods are ready after an initial deployment m| []<> | false m| envFrom | List of sources to populate environment variables in the container. The keys defined within a source must be a C_IDENTIFIER. All invalid keys will be reported as an event when the container is starting. When a key exists in multiple sources, the value associated with the last source will take precedence. Values defined by an Env with a duplicate key will take precedence. Cannot be updated. m| []https://{k8s-doc-link}/#envfromsource-v1-core[corev1.EnvFromSource] | false +m| global | Global contains attributes that will be applied to all resources managed by the Coherence Operator. m| *<> | false |=== <
> diff --git a/docs/coherence/030_cache_config.adoc b/docs/coherence/030_cache_config.adoc index 19b756bbc..f5fbd1df5 100644 --- a/docs/coherence/030_cache_config.adoc +++ b/docs/coherence/030_cache_config.adoc @@ -1,6 +1,6 @@ /////////////////////////////////////////////////////////////////////////////// - Copyright (c) 2020, Oracle and/or its affiliates. + 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. @@ -17,7 +17,7 @@ property will be set in the Coherence JVM. When the `spec.coherence.cacheConfig` is blank or not specified, Coherence use its default behaviour to find the cache configuration file to use. Typically, this is to use the first occurrence of `coherence-cache-config.xml` that is found on the classpath -(consult the https://docs.oracle.com/en/middleware/standalone/coherence/14.1.1.0/develop-applications/understanding-configuration.html#GUID-360B798E-2120-44A9-8B09-1FDD9AB40EB5[Coherence documentation] +(consult the https://{commercial-docs-base-url}/develop-applications/understanding-configuration.html#GUID-360B798E-2120-44A9-8B09-1FDD9AB40EB5[Coherence documentation] for an explanation of the default behaviour). To set a specific cache configuration file to use set the `spec.coherence.cacheConfig` field, for example: diff --git a/docs/coherence/040_override_file.adoc b/docs/coherence/040_override_file.adoc index 1f5d3d5e7..7b5182d6b 100644 --- a/docs/coherence/040_override_file.adoc +++ b/docs/coherence/040_override_file.adoc @@ -1,6 +1,6 @@ /////////////////////////////////////////////////////////////////////////////// - Copyright (c) 2020, Oracle and/or its affiliates. + 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. @@ -17,7 +17,7 @@ By setting this field the `coherence.override` system property will be set in th When the `spec.coherence.overrideConfig` is blank or not specified, Coherence use its default behaviour to find the operational configuration file to use. Typically, this is to use the first occurrence of `tangosol-coherence-override.xml` that is found on the classpath -(consult the https://docs.oracle.com/en/middleware/standalone/coherence/14.1.1.0/develop-applications/understanding-configuration.html#GUID-360B798E-2120-44A9-8B09-1FDD9AB40EB5[Coherence documentation] +(consult the https://{commercial-docs-base-url}/develop-applications/understanding-configuration.html#GUID-360B798E-2120-44A9-8B09-1FDD9AB40EB5[Coherence documentation] for an explanation of the default behaviour). To set a specific operational configuration file to use set the `spec.coherence.overrideConfig` field, for example: diff --git a/docs/coherence/070_wka.adoc b/docs/coherence/070_wka.adoc index 5f8f7c287..b9efdd8e7 100644 --- a/docs/coherence/070_wka.adoc +++ b/docs/coherence/070_wka.adoc @@ -1,6 +1,6 @@ /////////////////////////////////////////////////////////////////////////////// - Copyright (c) 2020, Oracle and/or its affiliates. + 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. @@ -14,7 +14,7 @@ A Coherence cluster is made up of one or more JVMs. In order for these JVMs to f discover other cluster members. The default mechanism for discovery is multicast broadcast but this does not work in most container environments. Coherence provides an alternative mechanism where the addresses of the hosts where the members of the cluster will run is provided in the form of a -https://docs.oracle.com/en/middleware/standalone/coherence/14.1.1.0/develop-applications/setting-cluster.html#GUID-E8CC7C9A-5739-4D12-B88E-A3575F20D63B["well known address" (or WKA) list]. +https://{commercial-docs-base-url}/develop-applications/setting-cluster.html#GUID-E8CC7C9A-5739-4D12-B88E-A3575F20D63B["well known address" (or WKA) list]. This address list is then used by Coherence when it starts in a JVM to discover other cluster members running on the hosts in the WKA list. diff --git a/docs/coherence/080_persistence.adoc b/docs/coherence/080_persistence.adoc index 74266c751..e56c1870a 100644 --- a/docs/coherence/080_persistence.adoc +++ b/docs/coherence/080_persistence.adoc @@ -1,6 +1,6 @@ /////////////////////////////////////////////////////////////////////////////// - Copyright (c) 2020, 2023, Oracle and/or its affiliates. + 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. @@ -17,7 +17,7 @@ as required. The `Coherence` CRD allows the default persistence mode, and the storage location of persistence data to be configured. Persistence can be configured in the `spec.coherence.persistence` section of the CRD. -See the https://docs.oracle.com/en/middleware/standalone/coherence/14.1.1.0/administer/persisting-caches.html#GUID-3DC46E44-21E4-4DC4-9D12-231DE57FE7A1[Coherence Persistence] +See the https://{commercial-docs-base-url}/administer/persisting-caches.html#GUID-3DC46E44-21E4-4DC4-9D12-231DE57FE7A1[Coherence Persistence] documentation for more details of how persistence works and its configuration. == Persistence Mode diff --git a/docs/logging/020_logging.adoc b/docs/logging/020_logging.adoc index bc380dfb1..f8d886be6 100644 --- a/docs/logging/020_logging.adoc +++ b/docs/logging/020_logging.adoc @@ -1,6 +1,6 @@ /////////////////////////////////////////////////////////////////////////////// - Copyright (c) 2020, Oracle and/or its affiliates. + 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. @@ -32,7 +32,7 @@ to write to log files. One way to do this is to add a Java Util Logging configur Coherence to use the JDK logger. In the `jvm.args` section of the `Coherence` CRD the system properties should be added to set the configuration file used by Java util logging and to configure Coherence logging. -See the Coherence https://docs.oracle.com/en/middleware/standalone/coherence/14.1.1.0/develop-applications/operational-configuration-elements.html[Logging Config] +See the Coherence https://{commercial-docs-base-url}/develop-applications/operational-configuration-elements.html[Logging Config] documentation for more details. There are alternative ways to configure the Java util logger besides using a configuration file, just as there are diff --git a/docs/management/020_management_over_rest.adoc b/docs/management/020_management_over_rest.adoc index 5157f85a6..29a17f900 100644 --- a/docs/management/020_management_over_rest.adoc +++ b/docs/management/020_management_over_rest.adoc @@ -1,6 +1,6 @@ /////////////////////////////////////////////////////////////////////////////// - Copyright (c) 2020, Oracle and/or its affiliates. + 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. @@ -21,7 +21,7 @@ Once the Management port has been exposed, for example via a load balancer or po endpoint is available at `http://host:port/management/coherence/cluster`. The Swagger JSON document for the API is available at `http://host:port/management/coherence/cluster/metadata-catalog`. -See the https://docs.oracle.com/en/middleware/standalone/coherence/14.1.1.0/rest-reference/[REST API for Managing Oracle Coherence] +See the https://{commercial-docs-base-url}/rest-reference/[REST API for Managing Oracle Coherence] documentation for full details on each of the endpoints. NOTE: Note: Use of Management over REST is available only when using the operator with clusters running @@ -223,9 +223,9 @@ Management over REST can be used for all Coherence management functions, the sam standard MBean access over JMX. Please see the -https://docs.oracle.com/en/middleware/standalone/coherence/14.1.1.0/rest-reference/[Coherence REST API] for more information on these features. +https://{commercial-docs-base-url}/rest-reference/[Coherence REST API] for more information on these features. -* https://docs.oracle.com/en/middleware/standalone/coherence/14.1.1.0/manage/using-jmx-manage-oracle-coherence.html#GUID-D160B16B-7C1B-4641-AE94-3310DF8082EC[Connecting JVisualVM to Management over REST] +* https://{commercial-docs-base-url}/manage/using-jmx-manage-oracle-coherence.html#GUID-D160B16B-7C1B-4641-AE94-3310DF8082EC[Connecting JVisualVM to Management over REST] * <> -* https://docs.oracle.com/en/middleware/standalone/coherence/14.1.1.0/rest-reference/op-management-coherence-cluster-members-memberidentifier-diagnostic-cmd-jfrcmd-post.html[Produce and extract a Java Flight Recorder (JFR) file] -* https://docs.oracle.com/en/middleware/standalone/coherence/14.1.1.0/rest-reference/api-reporter.html[Access the Reporter] +* https://{commercial-docs-base-url}/rest-reference/op-management-coherence-cluster-members-memberidentifier-diagnostic-cmd-jfrcmd-post.html[Produce and extract a Java Flight Recorder (JFR) file] +* https://{commercial-docs-base-url}/rest-reference/api-reporter.html[Access the Reporter] diff --git a/docs/management/100_tmb_test.adoc b/docs/management/100_tmb_test.adoc index c0c7dfdbf..97503b547 100644 --- a/docs/management/100_tmb_test.adoc +++ b/docs/management/100_tmb_test.adoc @@ -1,6 +1,6 @@ /////////////////////////////////////////////////////////////////////////////// - Copyright (c) 2020, 2022, Oracle and/or its affiliates. + 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. @@ -12,7 +12,7 @@ Coherence provides utilities that can be used to test network performance, which obviously has a big impact on a distributed system such as Coherence. The documentation for these utilities can be found in the official -https://docs.oracle.com/en/middleware/standalone/coherence/14.1.1.0/administer/performing-network-performance-test.html#GUID-7267AB06-6353-416E-B9FD-A75F7FBFE523[Coherence Documentation]. +https://{commercial-docs-base-url}/administer/performing-network-performance-test.html#GUID-7267AB06-6353-416E-B9FD-A75F7FBFE523[Coherence Documentation]. Whilst generally these tests would be run on server hardware, with more and more Coherence deployments moving into the cloud and into Kubernetes these tests can also be performed in `Pods` to measure inter-Pod network performance. @@ -69,7 +69,7 @@ spec: ---- <1> This example uses a Coherence CE image, but any image with `coherence.jar` in it could be used. <2> The command line that the container will execute is exactly the same as that for the listener process in the -https://docs.oracle.com/en/middleware/standalone/coherence/14.1.1.0/administer/performing-network-performance-test.html#GUID-7267AB06-6353-416E-B9FD-A75F7FBFE523[Coherence Documentation]. +https://{commercial-docs-base-url}/administer/performing-network-performance-test.html#GUID-7267AB06-6353-416E-B9FD-A75F7FBFE523[Coherence Documentation]. Start the listener `Pod`: [source,bash] @@ -113,7 +113,7 @@ spec: - tmb://message-bus-listener:8000 # <2> ---- <1> Again, the command line is the same as that for the sender process in the -https://docs.oracle.com/en/middleware/standalone/coherence/14.1.1.0/administer/performing-network-performance-test.html#GUID-7267AB06-6353-416E-B9FD-A75F7FBFE523[Coherence Documentation]. +https://{commercial-docs-base-url}/administer/performing-network-performance-test.html#GUID-7267AB06-6353-416E-B9FD-A75F7FBFE523[Coherence Documentation]. <2> The `peer` address uses the `Service` name `message-bus-listener` from the sender `yaml`. Start the sender `Pod`: diff --git a/docs/metrics/020_metrics.adoc b/docs/metrics/020_metrics.adoc index 076f7bdb5..35afb46ca 100644 --- a/docs/metrics/020_metrics.adoc +++ b/docs/metrics/020_metrics.adoc @@ -1,6 +1,6 @@ /////////////////////////////////////////////////////////////////////////////// - Copyright (c) 2020, 2022, Oracle and/or its affiliates. + 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. @@ -25,7 +25,7 @@ The example below shows how to enable and access Coherence metrics. Once the metrics port has been exposed, for example via a load balancer or port-forward command, the metrics endpoint is available at `http://host:port/metrics`. -See the https://docs.oracle.com/en/middleware/standalone/coherence/14.1.1.0/manage/using-coherence-metrics.html[Using Coherence Metrics] +See the https://{commercial-docs-base-url}/manage/using-coherence-metrics.html[Using Coherence Metrics] documentation for full details on the available metrics. === Deploy Coherence with Metrics Enabled diff --git a/examples/090_tls/README.adoc b/examples/090_tls/README.adoc index be4052561..f89f207b0 100644 --- a/examples/090_tls/README.adoc +++ b/examples/090_tls/README.adoc @@ -1,6 +1,6 @@ /////////////////////////////////////////////////////////////////////////////// - Copyright (c) 2021, 2022, Oracle and/or its affiliates. + Copyright (c) 2021, 2024, Oracle and/or its affiliates. Licensed under the Universal Permissive License v 1.0 as shown at http://oss.oracle.com/licenses/upl. @@ -407,7 +407,7 @@ These images can run secure or insecure depending on various system properties p When configuring Coherence to use TLS, we need to configure a socket provider that Coherence can use to create secure socket. We then tell Coherence to use this provider in various places, such as Extend connections, cluster member TCMP connections etc. This configuration is typically done by adding the provider configuration to the Coherence operational configuration override file. -The Coherence documentation has a lot of details on configuring socket providers in the section on https://docs.oracle.com/en/middleware/standalone/coherence/14.1.1.0/secure/using-ssl-secure-communication.html#GUID-21CBAF48-BA78-4373-AC90-BF668CF31776[Using SSL Secure Communication] +The Coherence documentation has a lot of details on configuring socket providers in the section on https://{commercial-docs-base-url}/secure/using-ssl-secure-communication.html#GUID-21CBAF48-BA78-4373-AC90-BF668CF31776[Using SSL Secure Communication] Below is an example that we will use on the server cluster members [source,xml] @@ -503,7 +503,7 @@ The configuration above is included in both of the example images that we built [#tcmp] == Secure Cluster Membership -Now we have a "tls" socket provider we can use it to secure Coherence. The Coherence documentation has a section on https://docs.oracle.com/en/middleware/standalone/coherence/14.1.1.0/secure/using-ssl-secure-communication.html#GUID-21CBAF48-BA78-4373-AC90-BF668CF31776[Securing Coherence TCMP with TLS]. +Now we have a "tls" socket provider we can use it to secure Coherence. The Coherence documentation has a section on https://{commercial-docs-base-url}/secure/using-ssl-secure-communication.html#GUID-21CBAF48-BA78-4373-AC90-BF668CF31776[Securing Coherence TCMP with TLS]. Securing communication between cluster members is very simple, we just set the `coherence.socketprovider` system property to the name of the socket provider we want to use. In our case this will be the "tls" provider we configured above, so we would use `-Dcoherence.socketprovider=tls` The yaml below is a `Coherence` resource that will cause the Operator to create a three member Coherence cluster. @@ -724,7 +724,7 @@ kubectl -n coherence-test delete -f manifests/coherence-cluster.yaml [#extend] === Secure Extend Connections -A common connection type to secure are client connections into the cluster from Coherence Extend clients. The Coherence documentation contains details on https://docs.oracle.com/en/middleware/standalone/coherence/14.1.1.0/secure/using-ssl-secure-communication.html#GUID-0F636928-8731-4228-909C-8B8AB09613DB[Using SSL to Secure Extend Client Communication] for more in-depth details. +A common connection type to secure are client connections into the cluster from Coherence Extend clients. The Coherence documentation contains details on https://{commercial-docs-base-url}/secure/using-ssl-secure-communication.html#GUID-0F636928-8731-4228-909C-8B8AB09613DB[Using SSL to Secure Extend Client Communication] for more in-depth details. As with securing TCMP, we can specify a socket provider in the Extend proxy configuration in the server's cache configuration file and also in the remote scheme in the client's cache configuration. In this example we will use exactly the same TLS socket provider configuration that we created above. The only difference being the name of the `PasswordProvider` class used by the client. At the time of writing this, Coherence does not include an implementation of `PasswordProvider` that reads from a file. The Coherence Operator injects one into the classpath of the server, but our simple client is not managed by the Operator. We have added a simple `FileBasedPasswordProvider` class to the client code in this example. diff --git a/examples/200_autoscaler/README.adoc b/examples/200_autoscaler/README.adoc index 93d5f1ab0..eec1b54e4 100644 --- a/examples/200_autoscaler/README.adoc +++ b/examples/200_autoscaler/README.adoc @@ -1,3 +1,10 @@ +/////////////////////////////////////////////////////////////////////////////// + + Copyright (c) 2021, 2024, Oracle and/or its affiliates. + Licensed under the Universal Permissive License v 1.0 as shown at + http://oss.oracle.com/licenses/upl. + +/////////////////////////////////////////////////////////////////////////////// = Autoscaling Coherence Clusters == Kubernetes Horizontal Pod autoscaler Example @@ -62,9 +69,9 @@ The Java code in this example contains a simple MBean class `HeapUsage` and corr that obtain heap use metrics in the way detailed above. There is also a configuration file `custom-mbeans.xml` that Coherence will use to automatically add the custom MBean to Coherence management and metrics. There is Coherence documentation on -https://docs.oracle.com/en/middleware/standalone/coherence/14.1.1.0/manage/using-coherence-metrics.html#GUID-CFC31D23-06B8-49AF-8996-ADBA806E0DD9[how to add custom metrics] +https://{commercial-docs-base-url}/manage/using-coherence-metrics.html#GUID-CFC31D23-06B8-49AF-8996-ADBA806E0DD9[how to add custom metrics] and -https://docs.oracle.com/en/middleware/standalone/coherence/14.1.1.0/manage/registering-custom-mbeans.html#GUID-1EE749C5-BC0D-4353-B5FE-1C5DCDEAE48C[how to register custom MBeans]. +https://{commercial-docs-base-url}/manage/registering-custom-mbeans.html#GUID-1EE749C5-BC0D-4353-B5FE-1C5DCDEAE48C[how to register custom MBeans]. The custom heap use MBean will be added with an ObjectName of `Coherence:type=HeapUsage,nodeId=1` where `nodeId` will change to match the Coherence member id for the specific JVM. There will be one heap usage MBean for each cluster member. diff --git a/examples/no-operator/03_extend_tls/README.adoc b/examples/no-operator/03_extend_tls/README.adoc index 0f6e2de04..b4a96ed29 100644 --- a/examples/no-operator/03_extend_tls/README.adoc +++ b/examples/no-operator/03_extend_tls/README.adoc @@ -1,6 +1,6 @@ /////////////////////////////////////////////////////////////////////////////// - Copyright (c) 2021, 2022, Oracle and/or its affiliates. + Copyright (c) 2021, 2024, Oracle and/or its affiliates. Licensed under the Universal Permissive License v 1.0 as shown at http://oss.oracle.com/licenses/upl. @@ -42,7 +42,7 @@ We will use these files to securely provide the passwords to the client and serv == Configure Coherence Extend TLS The Coherence documentation explains how to -https://docs.oracle.com/en/middleware/standalone/coherence/14.1.1.0/secure/using-ssl-secure-communication.html#GUID-90E20139-3945-4993-9048-7FBC93B243A3[Use TLS Secure Communication]. +https://{commercial-docs-base-url}/secure/using-ssl-secure-communication.html#GUID-90E20139-3945-4993-9048-7FBC93B243A3[Use TLS Secure Communication]. This example is going to use a standard approach to securing Extend with TLS. To provide the keystores and credentials the example will make use of Kubernetes `Secrets` to mount those files as `Volumes` in the `StatefulSet`. This is much more flexible and secure than baking them into an application's code or image. === Configure the Extend Proxy diff --git a/helm-charts/coherence-operator/templates/deployment.yaml b/helm-charts/coherence-operator/templates/deployment.yaml index 7bb025e74..b7c78677c 100644 --- a/helm-charts/coherence-operator/templates/deployment.yaml +++ b/helm-charts/coherence-operator/templates/deployment.yaml @@ -117,6 +117,16 @@ spec: {{- if (eq .Values.webhooks false) }} - --enable-webhook=false {{- end }} +{{- if (.Values.globalLabels) }} +{{- range $k, $v := .Values.globalLabels }} + - --global-label={{ $k }}={{ $v }} +{{- end }} +{{- end }} +{{- if (.Values.globalAnnotations) }} +{{- range $k, $v := .Values.globalAnnotations }} + - --global-annotation={{ $k }}={{ $v }} +{{- end }} +{{- end }} {{- end }} command: - "/files/runner" diff --git a/helm-charts/coherence-operator/values.yaml b/helm-charts/coherence-operator/values.yaml index 1126bf86c..783fada00 100644 --- a/helm-charts/coherence-operator/values.yaml +++ b/helm-charts/coherence-operator/values.yaml @@ -60,6 +60,14 @@ deploymentLabels: # Additional annotations that are added to the Operator Deployment. deploymentAnnotations: +# --------------------------------------------------------------------------- +# Additional labels that are added to all te resources managed by the Operator Deployment. +globalLabels: + +# --------------------------------------------------------------------------- +# Additional annotations that are added to all te resources managed by the Operator Deployment. +globalAnnotations: + # --------------------------------------------------------------------------- # Operator Pod securityContext # This sets the securityContext configuration for the Pod, for example diff --git a/pkg/operator/operator.go b/pkg/operator/operator.go index e25356f40..3f90e91a3 100644 --- a/pkg/operator/operator.go +++ b/pkg/operator/operator.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023, Oracle and/or its affiliates. + * 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. */ @@ -52,9 +52,12 @@ const ( FlagCertType = "cert-type" FlagCertIssuer = "cert-issuer" FlagCoherenceImage = "coherence-image" - FlagDevMode = "coherence-dev-mode" FlagCRD = "install-crd" + FlagDevMode = "coherence-dev-mode" + FlagDryRun = "dry-run" FlagEnableWebhook = "enable-webhook" + FlagGlobalAnnotation = "global-annotation" + FlagGlobalLabel = "global-label" FlagHealthAddress = "health-addr" FlagLeaderElection = "enable-leader-election" FlagMetricsAddress = "metrics-addr" @@ -89,13 +92,15 @@ const ( var setupLog = ctrl.Log.WithName("setup") +var currentViper *viper.Viper + var ( operatorVersion = "999.0.0" DefaultSiteLabels = []string{corev1.LabelTopologyZone, corev1.LabelFailureDomainBetaZone} DefaultRackLabels = []string{LabelOciNodeFaultDomain, corev1.LabelTopologyZone, corev1.LabelFailureDomainBetaZone} ) -func SetupOperatorManagerFlags(cmd *cobra.Command) { +func SetupOperatorManagerFlags(cmd *cobra.Command, v *viper.Viper) { flags := cmd.Flags() flags.String(FlagMetricsAddress, ":8080", "The address the metric endpoint binds to.") flags.String(FlagHealthAddress, ":8088", "The address the health endpoint binds to.") @@ -103,34 +108,22 @@ func SetupOperatorManagerFlags(cmd *cobra.Command) { "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") - SetupFlags(cmd) + SetupFlags(cmd, v) // Add flags registered by imported packages (e.g. glog and controller-runtime) flagSet := pflag.NewFlagSet("operator", pflag.ContinueOnError) flagSet.AddGoFlagSet(flag.CommandLine) - if err := viper.BindPFlags(flagSet); err != nil { - setupLog.Error(err, "binding flags") - os.Exit(1) - } - - // Validate the command line flags and environment variables - if err := ValidateFlags(); err != nil { - fmt.Println(err.Error()) - _ = cmd.Help() - os.Exit(1) - } - } -func SetupFlags(cmd *cobra.Command) { +func SetupFlags(cmd *cobra.Command, v *viper.Viper) { f, err := data.Assets.Open("assets/config.json") if err != nil { setupLog.Error(err, "finding config.json asset") os.Exit(1) } - viper.SetConfigType("json") - if err := viper.ReadConfig(f); err != nil { + v.SetConfigType("json") + if err := v.ReadConfig(f); err != nil { setupLog.Error(err, "reading configuration file") os.Exit(1) } @@ -250,55 +243,85 @@ func SetupFlags(cmd *cobra.Command) { "webhook-service", "The K8s service used for the webhook", ) + cmd.Flags().StringArray( + FlagGlobalAnnotation, + nil, + "An annotation to apply to all resources managed by the Operator (can be used multiple times)") + cmd.Flags().StringArray( + FlagGlobalLabel, + nil, + "A label to apply to all resources managed by the Operator (can be used multiple times)") // enable using dashed notation in flags and underscores in env - viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) + v.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) - if err := viper.BindPFlags(cmd.Flags()); err != nil { + if err := v.BindPFlags(cmd.Flags()); err != nil { setupLog.Error(err, "binding flags") os.Exit(1) } - viper.AutomaticEnv() + v.AutomaticEnv() } -func ValidateFlags() error { - certValidity := viper.GetDuration(FlagCACertValidity) - certRotateBefore := viper.GetDuration(FlagCACertRotateBefore) +func ValidateFlags(v *viper.Viper) error { + var err error + certValidity := v.GetDuration(FlagCACertValidity) + certRotateBefore := v.GetDuration(FlagCACertRotateBefore) if certRotateBefore > certValidity { return fmt.Errorf("%s must be larger than %s", FlagCACertValidity, FlagCACertRotateBefore) } - certType := viper.GetString(FlagCertType) + certType := v.GetString(FlagCertType) if certType != CertTypeSelfSigned && certType != CertTypeCertManager && certType != CertTypeManual { return fmt.Errorf("%s parameter is invalid", FlagCertType) } - return nil + _, err = GetGlobalAnnotations(v) + if err != nil { + return err + } + + _, err = GetGlobalLabels(v) + if err != nil { + return err + } + + return err +} + +func SetViper(v *viper.Viper) { + currentViper = v +} + +func GetViper() *viper.Viper { + if currentViper == nil { + return viper.GetViper() + } + return currentViper } func IsDevMode() bool { - return viper.GetBool(FlagDevMode) + return GetViper().GetBool(FlagDevMode) } func GetDefaultCoherenceImage() string { - return viper.GetString(FlagCoherenceImage) + return GetViper().GetString(FlagCoherenceImage) } func GetDefaultOperatorImage() string { - return viper.GetString(FlagOperatorImage) + return GetViper().GetString(FlagOperatorImage) } func GetRestHost() string { - return viper.GetString(FlagRestHost) + return GetViper().GetString(FlagRestHost) } func GetRestPort() int32 { - return viper.GetInt32(FlagRestPort) + return GetViper().GetInt32(FlagRestPort) } func GetRestServiceName() string { - s := viper.GetString(FlagServiceName) + s := GetViper().GetString(FlagServiceName) if s != "" { ns := GetNamespace() return s + "." + ns + ".svc" @@ -307,47 +330,51 @@ func GetRestServiceName() string { } func GetRestServicePort() int32 { - return viper.GetInt32(FlagServicePort) + return GetViper().GetInt32(FlagServicePort) } func GetSiteLabel() []string { - return viper.GetStringSlice(FlagSiteLabel) + return GetViper().GetStringSlice(FlagSiteLabel) } func GetRackLabel() []string { - return viper.GetStringSlice(FlagRackLabel) + return GetViper().GetStringSlice(FlagRackLabel) } func ShouldInstallCRDs() bool { - return viper.GetBool(FlagCRD) + return GetViper().GetBool(FlagCRD) && !IsDryRun() } func ShouldEnableWebhooks() bool { - return viper.GetBool(FlagEnableWebhook) + return GetViper().GetBool(FlagEnableWebhook) && !IsDryRun() +} + +func IsDryRun() bool { + return GetViper().GetBool(FlagDryRun) } func ShouldUseSelfSignedCerts() bool { - return viper.GetString(FlagCertType) == CertTypeSelfSigned + return GetViper().GetString(FlagCertType) == CertTypeSelfSigned } func ShouldUseCertManager() bool { - return viper.GetString(FlagCertType) == CertTypeCertManager + return GetViper().GetString(FlagCertType) == CertTypeCertManager } func GetNamespace() string { - return viper.GetString(FlagOperatorNamespace) + return GetViper().GetString(FlagOperatorNamespace) } func GetWebhookCertDir() string { - return viper.GetString(FlagWebhookCertDir) + return GetViper().GetString(FlagWebhookCertDir) } func GetCACertRotateBefore() time.Duration { - return viper.GetDuration(FlagCACertRotateBefore) + return GetViper().GetDuration(FlagCACertRotateBefore) } func GetWebhookServiceDNSNames() []string { var dns []string - s := viper.GetString(FlagWebhookService) + s := GetViper().GetString(FlagWebhookService) if IsDevMode() { dns = []string{s} } else { @@ -401,3 +428,45 @@ func GetWatchNamespace() []string { } return watches } + +func GetGlobalAnnotationsNoError() map[string]string { + m, _ := GetGlobalAnnotations(GetViper()) + return m +} + +func GetGlobalAnnotations(v *viper.Viper) (map[string]string, error) { + args := v.GetStringSlice(FlagGlobalAnnotation) + return stringSliceToMap(args, FlagGlobalAnnotation) +} + +func GetGlobalLabelsNoError() map[string]string { + m, _ := GetGlobalLabels(GetViper()) + return m +} + +func GetGlobalLabels(v *viper.Viper) (map[string]string, error) { + args := v.GetStringSlice(FlagGlobalLabel) + return stringSliceToMap(args, FlagGlobalLabel) +} + +func stringSliceToMap(args []string, flag string) (map[string]string, error) { + var m map[string]string + if args != nil { + m = make(map[string]string) + for _, arg := range args { + kv := strings.SplitN(arg, "=", 2) + if len(kv) <= 1 { + return nil, fmt.Errorf("invalid argument --%s=%s - must be in the format --%s=key=value", + flag, arg, flag) + } + key := strings.TrimSpace(kv[0]) + value := strings.TrimSpace(kv[1]) + if key == "" || value == "" { + return nil, fmt.Errorf("invalid argument --%s=%s - must be in the format --%s=\"key=value\" where the key and value cannot be blank", + flag, arg, flag) + } + m[key] = value + } + } + return m, nil +} diff --git a/pkg/runner/cmd_operator.go b/pkg/runner/cmd_operator.go index 476d7e3a8..6d695a4e2 100644 --- a/pkg/runner/cmd_operator.go +++ b/pkg/runner/cmd_operator.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023, Oracle and/or its affiliates. + * 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. */ @@ -57,7 +57,7 @@ func init() { } // operatorCommand runs the Coherence Operator manager -func operatorCommand() *cobra.Command { +func operatorCommand(v *viper.Viper) *cobra.Command { cmd := &cobra.Command{ Use: CommandOperator, Short: "Run the Coherence Operator", @@ -67,7 +67,7 @@ func operatorCommand() *cobra.Command { }, } - operator.SetupOperatorManagerFlags(cmd) + operator.SetupOperatorManagerFlags(cmd, v) return cmd } @@ -75,8 +75,8 @@ func operatorCommand() *cobra.Command { func execute() error { ctrl.SetLogger(zap.New(zap.UseDevMode(true))) - setupLog.Info(fmt.Sprintf("Operator Coherence Image: %s", viper.GetString(operator.FlagCoherenceImage))) - setupLog.Info(fmt.Sprintf("Operator Image: %s", viper.GetString(operator.FlagOperatorImage))) + setupLog.Info(fmt.Sprintf("Operator Coherence Image: %s", operator.GetDefaultCoherenceImage())) + setupLog.Info(fmt.Sprintf("Operator Image: %s", operator.GetDefaultOperatorImage())) cfg := ctrl.GetConfigOrDie() cs, err := clients.NewForConfig(cfg) @@ -160,59 +160,62 @@ func execute() error { return errors.Wrap(err, "unable to create CoherenceJob controller") } - // We intercept the signal handler here so that we can do clean-up before the Manager stops - handler := ctrl.SetupSignalHandler() + dryRun := operator.IsDryRun() + if !dryRun { + // We intercept the signal handler here so that we can do clean-up before the Manager stops + handler := ctrl.SetupSignalHandler() - // Set-up webhooks if required - var cr *webhook.CertReconciler - if operator.ShouldEnableWebhooks() { - // Set up the webhook certificate reconciler - cr = &webhook.CertReconciler{ - Clientset: cs, - } - if err := cr.SetupWithManager(handler, mgr); err != nil { - return errors.Wrap(err, " unable to create webhook certificate controller") + // Set-up webhooks if required + var cr *webhook.CertReconciler + if operator.ShouldEnableWebhooks() { + // Set up the webhook certificate reconciler + cr = &webhook.CertReconciler{ + Clientset: cs, + } + if err := cr.SetupWithManager(handler, mgr); err != nil { + return errors.Wrap(err, " unable to create webhook certificate controller") + } + + // Set up the webhooks + if err = (&coh.Coherence{}).SetupWebhookWithManager(mgr); err != nil { + return errors.Wrap(err, " unable to create webhook") + } + } else { + setupLog.Info("Operator is running with web-hooks disabled") } - // Set up the webhooks - if err = (&coh.Coherence{}).SetupWebhookWithManager(mgr); err != nil { - return errors.Wrap(err, " unable to create webhook") + // Create the REST server + restServer := rest.NewServer(cs) + if err := restServer.SetupWithManager(mgr); err != nil { + return errors.Wrap(err, " unable to start REST server") } - } else { - setupLog.Info("Operator is running with web-hooks disabled") - } - // Create the REST server - restServer := rest.NewServer(cs) - if err := restServer.SetupWithManager(mgr); err != nil { - return errors.Wrap(err, " unable to start REST server") - } + var health healthz.Checker = func(_ *http.Request) error { + <-restServer.Running() + return nil + } - var health healthz.Checker = func(_ *http.Request) error { - <-restServer.Running() - return nil - } + if err := mgr.AddHealthzCheck("health", health); err != nil { + return errors.Wrap(err, "unable to set up health check") + } + if err := mgr.AddReadyzCheck("ready", health); err != nil { + return errors.Wrap(err, "unable to set up ready check") + } - if err := mgr.AddHealthzCheck("health", health); err != nil { - return errors.Wrap(err, "unable to set up health check") - } - if err := mgr.AddReadyzCheck("ready", health); err != nil { - return errors.Wrap(err, "unable to set up ready check") - } + // +kubebuilder:scaffold:builder - // +kubebuilder:scaffold:builder + go func() { + <-handler.Done() + if cr != nil { + cr.Cleanup() + } + }() - go func() { - <-handler.Done() - if cr != nil { - cr.Cleanup() + setupLog.Info("starting manager") + if err := mgr.Start(handler); err != nil { + setupLog.Error(err, "problem running manager") + os.Exit(1) } - }() - - setupLog.Info("starting manager") - if err := mgr.Start(handler); err != nil { - setupLog.Error(err, "problem running manager") - os.Exit(1) } return nil diff --git a/pkg/runner/cmd_operator_test.go b/pkg/runner/cmd_operator_test.go new file mode 100644 index 000000000..1b55d0808 --- /dev/null +++ b/pkg/runner/cmd_operator_test.go @@ -0,0 +1,130 @@ +/* + * 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 runner + +import ( + . "github.com/onsi/gomega" + coh "github.com/oracle/coherence-operator/api/v1" + "github.com/oracle/coherence-operator/pkg/operator" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "testing" +) + +func TestBasicOperator(t *testing.T) { + g := NewGomegaWithT(t) + + d := &coh.Coherence{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + } + + args := []string{"operator", "--dry-run"} + env := EnvVarsFromDeployment(d) + + e, err := ExecuteWithArgs(env, args) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(e).NotTo(BeNil()) + + l := operator.GetGlobalLabelsNoError() + g.Expect(l).NotTo(BeNil()) + g.Expect(len(l)).To(Equal(0)) + + a := operator.GetGlobalAnnotationsNoError() + g.Expect(a).NotTo(BeNil()) + g.Expect(len(a)).To(Equal(0)) +} + +func TestOperatorWithSingleGlobalLabel(t *testing.T) { + g := NewGomegaWithT(t) + + d := &coh.Coherence{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + } + + args := []string{"operator", "--dry-run", "--global-label", "one=value-one"} + env := EnvVarsFromDeployment(d) + + e, err := ExecuteWithArgs(env, args) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(e).NotTo(BeNil()) + + l := operator.GetGlobalLabelsNoError() + g.Expect(l).NotTo(BeNil()) + g.Expect(len(l)).To(Equal(1)) + g.Expect(l["one"]).To(Equal("value-one")) +} + +func TestOperatorWithMultipleGlobalLabels(t *testing.T) { + g := NewGomegaWithT(t) + + d := &coh.Coherence{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + } + + args := []string{"operator", "--dry-run", + "--global-label", "one=value-one", + "--global-label", "two=value-two", + "--global-label", "three=value-three", + } + env := EnvVarsFromDeployment(d) + + e, err := ExecuteWithArgs(env, args) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(e).NotTo(BeNil()) + + l := operator.GetGlobalLabelsNoError() + g.Expect(l).NotTo(BeNil()) + g.Expect(len(l)).To(Equal(3)) + g.Expect(l["one"]).To(Equal("value-one")) + g.Expect(l["two"]).To(Equal("value-two")) + g.Expect(l["three"]).To(Equal("value-three")) +} + +func TestOperatorWithSingleGlobalAnnotation(t *testing.T) { + g := NewGomegaWithT(t) + + d := &coh.Coherence{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + } + + args := []string{"operator", "--dry-run", "--global-annotation", "one=value-one"} + env := EnvVarsFromDeployment(d) + + e, err := ExecuteWithArgs(env, args) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(e).NotTo(BeNil()) + + l := operator.GetGlobalAnnotationsNoError() + g.Expect(l).NotTo(BeNil()) + g.Expect(len(l)).To(Equal(1)) + g.Expect(l["one"]).To(Equal("value-one")) +} + +func TestOperatorWithMultipleGlobalAnnotations(t *testing.T) { + g := NewGomegaWithT(t) + + d := &coh.Coherence{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + } + + args := []string{"operator", "--dry-run", + "--global-annotation", "one=value-one", + "--global-annotation", "two=value-two", + "--global-annotation", "three=value-three", + } + env := EnvVarsFromDeployment(d) + + e, err := ExecuteWithArgs(env, args) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(e).NotTo(BeNil()) + + l := operator.GetGlobalAnnotationsNoError() + g.Expect(l).NotTo(BeNil()) + g.Expect(len(l)).To(Equal(3)) + g.Expect(l["one"]).To(Equal("value-one")) + g.Expect(l["two"]).To(Equal("value-two")) + g.Expect(l["three"]).To(Equal("value-three")) +} diff --git a/pkg/runner/cmd_server.go b/pkg/runner/cmd_server.go index c0dac4617..083d3f6a5 100644 --- a/pkg/runner/cmd_server.go +++ b/pkg/runner/cmd_server.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2022, Oracle and/or its affiliates. + * 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. */ @@ -23,7 +23,7 @@ const ( // serverCommand creates the corba "server" sub-command func serverCommand() *cobra.Command { - return &cobra.Command{ + cmd := &cobra.Command{ Use: CommandServer, Short: "Start a Coherence server", Long: "Starts a Coherence server", @@ -31,6 +31,8 @@ func serverCommand() *cobra.Command { return run(cmd, server) }, } + + return cmd } // Configure the runner to run a Coherence Server diff --git a/pkg/runner/runner.go b/pkg/runner/runner.go index 7eefa8ade..0f5a81676 100644 --- a/pkg/runner/runner.go +++ b/pkg/runner/runner.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2024, Oracle and/or its affiliates. + * 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. */ @@ -13,6 +13,7 @@ import ( "fmt" "github.com/go-logr/logr" v1 "github.com/oracle/coherence-operator/api/v1" + "github.com/oracle/coherence-operator/pkg/operator" "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -75,9 +76,6 @@ var ( // log is the logger used by the runner log = ctrl.Log.WithName("runner") - - // dryRun is true if the execution is a dry-run - dryRun = false ) // contextKey allows type safe Context Values. @@ -97,6 +95,7 @@ type Execution struct { // NewRootCommand builds the root cobra command that handles our command line tool. func NewRootCommand(env map[string]string) (*cobra.Command, *viper.Viper) { v := viper.New() + operator.SetViper(v) // rootCommand is the Cobra root Command to execute rootCmd := &cobra.Command{ @@ -112,7 +111,7 @@ func NewRootCommand(env map[string]string) (*cobra.Command, *viper.Viper) { }, } - rootCmd.PersistentFlags().BoolVar(&dryRun, "dry-run", false, "Just print information about the commands that would execute") + rootCmd.PersistentFlags().Bool(operator.FlagDryRun, false, "Just print information about the commands that would execute") rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", fmt.Sprintf("config file (default is $HOME/%s.yaml)", defaultConfig)) rootCmd.PersistentFlags().Bool("viper", true, "use Viper for configuration") @@ -123,7 +122,7 @@ func NewRootCommand(env map[string]string) (*cobra.Command, *viper.Viper) { rootCmd.AddCommand(statusCommand()) rootCmd.AddCommand(readyCommand()) rootCmd.AddCommand(nodeCommand()) - rootCmd.AddCommand(operatorCommand()) + rootCmd.AddCommand(operatorCommand(v)) rootCmd.AddCommand(networkTestCommand()) rootCmd.AddCommand(jShellCommand()) rootCmd.AddCommand(sleepCommand()) @@ -151,7 +150,8 @@ func initializeConfig(cmd *cobra.Command, v *viper.Viper, env map[string]string) // if we cannot parse the config file. if err := v.ReadInConfig(); err != nil { // It's okay if there isn't a config file - if _, ok := err.(viper.ConfigFileNotFoundError); !ok { + var configFileNotFoundError viper.ConfigFileNotFoundError + if !errors.As(err, &configFileNotFoundError) { return err } } @@ -174,7 +174,8 @@ func initializeConfig(cmd *cobra.Command, v *viper.Viper, env map[string]string) // Bind the current command's flags to viper bindFlags(cmd, v) - + _ = v.BindPFlags(cmd.Parent().Flags()) + _ = v.BindPFlags(cmd.PersistentFlags()) return nil } @@ -204,6 +205,7 @@ func Execute() (Execution, error) { // ExecuteWithArgs runs the runner with a given environment and argument overrides. func ExecuteWithArgs(env map[string]string, args []string) (Execution, error) { cmd, v := NewRootCommand(env) + if len(args) > 0 { cmd.SetArgs(args) } @@ -267,6 +269,7 @@ func maybeRun(cmd *cobra.Command, fn MaybeRunFunction) error { sep = ", " } + dryRun := operator.IsDryRun() log.Info("Executing command", "dryRun", dryRun, "application", e.App, "path", e.OsCmd.Path, "args", strings.Join(e.OsCmd.Args, " "), "env", b.String()) diff --git a/pkg/utils/storage.go b/pkg/utils/storage.go index 26435ac03..83c1e9764 100644 --- a/pkg/utils/storage.go +++ b/pkg/utils/storage.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023, Oracle and/or its affiliates. + * 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. */ @@ -37,7 +37,7 @@ type Storage interface { // GetPrevious obtains the deployment resources for the version prior to the specified version GetPrevious() coh.Resources // Store will store the deployment resources, this will create a new version in the store - Store(coh.Resources, metav1.Object) error + Store(coh.Resources, coh.CoherenceResource) error // Destroy will destroy the store Destroy() // GetHash will return the hash label of the owning resource @@ -121,7 +121,7 @@ func (in *secretStore) GetPrevious() coh.Resources { return in.previous } -func (in *secretStore) Store(r coh.Resources, owner metav1.Object) error { +func (in *secretStore) Store(r coh.Resources, owner coh.CoherenceResource) error { secret, exists, err := in.getSecret() if err != nil { // an error occurred other than NotFound @@ -147,8 +147,25 @@ func (in *secretStore) Store(r coh.Resources, owner metav1.Object) error { labels = make(map[string]string) } labels[coh.LabelCoherenceHash] = owner.GetLabels()[coh.LabelCoherenceHash] + + globalLabels := owner.CreateGlobalLabels() + for k, v := range globalLabels { + labels[k] = v + } secret.SetLabels(labels) + ann := secret.GetAnnotations() + globalAnn := owner.CreateGlobalAnnotations() + if globalAnn != nil { + if ann == nil { + ann = make(map[string]string) + } + for k, v := range globalAnn { + ann[k] = v + } + } + secret.SetAnnotations(ann) + secret.Data[storeKeyLatest] = newLatest secret.Data[storeKeyPrevious] = oldLatest diff --git a/test/e2e/helper/e2e-helpers.go b/test/e2e/helper/e2e-helpers.go index f85070f0c..4ca984add 100644 --- a/test/e2e/helper/e2e-helpers.go +++ b/test/e2e/helper/e2e-helpers.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023, Oracle and/or its affiliates. + * 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. */ @@ -168,7 +168,7 @@ func NewContext(startController bool, watchNamespaces ...string) (TestContext, e } // configure viper for the flags and env-vars - operator.SetupFlags(Cmd) + operator.SetupFlags(Cmd, viper.GetViper()) flagSet := pflag.NewFlagSet("operator", pflag.ContinueOnError) flagSet.AddGoFlagSet(flag.CommandLine) if err := viper.BindPFlags(flagSet); err != nil { diff --git a/test/e2e/local/clustering_test.go b/test/e2e/local/clustering_test.go index 4c59cc18a..1c8217c72 100644 --- a/test/e2e/local/clustering_test.go +++ b/test/e2e/local/clustering_test.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023, Oracle and/or its affiliates. + * 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. */ @@ -11,6 +11,7 @@ import ( "fmt" . "github.com/onsi/gomega" coh "github.com/oracle/coherence-operator/api/v1" + "github.com/oracle/coherence-operator/pkg/utils" "github.com/oracle/coherence-operator/test/e2e/helper" appsv1 "k8s.io/api/apps/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -41,7 +42,7 @@ func TestDeploymentWithOneReplica(t *testing.T) { helper.AssertDeployments(testContext, t, "deployment-one-replica.yaml") } -// Test that a deployment works using the a yaml file containing two Coherence +// Test that a deployment works using a yaml file containing two Coherence // specs that have the same cluster name. func TestTwoDeploymentsOneCluster(t *testing.T) { // Make sure we defer clean-up when we're done!! @@ -163,3 +164,53 @@ func TestAllowUnsafeDelete(t *testing.T) { hasFinalizer := controllerutil.ContainsFinalizer(&data, coh.CoherenceFinalizer) g.Expect(hasFinalizer).To(BeFalse()) } + +// Test that a deployment works using global labels +func TestGlobalLabels(t *testing.T) { + // Make sure we defer clean-up when we're done!! + testContext.CleanupAfterTest(t) + g := NewWithT(t) + + deployments, pods := helper.AssertDeployments(testContext, t, "deployment-global-label.yaml") + + podLabels := pods[0].GetLabels() + g.Expect(podLabels["one"]).To(Equal("value-one"), "expected label \"one\" in Pod") + g.Expect(podLabels["two"]).To(Equal("value-two"), "expected label \"two\" in Pod") + + data, ok := deployments["global-label"] + g.Expect(ok).To(BeTrue(), "did not find expected 'global-label' deployment") + + storage, err := utils.NewStorage(data.GetNamespacedName(), testContext.Manager) + g.Expect(err).NotTo(HaveOccurred()) + latest := storage.GetLatest() + for _, res := range latest.Items { + l := res.Spec.GetLabels() + g.Expect(l["one"]).To(Equal("value-one"), fmt.Sprintf("expected label \"one\" in %s %s", res.Kind.Name(), res.Name)) + g.Expect(l["two"]).To(Equal("value-two"), fmt.Sprintf("expected label \"two\" in %s %s", res.Kind.Name(), res.Name)) + } +} + +// Test that a deployment works using global labels +func TestGlobalAnnotations(t *testing.T) { + // Make sure we defer clean-up when we're done!! + testContext.CleanupAfterTest(t) + g := NewWithT(t) + + deployments, pods := helper.AssertDeployments(testContext, t, "deployment-global-annotation.yaml") + + podAnnotations := pods[0].GetAnnotations() + g.Expect(podAnnotations["one"]).To(Equal("value-one"), "expected label \"one\" in Pod") + g.Expect(podAnnotations["two"]).To(Equal("value-two"), "expected label \"two\" in Pod") + + data, ok := deployments["global-annotation"] + g.Expect(ok).To(BeTrue(), "did not find expected 'global-annotation' deployment") + + storage, err := utils.NewStorage(data.GetNamespacedName(), testContext.Manager) + g.Expect(err).NotTo(HaveOccurred()) + latest := storage.GetLatest() + for _, res := range latest.Items { + l := res.Spec.GetAnnotations() + g.Expect(l["one"]).To(Equal("value-one"), fmt.Sprintf("expected label \"one\" in %s %s", res.Kind.Name(), res.Name)) + g.Expect(l["two"]).To(Equal("value-two"), fmt.Sprintf("expected label \"two\" in %s %s", res.Kind.Name(), res.Name)) + } +} diff --git a/test/e2e/local/deployment-global-annotation.yaml b/test/e2e/local/deployment-global-annotation.yaml new file mode 100644 index 000000000..d4fe44f91 --- /dev/null +++ b/test/e2e/local/deployment-global-annotation.yaml @@ -0,0 +1,10 @@ +apiVersion: coherence.oracle.com/v1 +kind: Coherence +metadata: + name: global-annotation +spec: + global: + annotations: + one: "value-one" + two: "value-two" + diff --git a/test/e2e/local/deployment-global-label.yaml b/test/e2e/local/deployment-global-label.yaml new file mode 100644 index 000000000..cec6cd5db --- /dev/null +++ b/test/e2e/local/deployment-global-label.yaml @@ -0,0 +1,10 @@ +apiVersion: coherence.oracle.com/v1 +kind: Coherence +metadata: + name: global-label +spec: + global: + labels: + one: "value-one" + two: "value-two" +