github.com/verrazzano/verrazzano-monitoring-operator@v0.0.30/pkg/resources/ingresses/ingress.go (about)

     1  // Copyright (C) 2020, 2022, Oracle and/or its affiliates.
     2  // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
     3  
     4  package ingresses
     5  
     6  import (
     7  	"fmt"
     8  	"strconv"
     9  
    10  	vmcontrollerv1 "github.com/verrazzano/verrazzano-monitoring-operator/pkg/apis/vmcontroller/v1"
    11  	"github.com/verrazzano/verrazzano-monitoring-operator/pkg/config"
    12  	"github.com/verrazzano/verrazzano-monitoring-operator/pkg/constants"
    13  	"github.com/verrazzano/verrazzano-monitoring-operator/pkg/resources"
    14  	"go.uber.org/zap"
    15  	netv1 "k8s.io/api/networking/v1"
    16  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    17  )
    18  
    19  var defaultIngressClassName = "verrazzano-nginx"
    20  
    21  func createIngressRuleElement(vmo *vmcontrollerv1.VerrazzanoMonitoringInstance, componentDetails config.ComponentDetails) netv1.IngressRule {
    22  	serviceName := resources.GetMetaName(vmo.Name, componentDetails.Name)
    23  	endpointName := componentDetails.EndpointName
    24  	if endpointName == "" {
    25  		endpointName = componentDetails.Name
    26  	}
    27  	fqdn := fmt.Sprintf("%s.%s", endpointName, vmo.Spec.URI)
    28  	pathType := netv1.PathTypeImplementationSpecific
    29  
    30  	return netv1.IngressRule{
    31  		Host: fqdn,
    32  		IngressRuleValue: netv1.IngressRuleValue{
    33  			HTTP: &netv1.HTTPIngressRuleValue{
    34  				Paths: []netv1.HTTPIngressPath{
    35  					{
    36  						Path:     "/",
    37  						PathType: &pathType,
    38  						Backend: netv1.IngressBackend{
    39  							Service: &netv1.IngressServiceBackend{
    40  								Name: serviceName,
    41  								Port: netv1.ServiceBackendPort{
    42  									Number: int32(componentDetails.Port),
    43  								},
    44  							},
    45  						},
    46  					},
    47  				},
    48  			},
    49  		},
    50  	}
    51  }
    52  
    53  func createIngressElementNoBasicAuth(vmo *vmcontrollerv1.VerrazzanoMonitoringInstance, hostName string, componentDetails config.ComponentDetails, ingressRule netv1.IngressRule) (*netv1.Ingress, error) {
    54  	var hosts = []string{hostName}
    55  	ingressClassName := getIngressClassName(vmo)
    56  	ingress := &netv1.Ingress{
    57  		ObjectMeta: metav1.ObjectMeta{
    58  			Annotations:     map[string]string{},
    59  			Labels:          resources.GetMetaLabels(vmo),
    60  			Name:            fmt.Sprintf("%s%s-%s", constants.VMOServiceNamePrefix, vmo.Name, componentDetails.Name),
    61  			Namespace:       vmo.Namespace,
    62  			OwnerReferences: resources.GetOwnerReferences(vmo),
    63  		},
    64  		Spec: netv1.IngressSpec{
    65  
    66  			TLS: []netv1.IngressTLS{
    67  				{
    68  					Hosts:      hosts,
    69  					SecretName: fmt.Sprintf("%s-tls-%s", vmo.Name, componentDetails.Name),
    70  				},
    71  			},
    72  			Rules:            []netv1.IngressRule{ingressRule},
    73  			IngressClassName: &ingressClassName,
    74  		},
    75  	}
    76  
    77  	ingress.Annotations["nginx.ingress.kubernetes.io/proxy-body-size"] = constants.NginxClientMaxBodySize
    78  
    79  	if len(vmo.Spec.IngressTargetDNSName) != 0 {
    80  		ingress.Annotations["external-dns.alpha.kubernetes.io/target"] = vmo.Spec.IngressTargetDNSName
    81  		ingress.Annotations["external-dns.alpha.kubernetes.io/ttl"] = strconv.Itoa(constants.ExternalDNSTTLSeconds)
    82  	}
    83  	// if we specify AutoSecret: true we attach an annotation that will create a cert
    84  	if vmo.Spec.AutoSecret {
    85  		// we must create a secret name too
    86  		ingress.Annotations["kubernetes.io/tls-acme"] = "true"
    87  	} else {
    88  		ingress.Annotations["kubernetes.io/tls-acme"] = "false"
    89  	}
    90  
    91  	ingress.Annotations["cert-manager.io/common-name"] = hostName
    92  	return ingress, nil
    93  }
    94  
    95  func addBasicAuthIngressAnnotations(vmo *vmcontrollerv1.VerrazzanoMonitoringInstance, ingress *netv1.Ingress, healthLocations string) {
    96  	ingress.Annotations["nginx.ingress.kubernetes.io/auth-type"] = "basic"
    97  	ingress.Annotations["nginx.ingress.kubernetes.io/auth-secret"] = vmo.Spec.SecretName
    98  	ingress.Annotations["nginx.ingress.kubernetes.io/auth-realm"] = vmo.Spec.URI + " auth"
    99  	//For custom location snippets k8s recommends we use server-snippet instead of configuration-snippet
   100  	// With ingress controller 0.24.1 our code using configuration-snippet no longer works
   101  	ingress.Annotations["nginx.ingress.kubernetes.io/server-snippet"] = healthLocations
   102  }
   103  
   104  func createIngressElement(vmo *vmcontrollerv1.VerrazzanoMonitoringInstance, hostName string, componentDetails config.ComponentDetails, ingressRule netv1.IngressRule, healthLocations string) (*netv1.Ingress, error) {
   105  	ingress, err := createIngressElementNoBasicAuth(vmo, hostName, componentDetails, ingressRule)
   106  	if err != nil {
   107  		return ingress, err
   108  	}
   109  	addBasicAuthIngressAnnotations(vmo, ingress, healthLocations)
   110  	return ingress, nil
   111  }
   112  
   113  // New will return a new Service for VMO that needs to executed for on Complete
   114  func New(vmo *vmcontrollerv1.VerrazzanoMonitoringInstance, existingIngresses map[string]*netv1.Ingress) ([]*netv1.Ingress, error) {
   115  	var ingresses []*netv1.Ingress
   116  
   117  	// Only create ingress if URI and secret name specified
   118  	if len(vmo.Spec.URI) <= 0 {
   119  		zap.S().Debugw("URI not specified, skipping ingress creation")
   120  		return ingresses, nil
   121  	}
   122  
   123  	// Create Ingress Rule for API Endpoint
   124  	if !config.API.Disabled {
   125  		ingRule := createIngressRuleElement(vmo, config.API)
   126  		host := config.API.Name + "." + vmo.Spec.URI
   127  		healthLocations := noAuthOnHealthCheckSnippet(vmo, "", config.API)
   128  		ingress, err := createIngressElement(vmo, host, config.API, ingRule, healthLocations)
   129  		if err != nil {
   130  			return ingresses, err
   131  		}
   132  		setNginxRoutingAnnotations(ingress)
   133  		ingresses = append(ingresses, ingress)
   134  	}
   135  
   136  	if vmo.Spec.Grafana.Enabled {
   137  		if config.Grafana.OidcProxy != nil {
   138  			ingresses = append(ingresses, newOidcProxyIngress(vmo, &config.Grafana))
   139  		} else {
   140  			// Create Ingress Rule for Grafana Endpoint
   141  			ingRule := createIngressRuleElement(vmo, config.Grafana)
   142  			host := config.Grafana.Name + "." + vmo.Spec.URI
   143  			ingress, err := createIngressElementNoBasicAuth(vmo, host, config.Grafana, ingRule)
   144  			if err != nil {
   145  				return ingresses, err
   146  			}
   147  			ingresses = append(ingresses, ingress)
   148  		}
   149  	}
   150  	if vmo.Spec.Kibana.Enabled {
   151  		if config.Kibana.OidcProxy != nil {
   152  			ingress := newOidcProxyIngress(vmo, &config.OpenSearchDashboards)
   153  			ingresses = append(ingresses, ingress)
   154  			redirectIngress := createRedirectIngressIfNecessary(vmo, existingIngresses, &config.Kibana, &config.OpenSearchDashboardsRedirect)
   155  			if redirectIngress != nil {
   156  				redirectIngress.Annotations["nginx.ingress.kubernetes.io/proxy-body-size"] = "65M"
   157  				redirectIngress.Annotations["nginx.ingress.kubernetes.io/permanent-redirect"] = "https://" + resources.OidcProxyIngressHost(vmo, &config.OpenSearchDashboards)
   158  				ingresses = append(ingresses, redirectIngress)
   159  			}
   160  		} else {
   161  			// Create Ingress Rule for Kibana Endpoint
   162  			ingRule := createIngressRuleElement(vmo, config.OpenSearchDashboards)
   163  			host := config.Kibana.Name + "." + vmo.Spec.URI
   164  			healthLocations := noAuthOnHealthCheckSnippet(vmo, "", config.OpenSearchDashboards)
   165  			ingress, err := createIngressElement(vmo, host, config.Kibana, ingRule, healthLocations)
   166  			if err != nil {
   167  				return ingresses, err
   168  			}
   169  			ingresses = append(ingresses, ingress)
   170  			redirectIngress := createRedirectIngressIfNecessary(vmo, existingIngresses, &config.Kibana, &config.OpenSearchDashboardsRedirect)
   171  			if redirectIngress != nil {
   172  				redirectIngress.Annotations["nginx.ingress.kubernetes.io/proxy-body-size"] = "65M"
   173  				redirectIngress.Annotations["nginx.ingress.kubernetes.io/permanent-redirect"] = "https://" + resources.OidcProxyIngressHost(vmo, &config.OpenSearchDashboards)
   174  				ingresses = append(ingresses, redirectIngress)
   175  			}
   176  		}
   177  	}
   178  	if vmo.Spec.Elasticsearch.Enabled {
   179  		if config.ElasticsearchIngest.OidcProxy != nil {
   180  			ingress := newOidcProxyIngress(vmo, &config.OpensearchIngest)
   181  			ingress.Annotations["nginx.ingress.kubernetes.io/proxy-body-size"] = "65M"
   182  			ingresses = append(ingresses, ingress)
   183  			redirectIngress := createRedirectIngressIfNecessary(vmo, existingIngresses, &config.ElasticsearchIngest, &config.OpensearchIngestRedirect)
   184  			if redirectIngress != nil {
   185  				redirectIngress.Annotations["nginx.ingress.kubernetes.io/proxy-body-size"] = "65M"
   186  				redirectIngress.Annotations["nginx.ingress.kubernetes.io/permanent-redirect"] = "https://" + resources.OidcProxyIngressHost(vmo, &config.OpensearchIngest)
   187  				ingresses = append(ingresses, redirectIngress)
   188  			}
   189  		} else {
   190  			var ingress *netv1.Ingress
   191  			ingRule := createIngressRuleElement(vmo, config.ElasticsearchIngest)
   192  			host := config.ElasticsearchIngest.EndpointName + "." + vmo.Spec.URI
   193  			healthLocations := noAuthOnHealthCheckSnippet(vmo, "", config.ElasticsearchIngest)
   194  			ingress, err := createIngressElement(vmo, host, config.ElasticsearchIngest, ingRule, healthLocations)
   195  			if err != nil {
   196  				return ingresses, err
   197  			}
   198  			ingress.Annotations["nginx.ingress.kubernetes.io/proxy-read-timeout"] = constants.NginxProxyReadTimeoutForKibana
   199  			ingresses = append(ingresses, ingress)
   200  			redirectIngress := createRedirectIngressIfNecessary(vmo, existingIngresses, &config.ElasticsearchIngest, &config.OpensearchIngestRedirect)
   201  			if redirectIngress != nil {
   202  				redirectIngress.Annotations["nginx.ingress.kubernetes.io/proxy-body-size"] = "65M"
   203  				redirectIngress.Annotations["nginx.ingress.kubernetes.io/permanent-redirect"] = "https://" + resources.OidcProxyIngressHost(vmo, &config.OpensearchIngest)
   204  				ingresses = append(ingresses, redirectIngress)
   205  			}
   206  		}
   207  
   208  	}
   209  	return ingresses, nil
   210  }
   211  
   212  // setNginxRoutingAnnotations adds the nginx annotations required for routing via istio envoy
   213  func setNginxRoutingAnnotations(ingress *netv1.Ingress) {
   214  	ingress.Annotations["nginx.ingress.kubernetes.io/service-upstream"] = "true"
   215  	ingress.Annotations["nginx.ingress.kubernetes.io/upstream-vhost"] = "${service_name}.${namespace}.svc.cluster.local"
   216  }
   217  
   218  // noAuthOnHealthCheckSnippet returns an NGINX configuration snippet with Basic Authentication disabled for the the
   219  // specified component's health check path.
   220  func noAuthOnHealthCheckSnippet(vmo *vmcontrollerv1.VerrazzanoMonitoringInstance, disambiguationRoot string, componentDetails config.ComponentDetails) string {
   221  	// Added = check so nginx matches only this path i.e. strict check
   222  	return `location = ` + disambiguationRoot + componentDetails.LivenessHTTPPath + ` {
   223     auth_basic off;
   224     auth_request off;
   225     proxy_pass  ` + fmt.Sprintf("http://%s.%s.svc.cluster.local:%d%s", constants.VMOServiceNamePrefix+vmo.Name+"-"+componentDetails.Name, vmo.Namespace, componentDetails.Port, componentDetails.LivenessHTTPPath) + `;
   226  }
   227  `
   228  }
   229  
   230  // newOidcProxyIngress creates the Ingress of the OidcProxy
   231  func newOidcProxyIngress(vmo *vmcontrollerv1.VerrazzanoMonitoringInstance, component *config.ComponentDetails) *netv1.Ingress {
   232  	port, err := strconv.ParseInt(resources.AuthProxyPort(), 10, 32)
   233  	if err != nil {
   234  		port = 8775
   235  	}
   236  	serviceName := resources.AuthProxyMetaName()
   237  	ingressHost := resources.OidcProxyIngressHost(vmo, component)
   238  	pathType := netv1.PathTypeImplementationSpecific
   239  	ingressClassName := getIngressClassName(vmo)
   240  	ingressRule := netv1.IngressRule{
   241  		Host: ingressHost,
   242  		IngressRuleValue: netv1.IngressRuleValue{
   243  			HTTP: &netv1.HTTPIngressRuleValue{
   244  				Paths: []netv1.HTTPIngressPath{
   245  					{
   246  						Path:     "/()(.*)",
   247  						PathType: &pathType,
   248  						Backend: netv1.IngressBackend{
   249  							Service: &netv1.IngressServiceBackend{
   250  								Name: serviceName,
   251  								Port: netv1.ServiceBackendPort{
   252  									Number: int32(port),
   253  								},
   254  							},
   255  						},
   256  					},
   257  				},
   258  			},
   259  		},
   260  	}
   261  	ingress := &netv1.Ingress{
   262  		ObjectMeta: metav1.ObjectMeta{
   263  			Annotations:     map[string]string{},
   264  			Labels:          resources.GetMetaLabels(vmo),
   265  			Name:            fmt.Sprintf("%s%s-%s", constants.VMOServiceNamePrefix, vmo.Name, component.Name),
   266  			Namespace:       vmo.Namespace,
   267  			OwnerReferences: resources.GetOwnerReferences(vmo),
   268  		},
   269  		Spec: netv1.IngressSpec{
   270  			TLS: []netv1.IngressTLS{
   271  				{
   272  					Hosts:      []string{ingressHost},
   273  					SecretName: fmt.Sprintf("%s-tls-%s", vmo.Name, component.Name),
   274  				},
   275  			},
   276  			Rules:            []netv1.IngressRule{ingressRule},
   277  			IngressClassName: &ingressClassName,
   278  		},
   279  	}
   280  	ingress.Annotations["nginx.ingress.kubernetes.io/proxy-body-size"] = constants.NginxClientMaxBodySize
   281  	if len(vmo.Spec.IngressTargetDNSName) != 0 {
   282  		ingress.Annotations["external-dns.alpha.kubernetes.io/target"] = vmo.Spec.IngressTargetDNSName
   283  		ingress.Annotations["external-dns.alpha.kubernetes.io/ttl"] = strconv.Itoa(constants.ExternalDNSTTLSeconds)
   284  	}
   285  	if vmo.Spec.AutoSecret {
   286  		ingress.Annotations["kubernetes.io/tls-acme"] = "true"
   287  	} else {
   288  		ingress.Annotations["kubernetes.io/tls-acme"] = "false"
   289  	}
   290  	ingress.Annotations["nginx.ingress.kubernetes.io/rewrite-target"] = "/$2"
   291  	setNginxRoutingAnnotations(ingress)
   292  	ingress.Annotations["cert-manager.io/common-name"] = ingressHost
   293  	return ingress
   294  }
   295  
   296  func getIngressClassName(vmi *vmcontrollerv1.VerrazzanoMonitoringInstance) string {
   297  	if vmi.Spec.IngressClassName != nil && *vmi.Spec.IngressClassName != "" {
   298  		return *vmi.Spec.IngressClassName
   299  	}
   300  	return defaultIngressClassName
   301  }
   302  
   303  // createRedirectIngressIfNecessary creates a new ingress for permanent redirection if required
   304  // For upgrade, if the user has deprecated Elasticsearch/Kibana ingress
   305  // Then create a new ingress for permanent redirection
   306  func createRedirectIngressIfNecessary(vmo *vmcontrollerv1.VerrazzanoMonitoringInstance, existingIngresses map[string]*netv1.Ingress, deprecatedIngressComponent *config.ComponentDetails, component *config.ComponentDetails) *netv1.Ingress {
   307  	var ingress *netv1.Ingress
   308  	// If the existing ingress with deprecated component name exists then create a new ingress for permanent redirection
   309  	if _, ok := existingIngresses[resources.GetMetaName(vmo.Name, deprecatedIngressComponent.Name)]; ok {
   310  		ingress = newOidcProxyIngress(vmo, component)
   311  	}
   312  	// If the redirect ingress exists then return the original redirect ingress.
   313  	if _, ok := existingIngresses[resources.GetMetaName(vmo.Name, component.Name)]; ok {
   314  		ingress = newOidcProxyIngress(vmo, component)
   315  	}
   316  	return ingress
   317  }