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 b13db1d  Add option for Ingress TLS termination for SolrCloud. (#293)
b13db1d is described below

commit b13db1dd2cd72c6150d43c0694ac4efc8e6eb97a
Author: Houston Putman <hous...@apache.org>
AuthorDate: Thu Aug 5 12:59:52 2021 -0400

    Add option for Ingress TLS termination for SolrCloud. (#293)
---
 api/v1beta1/solrcloud_types.go                   | 53 ++++++++++-------
 config/crd/bases/solr.apache.org_solrclouds.yaml |  3 +
 controllers/solrcloud_controller.go              |  8 +--
 controllers/solrcloud_controller_ingress_test.go | 24 ++++----
 controllers/solrcloud_controller_tls_test.go     | 74 ++++++++++++++++++++++--
 controllers/util/solr_util.go                    | 53 +++++++++++++----
 docs/solr-cloud/solr-cloud-crd.md                | 71 +++++++++++++++++++++--
 helm/solr-operator/Chart.yaml                    |  7 +++
 helm/solr-operator/crds/crds.yaml                |  3 +
 helm/solr/README.md                              | 11 +++-
 helm/solr/values.yaml                            |  8 +++
 11 files changed, 257 insertions(+), 58 deletions(-)

diff --git a/api/v1beta1/solrcloud_types.go b/api/v1beta1/solrcloud_types.go
index b81d76c..a0db972 100644
--- a/api/v1beta1/solrcloud_types.go
+++ b/api/v1beta1/solrcloud_types.go
@@ -428,6 +428,16 @@ type ExternalAddressability struct {
        // Defaults to 80 if HideNodes=false and method=Ingress, otherwise this 
is optional.
        // +optional
        NodePortOverride int `json:"nodePortOverride,omitempty"`
+
+       // IngressTLSTerminationSecret defines a TLS Secret to use for TLS 
termination of all exposed addresses in the ingress.
+       //
+       // This is option is only available when Method=Ingress, because 
ExternalDNS and LoadBalancer Services do not support TLS termination.
+       // This option is also unavailable when the SolrCloud has TLS enabled 
via `spec.solrTLS`, in this case the Ingress cannot terminate TLS before 
reaching Solr.
+       //
+       // When using this option, the UseExternalAddress option will be 
disabled, since Solr cannot be running in HTTP mode and making internal 
requests in HTTPS.
+       //
+       // +optional
+       IngressTLSTerminationSecret string 
`json:"ingressTLSTerminationSecret,omitempty"`
 }
 
 // ExternalAddressability is a string enumeration type that enumerates
@@ -449,7 +459,7 @@ const (
 
 func (opts *ExternalAddressability) withDefaults() (changed bool) {
        // You can't use an externalAddress for Solr Nodes if the Nodes are 
hidden externally
-       if opts.UseExternalAddress && opts.HideNodes {
+       if opts.UseExternalAddress && (opts.HideNodes || 
opts.IngressTLSTerminationSecret != "") {
                changed = true
                opts.UseExternalAddress = false
        }
@@ -839,7 +849,7 @@ func (sc *SolrCloud) CommonServiceName() string {
 
 // InternalURLForCloud returns the name of the common service for the cloud
 func InternalURLForCloud(sc *SolrCloud) string {
-       return fmt.Sprintf("%s://%s-solrcloud-common.%s%s", sc.UrlScheme(), 
sc.Name, sc.Namespace, sc.CommonPortSuffix())
+       return fmt.Sprintf("%s://%s-solrcloud-common.%s%s", 
sc.UrlScheme(false), sc.Name, sc.Namespace, sc.CommonPortSuffix(false))
 }
 
 // HeadlessServiceName returns the name of the headless service for the cloud
@@ -903,10 +913,6 @@ func (sc *SolrCloud) CommonExternalPrefix() string {
        return fmt.Sprintf("%s-%s-solrcloud", sc.Namespace, sc.Name)
 }
 
-func (sc *SolrCloud) CommonExternalUrl(domainName string) string {
-       return fmt.Sprintf("%s.%s", sc.CommonExternalPrefix(), domainName)
-}
-
 func (sc *SolrCloud) NodeIngressPrefix(nodeName string) string {
        return fmt.Sprintf("%s-%s", sc.Namespace, nodeName)
 }
@@ -926,7 +932,7 @@ func (sc *SolrCloud) customKubeDomain() string {
 func (sc *SolrCloud) NodeHeadlessUrl(nodeName string, withPort bool) (url 
string) {
        url = fmt.Sprintf("%s.%s.%s", nodeName, sc.HeadlessServiceName(), 
sc.Namespace) + sc.customKubeDomain()
        if withPort {
-               url += sc.NodePortSuffix()
+               url += sc.NodePortSuffix(false)
        }
        return url
 }
@@ -934,17 +940,17 @@ func (sc *SolrCloud) NodeHeadlessUrl(nodeName string, 
withPort bool) (url string
 func (sc *SolrCloud) NodeServiceUrl(nodeName string, withPort bool) (url 
string) {
        url = fmt.Sprintf("%s.%s", nodeName, sc.Namespace) + 
sc.customKubeDomain()
        if withPort {
-               url += sc.NodePortSuffix()
+               url += sc.NodePortSuffix(false)
        }
        return url
 }
 
-func (sc *SolrCloud) CommonPortSuffix() string {
-       return sc.PortToSuffix(sc.Spec.SolrAddressability.CommonServicePort)
+func (sc *SolrCloud) CommonPortSuffix(external bool) string {
+       return sc.PortToSuffix(sc.Spec.SolrAddressability.CommonServicePort, 
external)
 }
 
-func (sc *SolrCloud) NodePortSuffix() string {
-       return sc.PortToSuffix(sc.NodePort())
+func (sc *SolrCloud) NodePortSuffix(external bool) string {
+       return sc.PortToSuffix(sc.NodePort(), external)
 }
 
 func (sc *SolrCloud) NodePort() int {
@@ -957,9 +963,9 @@ func (sc *SolrCloud) NodePort() int {
        return port
 }
 
-func (sc *SolrCloud) PortToSuffix(port int) string {
+func (sc *SolrCloud) PortToSuffix(port int, external bool) string {
        suffix := ""
-       if sc.UrlScheme() == "https" {
+       if sc.UrlScheme(external) == "https" {
                if port != 443 {
                        suffix = ":" + strconv.Itoa(port)
                }
@@ -984,7 +990,7 @@ func (sc *SolrCloud) InternalNodeUrl(nodeName string, 
withPort bool) string {
 func (sc *SolrCloud) InternalCommonUrl(withPort bool) (url string) {
        url = fmt.Sprintf("%s.%s", sc.CommonServiceName(), sc.Namespace) + 
sc.customKubeDomain()
        if withPort {
-               url += sc.CommonPortSuffix()
+               url += sc.CommonPortSuffix(false)
        }
        return url
 }
@@ -996,8 +1002,10 @@ func (sc *SolrCloud) ExternalNodeUrl(nodeName string, 
domainName string, withPor
                url = fmt.Sprintf("%s.%s", nodeName, 
sc.ExternalDnsDomain(domainName))
        }
        // TODO: Add LoadBalancer stuff here
-       if withPort {
-               url += sc.NodePortSuffix()
+
+       if withPort && sc.Spec.SolrAddressability.External.Method != Ingress {
+               // Ingress does not require a port, since the port is whatever 
the ingress is listening on (80 and 443)
+               url += sc.NodePortSuffix(true)
        }
        return url
 }
@@ -1008,16 +1016,21 @@ func (sc *SolrCloud) ExternalCommonUrl(domainName 
string, withPort bool) (url st
        } else if sc.Spec.SolrAddressability.External.Method == ExternalDNS {
                url = fmt.Sprintf("%s.%s", sc.CommonServiceName(), 
sc.ExternalDnsDomain(domainName))
        }
-       if withPort {
-               url += sc.CommonPortSuffix()
+       // TODO: Add LoadBalancer stuff here
+
+       if withPort && sc.Spec.SolrAddressability.External.Method != Ingress {
+               // Ingress does not require a port, since the port is whatever 
the ingress is listening on (80 and 443)
+               url += sc.CommonPortSuffix(true)
        }
        return url
 }
 
-func (sc *SolrCloud) UrlScheme() string {
+func (sc *SolrCloud) UrlScheme(external bool) string {
        urlScheme := "http"
        if sc.Spec.SolrTLS != nil {
                urlScheme = "https"
+       } else if external && sc.Spec.SolrAddressability.External != nil && 
sc.Spec.SolrAddressability.External.Method == Ingress && 
sc.Spec.SolrAddressability.External.IngressTLSTerminationSecret != "" {
+               urlScheme = "https"
        }
        return urlScheme
 }
diff --git a/config/crd/bases/solr.apache.org_solrclouds.yaml 
b/config/crd/bases/solr.apache.org_solrclouds.yaml
index 9f284bf..d1d2cf2 100644
--- a/config/crd/bases/solr.apache.org_solrclouds.yaml
+++ b/config/crd/bases/solr.apache.org_solrclouds.yaml
@@ -4499,6 +4499,9 @@ spec:
                       hideNodes:
                         description: Do not expose each of the Solr Node 
services externally. The number of services this affects could range from 1 (a 
headless service for ExternalDNS) to the number of Solr pods your cloud 
contains (individual node services for Ingress/LoadBalancer). Defaults to false.
                         type: boolean
+                      ingressTLSTerminationSecret:
+                        description: "IngressTLSTerminationSecret defines a 
TLS Secret to use for TLS termination of all exposed addresses in the ingress. 
\n This is option is only available when Method=Ingress, because ExternalDNS 
and LoadBalancer Services do not support TLS termination. This option is also 
unavailable when the SolrCloud has TLS enabled via `spec.solrTLS`, in this case 
the Ingress cannot terminate TLS before reaching Solr. \n When using this 
option, the UseExternalAddre [...]
+                        type: string
                       method:
                         description: The way in which this SolrCloud's 
service(s) should be made addressable externally.
                         enum:
diff --git a/controllers/solrcloud_controller.go 
b/controllers/solrcloud_controller.go
index 55c7b9d..bfd3c3a 100644
--- a/controllers/solrcloud_controller.go
+++ b/controllers/solrcloud_controller.go
@@ -608,9 +608,9 @@ func reconcileCloudStatus(r *SolrCloudReconciler, solrCloud 
*solr.SolrCloud, log
                nodeStatus := solr.SolrNodeStatus{}
                nodeStatus.Name = p.Name
                nodeStatus.NodeName = p.Spec.NodeName
-               nodeStatus.InternalAddress = solrCloud.UrlScheme() + "://" + 
solrCloud.InternalNodeUrl(nodeStatus.Name, true)
+               nodeStatus.InternalAddress = solrCloud.UrlScheme(false) + "://" 
+ solrCloud.InternalNodeUrl(nodeStatus.Name, true)
                if solrCloud.Spec.SolrAddressability.External != nil && 
!solrCloud.Spec.SolrAddressability.External.HideNodes {
-                       nodeStatus.ExternalAddress = solrCloud.UrlScheme() + 
"://" + solrCloud.ExternalNodeUrl(nodeStatus.Name, 
solrCloud.Spec.SolrAddressability.External.DomainName, true)
+                       nodeStatus.ExternalAddress = solrCloud.UrlScheme(true) 
+ "://" + solrCloud.ExternalNodeUrl(nodeStatus.Name, 
solrCloud.Spec.SolrAddressability.External.DomainName, true)
                }
                if len(p.Status.ContainerStatuses) > 0 {
                        // The first container should always be running solr
@@ -690,9 +690,9 @@ func reconcileCloudStatus(r *SolrCloudReconciler, solrCloud 
*solr.SolrCloud, log
                newStatus.Version = solrCloud.Spec.SolrImage.Tag
        }
 
-       newStatus.InternalCommonAddress = solrCloud.UrlScheme() + "://" + 
solrCloud.InternalCommonUrl(true)
+       newStatus.InternalCommonAddress = solrCloud.UrlScheme(false) + "://" + 
solrCloud.InternalCommonUrl(true)
        if solrCloud.Spec.SolrAddressability.External != nil && 
!solrCloud.Spec.SolrAddressability.External.HideCommon {
-               extAddress := solrCloud.UrlScheme() + "://" + 
solrCloud.ExternalCommonUrl(solrCloud.Spec.SolrAddressability.External.DomainName,
 true)
+               extAddress := solrCloud.UrlScheme(true) + "://" + 
solrCloud.ExternalCommonUrl(solrCloud.Spec.SolrAddressability.External.DomainName,
 true)
                newStatus.ExternalCommonAddress = &extAddress
        }
 
diff --git a/controllers/solrcloud_controller_ingress_test.go 
b/controllers/solrcloud_controller_ingress_test.go
index d28f7c4..a86a5c1 100644
--- a/controllers/solrcloud_controller_ingress_test.go
+++ b/controllers/solrcloud_controller_ingress_test.go
@@ -160,14 +160,14 @@ func TestIngressCloudReconcile(t *testing.T) {
        // Check the ingress
        ingress := expectIngress(g, requests, expectedCloudRequest, cloudIKey)
        testMapsEqual(t, "ingress labels", 
util.MergeLabelsOrAnnotations(instance.SharedLabelsWith(instance.Labels), 
testIngressLabels), ingress.Labels)
-       testMapsEqual(t, "ingress annotations", testIngressAnnotations, 
ingress.Annotations)
+       testMapsEqual(t, "ingress annotations", 
ingressLabelsWithDefaults(testIngressAnnotations), ingress.Annotations)
        testIngressRules(t, ingress, true, int(replicas), []string{testDomain}, 
4000, 100)
 
        // Check that the Addresses in the status are correct
        g.Eventually(func() error { return testClient.Get(context.TODO(), 
expectedCloudRequest.NamespacedName, instance) }, 
timeout).Should(gomega.Succeed())
        assert.Equal(t, 
"http://"+cloudCsKey.Name+"."+instance.Namespace+":4000";, 
instance.Status.InternalCommonAddress, "Wrong internal common address in 
status")
        assert.NotNil(t, instance.Status.ExternalCommonAddress, "External 
common address in Status should not be nil.")
-       assert.EqualValues(t, 
"http://"+instance.Namespace+"-"+instance.Name+"-solrcloud"+"."+testDomain+":4000";,
 *instance.Status.ExternalCommonAddress, "Wrong external common address in 
status")
+       assert.EqualValues(t, 
"http://"+instance.Namespace+"-"+instance.Name+"-solrcloud"+"."+testDomain, 
*instance.Status.ExternalCommonAddress, "Wrong external common address in 
status")
 }
 
 func TestIngressNoNodesCloudReconcile(t *testing.T) {
@@ -288,14 +288,14 @@ func TestIngressNoNodesCloudReconcile(t *testing.T) {
        // Check the ingress
        ingress := expectIngress(g, requests, expectedCloudRequest, cloudIKey)
        testMapsEqual(t, "ingress labels", 
util.MergeLabelsOrAnnotations(instance.SharedLabelsWith(instance.Labels), 
testIngressLabels), ingress.Labels)
-       testMapsEqual(t, "ingress annotations", testIngressAnnotations, 
ingress.Annotations)
+       testMapsEqual(t, "ingress annotations", 
ingressLabelsWithDefaults(testIngressAnnotations), ingress.Annotations)
        testIngressRules(t, ingress, true, 0, []string{testDomain}, 4000, 100)
 
        // Check that the Addresses in the status are correct
        g.Eventually(func() error { return testClient.Get(context.TODO(), 
expectedCloudRequest.NamespacedName, instance) }, 
timeout).Should(gomega.Succeed())
        assert.Equal(t, 
"http://"+cloudCsKey.Name+"."+instance.Namespace+":4000";, 
instance.Status.InternalCommonAddress, "Wrong internal common address in 
status")
        assert.NotNil(t, instance.Status.ExternalCommonAddress, "External 
common address in Status should not be nil.")
-       assert.EqualValues(t, 
"http://"+instance.Namespace+"-"+instance.Name+"-solrcloud"+"."+testDomain+":4000";,
 *instance.Status.ExternalCommonAddress, "Wrong external common address in 
status")
+       assert.EqualValues(t, 
"http://"+instance.Namespace+"-"+instance.Name+"-solrcloud"+"."+testDomain, 
*instance.Status.ExternalCommonAddress, "Wrong external common address in 
status")
 }
 
 func TestIngressNoCommonCloudReconcile(t *testing.T) {
@@ -422,7 +422,7 @@ func TestIngressNoCommonCloudReconcile(t *testing.T) {
        // Check the ingress
        ingress := expectIngress(g, requests, expectedCloudRequest, cloudIKey)
        testMapsEqual(t, "ingress labels", 
util.MergeLabelsOrAnnotations(instance.SharedLabelsWith(instance.Labels), 
testIngressLabels), ingress.Labels)
-       testMapsEqual(t, "ingress annotations", testIngressAnnotations, 
ingress.Annotations)
+       testMapsEqual(t, "ingress annotations", 
ingressLabelsWithDefaults(testIngressAnnotations), ingress.Annotations)
        testIngressRules(t, ingress, false, int(replicas), 
[]string{testDomain}, 4000, 100)
 
        // Check that the Addresses in the status are correct
@@ -549,14 +549,14 @@ func TestIngressUseInternalAddressCloudReconcile(t 
*testing.T) {
        // Check the ingress
        ingress := expectIngress(g, requests, expectedCloudRequest, cloudIKey)
        testMapsEqual(t, "ingress labels", 
util.MergeLabelsOrAnnotations(instance.SharedLabelsWith(instance.Labels), 
testIngressLabels), ingress.Labels)
-       testMapsEqual(t, "ingress annotations", testIngressAnnotations, 
ingress.Annotations)
+       testMapsEqual(t, "ingress annotations", 
ingressLabelsWithDefaults(testIngressAnnotations), ingress.Annotations)
        testIngressRules(t, ingress, true, int(replicas), []string{testDomain}, 
4000, 100)
 
        // Check that the Addresses in the status are correct
        g.Eventually(func() error { return testClient.Get(context.TODO(), 
expectedCloudRequest.NamespacedName, instance) }, 
timeout).Should(gomega.Succeed())
        assert.Equal(t, 
"http://"+cloudCsKey.Name+"."+instance.Namespace+":4000";, 
instance.Status.InternalCommonAddress, "Wrong internal common address in 
status")
        assert.NotNil(t, instance.Status.ExternalCommonAddress, "External 
common address in Status should not be nil.")
-       assert.EqualValues(t, 
"http://"+instance.Namespace+"-"+instance.Name+"-solrcloud"+"."+testDomain+":4000";,
 *instance.Status.ExternalCommonAddress, "Wrong external common address in 
status")
+       assert.EqualValues(t, 
"http://"+instance.Namespace+"-"+instance.Name+"-solrcloud"+"."+testDomain, 
*instance.Status.ExternalCommonAddress, "Wrong external common address in 
status")
 }
 
 func TestIngressExtraDomainsCloudReconcile(t *testing.T) {
@@ -684,14 +684,14 @@ func TestIngressExtraDomainsCloudReconcile(t *testing.T) {
        // Check the ingress
        ingress := expectIngress(g, requests, expectedCloudRequest, cloudIKey)
        testMapsEqual(t, "ingress labels", 
util.MergeLabelsOrAnnotations(instance.SharedLabelsWith(instance.Labels), 
testIngressLabels), ingress.Labels)
-       testMapsEqual(t, "ingress annotations", testIngressAnnotations, 
ingress.Annotations)
+       testMapsEqual(t, "ingress annotations", 
ingressLabelsWithDefaults(testIngressAnnotations), ingress.Annotations)
        testIngressRules(t, ingress, true, int(replicas), 
append([]string{testDomain}, testAdditionalDomains...), 4000, 100)
 
        // Check that the Addresses in the status are correct
        g.Eventually(func() error { return testClient.Get(context.TODO(), 
expectedCloudRequest.NamespacedName, instance) }, 
timeout).Should(gomega.Succeed())
        assert.Equal(t, 
"http://"+cloudCsKey.Name+"."+instance.Namespace+":4000";, 
instance.Status.InternalCommonAddress, "Wrong internal common address in 
status")
        assert.NotNil(t, instance.Status.ExternalCommonAddress, "External 
common address in Status should not be nil.")
-       assert.EqualValues(t, 
"http://"+instance.Namespace+"-"+instance.Name+"-solrcloud"+"."+testDomain+":4000";,
 *instance.Status.ExternalCommonAddress, "Wrong external common address in 
status")
+       assert.EqualValues(t, 
"http://"+instance.Namespace+"-"+instance.Name+"-solrcloud"+"."+testDomain, 
*instance.Status.ExternalCommonAddress, "Wrong external common address in 
status")
 }
 
 func TestIngressKubeDomainCloudReconcile(t *testing.T) {
@@ -808,7 +808,7 @@ func TestIngressKubeDomainCloudReconcile(t *testing.T) {
        // Check the ingress
        ingress := expectIngress(g, requests, expectedCloudRequest, cloudIKey)
        testMapsEqual(t, "ingress labels", 
util.MergeLabelsOrAnnotations(instance.SharedLabelsWith(instance.Labels), 
testIngressLabels), ingress.Labels)
-       testMapsEqual(t, "ingress annotations", testIngressAnnotations, 
ingress.Annotations)
+       testMapsEqual(t, "ingress annotations", 
ingressLabelsWithDefaults(testIngressAnnotations), ingress.Annotations)
        testIngressRules(t, ingress, true, int(replicas), []string{testDomain}, 
80, 100)
 
        // Check that the Addresses in the status are correct
@@ -861,3 +861,7 @@ func testIngressRules(t *testing.T, ingress *extv1.Ingress, 
withCommon bool, wit
                }
        }
 }
+
+func ingressLabelsWithDefaults(labels map[string]string) map[string]string {
+       return util.MergeLabelsOrAnnotations(labels, 
map[string]string{"nginx.ingress.kubernetes.io/backend-protocol": "HTTP"})
+}
diff --git a/controllers/solrcloud_controller_tls_test.go 
b/controllers/solrcloud_controller_tls_test.go
index 6afe170..44f3ac7 100644
--- a/controllers/solrcloud_controller_tls_test.go
+++ b/controllers/solrcloud_controller_tls_test.go
@@ -226,7 +226,7 @@ func TestEnableTLSOnExistingCluster(t *testing.T) {
        wg.Wait()
 
        expectStatefulSetTLSConfig(t, g, instance, false)
-       expectIngressTLSConfig(t, g, tlsSecretName)
+       expectPassthroughIngressTLSConfig(t, g, tlsSecretName, 
instance.Spec.SolrTLS)
 
        defer testClient.Delete(ctx, mockTLSSecret)
 }
@@ -392,6 +392,43 @@ func verifyReconcileUserSuppliedTLS(t *testing.T, instance 
*solr.SolrCloud, need
        }
 }
 
+func TestTLSCommonIngressTermination(t *testing.T) {
+       // now, update the config to enable TLS
+       tlsSecretName := "tls-cert-secret-from-user"
+
+       instance := buildTestSolrCloud()
+       instance.Spec.SolrSecurity = 
&solr.SolrSecurityOptions{AuthenticationType: solr.Basic}
+       instance.Spec.SolrAddressability.External.IngressTLSTerminationSecret = 
tlsSecretName
+
+       changed := instance.WithDefaults()
+       assert.True(t, changed, "WithDefaults should have changed the test 
SolrCloud instance")
+
+       g := gomega.NewGomegaWithT(t)
+       helper := NewTLSTestHelper(g)
+       defer func() {
+               helper.StopTest()
+       }()
+
+       ctx := context.TODO()
+       helper.ReconcileSolrCloud(ctx, instance, 1)
+
+       expectTerminateIngressTLSConfig(t, g, tlsSecretName, false)
+
+       // Check that the Addresses in the status are correct
+       g.Eventually(func() error {
+               return testClient.Get(context.TODO(), 
types.NamespacedName{Name: instance.Name, Namespace: instance.Namespace}, 
instance)
+       }, timeout).Should(gomega.Succeed())
+       assert.Equal(t, 
"http://"+instance.Name+"-solrcloud-common."+instance.Namespace, 
instance.Status.InternalCommonAddress, "Wrong internal common address in 
status")
+       if assert.NotNil(t, instance.Status.ExternalCommonAddress, "External 
common address in Status should not be nil.") {
+               assert.EqualValues(t, 
"https://"+instance.Namespace+"-"+instance.Name+"-solrcloud."+testDomain, 
*instance.Status.ExternalCommonAddress, "Wrong external common address in 
status")
+       }
+       assert.Equal(t, 
"http://"+instance.Name+"-solrcloud-common."+instance.Namespace, 
instance.Status.InternalCommonAddress, "Wrong internal common address in 
status")
+       if assert.NotNil(t, instance.Status.ExternalCommonAddress, "External 
common address in Status should not be nil.") {
+               assert.EqualValues(t, 
"https://"+instance.Namespace+"-"+instance.Name+"-solrcloud."+testDomain, 
*instance.Status.ExternalCommonAddress, "Wrong external common address in 
status")
+       }
+       defer testClient.Delete(ctx, instance)
+}
+
 func expectStatefulSetMountedTLSDirConfig(t *testing.T, g *gomega.GomegaWithT, 
sc *solr.SolrCloud) *appsv1.StatefulSet {
        ctx := context.TODO()
        stateful := &appsv1.StatefulSet{}
@@ -580,12 +617,38 @@ func expectStatefulSetBasicAuthConfig(t *testing.T, g 
*gomega.GomegaWithT, sc *s
        return stateful
 }
 
-func expectIngressTLSConfig(t *testing.T, g *gomega.GomegaWithT, 
expectedTLSSecretName string) {
+func expectPassthroughIngressTLSConfig(t *testing.T, g *gomega.GomegaWithT, 
expectedTLSSecretName string, solrTLS *solr.SolrTLSOptions) {
+       ingress := &netv1.Ingress{}
+       g.Eventually(func() error { return testClient.Get(context.TODO(), 
expectedIngressWithTLS, ingress) }, timeout).Should(gomega.Succeed())
+       assert.True(t, ingress.Spec.TLS != nil && len(ingress.Spec.TLS) == 1, 
"Wrong number of TLS Secrets for ingress")
+       assert.Equal(t, expectedTLSSecretName, ingress.Spec.TLS[0].SecretName, 
"Wrong secretName for ingress TLS")
+       if solrTLS != nil {
+               assert.Equal(t, "HTTPS", 
ingress.ObjectMeta.Annotations["nginx.ingress.kubernetes.io/backend-protocol"], 
"Ingress Backend Protocol annotation incorrect")
+       } else {
+               assert.Equal(t, "HTTP", 
ingress.ObjectMeta.Annotations["nginx.ingress.kubernetes.io/backend-protocol"], 
"Ingress Backend Protocol annotation incorrect")
+       }
+       if len(ingress.Spec.TLS) > 0 {
+               assert.Equal(t, "true", 
ingress.ObjectMeta.Annotations["nginx.ingress.kubernetes.io/ssl-redirect"], 
"Ingress SSL Redirect annotation incorrect")
+       } else {
+               assert.Equal(t, "", 
ingress.ObjectMeta.Annotations["nginx.ingress.kubernetes.io/ssl-redirect"], 
"Ingress SSL Redirect annotation incorrect")
+       }
+}
+
+func expectTerminateIngressTLSConfig(t *testing.T, g *gomega.GomegaWithT, 
expectedTLSSecretName string, isBackendTls bool) {
        ingress := &netv1.Ingress{}
        g.Eventually(func() error { return testClient.Get(context.TODO(), 
expectedIngressWithTLS, ingress) }, timeout).Should(gomega.Succeed())
-       assert.True(t, ingress.Spec.TLS != nil && len(ingress.Spec.TLS) == 1)
-       assert.Equal(t, expectedTLSSecretName, ingress.Spec.TLS[0].SecretName)
-       assert.Equal(t, "HTTPS", 
ingress.ObjectMeta.Annotations["nginx.ingress.kubernetes.io/backend-protocol"])
+       assert.Equal(t, 1, len(ingress.Spec.TLS), "Wrong number of TLS Secrets 
for ingress")
+       assert.Equal(t, expectedTLSSecretName, ingress.Spec.TLS[0].SecretName, 
"Wrong secretName for ingress TLS")
+       assert.Equal(t, 2, len(ingress.Spec.TLS[0].Hosts), "Wrong number of 
hosts for Ingress TLS termination")
+       assert.Equal(t, "default-foo-tls-solrcloud."+testDomain, 
ingress.Spec.TLS[0].Hosts[0], "Wrong common-host name for Ingress TLS 
termination")
+       assert.Equal(t, "default-foo-tls-solrcloud-0."+testDomain, 
ingress.Spec.TLS[0].Hosts[1], "Wrong common-host name for Ingress TLS 
termination")
+
+       if isBackendTls {
+               assert.Equal(t, "HTTPS", 
ingress.ObjectMeta.Annotations["nginx.ingress.kubernetes.io/backend-protocol"], 
"Ingress Backend Protocol annotation incorrect")
+       } else {
+               assert.Equal(t, "HTTP", 
ingress.ObjectMeta.Annotations["nginx.ingress.kubernetes.io/backend-protocol"], 
"Ingress Backend Protocol annotation incorrect")
+       }
+       assert.Equal(t, "true", 
ingress.ObjectMeta.Annotations["nginx.ingress.kubernetes.io/ssl-redirect"], 
"Ingress SSL Redirect annotation incorrect")
 }
 
 // Ensures config is setup for basic-auth enabled Solr pods
@@ -695,7 +758,6 @@ func buildTestSolrCloud() *solr.SolrCloud {
                                        Method:             solr.Ingress,
                                        UseExternalAddress: true,
                                        DomainName:         testDomain,
-                                       HideNodes:          true,
                                },
                        },
                },
diff --git a/controllers/util/solr_util.go b/controllers/util/solr_util.go
index b5a93e9..e34aa98 100644
--- a/controllers/util/solr_util.go
+++ b/controllers/util/solr_util.go
@@ -844,11 +844,26 @@ func GenerateIngress(solrCloud *solr.SolrCloud, nodeNames 
[]string) (ingress *ne
 
        extOpts := solrCloud.Spec.SolrAddressability.External
 
-       // Create advertised domain name and possible additional domain names
-       rules := CreateSolrIngressRules(solrCloud, nodeNames, 
append([]string{extOpts.DomainName}, extOpts.AdditionalDomainNames...))
+       // Create advertised domain name and possible additional domain names'
+       allDomains := append([]string{extOpts.DomainName}, 
extOpts.AdditionalDomainNames...)
+       rules, allHosts := CreateSolrIngressRules(solrCloud, nodeNames, 
allDomains)
 
        var ingressTLS []netv1.IngressTLS
        if solrCloud.Spec.SolrTLS != nil && solrCloud.Spec.SolrTLS.PKCS12Secret 
!= nil {
+               ingressTLS = append(ingressTLS, netv1.IngressTLS{SecretName: 
solrCloud.Spec.SolrTLS.PKCS12Secret.Name})
+       } // else if using mountedServerTLSDir, it's likely they'll have an 
auto-wired TLS solution for Ingress as well via annotations
+
+       if extOpts.IngressTLSTerminationSecret != "" {
+               ingressTLS = append(ingressTLS, netv1.IngressTLS{
+                       SecretName: extOpts.IngressTLSTerminationSecret,
+                       Hosts:      allHosts,
+               })
+       }
+       solrNodesRequireTLS := solrCloud.Spec.SolrTLS != nil
+       ingressFrontedByTLS := len(ingressTLS) > 0
+
+       // TLS Passthrough annotations
+       if solrNodesRequireTLS {
                if annotations == nil {
                        annotations = make(map[string]string, 1)
                }
@@ -856,8 +871,23 @@ func GenerateIngress(solrCloud *solr.SolrCloud, nodeNames 
[]string) (ingress *ne
                if !ok {
                        
annotations["nginx.ingress.kubernetes.io/backend-protocol"] = "HTTPS"
                }
-               ingressTLS = append(ingressTLS, netv1.IngressTLS{SecretName: 
solrCloud.Spec.SolrTLS.PKCS12Secret.Name})
-       } // else if using mountedServerTLSDir, it's likely they'll have an 
auto-wired TLS solution for Ingress as well via annotations
+       } else {
+               if annotations == nil {
+                       annotations = make(map[string]string, 1)
+               }
+               _, ok := 
annotations["nginx.ingress.kubernetes.io/backend-protocol"]
+               if !ok {
+                       
annotations["nginx.ingress.kubernetes.io/backend-protocol"] = "HTTP"
+               }
+       }
+
+       // TLS Accept annotations
+       if ingressFrontedByTLS {
+               _, ok := annotations["nginx.ingress.kubernetes.io/ssl-redirect"]
+               if !ok {
+                       annotations["nginx.ingress.kubernetes.io/ssl-redirect"] 
= "true"
+               }
+       }
 
        ingress = &netv1.Ingress{
                ObjectMeta: metav1.ObjectMeta{
@@ -878,21 +908,24 @@ func GenerateIngress(solrCloud *solr.SolrCloud, nodeNames 
[]string) (ingress *ne
 // solrCloud: SolrCloud instance
 // nodeNames: the names for each of the solr pods
 // domainName: string Domain for the ingress rule to use
-func CreateSolrIngressRules(solrCloud *solr.SolrCloud, nodeNames []string, 
domainNames []string) []netv1.IngressRule {
-       var ingressRules []netv1.IngressRule
+func CreateSolrIngressRules(solrCloud *solr.SolrCloud, nodeNames []string, 
domainNames []string) (ingressRules []netv1.IngressRule, allHosts []string) {
        if !solrCloud.Spec.SolrAddressability.External.HideCommon {
                for _, domainName := range domainNames {
-                       ingressRules = append(ingressRules, 
CreateCommonIngressRule(solrCloud, domainName))
+                       rule := CreateCommonIngressRule(solrCloud, domainName)
+                       ingressRules = append(ingressRules, rule)
+                       allHosts = append(allHosts, rule.Host)
                }
        }
        if !solrCloud.Spec.SolrAddressability.External.HideNodes {
                for _, nodeName := range nodeNames {
                        for _, domainName := range domainNames {
-                               ingressRules = append(ingressRules, 
CreateNodeIngressRule(solrCloud, nodeName, domainName))
+                               rule := CreateNodeIngressRule(solrCloud, 
nodeName, domainName)
+                               ingressRules = append(ingressRules, rule)
+                               allHosts = append(allHosts, rule.Host)
                        }
                }
        }
-       return ingressRules
+       return
 }
 
 // CreateCommonIngressRule returns a new Ingress Rule generated for a 
SolrCloud under the given domainName
@@ -1534,7 +1567,7 @@ func configureSecureProbeCommand(solrCloud 
*solr.SolrCloud, defaultProbeGetActio
                "-Dsolr.install.dir=\"/opt/solr\" 
-Dlog4j.configurationFile=\"/opt/solr/server/resources/log4j2-console.xml\" "+
                "-classpath 
\"/opt/solr/server/solr-webapp/webapp/WEB-INF/lib/*:/opt/solr/server/lib/ext/*:/opt/solr/server/lib/*\"
 "+
                "org.apache.solr.util.SolrCLI api -get %s://localhost:%d%s",
-               javaToolOptions, tlsJavaSysProps, enableBasicAuth, 
solrCloud.UrlScheme(), defaultProbeGetAction.Port.IntVal, 
defaultProbeGetAction.Path)
+               javaToolOptions, tlsJavaSysProps, enableBasicAuth, 
solrCloud.UrlScheme(false), defaultProbeGetAction.Port.IntVal, 
defaultProbeGetAction.Path)
        probeCommand = 
regexp.MustCompile(`\s+`).ReplaceAllString(strings.TrimSpace(probeCommand), " ")
 
        return probeCommand, vol, volMount
diff --git a/docs/solr-cloud/solr-cloud-crd.md 
b/docs/solr-cloud/solr-cloud-crd.md
index a5c3cef..6467948 100644
--- a/docs/solr-cloud/solr-cloud-crd.md
+++ b/docs/solr-cloud/solr-cloud-crd.md
@@ -307,7 +307,7 @@ data:
 ## Enable TLS Between Solr Pods
 _Since v0.3.0_
 
-A common approach to securing traffic to your Solr cluster is to perform **TLS 
termination** at the Ingress and leave all traffic between Solr pods 
un-encrypted.
+A common approach to securing traffic to your Solr cluster is to perform 
[**TLS termination** at the Ingress](#enable-ingress-tls-termination) and leave 
all traffic between Solr pods un-encrypted.
 However, depending on how you expose Solr on your network, you may also want 
to encrypt traffic between Solr pods.
 The Solr operator provides **optional** configuration settings to enable TLS 
for encrypting traffic between Solr pods.
 
@@ -322,7 +322,7 @@ Lastly, as of **v0.4.0**, you can supply the path to a 
directory containing TLS
 [cert-manager](https://cert-manager.io/docs/) is a popular Kubernetes 
controller for managing TLS certificates, including renewing certificates prior 
to expiration. 
 One of the primary benefits of cert-manager is it supports pluggable 
certificate `Issuer` implementations, including a self-signed Issuer for local 
development and an [ACME compliant](https://tools.ietf.org/html/rfc8555) Issuer 
for working with services like [Let’s Encrypt](https://letsencrypt.org/).
 
-If you already have a TLS certificate you want to use for Solr, then you don't 
need cert-manager and can skip down to [I already have a TLS 
Certificate](#-Already-Have-a-TLS-Certificate) later in this section.
+If you already have a TLS certificate you want to use for Solr, then you don't 
need cert-manager and can skip down to [I already have a TLS 
Certificate](#i-already-have-a-tls-certificate) later in this section.
 If you do not have a TLS certificate, then we recommend installing 
**cert-manager** as it makes working with TLS in Kubernetes much easier.
 
 #### Install cert-manager
@@ -341,7 +341,7 @@ issuers.cert-manager.io
 orders.acme.cert-manager.io
 ```
 
-If not intalled, use Helm to install it into the `cert-manager` namespace:
+If not installed, use Helm to install it into the `cert-manager` namespace:
 ```bash
 if ! helm repo list | grep -q "https://charts.jetstack.io";; then
   helm repo add jetstack https://charts.jetstack.io
@@ -532,7 +532,8 @@ When using the mounted TLS directory option, you need to 
ensure each Solr pod ge
 Consequently, we recommend using the `spec.updateStrategy.restartSchedule` to 
restart pods before the certificate expires. 
 Typically, with this scheme, a new certificate is issued whenever a pod is 
restarted.
 
-### Ingress
+
+### Ingress with TLS protected Solr
 
 The Solr operator may create an Ingress for exposing Solr pods externally. 
When TLS is enabled, the operator adds the following annotation and TLS 
settings to the Ingress manifest, such as:
 ```yaml
@@ -548,6 +549,10 @@ spec:
   - secretName: my-selfsigned-cert-tls
 ```
 
+If using the mounted TLS Directory option with an Ingress, you will need to 
inject the ingress with TLS information as well.
+The [Ingress TLS Termination section below](#enable-ingress-tls-termination) 
shows how this can be done when using cert-manager.
+
+
 ### Certificate Renewal and Rolling Restarts
 
 cert-manager automatically handles certificate renewal. From the docs:
@@ -640,7 +645,7 @@ which you can request TLS certificates from LetsEncrypt 
assuming you own the `k8
 Mutual TLS (mTLS) provides an additional layer of security by ensuring the 
client applications sending requests to Solr are trusted.
 To enable mTLS, simply set `spec.solrTLS.clientAuth` to either `Want` or 
`Need`. When mTLS is enabled, the Solr operator needs to
 supply a client certificate that is trusted by Solr; the operator makes API 
calls to Solr to get cluster status. 
-To configure the client certificate for the operator, see [Running the 
Operator > 
mTLS](../running-the-operator.md#Client-Auth-for-mTLS-enabled-Solr-clusters)
+To configure the client certificate for the operator, see [Running the 
Operator > 
mTLS](../running-the-operator.md#client-auth-for-mtls-enabled-solr-clusters)
 
 When mTLS is enabled, the liveness and readiness probes are configured to 
execute a local command on each Solr pod instead of the default HTTP Get 
request.
 Using a command is required so that we can use the correct TLS certificate 
when making an HTTPs call to the probe endpoints.
@@ -657,6 +662,62 @@ curl "https://localhost:8983/solr/admin/info/system"; -v \
 ```
 The `--cacert` option supplies the CA's certificate needed to trust the server 
certificate provided by the Solr pods during TLS handshake.
 
+## Enable Ingress TLS Termination
+_Since v0.4.0_
+
+A common approach to securing traffic to your Solr cluster is to perform **TLS 
termination** at the Ingress and either leave all traffic between Solr pods 
un-encrypted or use private CAs for inter-pod communication.
+The operator supports this paradigm, to ensure all external traffic is 
encrypted.
+
+```yaml
+kind: SolrCloud
+metadata:
+  name: search
+spec:
+  ... other SolrCloud CRD settings ...
+
+  solrAddressability:
+    external:
+      domainName: k8s.solr.cloud
+      method: Ingress
+      hideNodes: true
+      useExternalAddress: false
+      ingressTLSTerminationSecret: my-selfsigned-cert-tls
+```
+
+The only additional settings required here are:
+- Making sure that you are not using the external TLS address for Solr to 
communicate internally via `useExternalAddress: false`.
+  This will be ignored, even if it is set to `true`.
+- Adding a TLS secret through `ingressTLSTerminationSecret`, this is passed to 
the Kubernetes Ingress to handle the TLS termination.
+  _This ensures that the only way to communicate with your Solr cluster 
externally is through the TLS protected common-endpoint._
+
+To generate a TLS secret, follow the [instructions 
above](#use-cert-manager-to-issue-the-certificate) and use the templated 
Hostname: `<namespace>-<name>-solrcloud.<domain>`
+
+If you configure your SolrCloud correctly, cert-manager can auto-inject the 
TLS secrets for you as well:
+
+```yaml
+kind: SolrCloud
+metadata:
+  name: search
+  namespace: explore
+spec:
+  ... other SolrCloud CRD settings ...
+  customSolrKubeOptions:
+    ingressOptions:
+      annotations:
+        kubernetes.io/ingress.class: "nginx"
+        cert-manager.io/issuer: "<issuer-name>"
+        cert-manager.io/common-name: explore-search-solrcloud.apple.com
+  solrAddressability:
+    external:
+      domainName: k8s.solr.cloud
+      method: Ingress
+      hideNodes: true
+      useExternalAddress: false
+      ingressTLSTerminationSecret: myingress-cert
+```
+
+For more information on the Ingress TLS Termination options for cert-manager, 
[refer to the documentation](https://cert-manager.io/docs/usage/ingress/).
+
 ## Authentication and Authorization
 _Since v0.3.0_
 
diff --git a/helm/solr-operator/Chart.yaml b/helm/solr-operator/Chart.yaml
index e4cd341..4480034 100644
--- a/helm/solr-operator/Chart.yaml
+++ b/helm/solr-operator/Chart.yaml
@@ -126,6 +126,13 @@ annotations:
           url: https://github.com/apache/solr-operator/issues/291
         - name: Github PR
           url: https://github.com/apache/solr-operator/pull/292
+    - kind: added
+      description: Ability to terminate TLS at Ingress for SolrCloud.
+      links:
+        - name: Github Issue
+          url: https://github.com/apache/solr-operator/issues/268
+        - name: Github PR
+          url: https://github.com/apache/solr-operator/pull/293
   artifacthub.io/images: |
     - name: solr-operator
       image: apache/solr-operator:v0.4.0-prerelease
diff --git a/helm/solr-operator/crds/crds.yaml 
b/helm/solr-operator/crds/crds.yaml
index b2feddb..87de571 100644
--- a/helm/solr-operator/crds/crds.yaml
+++ b/helm/solr-operator/crds/crds.yaml
@@ -5626,6 +5626,9 @@ spec:
                       hideNodes:
                         description: Do not expose each of the Solr Node 
services externally. The number of services this affects could range from 1 (a 
headless service for ExternalDNS) to the number of Solr pods your cloud 
contains (individual node services for Ingress/LoadBalancer). Defaults to false.
                         type: boolean
+                      ingressTLSTerminationSecret:
+                        description: "IngressTLSTerminationSecret defines a 
TLS Secret to use for TLS termination of all exposed addresses in the ingress. 
\n This is option is only available when Method=Ingress, because ExternalDNS 
and LoadBalancer Services do not support TLS termination. This option is also 
unavailable when the SolrCloud has TLS enabled via `spec.solrTLS`, in this case 
the Ingress cannot terminate TLS before reaching Solr. \n When using this 
option, the UseExternalAddre [...]
+                        type: string
                       method:
                         description: The way in which this SolrCloud's 
service(s) should be made addressable externally.
                         enum:
diff --git a/helm/solr/README.md b/helm/solr/README.md
index e111984..ed90cb8 100644
--- a/helm/solr/README.md
+++ b/helm/solr/README.md
@@ -133,10 +133,11 @@ External addressability is disabled by default.
 | addressability.external.method | string | | The method by which Solr should 
be made addressable outside of the Kubernetes cluster. Either `Ingress` or 
`ExternalDNS` |
 | addressability.external.domainName | string | | The base domain name that 
Solr nodes should be addressed under. |
 | addressability.external.additionalDomainNames | []string | | Additional base 
domain names that Solr nodes should be addressed under. These are not used to 
advertise Solr locations, just the `domainName` is. |
+| addressability.external.useExternalAddress | boolean | `false` | Make the 
official hostname of the SolrCloud nodes the external address. This cannot be 
used when `hideNodes` is set to `true` or `ingressTLSTerminationSecret` is set 
to `true`. |
 | addressability.external.hideNodes | boolean | `false` | Do not make the 
individual Solr nodes addressable outside of the Kubernetes cluster. |
 | addressability.external.hideCommon | boolean | `false` | Do not make the 
load-balanced common Solr endpoint addressable outside of the Kubernetes 
cluster. |
-| addressability.external.nodePortOverride | int | | Override the port of 
individual Solr nodes when using the `Ingress` method. This will default to 
`80` if using an Ingress without TLS and `443` when using an Ingress with TLS. |
-
+| addressability.external.nodePortOverride | int | | Override the port of 
individual Solr nodes when using the `Ingress` method. This will default to 
`80` if using an Ingress without TLS and `443` when using an Ingress with Solr 
TLS enabled (not TLS Termination described below). |
+| addressability.external.ingressTLSTerminationSecret | int | | Name of 
Kubernetes Secret to terminate TLS when using the `Ingress` method. |
 
 ### ZK Options
 
@@ -196,7 +197,11 @@ Solr TLS is disabled by default. Provide any of the 
following to enable it.
 | solrTLS.trustStoreSecret.key | string |  | Key in the Secret that stores the 
Solr TLS truststore |
 | solrTLS.trustStorePasswordSecret.name | string |  | Name of the Secret that 
stores the Solr TLS truststore password |
 | solrTLS.trustStorePasswordSecret.key | string |  | Key in the Secret that 
stores the Solr TLS truststore password |
-| solrTLS.trustStorePasswordSecret.key | string |  | Key in the Secret that 
stores the Solr TLS truststore password |
+| solrTLS.mountedServerTLSDir.path | string | | The path on the main Solr 
container where the TLS files are mounted by some external agent or CSI Driver |
+| solrTLS.mountedServerTLSDir.keystoreFile | string | | Override the name of 
the keystore file; defaults to keystore.p12 |
+| solrTLS.mountedServerTLSDir.keystorePasswordFile | string | | Override the 
name of the keystore password file; defaults to keystore-password |
+| solrTLS.mountedServerTLSDir.truststoreFile | string | | Override the name of 
the truststore file; defaults truststore.p12. To use the same file as the 
keystore, override this variable with the name of your keystore file |
+| solrTLS.mountedServerTLSDir.truststorePasswordFile | string | | Override the 
name of the truststore password file; defaults to the same value as the 
KeystorePasswordFile |
 
 ### Global Options
 
diff --git a/helm/solr/values.yaml b/helm/solr/values.yaml
index 9a5f017..3cd5392 100644
--- a/helm/solr/values.yaml
+++ b/helm/solr/values.yaml
@@ -76,9 +76,11 @@ addressability:
     # method: "Ingress"
     # domainName: "example.com"
     # additionalDomainNames: []
+    # useExternalAddress: false
     # hideNodes: false
     # hideCommon: false
     # nodePortOverride: null
+    # ingressTLSTerminationSecret: ""
 
 # Specify how rolling updates should be managed for the Solr StatefulSet
 # 
https://apache.github.io/solr-operator/docs/solr-cloud/solr-cloud-crd.html#update-strategy
@@ -199,6 +201,12 @@ solrTLS: {}
   # verifyClientHostname: false
   # checkPeerName: false
   # restartOnTLSSecretUpdate: false
+  # mountedServerTLSDir:
+  #   path: /path/to/mounted/tls
+  #   keystoreFile: ""
+  #   keystorePasswordFile: ""
+  #   truststoreFile: ""
+  #   truststorePasswordFile: ""
 
 # Customize the Solr Pod for your needs
 podOptions:

Reply via email to