diff --git a/docs/orchestrating-elastic-stack-applications/accessing-elastic-services.asciidoc b/docs/orchestrating-elastic-stack-applications/accessing-elastic-services.asciidoc index 6d5364d2bf..e67d640807 100644 --- a/docs/orchestrating-elastic-stack-applications/accessing-elastic-services.asciidoc +++ b/docs/orchestrating-elastic-stack-applications/accessing-elastic-services.asciidoc @@ -213,52 +213,3 @@ PW=$(kubectl get secret "$NAME-es-elastic-user" -o go-template='{{.data.elastic curl --cacert tls.crt -u elastic:$PW https://$IP:9200/ ---- -Now you should get this message: - -[source,sh] ----- -curl: (51) SSL: no alternative certificate subject name matches target host name '35.198.131.115' ----- - -The request failed because the IP address used to access the service does not match the subject of the certificate, which is set to the service name. We can force curl to temporarily map the IP address to the expected service name by using the `--resolve` flag as follows: - -[source,sh] ----- -> curl --cacert tls.crt -u elastic:$PW https://$NAME-es-http:9200/ --resolve $NAME-es-http:9200:$IP -{ - "name" : "hulk-es-4qk62zd928", - "cluster_name" : "hulk", - "cluster_uuid" : "q6itjqFqRqW576FXF0uohg", - "version" : {...}, - "tagline" : "You Know, for Search" -} ----- - -You can add extra Subject Alternative Name (SAN) entries to the generated certificate by setting the `subjectAltNames` field of the Elasticsearch specification, as illustrated in the example below. Adding the external IP address to the SANs eliminates the need to explicitly configure clients to override the certificate subject comparison. - -[source,yaml] ----- -spec: - http: - service: - spec: - type: LoadBalancer - tls: - selfSignedCertificate: - subjectAltNames: - - ip: 35.198.131.115 ----- - -You can now reach Elasticsearch without `--resolve`: - -[source,sh] ----- -> curl --cacert tls.crt -u elastic:$PW https://$IP:9200/ -{ - "name" : "hulk-es-4qk62zd928", - "cluster_name" : "hulk", - "cluster_uuid" : "q6itjqFqRqW576FXF0uohg", - "version" : {...}, - "tagline" : "You Know, for Search" -} ----- diff --git a/pkg/controller/common/certificates/http_reconcile.go b/pkg/controller/common/certificates/http_reconcile.go index 267793981c..16d4b81383 100644 --- a/pkg/controller/common/certificates/http_reconcile.go +++ b/pkg/controller/common/certificates/http_reconcile.go @@ -349,10 +349,11 @@ func createValidatedHTTPCertificateTemplate( certCommonName, // eg. clusterName-es-http.default.es.local shortName, // eg. clusterName-es-http } - var ipAddresses []net.IP + var ipAddresses []net.IP //nolint:prealloc for _, svc := range svcs { dnsNames = append(dnsNames, k8s.GetServiceDNSName(svc)...) + ipAddresses = append(ipAddresses, k8s.GetServiceIPAddresses(svc)...) } if selfSignedCerts := tls.SelfSignedCertificate; selfSignedCerts != nil { diff --git a/pkg/utils/k8s/k8sutils.go b/pkg/utils/k8s/k8sutils.go index cf935203a3..05911441f2 100644 --- a/pkg/utils/k8s/k8sutils.go +++ b/pkg/utils/k8s/k8sutils.go @@ -6,6 +6,7 @@ package k8s import ( "fmt" + "net" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -14,6 +15,8 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/tools/record" "sigs.k8s.io/controller-runtime/pkg/client" + + netutil "github.com/elastic/cloud-on-k8s/pkg/utils/net" ) // ToObjectMeta returns an ObjectMeta based on the given NamespacedName. @@ -61,12 +64,43 @@ func PodNames(pods []corev1.Pod) []string { return names } -// GetServiceDNSName returns the fully qualified DNS name for a service +// GetServiceDNSName returns the fully qualified DNS name for a service along with any external names provided by ingresses. func GetServiceDNSName(svc corev1.Service) []string { - return []string{ + names := []string{ fmt.Sprintf("%s.%s.svc", svc.Name, svc.Namespace), fmt.Sprintf("%s.%s", svc.Name, svc.Namespace), } + + if svc.Spec.Type == corev1.ServiceTypeLoadBalancer { + for _, ingress := range svc.Status.LoadBalancer.Ingress { + if ingress.Hostname != "" { + names = append(names, ingress.Hostname) + } + } + } + + return names +} + +func GetServiceIPAddresses(svc corev1.Service) []net.IP { + var ipAddrs []net.IP + + if len(svc.Spec.ExternalIPs) > 0 { + ipAddrs = make([]net.IP, len(svc.Spec.ExternalIPs)) + for i, externalIP := range svc.Spec.ExternalIPs { + ipAddrs[i] = netutil.IPToRFCForm(net.ParseIP(externalIP)) + } + } + + if svc.Spec.Type == corev1.ServiceTypeLoadBalancer { + for _, ingress := range svc.Status.LoadBalancer.Ingress { + if ingress.IP != "" { + ipAddrs = append(ipAddrs, netutil.IPToRFCForm(net.ParseIP(ingress.IP))) + } + } + } + + return ipAddrs } // EmitErrorEvent emits an event if the error is report-worthy diff --git a/pkg/utils/k8s/k8sutils_test.go b/pkg/utils/k8s/k8sutils_test.go index 3529ba360b..cb344e7f9b 100644 --- a/pkg/utils/k8s/k8sutils_test.go +++ b/pkg/utils/k8s/k8sutils_test.go @@ -5,6 +5,7 @@ package k8s import ( + "net" "testing" "github.com/go-test/deep" @@ -13,6 +14,8 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + + netutil "github.com/elastic/cloud-on-k8s/pkg/utils/net" ) func TestToObjectMeta(t *testing.T) { @@ -47,6 +50,27 @@ func TestGetServiceDNSName(t *testing.T) { }, want: []string{"test-name.test-ns.svc", "test-name.test-ns"}, }, + { + name: "load balancer service", + args: args{ + svc: corev1.Service{ + ObjectMeta: metav1.ObjectMeta{Namespace: "test-ns", Name: "test-name"}, + Spec: corev1.ServiceSpec{Type: corev1.ServiceTypeLoadBalancer}, + Status: corev1.ServiceStatus{LoadBalancer: corev1.LoadBalancerStatus{Ingress: []corev1.LoadBalancerIngress{{Hostname: "mysvc.lb"}}}}, + }, + }, + want: []string{"test-name.test-ns.svc", "test-name.test-ns", "mysvc.lb"}, + }, + { + name: "load balancer service (no status)", + args: args{ + svc: corev1.Service{ + ObjectMeta: metav1.ObjectMeta{Namespace: "test-ns", Name: "test-name"}, + Spec: corev1.ServiceSpec{Type: corev1.ServiceTypeLoadBalancer}, + }, + }, + want: []string{"test-name.test-ns.svc", "test-name.test-ns"}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -57,6 +81,45 @@ func TestGetServiceDNSName(t *testing.T) { } } +func TestGetServiceIPAddresses(t *testing.T) { + testCases := []struct { + name string + svc corev1.Service + want []net.IP + }{ + { + name: "ClusterIP service", + svc: corev1.Service{Spec: corev1.ServiceSpec{Type: corev1.ServiceTypeClusterIP}}, + want: nil, + }, + { + name: "NodePort service with external IP addresses", + svc: corev1.Service{Spec: corev1.ServiceSpec{Type: corev1.ServiceTypeNodePort, ExternalIPs: []string{"1.2.3.4", "2001:db8:a0b:12f0::1"}}}, + want: []net.IP{netutil.IPToRFCForm(net.ParseIP("1.2.3.4")), netutil.IPToRFCForm(net.ParseIP("2001:db8:a0b:12f0::1"))}, + }, + { + name: "LoadBalancer service", + svc: corev1.Service{ + Spec: corev1.ServiceSpec{Type: corev1.ServiceTypeLoadBalancer}, + Status: corev1.ServiceStatus{LoadBalancer: corev1.LoadBalancerStatus{Ingress: []corev1.LoadBalancerIngress{{IP: "1.2.3.4"}}}}, + }, + want: []net.IP{netutil.IPToRFCForm(net.ParseIP("1.2.3.4"))}, + }, + { + name: "LoadBalancer service (no status)", + svc: corev1.Service{Spec: corev1.ServiceSpec{Type: corev1.ServiceTypeLoadBalancer}}, + want: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + have := GetServiceIPAddresses(tc.svc) + require.Equal(t, tc.want, have) + }) + } +} + func TestOverrideControllerReference(t *testing.T) { ownerRefFixture := func(name string, controller bool) metav1.OwnerReference {