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 }