This is an automated email from the ASF dual-hosted git repository.
houston pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr-operator.git
The following commit(s) were added to refs/heads/main by this push:
new 1fbc6aa Remove unused ingress and services during reconcile (#674)
1fbc6aa is described below
commit 1fbc6aad60c1e96fe4f0387d58acefb4ac4bd519
Author: Jan Høydahl <[email protected]>
AuthorDate: Thu Jan 16 00:23:29 2025 +0100
Remove unused ingress and services during reconcile (#674)
Co-authored-by: Jan Høydahl <[email protected]>
Co-authored-by: Jason Gerlowski <[email protected]>
Co-authored-by: Houston Putman <[email protected]>
---
controllers/controller_utils_test.go | 25 +++-
controllers/solrcloud_controller.go | 99 ++++++++++++++--
controllers/solrcloud_controller_backup_test.go | 2 +
controllers/solrcloud_controller_ingress_test.go | 87 ++++++++++++++
controllers/solrcloud_controller_test.go | 2 +-
controllers/util/common.go | 12 +-
controllers/util/solr_util.go | 26 +++--
helm/solr-operator/Chart.yaml | 7 ++
tests/e2e/resource_utils_test.go | 116 +++++++------------
tests/e2e/solrcloud_ingress_test.go | 138 +++++++++++++++++++++++
10 files changed, 417 insertions(+), 97 deletions(-)
diff --git a/controllers/controller_utils_test.go
b/controllers/controller_utils_test.go
index 561ccac..6cdb14f 100644
--- a/controllers/controller_utils_test.go
+++ b/controllers/controller_utils_test.go
@@ -241,7 +241,7 @@ func expectService(ctx context.Context, parentResource
client.Object, serviceNam
func expectServiceWithChecks(ctx context.Context, parentResource
client.Object, serviceName string, selectorLables map[string]string, isHeadless
bool, additionalChecks func(Gomega, *corev1.Service), additionalOffset ...int)
*corev1.Service {
service := &corev1.Service{}
EventuallyWithOffset(resolveOffset(additionalOffset), func(g Gomega) {
- Expect(k8sClient.Get(ctx, resourceKey(parentResource,
serviceName), service)).To(Succeed(), "Expected Service does not exist")
+ g.Expect(k8sClient.Get(ctx, resourceKey(parentResource,
serviceName), service)).To(Succeed(), "Expected Service does not exist")
g.Expect(service.Spec.Selector).To(Equal(selectorLables),
"Service is not pointing to the correct Pods.")
@@ -275,7 +275,7 @@ func expectServiceWithChecks(ctx context.Context,
parentResource client.Object,
func expectServiceWithConsistentChecks(ctx context.Context, parentResource
client.Object, serviceName string, selectorLables map[string]string, isHeadless
bool, additionalChecks func(Gomega, *corev1.Service), additionalOffset ...int)
*corev1.Service {
service := &corev1.Service{}
ConsistentlyWithOffset(resolveOffset(additionalOffset), func(g Gomega) {
- Expect(k8sClient.Get(ctx, resourceKey(parentResource,
serviceName), service)).To(Succeed(), "Expected Service does not exist")
+ g.Expect(k8sClient.Get(ctx, resourceKey(parentResource,
serviceName), service)).To(Succeed(), "Expected Service does not exist")
g.Expect(service.Spec.Selector).To(Equal(selectorLables),
"Service is not pointing to the correct Pods.")
@@ -299,10 +299,23 @@ func expectNoService(ctx context.Context, parentResource
client.Object, serviceN
}).Should(MatchError("services \""+serviceName+"\" not found"),
message, "Service exists when it should not")
}
+func eventuallyExpectNoService(ctx context.Context, parentResource
client.Object, serviceName string, message string, additionalOffset ...int) {
+ EventuallyWithOffset(resolveOffset(additionalOffset), func() error {
+ return k8sClient.Get(ctx, resourceKey(parentResource,
serviceName), &corev1.Service{})
+ }).Should(MatchError("services \""+serviceName+"\" not found"),
message, "Service exists when it should not")
+}
+
func expectNoServices(ctx context.Context, parentResource client.Object,
message string, serviceNames []string, additionalOffset ...int) {
ConsistentlyWithOffset(resolveOffset(additionalOffset), func(g Gomega) {
for _, serviceName := range serviceNames {
- g.Expect(k8sClient.Get(ctx, resourceKey(parentResource,
serviceName), &corev1.Service{})).To(MatchError("services \""+serviceName+"\"
not found"), message)
+ g.Expect(k8sClient.Get(ctx, resourceKey(parentResource,
serviceName), &corev1.Service{})).To(MatchError("services \""+serviceName+"\"
not found"), message, "service", serviceName)
+ }
+ }).Should(Succeed())
+}
+func eventuallyExpectNoServices(ctx context.Context, parentResource
client.Object, message string, serviceNames []string, additionalOffset ...int) {
+ EventuallyWithOffset(resolveOffset(additionalOffset), func(g Gomega) {
+ for _, serviceName := range serviceNames {
+ g.Expect(k8sClient.Get(ctx, resourceKey(parentResource,
serviceName), &corev1.Service{})).To(MatchError("services \""+serviceName+"\"
not found"), message, "service", serviceName)
}
}).Should(Succeed())
}
@@ -356,6 +369,12 @@ func expectNoIngress(ctx context.Context, parentResource
client.Object, ingressN
}).Should(MatchError("ingresses.networking.k8s.io \""+ingressName+"\"
not found"), "Ingress exists when it should not")
}
+func eventuallyExpectNoIngress(ctx context.Context, parentResource
client.Object, ingressName string, additionalOffset ...int) {
+ EventuallyWithOffset(resolveOffset(additionalOffset), func() error {
+ return k8sClient.Get(ctx, resourceKey(parentResource,
ingressName), &netv1.Ingress{})
+ }).Should(MatchError("ingresses.networking.k8s.io \""+ingressName+"\"
not found"), "Ingress exists when it should not")
+}
+
func expectPodDisruptionBudget(ctx context.Context, parentResource
client.Object, podDisruptionBudgetName string, selector *metav1.LabelSelector,
maxUnavailable intstr.IntOrString, additionalOffset ...int)
*policyv1.PodDisruptionBudget {
return expectPodDisruptionBudgetWithChecks(ctx, parentResource,
podDisruptionBudgetName, selector, maxUnavailable, nil,
resolveOffset(additionalOffset))
}
diff --git a/controllers/solrcloud_controller.go
b/controllers/solrcloud_controller.go
index 9940ff9..affc220 100644
--- a/controllers/solrcloud_controller.go
+++ b/controllers/solrcloud_controller.go
@@ -326,7 +326,6 @@ func (r *SolrCloudReconciler) Reconcile(ctx
context.Context, req ctrl.Request) (
}
var statefulSet *appsv1.StatefulSet
-
if !blockReconciliationOfStatefulSet {
// Generate StatefulSet that should exist
expectedStatefulSet := util.GenerateStatefulSet(instance,
&newStatus, hostNameIpMap, reconcileConfigInfo, tls, security)
@@ -380,12 +379,12 @@ func (r *SolrCloudReconciler) Reconcile(ctx
context.Context, req ctrl.Request) (
}
} else {
// If we are blocking the reconciliation of the statefulSet, we
still want to find information about it.
- err = r.Get(ctx, types.NamespacedName{Name:
instance.StatefulSetName(), Namespace: instance.Namespace}, statefulSet)
- if err != nil {
- if !errors.IsNotFound(err) {
- return requeueOrNot, err
- } else {
+ statefulSet = &appsv1.StatefulSet{}
+ if e := r.Get(ctx, types.NamespacedName{Name:
instance.StatefulSetName(), Namespace: instance.Namespace}, statefulSet); e !=
nil {
+ if errors.IsNotFound(e) {
statefulSet = nil
+ } else {
+ err = e
}
}
}
@@ -424,6 +423,17 @@ func (r *SolrCloudReconciler) Reconcile(ctx
context.Context, req ctrl.Request) (
if err != nil {
return requeueOrNot, err
}
+ } else {
+ // If ingress exists, delete it
+ foundIngress := &netv1.Ingress{}
+ err = r.Get(ctx, types.NamespacedName{Name:
instance.CommonIngressName(), Namespace: instance.GetNamespace()}, foundIngress)
+ if err == nil {
+ err = r.Delete(ctx, foundIngress)
+ if err != nil {
+ return requeueOrNot, err
+ }
+ logger.Info("Deleted Ingress")
+ }
}
// *********************************************************
@@ -638,6 +648,12 @@ func (r *SolrCloudReconciler) Reconcile(ctx
context.Context, req ctrl.Request) (
}
}
+ // Remove unused services if necessary
+ err = r.cleanupUnconfiguredServices(ctx, instance, podList, logger)
+ if err != nil && !errors.IsNotFound(err) {
+ return requeueOrNot, err
+ }
+
if !reflect.DeepEqual(instance.Status, newStatus) {
logger.Info("Updating SolrCloud Status", "status", newStatus)
oldInstance := instance.DeepCopy()
@@ -651,7 +667,76 @@ func (r *SolrCloudReconciler) Reconcile(ctx
context.Context, req ctrl.Request) (
return requeueOrNot, err
}
-// InitializePods Ensure that all SolrCloud Pods are initialized
+// cleanupUnconfiguredServices remove services that are no longer defined by
the SolrCloud resource, and no longer in use by pods.
+// This does not currently include removing per-node services that are no
longer in use because the SolrCloud has been scaled down.
+func (r *SolrCloudReconciler) cleanupUnconfiguredServices(ctx context.Context,
solrCloud *solrv1beta1.SolrCloud, podList []corev1.Pod, logger logr.Logger)
(err error) {
+ var onlyServiceTypeInUse string
+ if solrCloud.UsesHeadlessService() {
+ onlyServiceTypeInUse = util.HeadlessServiceType
+ } else if solrCloud.UsesIndividualNodeServices() {
+ onlyServiceTypeInUse = util.PerNodeServiceType
+ } else {
+ // This should never happen, there are only 2 options
+ return
+ }
+ for _, pod := range podList {
+ if pod.Annotations == nil &&
pod.Annotations[util.ServiceTypeAnnotation] != "" {
+ if onlyServiceTypeInUse !=
pod.Annotations[util.ServiceTypeAnnotation] {
+ // Only remove services if all pods are using
the same, and configured, type of service.
+ // Otherwise, we are in transition between
service types and need to wait to delete anything.
+ return
+ }
+ } else {
+ // If we have a pod missing this annotation, then it
has not been fully updated to a supported Operator version.
+ // We don't have the information, so assume both
serviceTypes are in use, and don't remove anything.
+ return
+ }
+ }
+
+ // If we are at this point, then we can assume we are completely
transitioned and can delete the unused services
+ if solrCloud.UsesHeadlessService() {
+ err = r.deleteServicesOfType(ctx, solrCloud,
util.PerNodeServiceType, logger)
+ } else if solrCloud.UsesIndividualNodeServices() {
+ err = r.deleteServicesOfType(ctx, solrCloud,
util.HeadlessServiceType, logger)
+ }
+ return
+}
+
+func (r *SolrCloudReconciler) deleteServicesOfType(ctx context.Context,
solrCloud *solrv1beta1.SolrCloud, serviceType string, logger logr.Logger) (err
error) {
+ foundServices := &corev1.ServiceList{}
+ selectorLabels := solrCloud.SharedLabels()
+ selectorLabels[util.ServiceTypeAnnotation] = serviceType
+
+ serviceSelector := labels.SelectorFromSet(selectorLabels)
+ listOps := &client.ListOptions{
+ Namespace: solrCloud.Namespace,
+ LabelSelector: serviceSelector,
+ }
+
+ if err = r.List(ctx, foundServices, listOps); err != nil {
+ logger.Error(err, "Error listing services for SolrCloud",
"serviceType", serviceType)
+ return
+ }
+
+ for _, headlessService := range foundServices.Items {
+ if e := r.deleteService(ctx, &headlessService, logger); e !=
nil {
+ // Don't break, just add the error for later
+ err = e
+ }
+ }
+ return
+}
+
+func (r *SolrCloudReconciler) deleteService(ctx context.Context, service
*corev1.Service, logger logr.Logger) (err error) {
+ logger.Info("Deleting Service for SolrCloud", "Service", service.Name)
+ err = r.Client.Delete(ctx, service)
+ if err != nil && !errors.IsNotFound(err) {
+ logger.Error(err, "Error deleting unused Service for
SolrCloud", "Service", service.Name)
+ }
+ return
+}
+
+// initializePods Ensure that all SolrCloud Pods are initialized
func (r *SolrCloudReconciler) initializePods(ctx context.Context, solrCloud
*solrv1beta1.SolrCloud, statefulSet *appsv1.StatefulSet, logger logr.Logger)
(podSelector labels.Selector, podList []corev1.Pod, err error) {
foundPods := &corev1.PodList{}
selectorLabels := solrCloud.SharedLabels()
diff --git a/controllers/solrcloud_controller_backup_test.go
b/controllers/solrcloud_controller_backup_test.go
index a0be8d5..7f223fc 100644
--- a/controllers/solrcloud_controller_backup_test.go
+++ b/controllers/solrcloud_controller_backup_test.go
@@ -98,6 +98,7 @@ var _ = FDescribe("SolrCloud controller - Backup
Repositories", func() {
Expect(statefulSet.Spec.Template.Annotations).To(Equal(util.MergeLabelsOrAnnotations(testPodAnnotations,
map[string]string{
"solr.apache.org/solrXmlMd5":
solrXmlMd5,
util.SolrBackupRepositoriesAnnotation:
"test-repo",
+ util.ServiceTypeAnnotation:
util.HeadlessServiceType,
})), "Incorrect pod annotations")
// Env Variable Tests
@@ -321,6 +322,7 @@ var _ = FDescribe("SolrCloud controller - Backup
Repositories", func() {
Expect(statefulSet.Spec.Template.Annotations).To(Equal(map[string]string{
"solr.apache.org/solrXmlMd5":
fmt.Sprintf("%x", md5.Sum([]byte(configMap.Data["solr.xml"]))),
util.SolrBackupRepositoriesAnnotation:
"another,test-repo",
+ util.ServiceTypeAnnotation:
util.HeadlessServiceType,
}), "Incorrect pod annotations")
})
})
diff --git a/controllers/solrcloud_controller_ingress_test.go
b/controllers/solrcloud_controller_ingress_test.go
index 8a616a9..87fb57b 100644
--- a/controllers/solrcloud_controller_ingress_test.go
+++ b/controllers/solrcloud_controller_ingress_test.go
@@ -163,6 +163,92 @@ var _ = FDescribe("SolrCloud controller - Ingress", func()
{
})
})
+ FContext("Full Ingress - Switch off after creation", func() {
+ BeforeEach(func() {
+ solrCloud.Spec.SolrAddressability =
solrv1beta1.SolrAddressabilityOptions{
+ External: &solrv1beta1.ExternalAddressability{
+ Method: solrv1beta1.Ingress,
+ UseExternalAddress: true,
+ DomainName: testDomain,
+ NodePortOverride: 100,
+ },
+ PodPort: 3000,
+ CommonServicePort: 4000,
+ }
+ })
+ FIt("has the correct resources", func(ctx context.Context) {
+ By("testing the Solr StatefulSet")
+ statefulSet := expectStatefulSet(ctx, solrCloud,
solrCloud.StatefulSetName())
+ // Pod Annotations test
+
Expect(statefulSet.Spec.Template.Annotations).To(HaveKeyWithValue(util.ServiceTypeAnnotation,
util.PerNodeServiceType), "Since external address is used for advertising, the
perNode service should be specified in the pod annotations.")
+
+ By("testing the Solr Common Service")
+ commonService := expectService(ctx, solrCloud,
solrCloud.CommonServiceName(), statefulSet.Spec.Selector.MatchLabels, false)
+
Expect(commonService.Annotations).To(Equal(testCommonServiceAnnotations),
"Incorrect common service annotations")
+
Expect(commonService.Spec.Ports[0].Name).To(Equal("solr-client"), "Wrong port
name on common Service")
+
Expect(commonService.Spec.Ports[0].Port).To(Equal(int32(4000)), "Wrong port on
common Service")
+
Expect(commonService.Spec.Ports[0].TargetPort.StrVal).To(Equal("solr-client"),
"Wrong podPort name on common Service")
+
+ By("ensuring the Solr Headless Service does not exist")
+ expectNoService(ctx, solrCloud,
solrCloud.HeadlessServiceName(), "Headless service shouldn't exist, but it
does.")
+
+ By("making sure the individual Solr Node Services exist
and route correctly")
+ nodeNames := solrCloud.GetAllSolrPodNames()
+ Expect(nodeNames).To(HaveLen(replicas), "SolrCloud has
incorrect number of nodeNames.")
+ for _, nodeName := range nodeNames {
+ service := expectService(ctx, solrCloud,
nodeName, util.MergeLabelsOrAnnotations(statefulSet.Spec.Selector.MatchLabels,
map[string]string{"statefulset.kubernetes.io/pod-name": nodeName}), false)
+ expectedNodeServiceLabels :=
util.MergeLabelsOrAnnotations(solrCloud.SharedLabelsWith(solrCloud.Labels),
map[string]string{"service-type": "external"})
+
Expect(service.Labels).To(Equal(util.MergeLabelsOrAnnotations(expectedNodeServiceLabels,
testNodeServiceLabels)), "Incorrect node '"+nodeName+"' service labels")
+
Expect(service.Annotations).To(Equal(testNodeServiceAnnotations), "Incorrect
node '"+nodeName+"' service annotations")
+
Expect(service.Spec.Ports[0].Name).To(Equal("solr-client"), "Wrong port name on
common Service")
+
Expect(service.Spec.Ports[0].Port).To(Equal(int32(100)), "Wrong port on node
Service")
+
Expect(service.Spec.Ports[0].TargetPort.StrVal).To(Equal("solr-client"), "Wrong
podPort name on node Service")
+ }
+
+ By("making sure Ingress was created correctly")
+ ingress := expectIngress(ctx, solrCloud,
solrCloud.CommonIngressName())
+
Expect(ingress.Labels).To(Equal(util.MergeLabelsOrAnnotations(solrCloud.SharedLabelsWith(solrCloud.Labels),
testIngressLabels)), "Incorrect ingress labels")
+
Expect(ingress.Annotations).To(Equal(ingressLabelsWithDefaults(testIngressAnnotations)),
"Incorrect ingress annotations")
+ Expect(ingress.Spec.IngressClassName).To(Not(BeNil()),
"Ingress class name should not be nil")
+
Expect(*ingress.Spec.IngressClassName).To(Equal(testIngressClass), "Incorrect
ingress class name")
+ testIngressRules(solrCloud, ingress, true, replicas,
4000, 100, testDomain)
+
+ By("making sure the node addresses in the Status are
correct")
+ expectSolrCloudStatusWithChecks(ctx, solrCloud, func(g
Gomega, found *solrv1beta1.SolrCloudStatus) {
+
g.Expect(found.InternalCommonAddress).To(Equal("http://"+solrCloud.CommonServiceName()+"."+solrCloud.Namespace+":4000"),
"Wrong internal common address in status")
+
g.Expect(found.ExternalCommonAddress).To(Not(BeNil()), "External common address
in status should not be nil")
+
g.Expect(*found.ExternalCommonAddress).To(Equal("http://"+solrCloud.Namespace+"-"+solrCloud.Name+"-solrcloud"+"."+testDomain),
"Wrong external common address in status")
+ })
+
+ By("Turning off node external addressability and making
sure the node services are deleted")
+ expectSolrCloudWithChecks(ctx, solrCloud, func(g
Gomega, found *solrv1beta1.SolrCloud) {
+
found.Spec.SolrAddressability.External.UseExternalAddress = false
+
found.Spec.SolrAddressability.External.HideNodes = true
+ g.Expect(k8sClient.Update(ctx,
found)).To(Succeed(), "Couldn't update the solrCloud to not advertise the nodes
externally.")
+ })
+
+ // Since node external addressability is off, but
common external addressability is on, the ingress should exist, but the node
services should not
+ eventuallyExpectNoServices(ctx, solrCloud, "Node
services shouldn't exist after the update, but they do.", nodeNames)
+
+ expectIngress(ctx, solrCloud,
solrCloud.CommonIngressName())
+
+ headlessService := expectService(ctx, solrCloud,
solrCloud.HeadlessServiceName(), statefulSet.Spec.Selector.MatchLabels, true)
+
Expect(headlessService.Annotations).To(Equal(testHeadlessServiceAnnotations),
"Incorrect headless service annotations")
+
Expect(headlessService.Spec.Ports[0].Name).To(Equal("solr-client"), "Wrong port
name on common Service")
+
Expect(headlessService.Spec.Ports[0].Port).To(Equal(int32(3000)), "Wrong port
on headless Service")
+
Expect(headlessService.Spec.Ports[0].TargetPort.StrVal).To(Equal("solr-client"),
"Wrong podPort name on headless Service")
+
Expect(headlessService.Spec.Ports[0].Protocol).To(Equal(corev1.ProtocolTCP),
"Wrong protocol on headless Service")
+
Expect(headlessService.Spec.Ports[0].AppProtocol).To(BeNil(), "AppProtocol on
headless Service should be nil when not running with TLS")
+
+ By("Turning off common external addressability and
making sure the ingress is deleted")
+ expectSolrCloudWithChecks(ctx, solrCloud, func(g
Gomega, found *solrv1beta1.SolrCloud) {
+ found.Spec.SolrAddressability.External = nil
+ g.Expect(k8sClient.Update(ctx,
found)).To(Succeed(), "Couldn't update the solrCloud to remove external
addressability")
+ })
+ eventuallyExpectNoIngress(ctx, solrCloud,
solrCloud.CommonIngressName())
+ })
+ })
+
FContext("Hide Nodes from external connections - Using default ingress
class", func() {
ingressClass := &netv1.IngressClass{
ObjectMeta: metav1.ObjectMeta{
@@ -206,6 +292,7 @@ var _ = FDescribe("SolrCloud controller - Ingress", func() {
By("testing the Solr StatefulSet")
statefulSet := expectStatefulSet(ctx, solrCloud,
solrCloud.StatefulSetName())
+
Expect(statefulSet.Spec.Template.Annotations).To(HaveKeyWithValue(util.ServiceTypeAnnotation,
util.HeadlessServiceType), "Since external address is not used for
advertising, the headless service should be specified in the pod annotations.")
Expect(statefulSet.Spec.Template.Spec.Containers).To(HaveLen(1), "Solr
StatefulSet requires a container.")
diff --git a/controllers/solrcloud_controller_test.go
b/controllers/solrcloud_controller_test.go
index 1bd01ff..5dca7f0 100644
--- a/controllers/solrcloud_controller_test.go
+++ b/controllers/solrcloud_controller_test.go
@@ -280,7 +280,7 @@ var _ = FDescribe("SolrCloud controller - General", func() {
Expect(statefulSet.Spec.Template.ObjectMeta.Annotations).To(HaveKey(util.SolrScheduledRestartAnnotation),
"Pod Template does not have scheduled restart annotation when it should")
// Remove the annotation when we know that it exists,
we don't know the exact value so we can't check it below.
delete(statefulSet.Spec.Template.Annotations,
util.SolrScheduledRestartAnnotation)
-
Expect(statefulSet.Spec.Template.Annotations).To(Equal(util.MergeLabelsOrAnnotations(map[string]string{"solr.apache.org/solrXmlMd5":
fmt.Sprintf("%x", md5.Sum([]byte(configMap.Data["solr.xml"])))},
testPodAnnotations)), "Incorrect pod annotations")
+
Expect(statefulSet.Spec.Template.Annotations).To(Equal(util.MergeLabelsOrAnnotations(map[string]string{util.ServiceTypeAnnotation:
util.HeadlessServiceType, "solr.apache.org/solrXmlMd5": fmt.Sprintf("%x",
md5.Sum([]byte(configMap.Data["solr.xml"])))}, testPodAnnotations)), "Incorrect
pod annotations")
Expect(statefulSet.Spec.Template.Spec.NodeSelector).To(Equal(testNodeSelectors),
"Incorrect pod node selectors")
Expect(statefulSet.Spec.Template.Spec.Containers[0].LivenessProbe,
testProbeLivenessNonDefaults, "Incorrect Liveness Probe")
diff --git a/controllers/util/common.go b/controllers/util/common.go
index 9882519..7b2e5c9 100644
--- a/controllers/util/common.go
+++ b/controllers/util/common.go
@@ -431,8 +431,9 @@ func CopyPodTemplates(from, to *corev1.PodTemplateSpec,
basePath string, logger
requireUpdate = CopyPodVolumes(&from.Spec.Volumes, &to.Spec.Volumes,
basePath+"Spec.Volumes", logger) || requireUpdate
if !DeepEqualWithNils(to.Spec.HostAliases, from.Spec.HostAliases) {
- requireUpdate = true
+ hostAliasUpdate := false
if to.Spec.HostAliases == nil {
+ hostAliasUpdate = true
to.Spec.HostAliases = from.Spec.HostAliases
} else {
// Do not remove aliases that are no longer used.
@@ -443,19 +444,22 @@ func CopyPodTemplates(from, to *corev1.PodTemplateSpec,
basePath string, logger
if fromAlias.Hostnames[0] ==
toAlias.Hostnames[0] {
found = true
if !DeepEqualWithNils(toAlias,
fromAlias) {
- requireUpdate = true
+ hostAliasUpdate = true
to.Spec.HostAliases[i]
= fromAlias
break
}
}
}
if !found {
- requireUpdate = true
+ hostAliasUpdate = true
to.Spec.HostAliases =
append(to.Spec.HostAliases, fromAlias)
}
}
}
- logger.Info("Update required because field changed", "field",
basePath+"Spec.HostAliases", "from", to.Spec.HostAliases, "to",
from.Spec.HostAliases)
+ if hostAliasUpdate {
+ requireUpdate = true
+ logger.Info("Update required because field changed",
"field", basePath+"Spec.HostAliases", "from", to.Spec.HostAliases, "to",
from.Spec.HostAliases)
+ }
}
if !DeepEqualWithNils(to.Spec.ImagePullSecrets,
from.Spec.ImagePullSecrets) {
diff --git a/controllers/util/solr_util.go b/controllers/util/solr_util.go
index db4c408..478145a 100644
--- a/controllers/util/solr_util.go
+++ b/controllers/util/solr_util.go
@@ -52,6 +52,10 @@ const (
SolrXmlFile = "solr.xml"
LogXmlMd5Annotation = "solr.apache.org/logXmlMd5"
LogXmlFile = "log4j2.xml"
+ ServiceTypeAnnotation = "service-type" //
"solr.apache.org/serviceType"
+ HeadlessServiceType = "headless"
+ PerNodeServiceType = "external"
+ CommonServiceType = "common"
// Protected StatefulSet annotations
// These are to be saved on a statefulSet update
@@ -125,6 +129,16 @@ func GenerateStatefulSet(solrCloud *solr.SolrCloud,
solrCloudStatus *solr.SolrCl
}
shareProcessNamespace = customPodOptions.ShareProcessNamespace
}
+ if podAnnotations == nil {
+ podAnnotations = make(map[string]string, 1)
+ }
+ var serviceType string
+ if solrCloud.UsesHeadlessService() {
+ serviceType = HeadlessServiceType
+ } else if solrCloud.UsesIndividualNodeServices() {
+ serviceType = PerNodeServiceType
+ }
+ podAnnotations[ServiceTypeAnnotation] = serviceType
// The isNotStopped readiness gate will always be used for
managedUpdates
podReadinessGates := []corev1.PodReadinessGate{
@@ -413,9 +427,6 @@ func GenerateStatefulSet(solrCloud *solr.SolrCloud,
solrCloudStatus *solr.SolrCl
// Did the user provide a custom log config?
if reconcileConfigInfo[LogXmlFile] != "" {
if reconcileConfigInfo[LogXmlMd5Annotation] != "" {
- if podAnnotations == nil {
- podAnnotations = make(map[string]string, 1)
- }
podAnnotations[LogXmlMd5Annotation] =
reconcileConfigInfo[LogXmlMd5Annotation]
}
@@ -432,9 +443,6 @@ func GenerateStatefulSet(solrCloud *solr.SolrCloud,
solrCloudStatus *solr.SolrCl
// track the MD5 of the custom solr.xml in the pod spec annotations,
// so we get a rolling restart when the configMap changes
if reconcileConfigInfo[SolrXmlMd5Annotation] != "" {
- if podAnnotations == nil {
- podAnnotations = make(map[string]string, 1)
- }
podAnnotations[SolrXmlMd5Annotation] =
reconcileConfigInfo[SolrXmlMd5Annotation]
}
@@ -912,7 +920,7 @@ func getAppProtocol(solrCloud *solr.SolrCloud) *string {
// solrCloud: SolrCloud instance
func GenerateCommonService(solrCloud *solr.SolrCloud) *corev1.Service {
labels := solrCloud.SharedLabelsWith(solrCloud.GetLabels())
- labels["service-type"] = "common"
+ labels[ServiceTypeAnnotation] = CommonServiceType
selectorLabels := solrCloud.SharedLabels()
selectorLabels["technology"] = solr.SolrTechnologyLabel
@@ -964,7 +972,7 @@ func GenerateCommonService(solrCloud *solr.SolrCloud)
*corev1.Service {
// solrCloud: SolrCloud instance
func GenerateHeadlessService(solrCloud *solr.SolrCloud) *corev1.Service {
labels := solrCloud.SharedLabelsWith(solrCloud.GetLabels())
- labels["service-type"] = "headless"
+ labels[ServiceTypeAnnotation] = HeadlessServiceType
selectorLabels := solrCloud.SharedLabels()
selectorLabels["technology"] = solr.SolrTechnologyLabel
@@ -1019,7 +1027,7 @@ func GenerateHeadlessService(solrCloud *solr.SolrCloud)
*corev1.Service {
// nodeName: string node
func GenerateNodeService(solrCloud *solr.SolrCloud, nodeName string)
*corev1.Service {
labels := solrCloud.SharedLabelsWith(solrCloud.GetLabels())
- labels["service-type"] = "external"
+ labels[ServiceTypeAnnotation] = PerNodeServiceType
selectorLabels := solrCloud.SharedLabels()
selectorLabels["technology"] = solr.SolrTechnologyLabel
diff --git a/helm/solr-operator/Chart.yaml b/helm/solr-operator/Chart.yaml
index 2509c43..ec5275f 100644
--- a/helm/solr-operator/Chart.yaml
+++ b/helm/solr-operator/Chart.yaml
@@ -134,6 +134,13 @@ annotations:
url: https://github.com/apache/solr-operator/issues/489
- name: Github PR
url: https://github.com/apache/solr-operator/pull/743
+ - kind: fixed
+ description: The operator will now delete ingress and per-node services
when external ingress is disabled.
+ links:
+ - name: Github Issue
+ url: https://github.com/apache/solr-operator/issues/673
+ - name: Github PR
+ url: https://github.com/apache/solr-operator/pull/674
artifacthub.io/images: |
- name: solr-operator
image: apache/solr-operator:v0.9.0-prerelease
diff --git a/tests/e2e/resource_utils_test.go b/tests/e2e/resource_utils_test.go
index a58904c..31b88c6 100644
--- a/tests/e2e/resource_utils_test.go
+++ b/tests/e2e/resource_utils_test.go
@@ -346,6 +346,27 @@ func expectNoPodNow(ctx context.Context, parentResource
client.Object, podName s
).To(MatchError("pods \""+podName+"\" not found"), "Pod exists when it
should not")
}
+func expectPodNow(ctx context.Context, parentResource client.Object, podName
string, additionalOffset ...int) *corev1.Pod {
+ pod := &corev1.Pod{}
+ ExpectWithOffset(
+ resolveOffset(additionalOffset),
+ k8sClient.Get(ctx, resourceKey(parentResource, podName), pod),
+ ).To(Succeed(), "Pod should exist", "name", podName, "namespace",
parentResource.GetNamespace())
+ return pod
+}
+
+func expectPodWithChecks(ctx context.Context, parentResource client.Object,
podName string, additionalChecks func(Gomega, *corev1.Pod), additionalOffset
...int) *corev1.Pod {
+ pod := &corev1.Pod{}
+ EventuallyWithOffset(resolveOffset(additionalOffset), func(g Gomega) {
+ g.Expect(k8sClient.Get(ctx, resourceKey(parentResource,
podName), pod)).To(Succeed(), "Expected Pod does not exist")
+
+ if additionalChecks != nil {
+ additionalChecks(g, pod)
+ }
+ }).Should(Succeed())
+ return pod
+}
+
func expectService(ctx context.Context, parentResource client.Object,
serviceName string, selectorLables map[string]string, isHeadless bool,
additionalOffset ...int) *corev1.Service {
return expectServiceWithChecks(ctx, parentResource, serviceName,
selectorLables, isHeadless, nil, resolveOffset(additionalOffset))
}
@@ -353,7 +374,7 @@ func expectService(ctx context.Context, parentResource
client.Object, serviceNam
func expectServiceWithChecks(ctx context.Context, parentResource
client.Object, serviceName string, selectorLables map[string]string, isHeadless
bool, additionalChecks func(Gomega, *corev1.Service), additionalOffset ...int)
*corev1.Service {
service := &corev1.Service{}
EventuallyWithOffset(resolveOffset(additionalOffset), func(g Gomega) {
- Expect(k8sClient.Get(ctx, resourceKey(parentResource,
serviceName), service)).To(Succeed(), "Expected Service does not exist")
+ g.Expect(k8sClient.Get(ctx, resourceKey(parentResource,
serviceName), service)).To(Succeed(), "Expected Service does not exist")
g.Expect(service.Spec.Selector).To(Equal(selectorLables),
"Service is not pointing to the correct Pods.")
@@ -367,27 +388,13 @@ func expectServiceWithChecks(ctx context.Context,
parentResource client.Object,
additionalChecks(g, service)
}
}).Should(Succeed())
-
- By("recreating the Service after it is deleted")
- ExpectWithOffset(resolveOffset(additionalOffset), k8sClient.Delete(ctx,
service)).To(Succeed())
- EventuallyWithOffset(
- resolveOffset(additionalOffset),
- func() (types.UID, error) {
- newResource := &corev1.Service{}
- err := k8sClient.Get(ctx, resourceKey(parentResource,
serviceName), newResource)
- if err != nil {
- return "", err
- }
- return newResource.UID, nil
- }).Should(And(Not(BeEmpty()), Not(Equal(service.UID))), "New
Service, with new UID, not created.")
-
return service
}
func expectServiceWithConsistentChecks(ctx context.Context, parentResource
client.Object, serviceName string, selectorLables map[string]string, isHeadless
bool, additionalChecks func(Gomega, *corev1.Service), additionalOffset ...int)
*corev1.Service {
service := &corev1.Service{}
ConsistentlyWithOffset(resolveOffset(additionalOffset), func(g Gomega) {
- Expect(k8sClient.Get(ctx, resourceKey(parentResource,
serviceName), service)).To(Succeed(), "Expected Service does not exist")
+ g.Expect(k8sClient.Get(ctx, resourceKey(parentResource,
serviceName), service)).To(Succeed(), "Expected Service does not exist")
g.Expect(service.Spec.Selector).To(Equal(selectorLables),
"Service is not pointing to the correct Pods.")
@@ -411,10 +418,23 @@ func expectNoService(ctx context.Context, parentResource
client.Object, serviceN
}).Should(MatchError("services \""+serviceName+"\" not found"),
message, "Service exists when it should not")
}
+func eventuallyExpectNoService(ctx context.Context, parentResource
client.Object, serviceName string, message string, additionalOffset ...int) {
+ EventuallyWithOffset(resolveOffset(additionalOffset), func() error {
+ return k8sClient.Get(ctx, resourceKey(parentResource,
serviceName), &corev1.Service{})
+ }).Should(MatchError("services \""+serviceName+"\" not found"),
message, "Service exists when it should not")
+}
+
func expectNoServices(ctx context.Context, parentResource client.Object,
message string, serviceNames []string, additionalOffset ...int) {
ConsistentlyWithOffset(resolveOffset(additionalOffset), func(g Gomega) {
for _, serviceName := range serviceNames {
- g.Expect(k8sClient.Get(ctx, resourceKey(parentResource,
serviceName), &corev1.Service{})).To(MatchError("services \""+serviceName+"\"
not found"), message)
+ g.Expect(k8sClient.Get(ctx, resourceKey(parentResource,
serviceName), &corev1.Service{})).To(MatchError("services \""+serviceName+"\"
not found"), message, "service", serviceName)
+ }
+ }).Should(Succeed())
+}
+func eventuallyExpectNoServices(ctx context.Context, parentResource
client.Object, message string, serviceNames []string, additionalOffset ...int) {
+ EventuallyWithOffset(resolveOffset(additionalOffset), func(g Gomega) {
+ for _, serviceName := range serviceNames {
+ g.Expect(k8sClient.Get(ctx, resourceKey(parentResource,
serviceName), &corev1.Service{})).To(MatchError("services \""+serviceName+"\"
not found"), message, "service", serviceName)
}
}).Should(Succeed())
}
@@ -432,20 +452,6 @@ func expectIngressWithChecks(ctx context.Context,
parentResource client.Object,
additionalChecks(g, ingress)
}
}).Should(Succeed())
-
- By("recreating the Ingress after it is deleted")
- ExpectWithOffset(resolveOffset(additionalOffset), k8sClient.Delete(ctx,
ingress)).To(Succeed())
- EventuallyWithOffset(
- resolveOffset(additionalOffset),
- func() (types.UID, error) {
- newResource := &netv1.Ingress{}
- err := k8sClient.Get(ctx, resourceKey(parentResource,
ingressName), newResource)
- if err != nil {
- return "", err
- }
- return newResource.UID, nil
- }).Should(And(Not(BeEmpty()), Not(Equal(ingress.UID))), "New
Ingress, with new UID, not created.")
-
return ingress
}
@@ -468,6 +474,12 @@ func expectNoIngress(ctx context.Context, parentResource
client.Object, ingressN
}).Should(MatchError("ingresses.networking.k8s.io \""+ingressName+"\"
not found"), "Ingress exists when it should not")
}
+func eventuallyExpectNoIngress(ctx context.Context, parentResource
client.Object, ingressName string, additionalOffset ...int) {
+ EventuallyWithOffset(resolveOffset(additionalOffset), func() error {
+ return k8sClient.Get(ctx, resourceKey(parentResource,
ingressName), &netv1.Ingress{})
+ }).Should(MatchError("ingresses.networking.k8s.io \""+ingressName+"\"
not found"), "Ingress exists when it should not")
+}
+
func expectPodDisruptionBudget(ctx context.Context, parentResource
client.Object, podDisruptionBudgetName string, selector *metav1.LabelSelector,
maxUnavailable intstr.IntOrString, additionalOffset ...int)
*policyv1.PodDisruptionBudget {
return expectPodDisruptionBudgetWithChecks(ctx, parentResource,
podDisruptionBudgetName, selector, maxUnavailable, nil,
resolveOffset(additionalOffset))
}
@@ -485,20 +497,6 @@ func expectPodDisruptionBudgetWithChecks(ctx
context.Context, parentResource cli
additionalChecks(g, podDisruptionBudget)
}
}).Should(Succeed())
-
- By("recreating the PodDisruptionBudget after it is deleted")
- ExpectWithOffset(resolveOffset(additionalOffset), k8sClient.Delete(ctx,
podDisruptionBudget)).To(Succeed())
- EventuallyWithOffset(
- resolveOffset(additionalOffset),
- func() (types.UID, error) {
- newResource := &policyv1.PodDisruptionBudget{}
- err := k8sClient.Get(ctx, resourceKey(parentResource,
podDisruptionBudgetName), newResource)
- if err != nil {
- return "", err
- }
- return newResource.UID, nil
- }).Should(And(Not(BeEmpty()),
Not(Equal(podDisruptionBudget.UID))), "New PodDisruptionBudget, with new UID,
not created.")
-
return podDisruptionBudget
}
@@ -518,20 +516,6 @@ func expectConfigMapWithChecks(ctx context.Context,
parentResource client.Object
additionalChecks(g, configMap)
}
}).Should(Succeed())
-
- By("recreating the ConfigMap after it is deleted")
- ExpectWithOffset(resolveOffset(additionalOffset), k8sClient.Delete(ctx,
configMap)).To(Succeed())
- EventuallyWithOffset(
- resolveOffset(additionalOffset),
- func() (types.UID, error) {
- newResource := &corev1.ConfigMap{}
- err := k8sClient.Get(ctx, resourceKey(parentResource,
configMapName), newResource)
- if err != nil {
- return "", err
- }
- return newResource.UID, nil
- }).Should(And(Not(BeEmpty()), Not(Equal(configMap.UID))), "New
ConfigMap, with new UID, not created.")
-
return configMap
}
@@ -574,20 +558,6 @@ func expectDeploymentWithChecks(ctx context.Context,
parentResource client.Objec
additionalChecks(g, deployment)
}
}).Should(Succeed())
-
- By("recreating the Deployment after it is deleted")
- ExpectWithOffset(resolveOffset(additionalOffset), k8sClient.Delete(ctx,
deployment)).To(Succeed())
- EventuallyWithOffset(
- resolveOffset(additionalOffset),
- func() (types.UID, error) {
- newResource := &appsv1.Deployment{}
- err := k8sClient.Get(ctx, resourceKey(parentResource,
deploymentName), newResource)
- if err != nil {
- return "", err
- }
- return newResource.UID, nil
- }).Should(And(Not(BeEmpty()), Not(Equal(deployment.UID))), "New
Deployment, with new UID, not created.")
-
return deployment
}
diff --git a/tests/e2e/solrcloud_ingress_test.go
b/tests/e2e/solrcloud_ingress_test.go
new file mode 100644
index 0000000..e9a45ab
--- /dev/null
+++ b/tests/e2e/solrcloud_ingress_test.go
@@ -0,0 +1,138 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package e2e
+
+import (
+ "context"
+ solrv1beta1 "github.com/apache/solr-operator/api/v1beta1"
+ "github.com/apache/solr-operator/controllers/util"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ corev1 "k8s.io/api/core/v1"
+)
+
+var _ = FDescribe("E2E - SolrCloud - Ingress", func() {
+ var (
+ solrCloud *solrv1beta1.SolrCloud
+ )
+
+ BeforeEach(func() {
+ solrCloud = generateBaseSolrCloud(1)
+ solrCloud.Spec.SolrAddressability =
solrv1beta1.SolrAddressabilityOptions{
+ External: &solrv1beta1.ExternalAddressability{
+ Method: solrv1beta1.Ingress,
+ UseExternalAddress: true,
+ DomainName: testDomain,
+ },
+ }
+ })
+
+ JustBeforeEach(func(ctx context.Context) {
+ By("creating the SolrCloud")
+ Expect(k8sClient.Create(ctx, solrCloud)).To(Succeed())
+
+ DeferCleanup(func(ctx context.Context) {
+ cleanupTest(ctx, solrCloud)
+ })
+
+ By("Waiting for the SolrCloud to come up healthy")
+ solrCloud = expectSolrCloudToBeReady(ctx, solrCloud)
+
+ By("creating a first Solr Collection")
+ createAndQueryCollection(ctx, solrCloud, "basic", 1, 1)
+ })
+
+ FContext("Can Remove Ingress and Services when changing
addressability", func() {
+
+ FIt("Can adapt to changing needs", func(ctx context.Context) {
+ By("testing the Solr StatefulSet")
+ statefulSet := expectStatefulSet(ctx, solrCloud,
solrCloud.StatefulSetName())
+ // Pod Annotations test
+
Expect(statefulSet.Spec.Template.Annotations).To(HaveKeyWithValue(util.ServiceTypeAnnotation,
util.PerNodeServiceType), "Since external address is used for advertising, the
perNode service should be specified in the pod annotations.")
+
+ By("testing the Solr Common Service")
+ expectService(ctx, solrCloud,
solrCloud.CommonServiceName(), statefulSet.Spec.Selector.MatchLabels, false)
+
+ By("ensuring the Solr Headless Service does not exist")
+ expectNoService(ctx, solrCloud,
solrCloud.HeadlessServiceName(), "Headless service shouldn't exist, but it
does.")
+
+ By("making sure the individual Solr Node Services exist
and route correctly")
+ nodeNames := solrCloud.GetAllSolrPodNames()
+ Expect(nodeNames).To(HaveLen(1), "SolrCloud has
incorrect number of nodeNames.")
+ for _, nodeName := range nodeNames {
+ expectService(ctx, solrCloud, nodeName,
util.MergeLabelsOrAnnotations(statefulSet.Spec.Selector.MatchLabels,
map[string]string{"statefulset.kubernetes.io/pod-name": nodeName}), false)
+ }
+
+ By("making sure Ingress was created correctly")
+ expectIngress(ctx, solrCloud,
solrCloud.CommonIngressName())
+
+ By("Turning off node external addressability and making
sure the node services are deleted")
+ expectSolrCloudWithChecks(ctx, solrCloud, func(g
Gomega, found *solrv1beta1.SolrCloud) {
+
found.Spec.SolrAddressability.External.UseExternalAddress = false
+
found.Spec.SolrAddressability.External.HideNodes = true
+ g.Expect(k8sClient.Update(ctx,
found)).To(Succeed(), "Couldn't update the solrCloud to not advertise the nodes
externally.")
+ })
+
+ // Since node external addressability is off, but
common external addressability is on, the ingress should exist, but the node
services should not
+ expectIngress(ctx, solrCloud,
solrCloud.CommonIngressName())
+
+ expectService(ctx, solrCloud,
solrCloud.HeadlessServiceName(), statefulSet.Spec.Selector.MatchLabels, true)
+
+ eventuallyExpectNoServices(ctx, solrCloud, "Node
services shouldn't exist after the update, but they do.", nodeNames)
+ pod := expectPodNow(ctx, solrCloud,
solrCloud.GetSolrPodName(0))
+
Expect(pod.Annotations).To(HaveKeyWithValue(util.ServiceTypeAnnotation,
util.HeadlessServiceType))
+
+ By("Turning off common external addressability and
making sure the ingress is deleted")
+ expectSolrCloudWithChecks(ctx, solrCloud, func(g
Gomega, found *solrv1beta1.SolrCloud) {
+ found.Spec.SolrAddressability.External = nil
+ g.Expect(k8sClient.Update(ctx,
found)).To(Succeed(), "Couldn't update the solrCloud to remove external
addressability")
+ })
+ eventuallyExpectNoIngress(ctx, solrCloud,
solrCloud.CommonIngressName())
+
+ By("Turning back on common external addressability and
making sure the headless service is deleted")
+ expectSolrCloudWithChecks(ctx, solrCloud, func(g
Gomega, found *solrv1beta1.SolrCloud) {
+ found.Spec.SolrAddressability =
solrv1beta1.SolrAddressabilityOptions{
+ External:
&solrv1beta1.ExternalAddressability{
+ Method:
solrv1beta1.Ingress,
+ UseExternalAddress: true,
+ DomainName: testDomain,
+ },
+ }
+ g.Expect(k8sClient.Update(ctx,
found)).To(Succeed(), "Couldn't update the solrCloud to add external
addressability")
+ })
+ expectIngress(ctx, solrCloud,
solrCloud.CommonIngressName())
+
+ By("testing the Solr Common Service")
+ expectService(ctx, solrCloud,
solrCloud.CommonServiceName(), statefulSet.Spec.Selector.MatchLabels, false)
+
+ By("making sure the individual Solr Node Services are
created")
+
+ Expect(nodeNames).To(HaveLen(1), "SolrCloud has
incorrect number of nodeNames.")
+ for _, nodeName := range nodeNames {
+ expectService(ctx, solrCloud, nodeName,
util.MergeLabelsOrAnnotations(statefulSet.Spec.Selector.MatchLabels,
map[string]string{"statefulset.kubernetes.io/pod-name": nodeName}), false)
+ }
+
+ By("ensuring the Solr Headless Service is deleted after
the pod is updated")
+ expectPodWithChecks(ctx, solrCloud,
solrCloud.GetSolrPodName(0), func(g Gomega, pod *corev1.Pod) {
+
g.Expect(pod.Annotations).To(HaveKeyWithValue(util.ServiceTypeAnnotation,
util.PerNodeServiceType))
+ })
+ expectNoService(ctx, solrCloud,
solrCloud.HeadlessServiceName(), "Headless service shouldn't exist, but it
does.")
+
+ })
+ })
+})