github.com/argoproj-labs/argocd-operator@v0.10.0/controllers/argocd/route.go (about)

     1  // Copyright 2019 ArgoCD Operator Developers
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  // 	http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package argocd
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"strings"
    21  
    22  	routev1 "github.com/openshift/api/route/v1"
    23  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    24  	"k8s.io/apimachinery/pkg/util/intstr"
    25  	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
    26  
    27  	argoproj "github.com/argoproj-labs/argocd-operator/api/v1beta1"
    28  	"github.com/argoproj-labs/argocd-operator/common"
    29  	"github.com/argoproj-labs/argocd-operator/controllers/argoutil"
    30  )
    31  
    32  const (
    33  	maxLabelLength    = 63
    34  	maxHostnameLength = 253
    35  	minFirstLabelSize = 20
    36  )
    37  
    38  var routeAPIFound = false
    39  
    40  // IsRouteAPIAvailable returns true if the Route API is present.
    41  func IsRouteAPIAvailable() bool {
    42  	return routeAPIFound
    43  }
    44  
    45  // verifyRouteAPI will verify that the Route API is present.
    46  func verifyRouteAPI() error {
    47  	found, err := argoutil.VerifyAPI(routev1.GroupName, routev1.GroupVersion.Version)
    48  	if err != nil {
    49  		return err
    50  	}
    51  	routeAPIFound = found
    52  	return nil
    53  }
    54  
    55  // newRoute returns a new Route instance for the given ArgoCD.
    56  func newRoute(cr *argoproj.ArgoCD) *routev1.Route {
    57  	return &routev1.Route{
    58  		ObjectMeta: metav1.ObjectMeta{
    59  			Name:      cr.Name,
    60  			Namespace: cr.Namespace,
    61  			Labels:    argoutil.LabelsForCluster(cr),
    62  		},
    63  	}
    64  }
    65  
    66  // newRouteWithName returns a new Route with the given name and ArgoCD.
    67  func newRouteWithName(name string, cr *argoproj.ArgoCD) *routev1.Route {
    68  	route := newRoute(cr)
    69  	route.ObjectMeta.Name = name
    70  
    71  	lbls := route.ObjectMeta.Labels
    72  	lbls[common.ArgoCDKeyName] = name
    73  	route.ObjectMeta.Labels = lbls
    74  
    75  	return route
    76  }
    77  
    78  // newRouteWithSuffix returns a new Route with the given name suffix for the ArgoCD.
    79  func newRouteWithSuffix(suffix string, cr *argoproj.ArgoCD) *routev1.Route {
    80  	return newRouteWithName(fmt.Sprintf("%s-%s", cr.Name, suffix), cr)
    81  }
    82  
    83  // reconcileRoutes will ensure that all ArgoCD Routes are present.
    84  func (r *ReconcileArgoCD) reconcileRoutes(cr *argoproj.ArgoCD) error {
    85  	if err := r.reconcileGrafanaRoute(cr); err != nil {
    86  		return err
    87  	}
    88  
    89  	if err := r.reconcilePrometheusRoute(cr); err != nil {
    90  		return err
    91  	}
    92  
    93  	if err := r.reconcileServerRoute(cr); err != nil {
    94  		return err
    95  	}
    96  
    97  	if err := r.reconcileApplicationSetControllerWebhookRoute(cr); err != nil {
    98  		return err
    99  	}
   100  
   101  	return nil
   102  }
   103  
   104  // reconcileGrafanaRoute will ensure that the ArgoCD Grafana Route is present.
   105  func (r *ReconcileArgoCD) reconcileGrafanaRoute(cr *argoproj.ArgoCD) error {
   106  	route := newRouteWithSuffix("grafana", cr)
   107  	if argoutil.IsObjectFound(r.Client, cr.Namespace, route.Name, route) {
   108  		if !cr.Spec.Grafana.Enabled || !cr.Spec.Grafana.Route.Enabled {
   109  			// Route exists but enabled flag has been set to false, delete the Route
   110  			return r.Client.Delete(context.TODO(), route)
   111  		}
   112  		log.Info(grafanaDeprecatedWarning)
   113  		return nil // Route found, do nothing
   114  	}
   115  
   116  	if !cr.Spec.Grafana.Enabled || !cr.Spec.Grafana.Route.Enabled {
   117  		return nil // Grafana itself or Route not enabled, do nothing.
   118  	}
   119  
   120  	log.Info(grafanaDeprecatedWarning)
   121  
   122  	return nil
   123  }
   124  
   125  // reconcilePrometheusRoute will ensure that the ArgoCD Prometheus Route is present.
   126  func (r *ReconcileArgoCD) reconcilePrometheusRoute(cr *argoproj.ArgoCD) error {
   127  	route := newRouteWithSuffix("prometheus", cr)
   128  	if argoutil.IsObjectFound(r.Client, cr.Namespace, route.Name, route) {
   129  		if !cr.Spec.Prometheus.Enabled || !cr.Spec.Prometheus.Route.Enabled {
   130  			// Route exists but enabled flag has been set to false, delete the Route
   131  			return r.Client.Delete(context.TODO(), route)
   132  		}
   133  		return nil // Route found, do nothing
   134  	}
   135  
   136  	if !cr.Spec.Prometheus.Enabled || !cr.Spec.Prometheus.Route.Enabled {
   137  		return nil // Prometheus itself or Route not enabled, do nothing.
   138  	}
   139  
   140  	// Allow override of the Annotations for the Route.
   141  	if len(cr.Spec.Prometheus.Route.Annotations) > 0 {
   142  		route.Annotations = cr.Spec.Prometheus.Route.Annotations
   143  	}
   144  
   145  	// Allow override of the Labels for the Route.
   146  	if len(cr.Spec.Prometheus.Route.Labels) > 0 {
   147  		labels := route.Labels
   148  		for key, val := range cr.Spec.Prometheus.Route.Labels {
   149  			labels[key] = val
   150  		}
   151  		route.Labels = labels
   152  	}
   153  
   154  	// Allow override of the Host for the Route.
   155  	if len(cr.Spec.Prometheus.Host) > 0 {
   156  		route.Spec.Host = cr.Spec.Prometheus.Host // TODO: What additional role needed for this?
   157  	}
   158  
   159  	route.Spec.Port = &routev1.RoutePort{
   160  		TargetPort: intstr.FromString("web"),
   161  	}
   162  
   163  	// Allow override of TLS options for the Route
   164  	if cr.Spec.Prometheus.Route.TLS != nil {
   165  		route.Spec.TLS = cr.Spec.Prometheus.Route.TLS
   166  	}
   167  
   168  	route.Spec.To.Kind = "Service"
   169  	route.Spec.To.Name = "prometheus-operated"
   170  
   171  	// Allow override of the WildcardPolicy for the Route
   172  	if cr.Spec.Prometheus.Route.WildcardPolicy != nil && len(*cr.Spec.Prometheus.Route.WildcardPolicy) > 0 {
   173  		route.Spec.WildcardPolicy = *cr.Spec.Prometheus.Route.WildcardPolicy
   174  	}
   175  
   176  	if err := controllerutil.SetControllerReference(cr, route, r.Scheme); err != nil {
   177  		return err
   178  	}
   179  	return r.Client.Create(context.TODO(), route)
   180  }
   181  
   182  // reconcileServerRoute will ensure that the ArgoCD Server Route is present.
   183  func (r *ReconcileArgoCD) reconcileServerRoute(cr *argoproj.ArgoCD) error {
   184  
   185  	route := newRouteWithSuffix("server", cr)
   186  	found := argoutil.IsObjectFound(r.Client, cr.Namespace, route.Name, route)
   187  	if found {
   188  		if !cr.Spec.Server.Route.Enabled {
   189  			// Route exists but enabled flag has been set to false, delete the Route
   190  			return r.Client.Delete(context.TODO(), route)
   191  		}
   192  	}
   193  
   194  	if !cr.Spec.Server.Route.Enabled {
   195  		return nil // Route not enabled, move along...
   196  	}
   197  
   198  	// Allow override of the Annotations for the Route.
   199  	if len(cr.Spec.Server.Route.Annotations) > 0 {
   200  		route.Annotations = cr.Spec.Server.Route.Annotations
   201  	}
   202  
   203  	// Allow override of the Labels for the Route.
   204  	if len(cr.Spec.Server.Route.Labels) > 0 {
   205  		labels := route.Labels
   206  		for key, val := range cr.Spec.Server.Route.Labels {
   207  			labels[key] = val
   208  		}
   209  		route.Labels = labels
   210  	}
   211  
   212  	// Allow override of the Host for the Route.
   213  	if len(cr.Spec.Server.Host) > 0 {
   214  		route.Spec.Host = cr.Spec.Server.Host // TODO: What additional role needed for this?
   215  	}
   216  
   217  	hostname, err := shortenHostname(route.Spec.Host)
   218  	if err != nil {
   219  		return err
   220  	}
   221  
   222  	route.Spec.Host = hostname
   223  
   224  	if cr.Spec.Server.Insecure {
   225  		// Disable TLS and rely on the cluster certificate.
   226  		route.Spec.Port = &routev1.RoutePort{
   227  			TargetPort: intstr.FromString("http"),
   228  		}
   229  		route.Spec.TLS = &routev1.TLSConfig{
   230  			InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyRedirect,
   231  			Termination:                   routev1.TLSTerminationEdge,
   232  		}
   233  	} else {
   234  		// Server is using TLS configure passthrough.
   235  		route.Spec.Port = &routev1.RoutePort{
   236  			TargetPort: intstr.FromString("https"),
   237  		}
   238  		route.Spec.TLS = &routev1.TLSConfig{
   239  			InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyRedirect,
   240  			Termination:                   routev1.TLSTerminationPassthrough,
   241  		}
   242  	}
   243  
   244  	// Allow override of TLS options for the Route
   245  	if cr.Spec.Server.Route.TLS != nil {
   246  		route.Spec.TLS = cr.Spec.Server.Route.TLS
   247  	}
   248  
   249  	route.Spec.To.Kind = "Service"
   250  	route.Spec.To.Name = nameWithSuffix("server", cr)
   251  
   252  	// Allow override of the WildcardPolicy for the Route
   253  	if cr.Spec.Server.Route.WildcardPolicy != nil && len(*cr.Spec.Server.Route.WildcardPolicy) > 0 {
   254  		route.Spec.WildcardPolicy = *cr.Spec.Server.Route.WildcardPolicy
   255  	}
   256  
   257  	if err := controllerutil.SetControllerReference(cr, route, r.Scheme); err != nil {
   258  		return err
   259  	}
   260  	if !found {
   261  		return r.Client.Create(context.TODO(), route)
   262  	}
   263  	return r.Client.Update(context.TODO(), route)
   264  }
   265  
   266  // reconcileApplicationSetControllerWebhookRoute will ensure that the ArgoCD Server Route is present.
   267  func (r *ReconcileArgoCD) reconcileApplicationSetControllerWebhookRoute(cr *argoproj.ArgoCD) error {
   268  	name := fmt.Sprintf("%s-%s", common.ApplicationSetServiceNameSuffix, "webhook")
   269  	route := newRouteWithSuffix(name, cr)
   270  	found := argoutil.IsObjectFound(r.Client, cr.Namespace, route.Name, route)
   271  	if found {
   272  		if cr.Spec.ApplicationSet == nil || !cr.Spec.ApplicationSet.WebhookServer.Route.Enabled {
   273  			// Route exists but enabled flag has been set to false, delete the Route
   274  			return r.Client.Delete(context.TODO(), route)
   275  		}
   276  	}
   277  
   278  	if cr.Spec.ApplicationSet == nil || !cr.Spec.ApplicationSet.WebhookServer.Route.Enabled {
   279  		return nil // Route not enabled, move along...
   280  	}
   281  
   282  	// Allow override of the Annotations for the Route.
   283  	if len(cr.Spec.ApplicationSet.WebhookServer.Route.Annotations) > 0 {
   284  		route.Annotations = cr.Spec.ApplicationSet.WebhookServer.Route.Annotations
   285  	}
   286  
   287  	// Allow override of the Labels for the Route.
   288  	if len(cr.Spec.ApplicationSet.WebhookServer.Route.Labels) > 0 {
   289  		labels := route.Labels
   290  		for key, val := range cr.Spec.ApplicationSet.WebhookServer.Route.Labels {
   291  			labels[key] = val
   292  		}
   293  		route.Labels = labels
   294  	}
   295  
   296  	// Allow override of the Host for the Route.
   297  	if len(cr.Spec.ApplicationSet.WebhookServer.Host) > 0 {
   298  		route.Spec.Host = cr.Spec.ApplicationSet.WebhookServer.Host
   299  	}
   300  
   301  	hostname, err := shortenHostname(route.Spec.Host)
   302  	if err != nil {
   303  		return err
   304  	}
   305  
   306  	route.Spec.Host = hostname
   307  
   308  	route.Spec.Port = &routev1.RoutePort{
   309  		TargetPort: intstr.FromString("webhook"),
   310  	}
   311  
   312  	// Allow override of TLS options for the Route
   313  	if cr.Spec.ApplicationSet.WebhookServer.Route.TLS != nil {
   314  		tls := &routev1.TLSConfig{}
   315  
   316  		// Set Termination
   317  		if cr.Spec.ApplicationSet.WebhookServer.Route.TLS.Termination != "" {
   318  			tls.Termination = cr.Spec.ApplicationSet.WebhookServer.Route.TLS.Termination
   319  		} else {
   320  			tls.Termination = routev1.TLSTerminationEdge
   321  		}
   322  
   323  		// Set Certificate
   324  		if cr.Spec.ApplicationSet.WebhookServer.Route.TLS.Certificate != "" {
   325  			tls.Certificate = cr.Spec.ApplicationSet.WebhookServer.Route.TLS.Certificate
   326  		}
   327  
   328  		// Set Key
   329  		if cr.Spec.ApplicationSet.WebhookServer.Route.TLS.Key != "" {
   330  			tls.Key = cr.Spec.ApplicationSet.WebhookServer.Route.TLS.Key
   331  		}
   332  
   333  		// Set CACertificate
   334  		if cr.Spec.ApplicationSet.WebhookServer.Route.TLS.CACertificate != "" {
   335  			tls.CACertificate = cr.Spec.ApplicationSet.WebhookServer.Route.TLS.CACertificate
   336  		}
   337  
   338  		// Set DestinationCACertificate
   339  		if cr.Spec.ApplicationSet.WebhookServer.Route.TLS.DestinationCACertificate != "" {
   340  			tls.DestinationCACertificate = cr.Spec.ApplicationSet.WebhookServer.Route.TLS.DestinationCACertificate
   341  		}
   342  
   343  		// Set InsecureEdgeTerminationPolicy
   344  		if cr.Spec.ApplicationSet.WebhookServer.Route.TLS.InsecureEdgeTerminationPolicy != "" {
   345  			tls.InsecureEdgeTerminationPolicy = cr.Spec.ApplicationSet.WebhookServer.Route.TLS.InsecureEdgeTerminationPolicy
   346  		} else {
   347  			tls.InsecureEdgeTerminationPolicy = routev1.InsecureEdgeTerminationPolicyRedirect
   348  		}
   349  
   350  		route.Spec.TLS = tls
   351  	} else {
   352  		// Disable TLS and rely on the cluster certificate.
   353  		route.Spec.TLS = &routev1.TLSConfig{
   354  			InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyRedirect,
   355  			Termination:                   routev1.TLSTerminationEdge,
   356  		}
   357  	}
   358  
   359  	route.Spec.To.Kind = "Service"
   360  	route.Spec.To.Name = nameWithSuffix(common.ApplicationSetServiceNameSuffix, cr)
   361  
   362  	// Allow override of the WildcardPolicy for the Route
   363  	if cr.Spec.ApplicationSet.WebhookServer.Route.WildcardPolicy != nil && len(*cr.Spec.ApplicationSet.WebhookServer.Route.WildcardPolicy) > 0 {
   364  		route.Spec.WildcardPolicy = *cr.Spec.ApplicationSet.WebhookServer.Route.WildcardPolicy
   365  	}
   366  
   367  	if err := controllerutil.SetControllerReference(cr, route, r.Scheme); err != nil {
   368  		return err
   369  	}
   370  	if !found {
   371  		return r.Client.Create(context.TODO(), route)
   372  	}
   373  	return r.Client.Update(context.TODO(), route)
   374  }
   375  
   376  // The algorithm used by this function is:
   377  // - If the FIRST label ("console-openshift-console" in the above case) is longer than 63 characters, shorten (truncate the end) it to 63.
   378  // - If any other label is longer than 63 characters, return an error
   379  // - After all the labels are 63 characters or less, check the length of the overall hostname:
   380  //   - If the overall hostname is > 253, then shorten the FIRST label until the host name is < 253
   381  //   - After the FIRST label has been shortened, if it is < 20, then return an error (this is a sanity test to ensure the label is likely to be unique)
   382  func shortenHostname(hostname string) (string, error) {
   383  	if hostname == "" {
   384  		return "", nil
   385  	}
   386  
   387  	// Return the hostname as it is if hostname is already within the size limit
   388  	if len(hostname) <= maxHostnameLength {
   389  		return hostname, nil
   390  	}
   391  
   392  	// Split the hostname into labels
   393  	labels := strings.Split(hostname, ".")
   394  
   395  	// Check and truncate the FIRST label if longer than 63 characters
   396  	if len(labels[0]) > maxLabelLength {
   397  		labels[0] = labels[0][:maxLabelLength]
   398  	}
   399  
   400  	// Check other labels and return an error if any is longer than 63 characters
   401  	for _, label := range labels[1:] {
   402  		if len(label) > maxLabelLength {
   403  			return "", fmt.Errorf("label length exceeds 63 characters")
   404  		}
   405  	}
   406  
   407  	// Join the labels back into a hostname
   408  	resultHostname := strings.Join(labels, ".")
   409  
   410  	// Check and shorten the overall hostname
   411  	if len(resultHostname) > maxHostnameLength {
   412  		// Shorten the first label until the length is less than 253
   413  		for len(resultHostname) > maxHostnameLength && len(labels[0]) > 20 {
   414  			labels[0] = labels[0][:len(labels[0])-1]
   415  			resultHostname = strings.Join(labels, ".")
   416  		}
   417  
   418  		// Check if the first label is still less than 20 characters
   419  		if len(labels[0]) < minFirstLabelSize {
   420  			return "", fmt.Errorf("shortened first label is less than 20 characters")
   421  		}
   422  	}
   423  	return resultHostname, nil
   424  }