github.com/argoproj-labs/argocd-operator@v0.10.0/controllers/argocd/util.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 "bytes" 19 "context" 20 "crypto/rand" 21 "encoding/base64" 22 "fmt" 23 "os" 24 "reflect" 25 "sort" 26 "strconv" 27 "strings" 28 "text/template" 29 30 "sigs.k8s.io/controller-runtime/pkg/builder" 31 32 "gopkg.in/yaml.v2" 33 34 "github.com/argoproj/argo-cd/v2/util/glob" 35 36 "github.com/argoproj-labs/argocd-operator/api/v1alpha1" 37 argoproj "github.com/argoproj-labs/argocd-operator/api/v1beta1" 38 "github.com/argoproj-labs/argocd-operator/common" 39 "github.com/argoproj-labs/argocd-operator/controllers/argoutil" 40 41 monitoringv1 "github.com/coreos/prometheus-operator/pkg/apis/monitoring/v1" 42 oappsv1 "github.com/openshift/api/apps/v1" 43 configv1 "github.com/openshift/api/config/v1" 44 routev1 "github.com/openshift/api/route/v1" 45 configv1client "github.com/openshift/client-go/config/clientset/versioned/typed/config/v1" 46 "github.com/sethvargo/go-password/password" 47 "golang.org/x/mod/semver" 48 appsv1 "k8s.io/api/apps/v1" 49 corev1 "k8s.io/api/core/v1" 50 networkingv1 "k8s.io/api/networking/v1" 51 v1 "k8s.io/api/rbac/v1" 52 53 apierrors "k8s.io/apimachinery/pkg/api/errors" 54 55 "k8s.io/apimachinery/pkg/api/resource" 56 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 57 "k8s.io/apimachinery/pkg/labels" 58 "k8s.io/apimachinery/pkg/selection" 59 "k8s.io/apimachinery/pkg/types" 60 "k8s.io/client-go/kubernetes" 61 "sigs.k8s.io/controller-runtime/pkg/client" 62 "sigs.k8s.io/controller-runtime/pkg/client/config" 63 "sigs.k8s.io/controller-runtime/pkg/event" 64 "sigs.k8s.io/controller-runtime/pkg/handler" 65 "sigs.k8s.io/controller-runtime/pkg/predicate" 66 ) 67 68 const ( 69 grafanaDeprecatedWarning = "Warning: grafana field is deprecated from ArgoCD: field will be ignored." 70 ) 71 72 var ( 73 versionAPIFound = false 74 ) 75 76 // IsVersionAPIAvailable returns true if the version api is present 77 func IsVersionAPIAvailable() bool { 78 return versionAPIFound 79 } 80 81 // verifyVersionAPI will verify that the template API is present. 82 func verifyVersionAPI() error { 83 found, err := argoutil.VerifyAPI(configv1.GroupName, configv1.GroupVersion.Version) 84 if err != nil { 85 return err 86 } 87 versionAPIFound = found 88 return nil 89 } 90 91 // generateArgoAdminPassword will generate and return the admin password for Argo CD. 92 func generateArgoAdminPassword() ([]byte, error) { 93 pass, err := password.Generate( 94 common.ArgoCDDefaultAdminPasswordLength, 95 common.ArgoCDDefaultAdminPasswordNumDigits, 96 common.ArgoCDDefaultAdminPasswordNumSymbols, 97 false, false) 98 99 return []byte(pass), err 100 } 101 102 // generateArgoServerKey will generate and return the server signature key for session validation. 103 func generateArgoServerSessionKey() ([]byte, error) { 104 pass, err := password.Generate( 105 common.ArgoCDDefaultServerSessionKeyLength, 106 common.ArgoCDDefaultServerSessionKeyNumDigits, 107 common.ArgoCDDefaultServerSessionKeyNumSymbols, 108 false, false) 109 110 return []byte(pass), err 111 } 112 113 // getArgoApplicationControllerResources will return the ResourceRequirements for the Argo CD application controller container. 114 func getArgoApplicationControllerResources(cr *argoproj.ArgoCD) corev1.ResourceRequirements { 115 resources := corev1.ResourceRequirements{} 116 117 // Allow override of resource requirements from CR 118 if cr.Spec.Controller.Resources != nil { 119 resources = *cr.Spec.Controller.Resources 120 } 121 122 return resources 123 } 124 125 // getArgoApplicationControllerCommand will return the command for the ArgoCD Application Controller component. 126 func getArgoApplicationControllerCommand(cr *argoproj.ArgoCD, useTLSForRedis bool) []string { 127 cmd := []string{ 128 "argocd-application-controller", 129 "--operation-processors", fmt.Sprint(getArgoServerOperationProcessors(cr)), 130 } 131 132 if cr.Spec.Redis.IsEnabled() { 133 cmd = append(cmd, "--redis", getRedisServerAddress(cr)) 134 } else { 135 log.Info("Redis is Disabled. Skipping adding Redis configuration to Application Controller.") 136 } 137 138 if useTLSForRedis { 139 cmd = append(cmd, "--redis-use-tls") 140 if isRedisTLSVerificationDisabled(cr) { 141 cmd = append(cmd, "--redis-insecure-skip-tls-verify") 142 } else { 143 cmd = append(cmd, "--redis-ca-certificate", "/app/config/controller/tls/redis/tls.crt") 144 } 145 } 146 147 if cr.Spec.Repo.IsEnabled() { 148 cmd = append(cmd, "--repo-server", getRepoServerAddress(cr)) 149 } else { 150 log.Info("Repo Server is disabled. This would affect the functioning of Application Controller.") 151 } 152 153 cmd = append(cmd, "--status-processors", fmt.Sprint(getArgoServerStatusProcessors(cr))) 154 cmd = append(cmd, "--kubectl-parallelism-limit", fmt.Sprint(getArgoControllerParellismLimit(cr))) 155 156 if cr.Spec.SourceNamespaces != nil && len(cr.Spec.SourceNamespaces) > 0 { 157 cmd = append(cmd, "--application-namespaces", fmt.Sprint(strings.Join(cr.Spec.SourceNamespaces, ","))) 158 } 159 160 cmd = append(cmd, "--loglevel") 161 cmd = append(cmd, getLogLevel(cr.Spec.Controller.LogLevel)) 162 163 cmd = append(cmd, "--logformat") 164 cmd = append(cmd, getLogFormat(cr.Spec.Controller.LogFormat)) 165 166 return cmd 167 } 168 169 // getArgoContainerImage will return the container image for ArgoCD. 170 func getArgoContainerImage(cr *argoproj.ArgoCD) string { 171 defaultTag, defaultImg := false, false 172 img := cr.Spec.Image 173 if img == "" { 174 img = common.ArgoCDDefaultArgoImage 175 defaultImg = true 176 } 177 178 tag := cr.Spec.Version 179 if tag == "" { 180 tag = common.ArgoCDDefaultArgoVersion 181 defaultTag = true 182 } 183 if e := os.Getenv(common.ArgoCDImageEnvName); e != "" && (defaultTag && defaultImg) { 184 return e 185 } 186 187 return argoutil.CombineImageTag(img, tag) 188 } 189 190 // getRepoServerContainerImage will return the container image for the Repo server. 191 // 192 // There are three possible options for configuring the image, and this is the 193 // order of preference. 194 // 195 // 1. from the Spec, the spec.repo field has an image and version to use for 196 // generating an image reference. 197 // 2. from the Environment, this looks for the `ARGOCD_REPOSERVER_IMAGE` field and uses 198 // that if the spec is not configured. 199 // 3. the default is configured in common.ArgoCDDefaultRepoServerVersion and 200 // common.ArgoCDDefaultRepoServerImage. 201 func getRepoServerContainerImage(cr *argoproj.ArgoCD) string { 202 defaultImg, defaultTag := false, false 203 img := cr.Spec.Repo.Image 204 if img == "" { 205 img = common.ArgoCDDefaultArgoImage 206 defaultImg = true 207 } 208 209 tag := cr.Spec.Repo.Version 210 if tag == "" { 211 tag = common.ArgoCDDefaultArgoVersion 212 defaultTag = true 213 } 214 if e := os.Getenv(common.ArgoCDImageEnvName); e != "" && (defaultTag && defaultImg) { 215 return e 216 } 217 return argoutil.CombineImageTag(img, tag) 218 } 219 220 // getArgoRepoResources will return the ResourceRequirements for the Argo CD Repo server container. 221 func getArgoRepoResources(cr *argoproj.ArgoCD) corev1.ResourceRequirements { 222 resources := corev1.ResourceRequirements{} 223 224 // Allow override of resource requirements from CR 225 if cr.Spec.Repo.Resources != nil { 226 resources = *cr.Spec.Repo.Resources 227 } 228 229 return resources 230 } 231 232 // getArgoServerInsecure returns the insecure value for the ArgoCD Server component. 233 func getArgoServerInsecure(cr *argoproj.ArgoCD) bool { 234 return cr.Spec.Server.Insecure 235 } 236 237 func isRepoServerTLSVerificationRequested(cr *argoproj.ArgoCD) bool { 238 return cr.Spec.Repo.VerifyTLS 239 } 240 241 func isRedisTLSVerificationDisabled(cr *argoproj.ArgoCD) bool { 242 return cr.Spec.Redis.DisableTLSVerification 243 } 244 245 // getArgoServerGRPCHost will return the GRPC host for the given ArgoCD. 246 func getArgoServerGRPCHost(cr *argoproj.ArgoCD) string { 247 host := nameWithSuffix("grpc", cr) 248 if len(cr.Spec.Server.GRPC.Host) > 0 { 249 host = cr.Spec.Server.GRPC.Host 250 } 251 return host 252 } 253 254 // getArgoServerHost will return the host for the given ArgoCD. 255 func getArgoServerHost(cr *argoproj.ArgoCD) string { 256 host := cr.Name 257 if len(cr.Spec.Server.Host) > 0 { 258 host = cr.Spec.Server.Host 259 } 260 return host 261 } 262 263 // getArgoServerResources will return the ResourceRequirements for the Argo CD server container. 264 func getArgoServerResources(cr *argoproj.ArgoCD) corev1.ResourceRequirements { 265 resources := corev1.ResourceRequirements{} 266 267 if cr.Spec.Server.Autoscale.Enabled { 268 resources = corev1.ResourceRequirements{ 269 Limits: corev1.ResourceList{ 270 corev1.ResourceCPU: resource.MustParse(common.ArgoCDDefaultServerResourceLimitCPU), 271 corev1.ResourceMemory: resource.MustParse(common.ArgoCDDefaultServerResourceLimitMemory), 272 }, 273 Requests: corev1.ResourceList{ 274 corev1.ResourceCPU: resource.MustParse(common.ArgoCDDefaultServerResourceRequestCPU), 275 corev1.ResourceMemory: resource.MustParse(common.ArgoCDDefaultServerResourceRequestMemory), 276 }, 277 } 278 } 279 280 // Allow override of resource requirements from CR 281 if cr.Spec.Server.Resources != nil { 282 resources = *cr.Spec.Server.Resources 283 } 284 285 return resources 286 } 287 288 // getArgoServerURI will return the URI for the ArgoCD server. 289 // The hostname for argocd-server is from the route, ingress, an external hostname or service name in that order. 290 func (r *ReconcileArgoCD) getArgoServerURI(cr *argoproj.ArgoCD) string { 291 host := nameWithSuffix("server", cr) // Default to service name 292 293 // Use the external hostname provided by the user 294 if cr.Spec.Server.Host != "" { 295 host = cr.Spec.Server.Host 296 } 297 298 // Use Ingress host if enabled 299 if cr.Spec.Server.Ingress.Enabled { 300 ing := newIngressWithSuffix("server", cr) 301 if argoutil.IsObjectFound(r.Client, cr.Namespace, ing.Name, ing) { 302 host = ing.Spec.Rules[0].Host 303 } 304 } 305 306 // Use Route host if available, override Ingress if both exist 307 if IsRouteAPIAvailable() { 308 route := newRouteWithSuffix("server", cr) 309 if argoutil.IsObjectFound(r.Client, cr.Namespace, route.Name, route) { 310 host = route.Spec.Host 311 } 312 } 313 314 return fmt.Sprintf("https://%s", host) // TODO: Safe to assume HTTPS here? 315 } 316 317 // getArgoServerOperationProcessors will return the numeric Operation Processors value for the ArgoCD Server. 318 func getArgoServerOperationProcessors(cr *argoproj.ArgoCD) int32 { 319 op := common.ArgoCDDefaultServerOperationProcessors 320 if cr.Spec.Controller.Processors.Operation > 0 { 321 op = cr.Spec.Controller.Processors.Operation 322 } 323 return op 324 } 325 326 // getArgoServerStatusProcessors will return the numeric Status Processors value for the ArgoCD Server. 327 func getArgoServerStatusProcessors(cr *argoproj.ArgoCD) int32 { 328 sp := common.ArgoCDDefaultServerStatusProcessors 329 if cr.Spec.Controller.Processors.Status > 0 { 330 sp = cr.Spec.Controller.Processors.Status 331 } 332 return sp 333 } 334 335 // getArgoControllerParellismLimit returns the parallelism limit for the application controller 336 func getArgoControllerParellismLimit(cr *argoproj.ArgoCD) int32 { 337 pl := common.ArgoCDDefaultControllerParallelismLimit 338 if cr.Spec.Controller.ParallelismLimit > 0 { 339 pl = cr.Spec.Controller.ParallelismLimit 340 } 341 return pl 342 } 343 344 // getRedisConfigPath will return the path for the Redis configuration templates. 345 func getRedisConfigPath() string { 346 path := os.Getenv("REDIS_CONFIG_PATH") 347 if len(path) > 0 { 348 return path 349 } 350 return common.ArgoCDDefaultRedisConfigPath 351 } 352 353 // getRedisInitScript will load the redis configuration from a template on disk for the given ArgoCD. 354 // If an error occurs, an empty string value will be returned. 355 func getRedisConf(useTLSForRedis bool) string { 356 path := fmt.Sprintf("%s/redis.conf.tpl", getRedisConfigPath()) 357 params := map[string]string{ 358 "UseTLS": strconv.FormatBool(useTLSForRedis), 359 } 360 conf, err := loadTemplateFile(path, params) 361 if err != nil { 362 log.Error(err, "unable to load redis configuration") 363 return "" 364 } 365 return conf 366 } 367 368 // getRedisContainerImage will return the container image for the Redis server. 369 func getRedisContainerImage(cr *argoproj.ArgoCD) string { 370 defaultImg, defaultTag := false, false 371 img := cr.Spec.Redis.Image 372 if img == "" { 373 img = common.ArgoCDDefaultRedisImage 374 defaultImg = true 375 } 376 tag := cr.Spec.Redis.Version 377 if tag == "" { 378 tag = common.ArgoCDDefaultRedisVersion 379 defaultTag = true 380 } 381 if e := os.Getenv(common.ArgoCDRedisImageEnvName); e != "" && (defaultTag && defaultImg) { 382 return e 383 } 384 return argoutil.CombineImageTag(img, tag) 385 } 386 387 // getRedisHAContainerImage will return the container image for the Redis server in HA mode. 388 func getRedisHAContainerImage(cr *argoproj.ArgoCD) string { 389 defaultImg, defaultTag := false, false 390 img := cr.Spec.Redis.Image 391 if img == "" { 392 img = common.ArgoCDDefaultRedisImage 393 defaultImg = true 394 } 395 tag := cr.Spec.Redis.Version 396 if tag == "" { 397 tag = common.ArgoCDDefaultRedisVersionHA 398 defaultTag = true 399 } 400 if e := os.Getenv(common.ArgoCDRedisHAImageEnvName); e != "" && (defaultTag && defaultImg) { 401 return e 402 } 403 return argoutil.CombineImageTag(img, tag) 404 } 405 406 // getRedisHAProxyAddress will return the Redis HA Proxy service address for the given ArgoCD. 407 func getRedisHAProxyAddress(cr *argoproj.ArgoCD) string { 408 return fqdnServiceRef("redis-ha-haproxy", common.ArgoCDDefaultRedisPort, cr) 409 } 410 411 // getRedisHAProxyContainerImage will return the container image for the Redis HA Proxy. 412 func getRedisHAProxyContainerImage(cr *argoproj.ArgoCD) string { 413 defaultImg, defaultTag := false, false 414 img := cr.Spec.HA.RedisProxyImage 415 if len(img) <= 0 { 416 img = common.ArgoCDDefaultRedisHAProxyImage 417 defaultImg = true 418 } 419 420 tag := cr.Spec.HA.RedisProxyVersion 421 if len(tag) <= 0 { 422 tag = common.ArgoCDDefaultRedisHAProxyVersion 423 defaultTag = true 424 } 425 426 if e := os.Getenv(common.ArgoCDRedisHAProxyImageEnvName); e != "" && (defaultTag && defaultImg) { 427 return e 428 } 429 430 return argoutil.CombineImageTag(img, tag) 431 } 432 433 // getRedisInitScript will load the redis init script from a template on disk for the given ArgoCD. 434 // If an error occurs, an empty string value will be returned. 435 func getRedisInitScript(cr *argoproj.ArgoCD, useTLSForRedis bool) string { 436 path := fmt.Sprintf("%s/init.sh.tpl", getRedisConfigPath()) 437 vars := map[string]string{ 438 "ServiceName": nameWithSuffix("redis-ha", cr), 439 "UseTLS": strconv.FormatBool(useTLSForRedis), 440 } 441 442 script, err := loadTemplateFile(path, vars) 443 if err != nil { 444 log.Error(err, "unable to load redis init-script") 445 return "" 446 } 447 return script 448 } 449 450 // getRedisHAProxySConfig will load the Redis HA Proxy configuration from a template on disk for the given ArgoCD. 451 // If an error occurs, an empty string value will be returned. 452 func getRedisHAProxyConfig(cr *argoproj.ArgoCD, useTLSForRedis bool) string { 453 path := fmt.Sprintf("%s/haproxy.cfg.tpl", getRedisConfigPath()) 454 vars := map[string]string{ 455 "ServiceName": nameWithSuffix("redis-ha", cr), 456 "UseTLS": strconv.FormatBool(useTLSForRedis), 457 } 458 459 script, err := loadTemplateFile(path, vars) 460 if err != nil { 461 log.Error(err, "unable to load redis haproxy configuration") 462 return "" 463 } 464 return script 465 } 466 467 // getRedisHAProxyScript will load the Redis HA Proxy init script from a template on disk for the given ArgoCD. 468 // If an error occurs, an empty string value will be returned. 469 func getRedisHAProxyScript(cr *argoproj.ArgoCD) string { 470 path := fmt.Sprintf("%s/haproxy_init.sh.tpl", getRedisConfigPath()) 471 vars := map[string]string{ 472 "ServiceName": nameWithSuffix("redis-ha", cr), 473 } 474 475 script, err := loadTemplateFile(path, vars) 476 if err != nil { 477 log.Error(err, "unable to load redis haproxy init script") 478 return "" 479 } 480 return script 481 } 482 483 // getRedisResources will return the ResourceRequirements for the Redis container. 484 func getRedisResources(cr *argoproj.ArgoCD) corev1.ResourceRequirements { 485 resources := corev1.ResourceRequirements{} 486 487 // Allow override of resource requirements from CR 488 if cr.Spec.Redis.Resources != nil { 489 resources = *cr.Spec.Redis.Resources 490 } 491 492 return resources 493 } 494 495 // getRedisHAResources will return the ResourceRequirements for the Redis HA. 496 func getRedisHAResources(cr *argoproj.ArgoCD) corev1.ResourceRequirements { 497 resources := corev1.ResourceRequirements{} 498 499 // Allow override of resource requirements from CR 500 if cr.Spec.HA.Resources != nil { 501 resources = *cr.Spec.HA.Resources 502 } 503 504 return resources 505 } 506 507 // getRedisSentinelConf will load the redis sentinel configuration from a template on disk for the given ArgoCD. 508 // If an error occurs, an empty string value will be returned. 509 func getRedisSentinelConf(useTLSForRedis bool) string { 510 path := fmt.Sprintf("%s/sentinel.conf.tpl", getRedisConfigPath()) 511 params := map[string]string{ 512 "UseTLS": strconv.FormatBool(useTLSForRedis), 513 } 514 conf, err := loadTemplateFile(path, params) 515 if err != nil { 516 log.Error(err, "unable to load redis sentinel configuration") 517 return "" 518 } 519 return conf 520 } 521 522 // getRedisLivenessScript will load the redis liveness script from a template on disk for the given ArgoCD. 523 // If an error occurs, an empty string value will be returned. 524 func getRedisLivenessScript(useTLSForRedis bool) string { 525 path := fmt.Sprintf("%s/redis_liveness.sh.tpl", getRedisConfigPath()) 526 params := map[string]string{ 527 "UseTLS": strconv.FormatBool(useTLSForRedis), 528 } 529 conf, err := loadTemplateFile(path, params) 530 if err != nil { 531 log.Error(err, "unable to load redis liveness script") 532 return "" 533 } 534 return conf 535 } 536 537 // getRedisReadinessScript will load the redis readiness script from a template on disk for the given ArgoCD. 538 // If an error occurs, an empty string value will be returned. 539 func getRedisReadinessScript(useTLSForRedis bool) string { 540 path := fmt.Sprintf("%s/redis_readiness.sh.tpl", getRedisConfigPath()) 541 params := map[string]string{ 542 "UseTLS": strconv.FormatBool(useTLSForRedis), 543 } 544 conf, err := loadTemplateFile(path, params) 545 if err != nil { 546 log.Error(err, "unable to load redis readiness script") 547 return "" 548 } 549 return conf 550 } 551 552 // getSentinelLivenessScript will load the redis liveness script from a template on disk for the given ArgoCD. 553 // If an error occurs, an empty string value will be returned. 554 func getSentinelLivenessScript(useTLSForRedis bool) string { 555 path := fmt.Sprintf("%s/sentinel_liveness.sh.tpl", getRedisConfigPath()) 556 params := map[string]string{ 557 "UseTLS": strconv.FormatBool(useTLSForRedis), 558 } 559 conf, err := loadTemplateFile(path, params) 560 if err != nil { 561 log.Error(err, "unable to load sentinel liveness script") 562 return "" 563 } 564 return conf 565 } 566 567 // getRedisServerAddress will return the Redis service address for the given ArgoCD. 568 func getRedisServerAddress(cr *argoproj.ArgoCD) string { 569 if cr.Spec.Redis.Remote != nil && *cr.Spec.Redis.Remote != "" { 570 return *cr.Spec.Redis.Remote 571 } 572 if cr.Spec.HA.Enabled { 573 return getRedisHAProxyAddress(cr) 574 } 575 return fqdnServiceRef(common.ArgoCDDefaultRedisSuffix, common.ArgoCDDefaultRedisPort, cr) 576 } 577 578 // loadTemplateFile will parse a template with the given path and execute it with the given params. 579 func loadTemplateFile(path string, params map[string]string) (string, error) { 580 tmpl, err := template.ParseFiles(path) 581 if err != nil { 582 log.Error(err, "unable to parse template") 583 return "", err 584 } 585 586 buf := new(bytes.Buffer) 587 err = tmpl.Execute(buf, params) 588 if err != nil { 589 log.Error(err, "unable to execute template") 590 return "", err 591 } 592 return buf.String(), nil 593 } 594 595 // nameWithSuffix will return a name based on the given ArgoCD. The given suffix is appended to the generated name. 596 // Example: Given an ArgoCD with the name "example-argocd", providing the suffix "foo" would result in the value of 597 // "example-argocd-foo" being returned. 598 func nameWithSuffix(suffix string, cr *argoproj.ArgoCD) string { 599 return fmt.Sprintf("%s-%s", cr.Name, suffix) 600 } 601 602 // fqdnServiceRef will return the FQDN referencing a specific service name, as set up by the operator, with the 603 // given port. 604 func fqdnServiceRef(service string, port int, cr *argoproj.ArgoCD) string { 605 return fmt.Sprintf("%s.%s.svc.cluster.local:%d", nameWithSuffix(service, cr), cr.Namespace, port) 606 } 607 608 // InspectCluster will verify the availability of extra features available to the cluster, such as Prometheus and 609 // OpenShift Routes. 610 func InspectCluster() error { 611 if err := verifyPrometheusAPI(); err != nil { 612 return err 613 } 614 615 if err := verifyRouteAPI(); err != nil { 616 return err 617 } 618 619 if err := verifyTemplateAPI(); err != nil { 620 return err 621 } 622 623 if err := verifyVersionAPI(); err != nil { 624 return err 625 } 626 return nil 627 } 628 629 // reconcileCertificateAuthority will reconcile all Certificate Authority resources. 630 func (r *ReconcileArgoCD) reconcileCertificateAuthority(cr *argoproj.ArgoCD) error { 631 log.Info("reconciling CA secret") 632 if err := r.reconcileClusterCASecret(cr); err != nil { 633 return err 634 } 635 636 log.Info("reconciling CA config map") 637 if err := r.reconcileCAConfigMap(cr); err != nil { 638 return err 639 } 640 return nil 641 } 642 643 func (r *ReconcileArgoCD) redisShouldUseTLS(cr *argoproj.ArgoCD) bool { 644 var tlsSecretObj corev1.Secret 645 tlsSecretName := types.NamespacedName{Namespace: cr.Namespace, Name: common.ArgoCDRedisServerTLSSecretName} 646 err := r.Client.Get(context.TODO(), tlsSecretName, &tlsSecretObj) 647 if err != nil { 648 if !apierrors.IsNotFound(err) { 649 log.Error(err, "error looking up redis tls secret") 650 } 651 return false 652 } 653 654 secretOwnerRefs := tlsSecretObj.GetOwnerReferences() 655 if len(secretOwnerRefs) > 0 { 656 // OpenShift service CA makes the owner reference for the TLS secret to the 657 // service, which in turn is owned by the controller. This method performs 658 // a lookup of the controller through the intermediate owning service. 659 for _, secretOwner := range secretOwnerRefs { 660 if isOwnerOfInterest(secretOwner) { 661 key := client.ObjectKey{Name: secretOwner.Name, Namespace: tlsSecretObj.GetNamespace()} 662 svc := &corev1.Service{} 663 664 // Get the owning object of the secret 665 err := r.Client.Get(context.TODO(), key, svc) 666 if err != nil { 667 log.Error(err, fmt.Sprintf("could not get owner of secret %s", tlsSecretObj.GetName())) 668 return false 669 } 670 671 // If there's an object of kind ArgoCD in the owner's list, 672 // this will be our reconciled object. 673 serviceOwnerRefs := svc.GetOwnerReferences() 674 for _, serviceOwner := range serviceOwnerRefs { 675 if serviceOwner.Kind == "ArgoCD" { 676 return true 677 } 678 } 679 } 680 } 681 } else { 682 // For secrets without owner (i.e. manually created), we apply some 683 // heuristics. This may not be as accurate (e.g. if the user made a 684 // typo in the resource's name), but should be good enough for now. 685 if _, ok := tlsSecretObj.Annotations[common.AnnotationName]; ok { 686 return true 687 } 688 } 689 return false 690 } 691 692 // reconcileResources will reconcile common ArgoCD resources. 693 func (r *ReconcileArgoCD) reconcileResources(cr *argoproj.ArgoCD) error { 694 695 // we reconcile SSO first so that we can catch and throw errors for any illegal SSO configurations right away, and return control from here 696 // preventing dex resources from getting created anyway through the other function calls, effectively bypassing the SSO checks 697 log.Info("reconciling SSO") 698 if err := r.reconcileSSO(cr); err != nil { 699 log.Info(err.Error()) 700 } 701 702 log.Info("reconciling status") 703 if err := r.reconcileStatus(cr); err != nil { 704 log.Info(err.Error()) 705 } 706 707 log.Info("reconciling roles") 708 if err := r.reconcileRoles(cr); err != nil { 709 log.Info(err.Error()) 710 return err 711 } 712 713 log.Info("reconciling rolebindings") 714 if err := r.reconcileRoleBindings(cr); err != nil { 715 log.Info(err.Error()) 716 return err 717 } 718 719 log.Info("reconciling service accounts") 720 if err := r.reconcileServiceAccounts(cr); err != nil { 721 log.Info(err.Error()) 722 return err 723 } 724 725 log.Info("reconciling certificate authority") 726 if err := r.reconcileCertificateAuthority(cr); err != nil { 727 return err 728 } 729 730 log.Info("reconciling secrets") 731 if err := r.reconcileSecrets(cr); err != nil { 732 return err 733 } 734 735 useTLSForRedis := r.redisShouldUseTLS(cr) 736 737 log.Info("reconciling config maps") 738 if err := r.reconcileConfigMaps(cr, useTLSForRedis); err != nil { 739 return err 740 } 741 742 log.Info("reconciling services") 743 if err := r.reconcileServices(cr); err != nil { 744 return err 745 } 746 747 log.Info("reconciling deployments") 748 if err := r.reconcileDeployments(cr, useTLSForRedis); err != nil { 749 return err 750 } 751 752 log.Info("reconciling statefulsets") 753 if err := r.reconcileStatefulSets(cr, useTLSForRedis); err != nil { 754 return err 755 } 756 757 log.Info("reconciling autoscalers") 758 if err := r.reconcileAutoscalers(cr); err != nil { 759 return err 760 } 761 762 log.Info("reconciling ingresses") 763 if err := r.reconcileIngresses(cr); err != nil { 764 return err 765 } 766 767 if IsRouteAPIAvailable() { 768 log.Info("reconciling routes") 769 if err := r.reconcileRoutes(cr); err != nil { 770 return err 771 } 772 } 773 774 if IsPrometheusAPIAvailable() { 775 log.Info("reconciling prometheus") 776 if err := r.reconcilePrometheus(cr); err != nil { 777 return err 778 } 779 780 // Reconciles prometheusRule created to alert based on argo-cd workload status 781 if err := r.reconcilePrometheusRule(cr); err != nil { 782 return err 783 } 784 785 if err := r.reconcileMetricsServiceMonitor(cr); err != nil { 786 return err 787 } 788 789 if err := r.reconcileRepoServerServiceMonitor(cr); err != nil { 790 return err 791 } 792 793 if err := r.reconcileServerMetricsServiceMonitor(cr); err != nil { 794 return err 795 } 796 } 797 798 // check ManagedApplicationSetSourceNamespaces for proper cleanup 799 if cr.Spec.ApplicationSet != nil || len(r.ManagedApplicationSetSourceNamespaces) > 0 { 800 log.Info("reconciling ApplicationSet controller") 801 if err := r.reconcileApplicationSetController(cr); err != nil { 802 return err 803 } 804 } 805 806 if cr.Spec.Notifications.Enabled { 807 log.Info("reconciling Notifications controller") 808 if err := r.reconcileNotificationsController(cr); err != nil { 809 return err 810 } 811 } 812 813 if err := r.reconcileRepoServerTLSSecret(cr); err != nil { 814 return err 815 } 816 817 if err := r.reconcileRedisTLSSecret(cr, useTLSForRedis); err != nil { 818 return err 819 } 820 821 return nil 822 } 823 824 func (r *ReconcileArgoCD) deleteClusterResources(cr *argoproj.ArgoCD) error { 825 selector, err := argocdInstanceSelector(cr.Name) 826 if err != nil { 827 return err 828 } 829 830 clusterRoleList := &v1.ClusterRoleList{} 831 if err := filterObjectsBySelector(r.Client, clusterRoleList, selector); err != nil { 832 return fmt.Errorf("failed to filter ClusterRoles for %s: %w", cr.Name, err) 833 } 834 835 if err := deleteClusterRoles(r.Client, clusterRoleList); err != nil { 836 return err 837 } 838 839 clusterBindingsList := &v1.ClusterRoleBindingList{} 840 if err := filterObjectsBySelector(r.Client, clusterBindingsList, selector); err != nil { 841 return fmt.Errorf("failed to filter ClusterRoleBindings for %s: %w", cr.Name, err) 842 } 843 844 if err := deleteClusterRoleBindings(r.Client, clusterBindingsList); err != nil { 845 return err 846 } 847 848 return nil 849 } 850 851 func (r *ReconcileArgoCD) removeManagedByLabelFromNamespaces(namespace string) error { 852 nsList := &corev1.NamespaceList{} 853 listOption := client.MatchingLabels{ 854 common.ArgoCDManagedByLabel: namespace, 855 } 856 if err := r.Client.List(context.TODO(), nsList, listOption); err != nil { 857 return err 858 } 859 860 nsList.Items = append(nsList.Items, corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}}) 861 for _, n := range nsList.Items { 862 ns := &corev1.Namespace{} 863 if err := r.Client.Get(context.TODO(), types.NamespacedName{Name: n.Name}, ns); err != nil { 864 return err 865 } 866 867 if ns.Labels == nil { 868 continue 869 } 870 871 if n, ok := ns.Labels[common.ArgoCDManagedByLabel]; !ok || n != namespace { 872 continue 873 } 874 delete(ns.Labels, common.ArgoCDManagedByLabel) 875 if err := r.Client.Update(context.TODO(), ns); err != nil { 876 log.Error(err, fmt.Sprintf("failed to remove label from namespace [%s]", ns.Name)) 877 } 878 } 879 return nil 880 } 881 882 func filterObjectsBySelector(c client.Client, objectList client.ObjectList, selector labels.Selector) error { 883 return c.List(context.TODO(), objectList, client.MatchingLabelsSelector{Selector: selector}) 884 } 885 886 func argocdInstanceSelector(name string) (labels.Selector, error) { 887 selector := labels.NewSelector() 888 requirement, err := labels.NewRequirement(common.ArgoCDKeyManagedBy, selection.Equals, []string{name}) 889 if err != nil { 890 return nil, fmt.Errorf("failed to create a requirement for %w", err) 891 } 892 return selector.Add(*requirement), nil 893 } 894 895 func (r *ReconcileArgoCD) removeDeletionFinalizer(argocd *argoproj.ArgoCD) error { 896 argocd.Finalizers = removeString(argocd.GetFinalizers(), common.ArgoCDDeletionFinalizer) 897 if err := r.Client.Update(context.TODO(), argocd); err != nil { 898 return fmt.Errorf("failed to remove deletion finalizer from %s: %w", argocd.Name, err) 899 } 900 return nil 901 } 902 903 func (r *ReconcileArgoCD) addDeletionFinalizer(argocd *argoproj.ArgoCD) error { 904 argocd.Finalizers = append(argocd.Finalizers, common.ArgoCDDeletionFinalizer) 905 if err := r.Client.Update(context.TODO(), argocd); err != nil { 906 return fmt.Errorf("failed to add deletion finalizer for %s: %w", argocd.Name, err) 907 } 908 return nil 909 } 910 911 func removeString(slice []string, s string) []string { 912 var result []string 913 for _, item := range slice { 914 if item == s { 915 continue 916 } 917 result = append(result, item) 918 } 919 return result 920 } 921 922 // setResourceWatches will register Watches for each of the supported Resources. 923 func (r *ReconcileArgoCD) setResourceWatches(bldr *builder.Builder, clusterResourceMapper, tlsSecretMapper, namespaceResourceMapper, clusterSecretResourceMapper, applicationSetGitlabSCMTLSConfigMapMapper handler.MapFunc) *builder.Builder { 924 925 deploymentConfigPred := predicate.Funcs{ 926 UpdateFunc: func(e event.UpdateEvent) bool { 927 // Ignore updates to CR status in which case metadata.Generation does not change 928 var count int32 = 1 929 newDC, ok := e.ObjectNew.(*oappsv1.DeploymentConfig) 930 if !ok { 931 return false 932 } 933 oldDC, ok := e.ObjectOld.(*oappsv1.DeploymentConfig) 934 if !ok { 935 return false 936 } 937 if newDC.Name == defaultKeycloakIdentifier { 938 if newDC.Status.AvailableReplicas == count { 939 return true 940 } 941 if newDC.Status.AvailableReplicas == int32(0) && 942 !reflect.DeepEqual(oldDC.Status.AvailableReplicas, newDC.Status.AvailableReplicas) { 943 // Handle the deletion of keycloak pod. 944 log.Info(fmt.Sprintf("Handle the pod deletion event for keycloak deployment config %s in namespace %s", 945 newDC.Name, newDC.Namespace)) 946 err := handleKeycloakPodDeletion(newDC) 947 if err != nil { 948 log.Error(err, fmt.Sprintf("Failed to update Deployment Config %s for keycloak pod deletion in namespace %s", 949 newDC.Name, newDC.Namespace)) 950 } 951 } 952 } 953 return false 954 }, 955 } 956 957 deleteSSOPred := predicate.Funcs{ 958 UpdateFunc: func(e event.UpdateEvent) bool { 959 newCR, ok := e.ObjectNew.(*argoproj.ArgoCD) 960 if !ok { 961 return false 962 } 963 oldCR, ok := e.ObjectOld.(*argoproj.ArgoCD) 964 if !ok { 965 return false 966 } 967 968 // Handle deletion of SSO from Argo CD custom resource 969 if !reflect.DeepEqual(oldCR.Spec.SSO, newCR.Spec.SSO) && newCR.Spec.SSO == nil { 970 err := r.deleteSSOConfiguration(newCR, oldCR) 971 if err != nil { 972 log.Error(err, fmt.Sprintf("Failed to delete SSO Configuration for ArgoCD %s in namespace %s", 973 newCR.Name, newCR.Namespace)) 974 } 975 } 976 977 // Trigger reconciliation of SSO on update event 978 if !reflect.DeepEqual(oldCR.Spec.SSO, newCR.Spec.SSO) && newCR.Spec.SSO != nil && oldCR.Spec.SSO != nil { 979 err := r.reconcileSSO(newCR) 980 if err != nil { 981 log.Error(err, fmt.Sprintf("Failed to update existing SSO Configuration for ArgoCD %s in namespace %s", 982 newCR.Name, newCR.Namespace)) 983 } 984 } 985 return true 986 }, 987 } 988 989 // Add new predicate to delete Notifications Resources. The predicate watches the Argo CD CR for changes to the `.spec.Notifications.Enabled` 990 // field. When a change is detected that results in notifications being disabled, we trigger deletion of notifications resources 991 deleteNotificationsPred := predicate.Funcs{ 992 UpdateFunc: func(e event.UpdateEvent) bool { 993 newCR, ok := e.ObjectNew.(*argoproj.ArgoCD) 994 if !ok { 995 return false 996 } 997 oldCR, ok := e.ObjectOld.(*argoproj.ArgoCD) 998 if !ok { 999 return false 1000 } 1001 if oldCR.Spec.Notifications.Enabled && !newCR.Spec.Notifications.Enabled { 1002 err := r.deleteNotificationsResources(newCR) 1003 if err != nil { 1004 log.Error(err, fmt.Sprintf("Failed to delete notifications controller resources for ArgoCD %s in namespace %s", 1005 newCR.Name, newCR.Namespace)) 1006 } 1007 } 1008 return true 1009 }, 1010 } 1011 1012 // Watch for changes to primary resource ArgoCD 1013 bldr.For(&argoproj.ArgoCD{}, builder.WithPredicates(deleteSSOPred, deleteNotificationsPred)) 1014 1015 // Watch for changes to ConfigMap sub-resources owned by ArgoCD instances. 1016 bldr.Owns(&corev1.ConfigMap{}) 1017 1018 // Watch for changes to Secret sub-resources owned by ArgoCD instances. 1019 bldr.Owns(&corev1.Secret{}) 1020 1021 // Watch for changes to Service sub-resources owned by ArgoCD instances. 1022 bldr.Owns(&corev1.Service{}) 1023 1024 // Watch for changes to Deployment sub-resources owned by ArgoCD instances. 1025 bldr.Owns(&appsv1.Deployment{}) 1026 1027 // Watch for changes to Ingress sub-resources owned by ArgoCD instances. 1028 bldr.Owns(&networkingv1.Ingress{}) 1029 1030 bldr.Owns(&v1.Role{}) 1031 1032 bldr.Owns(&v1.RoleBinding{}) 1033 1034 clusterResourceHandler := handler.EnqueueRequestsFromMapFunc(clusterResourceMapper) 1035 1036 clusterSecretResourceHandler := handler.EnqueueRequestsFromMapFunc(clusterSecretResourceMapper) 1037 1038 appSetGitlabSCMTLSConfigMapHandler := handler.EnqueueRequestsFromMapFunc(applicationSetGitlabSCMTLSConfigMapMapper) 1039 1040 tlsSecretHandler := handler.EnqueueRequestsFromMapFunc(tlsSecretMapper) 1041 1042 bldr.Watches(&v1.ClusterRoleBinding{}, clusterResourceHandler) 1043 1044 bldr.Watches(&v1.ClusterRole{}, clusterResourceHandler) 1045 1046 bldr.Watches(&corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{ 1047 Name: common.ArgoCDAppSetGitlabSCMTLSCertsConfigMapName, 1048 }}, appSetGitlabSCMTLSConfigMapHandler) 1049 1050 // Watch for secrets of type TLS that might be created by external processes 1051 bldr.Watches(&corev1.Secret{Type: corev1.SecretTypeTLS}, tlsSecretHandler) 1052 1053 // Watch for cluster secrets added to the argocd instance 1054 bldr.Watches(&corev1.Secret{ObjectMeta: metav1.ObjectMeta{ 1055 Labels: map[string]string{ 1056 common.ArgoCDManagedByClusterArgoCDLabel: "cluster", 1057 }}}, clusterSecretResourceHandler) 1058 1059 // Watch for changes to Secret sub-resources owned by ArgoCD instances. 1060 bldr.Owns(&appsv1.StatefulSet{}) 1061 1062 // Inspect cluster to verify availability of extra features 1063 // This sets the flags that are used in subsequent checks 1064 if err := InspectCluster(); err != nil { 1065 log.Info("unable to inspect cluster") 1066 } 1067 1068 if IsRouteAPIAvailable() { 1069 // Watch OpenShift Route sub-resources owned by ArgoCD instances. 1070 bldr.Owns(&routev1.Route{}) 1071 } 1072 1073 if IsPrometheusAPIAvailable() { 1074 // Watch Prometheus sub-resources owned by ArgoCD instances. 1075 bldr.Owns(&monitoringv1.Prometheus{}) 1076 1077 // Watch Prometheus ServiceMonitor sub-resources owned by ArgoCD instances. 1078 bldr.Owns(&monitoringv1.ServiceMonitor{}) 1079 } 1080 1081 if IsTemplateAPIAvailable() { 1082 // Watch for the changes to Deployment Config 1083 bldr.Owns(&oappsv1.DeploymentConfig{}, builder.WithPredicates(deploymentConfigPred)) 1084 1085 } 1086 1087 // Watch for changes to NotificationsConfiguration CR 1088 bldr.Owns(&v1alpha1.NotificationsConfiguration{}) 1089 1090 namespaceHandler := handler.EnqueueRequestsFromMapFunc(namespaceResourceMapper) 1091 1092 bldr.Watches(&corev1.Namespace{}, namespaceHandler, builder.WithPredicates(namespaceFilterPredicate())) 1093 1094 return bldr 1095 } 1096 1097 // boolPtr returns a pointer to val 1098 func boolPtr(val bool) *bool { 1099 return &val 1100 } 1101 1102 func int64Ptr(val int64) *int64 { 1103 return &val 1104 } 1105 1106 // triggerRollout will trigger a rollout of a Kubernetes resource specified as 1107 // obj. It currently supports Deployment and StatefulSet resources. 1108 func (r *ReconcileArgoCD) triggerRollout(obj interface{}, key string) error { 1109 switch res := obj.(type) { 1110 case *appsv1.Deployment: 1111 return r.triggerDeploymentRollout(res, key) 1112 case *appsv1.StatefulSet: 1113 return r.triggerStatefulSetRollout(res, key) 1114 default: 1115 return fmt.Errorf("resource of unknown type %T, cannot trigger rollout", res) 1116 } 1117 } 1118 1119 func allowedNamespace(current string, namespaces string) bool { 1120 1121 clusterConfigNamespaces := splitList(namespaces) 1122 if len(clusterConfigNamespaces) > 0 { 1123 if clusterConfigNamespaces[0] == "*" { 1124 return true 1125 } 1126 1127 for _, n := range clusterConfigNamespaces { 1128 if n == current { 1129 return true 1130 } 1131 } 1132 } 1133 return false 1134 } 1135 1136 func splitList(s string) []string { 1137 elems := strings.Split(s, ",") 1138 for i := range elems { 1139 elems[i] = strings.TrimSpace(elems[i]) 1140 } 1141 return elems 1142 } 1143 1144 func containsString(arr []string, s string) bool { 1145 for _, val := range arr { 1146 if strings.TrimSpace(val) == s { 1147 return true 1148 } 1149 } 1150 return false 1151 } 1152 1153 // DeprecationEventEmissionStatus is meant to track which deprecation events have been emitted already. This is temporary and can be removed in v0.0.6 once we have provided enough 1154 // deprecation notice 1155 type DeprecationEventEmissionStatus struct { 1156 SSOSpecDeprecationWarningEmitted bool 1157 DexSpecDeprecationWarningEmitted bool 1158 DisableDexDeprecationWarningEmitted bool 1159 } 1160 1161 // DeprecationEventEmissionTracker map stores the namespace containing ArgoCD instance as key and DeprecationEventEmissionStatus as value, 1162 // where DeprecationEventEmissionStatus tracks the events that have been emitted for the instance in the particular namespace. 1163 // This is temporary and can be removed in v0.0.6 when we remove the deprecated fields. 1164 var DeprecationEventEmissionTracker = make(map[string]DeprecationEventEmissionStatus) 1165 1166 func namespaceFilterPredicate() predicate.Predicate { 1167 return predicate.Funcs{ 1168 UpdateFunc: func(e event.UpdateEvent) bool { 1169 // This checks if ArgoCDManagedByLabel exists in newMeta, if exists then - 1170 // 1. Check if oldMeta had the label or not? if no, return true 1171 // 2. if yes, check if the old and new values are different, if yes, 1172 // first deleteRBACs for the old value & return true. 1173 // Event is then handled by the reconciler, which would create appropriate RBACs. 1174 if valNew, ok := e.ObjectNew.GetLabels()[common.ArgoCDManagedByLabel]; ok { 1175 if valOld, ok := e.ObjectOld.GetLabels()[common.ArgoCDManagedByLabel]; ok && valOld != valNew { 1176 k8sClient, err := initK8sClient() 1177 if err != nil { 1178 return false 1179 } 1180 if err := deleteRBACsForNamespace(e.ObjectOld.GetName(), k8sClient); err != nil { 1181 log.Error(err, fmt.Sprintf("failed to delete RBACs for namespace: %s", e.ObjectOld.GetName())) 1182 } else { 1183 log.Info(fmt.Sprintf("Successfully removed the RBACs for namespace: %s", e.ObjectOld.GetName())) 1184 } 1185 1186 // Delete namespace from cluster secret of previously managing argocd instance 1187 if err = deleteManagedNamespaceFromClusterSecret(valOld, e.ObjectOld.GetName(), k8sClient); err != nil { 1188 log.Error(err, fmt.Sprintf("unable to delete namespace %s from cluster secret", e.ObjectOld.GetName())) 1189 } else { 1190 log.Info(fmt.Sprintf("Successfully deleted namespace %s from cluster secret", e.ObjectOld.GetName())) 1191 } 1192 } 1193 return true 1194 } 1195 // This checks if the old meta had the label, if it did, delete the RBACs for the namespace 1196 // which were created when the label was added to the namespace. 1197 if ns, ok := e.ObjectOld.GetLabels()[common.ArgoCDManagedByLabel]; ok && ns != "" { 1198 k8sClient, err := initK8sClient() 1199 if err != nil { 1200 return false 1201 } 1202 if err := deleteRBACsForNamespace(e.ObjectOld.GetName(), k8sClient); err != nil { 1203 log.Error(err, fmt.Sprintf("failed to delete RBACs for namespace: %s", e.ObjectOld.GetName())) 1204 } else { 1205 log.Info(fmt.Sprintf("Successfully removed the RBACs for namespace: %s", e.ObjectOld.GetName())) 1206 } 1207 1208 // Delete managed namespace from cluster secret 1209 if err = deleteManagedNamespaceFromClusterSecret(ns, e.ObjectOld.GetName(), k8sClient); err != nil { 1210 log.Error(err, fmt.Sprintf("unable to delete namespace %s from cluster secret", e.ObjectOld.GetName())) 1211 } else { 1212 log.Info(fmt.Sprintf("Successfully deleted namespace %s from cluster secret", e.ObjectOld.GetName())) 1213 } 1214 1215 } 1216 return false 1217 }, 1218 DeleteFunc: func(e event.DeleteEvent) bool { 1219 if ns, ok := e.Object.GetLabels()[common.ArgoCDManagedByLabel]; ok && ns != "" { 1220 k8sClient, err := initK8sClient() 1221 1222 if err != nil { 1223 return false 1224 } 1225 // Delete managed namespace from cluster secret 1226 err = deleteManagedNamespaceFromClusterSecret(ns, e.Object.GetName(), k8sClient) 1227 if err != nil { 1228 log.Error(err, fmt.Sprintf("unable to delete namespace %s from cluster secret", e.Object.GetName())) 1229 } else { 1230 log.Info(fmt.Sprintf("Successfully deleted namespace %s from cluster secret", e.Object.GetName())) 1231 } 1232 } 1233 1234 // if a namespace is deleted, remove it from deprecationEventEmissionTracker (if exists) so that if a namespace with the same name 1235 // is created in the future and contains an Argo CD instance, it will be tracked appropriately 1236 delete(DeprecationEventEmissionTracker, e.Object.GetName()) 1237 return false 1238 }, 1239 } 1240 } 1241 1242 // deleteRBACsForNamespace deletes the RBACs when the label from the namespace is removed. 1243 func deleteRBACsForNamespace(sourceNS string, k8sClient kubernetes.Interface) error { 1244 log.Info(fmt.Sprintf("Removing the RBACs created for the namespace: %s", sourceNS)) 1245 1246 // List all the roles created for ArgoCD using the label selector 1247 labelSelector := metav1.LabelSelector{MatchLabels: map[string]string{common.ArgoCDKeyPartOf: common.ArgoCDAppName}} 1248 roles, err := k8sClient.RbacV1().Roles(sourceNS).List(context.TODO(), metav1.ListOptions{LabelSelector: labels.Set(labelSelector.MatchLabels).String()}) 1249 if err != nil { 1250 log.Error(err, fmt.Sprintf("failed to list roles for namespace: %s", sourceNS)) 1251 return err 1252 } 1253 1254 // Delete all the retrieved roles 1255 for _, role := range roles.Items { 1256 err = k8sClient.RbacV1().Roles(sourceNS).Delete(context.TODO(), role.Name, metav1.DeleteOptions{}) 1257 if err != nil { 1258 log.Error(err, fmt.Sprintf("failed to delete roles for namespace: %s", sourceNS)) 1259 } 1260 } 1261 1262 // List all the roles bindings created for ArgoCD using the label selector 1263 roleBindings, err := k8sClient.RbacV1().RoleBindings(sourceNS).List(context.TODO(), metav1.ListOptions{LabelSelector: labels.Set(labelSelector.MatchLabels).String()}) 1264 if err != nil { 1265 log.Error(err, fmt.Sprintf("failed to list role bindings for namespace: %s", sourceNS)) 1266 return err 1267 } 1268 1269 // Delete all the retrieved role bindings 1270 for _, roleBinding := range roleBindings.Items { 1271 err = k8sClient.RbacV1().RoleBindings(sourceNS).Delete(context.TODO(), roleBinding.Name, metav1.DeleteOptions{}) 1272 if err != nil { 1273 log.Error(err, fmt.Sprintf("failed to delete role binding for namespace: %s", sourceNS)) 1274 } 1275 } 1276 1277 return nil 1278 } 1279 1280 func deleteManagedNamespaceFromClusterSecret(ownerNS, sourceNS string, k8sClient kubernetes.Interface) error { 1281 1282 // Get the cluster secret used for configuring ArgoCD 1283 labelSelector := metav1.LabelSelector{MatchLabels: map[string]string{common.ArgoCDSecretTypeLabel: "cluster"}} 1284 secrets, err := k8sClient.CoreV1().Secrets(ownerNS).List(context.TODO(), metav1.ListOptions{LabelSelector: labels.Set(labelSelector.MatchLabels).String()}) 1285 if err != nil { 1286 log.Error(err, fmt.Sprintf("failed to retrieve secrets for namespace: %s", ownerNS)) 1287 return err 1288 } 1289 for _, secret := range secrets.Items { 1290 if string(secret.Data["server"]) != common.ArgoCDDefaultServer { 1291 continue 1292 } 1293 if namespaces, ok := secret.Data["namespaces"]; ok { 1294 namespaceList := strings.Split(string(namespaces), ",") 1295 var result []string 1296 1297 for _, n := range namespaceList { 1298 // remove the namespace from the list of namespaces 1299 if strings.TrimSpace(n) == sourceNS { 1300 continue 1301 } 1302 result = append(result, strings.TrimSpace(n)) 1303 sort.Strings(result) 1304 secret.Data["namespaces"] = []byte(strings.Join(result, ",")) 1305 } 1306 // Update the secret with the updated list of namespaces 1307 if _, err = k8sClient.CoreV1().Secrets(ownerNS).Update(context.TODO(), &secret, metav1.UpdateOptions{}); err != nil { 1308 log.Error(err, fmt.Sprintf("failed to update cluster permission secret for namespace: %s", ownerNS)) 1309 return err 1310 } 1311 } 1312 } 1313 return nil 1314 } 1315 1316 func initK8sClient() (*kubernetes.Clientset, error) { 1317 cfg, err := config.GetConfig() 1318 if err != nil { 1319 log.Error(err, "unable to get k8s config") 1320 return nil, err 1321 } 1322 1323 k8sClient, err := kubernetes.NewForConfig(cfg) 1324 if err != nil { 1325 log.Error(err, "unable to create k8s client") 1326 return nil, err 1327 } 1328 1329 return k8sClient, nil 1330 } 1331 1332 // getLogLevel returns the log level for a specified component if it is set or returns the default log level if it is not set 1333 func getLogLevel(logField string) string { 1334 1335 switch strings.ToLower(logField) { 1336 case "debug", 1337 "info", 1338 "warn", 1339 "error": 1340 return logField 1341 } 1342 return common.ArgoCDDefaultLogLevel 1343 } 1344 1345 // getLogFormat returns the log format for a specified component if it is set or returns the default log format if it is not set 1346 func getLogFormat(logField string) string { 1347 switch strings.ToLower(logField) { 1348 case "text", 1349 "json": 1350 return logField 1351 } 1352 return common.ArgoCDDefaultLogFormat 1353 } 1354 1355 func (r *ReconcileArgoCD) setManagedNamespaces(cr *argoproj.ArgoCD) error { 1356 namespaces := &corev1.NamespaceList{} 1357 listOption := client.MatchingLabels{ 1358 common.ArgoCDManagedByLabel: cr.Namespace, 1359 } 1360 1361 // get the list of namespaces managed by the Argo CD instance 1362 if err := r.Client.List(context.TODO(), namespaces, listOption); err != nil { 1363 return err 1364 } 1365 1366 namespaces.Items = append(namespaces.Items, corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: cr.Namespace}}) 1367 r.ManagedNamespaces = namespaces 1368 return nil 1369 } 1370 1371 // getSourceNamespaces retrieves a list of namespaces that match the sourceNamespaces 1372 // pattern specified in the given ArgoCD 1373 func (r *ReconcileArgoCD) getSourceNamespaces(cr *argoproj.ArgoCD) ([]string, error) { 1374 sourceNamespaces := []string{} 1375 namespaces := &corev1.NamespaceList{} 1376 1377 if err := r.Client.List(context.TODO(), namespaces, &client.ListOptions{}); err != nil { 1378 return nil, err 1379 } 1380 1381 for _, namespace := range namespaces.Items { 1382 if glob.MatchStringInList(cr.Spec.SourceNamespaces, namespace.Name, false) { 1383 sourceNamespaces = append(sourceNamespaces, namespace.Name) 1384 } 1385 } 1386 1387 return sourceNamespaces, nil 1388 } 1389 1390 func (r *ReconcileArgoCD) setManagedSourceNamespaces(cr *argoproj.ArgoCD) error { 1391 r.ManagedSourceNamespaces = make(map[string]string) 1392 namespaces := &corev1.NamespaceList{} 1393 listOption := client.MatchingLabels{ 1394 common.ArgoCDManagedByClusterArgoCDLabel: cr.Namespace, 1395 } 1396 1397 // get the list of namespaces managed by the Argo CD instance 1398 if err := r.Client.List(context.TODO(), namespaces, listOption); err != nil { 1399 return err 1400 } 1401 1402 for _, namespace := range namespaces.Items { 1403 r.ManagedSourceNamespaces[namespace.Name] = "" 1404 } 1405 1406 return nil 1407 } 1408 1409 // removeUnmanagedSourceNamespaceResources cleansup resources from SourceNamespaces if namespace is not managed by argocd instance. 1410 // It also removes the managed-by-cluster-argocd label from the namespace 1411 func (r *ReconcileArgoCD) removeUnmanagedSourceNamespaceResources(cr *argoproj.ArgoCD) error { 1412 1413 for ns := range r.ManagedSourceNamespaces { 1414 managedNamespace := false 1415 if cr.GetDeletionTimestamp() == nil { 1416 sourceNamespaces, err := r.getSourceNamespaces(cr) 1417 if err != nil { 1418 return err 1419 } 1420 for _, namespace := range sourceNamespaces { 1421 if namespace == ns { 1422 managedNamespace = true 1423 break 1424 } 1425 } 1426 } 1427 1428 if !managedNamespace { 1429 if err := r.cleanupUnmanagedSourceNamespaceResources(cr, ns); err != nil { 1430 log.Error(err, fmt.Sprintf("error cleaning up resources for namespace %s", ns)) 1431 continue 1432 } 1433 delete(r.ManagedSourceNamespaces, ns) 1434 } 1435 } 1436 return nil 1437 } 1438 1439 func (r *ReconcileArgoCD) cleanupUnmanagedSourceNamespaceResources(cr *argoproj.ArgoCD, ns string) error { 1440 namespace := corev1.Namespace{} 1441 if err := r.Client.Get(context.TODO(), types.NamespacedName{Name: ns}, &namespace); err != nil { 1442 if !apierrors.IsNotFound(err) { 1443 return err 1444 } 1445 return nil 1446 } 1447 // Remove managed-by-cluster-argocd from the namespace 1448 delete(namespace.Labels, common.ArgoCDManagedByClusterArgoCDLabel) 1449 if err := r.Client.Update(context.TODO(), &namespace); err != nil { 1450 log.Error(err, fmt.Sprintf("failed to remove label from namespace [%s]", namespace.Name)) 1451 } 1452 1453 // Delete Roles for SourceNamespaces 1454 existingRole := v1.Role{} 1455 roleName := getRoleNameForApplicationSourceNamespaces(namespace.Name, cr) 1456 if err := r.Client.Get(context.TODO(), types.NamespacedName{Name: roleName, Namespace: namespace.Name}, &existingRole); err != nil { 1457 if !apierrors.IsNotFound(err) { 1458 return fmt.Errorf("failed to fetch the role for the service account associated with %s : %s", common.ArgoCDServerComponent, err) 1459 } 1460 } 1461 if existingRole.Name != "" { 1462 if err := r.Client.Delete(context.TODO(), &existingRole); err != nil { 1463 return err 1464 } 1465 } 1466 // Delete RoleBindings for SourceNamespaces 1467 existingRoleBinding := &v1.RoleBinding{} 1468 roleBindingName := getRoleBindingNameForSourceNamespaces(cr.Name, namespace.Name) 1469 if err := r.Client.Get(context.TODO(), types.NamespacedName{Name: roleBindingName, Namespace: namespace.Name}, existingRoleBinding); err != nil { 1470 if !apierrors.IsNotFound(err) { 1471 return fmt.Errorf("failed to get the rolebinding associated with %s : %s", common.ArgoCDServerComponent, err) 1472 } 1473 } 1474 if existingRoleBinding.Name != "" { 1475 if err := r.Client.Delete(context.TODO(), existingRoleBinding); err != nil { 1476 return err 1477 } 1478 } 1479 return nil 1480 } 1481 1482 func isProxyCluster() bool { 1483 cfg, err := config.GetConfig() 1484 if err != nil { 1485 log.Error(err, "failed to get k8s config") 1486 } 1487 1488 // Initialize config client. 1489 configClient, err := configv1client.NewForConfig(cfg) 1490 if err != nil { 1491 log.Error(err, "failed to initialize openshift config client") 1492 return false 1493 } 1494 1495 proxy, err := configClient.Proxies().Get(context.TODO(), "cluster", metav1.GetOptions{}) 1496 if err != nil { 1497 log.Error(err, "failed to get proxy configuration") 1498 return false 1499 } 1500 1501 if proxy.Spec.HTTPSProxy != "" { 1502 log.Info("proxy configuration detected") 1503 return true 1504 } 1505 1506 return false 1507 } 1508 1509 func getOpenShiftAPIURL() string { 1510 k8s, err := initK8sClient() 1511 if err != nil { 1512 log.Error(err, "failed to initialize k8s client") 1513 } 1514 1515 cm, err := k8s.CoreV1().ConfigMaps("openshift-console").Get(context.TODO(), "console-config", metav1.GetOptions{}) 1516 if err != nil { 1517 log.Error(err, "") 1518 } 1519 1520 var cf string 1521 if v, ok := cm.Data["console-config.yaml"]; ok { 1522 cf = v 1523 } 1524 1525 data := make(map[string]interface{}) 1526 err = yaml.Unmarshal([]byte(cf), data) 1527 if err != nil { 1528 log.Error(err, "") 1529 } 1530 1531 var apiURL interface{} 1532 var out string 1533 if c, ok := data["clusterInfo"]; ok { 1534 ci, _ := c.(map[interface{}]interface{}) 1535 1536 apiURL = ci["masterPublicURL"] 1537 out = fmt.Sprintf("%v", apiURL) 1538 } 1539 1540 return out 1541 } 1542 1543 func AddSeccompProfileForOpenShift(client client.Client, podspec *corev1.PodSpec) { 1544 if !IsVersionAPIAvailable() { 1545 return 1546 } 1547 version, err := getClusterVersion(client) 1548 if err != nil { 1549 log.Error(err, "couldn't get OpenShift version") 1550 } 1551 if version == "" || semver.Compare(fmt.Sprintf("v%s", version), "v4.10.999") > 0 { 1552 if podspec.SecurityContext == nil { 1553 podspec.SecurityContext = &corev1.PodSecurityContext{} 1554 } 1555 if podspec.SecurityContext.SeccompProfile == nil { 1556 podspec.SecurityContext.SeccompProfile = &corev1.SeccompProfile{} 1557 } 1558 if len(podspec.SecurityContext.SeccompProfile.Type) == 0 { 1559 podspec.SecurityContext.SeccompProfile.Type = corev1.SeccompProfileTypeRuntimeDefault 1560 } 1561 } 1562 } 1563 1564 // getClusterVersion returns the OpenShift Cluster version in which the operator is installed 1565 func getClusterVersion(client client.Client) (string, error) { 1566 if !IsVersionAPIAvailable() { 1567 return "", nil 1568 } 1569 clusterVersion := &configv1.ClusterVersion{} 1570 err := client.Get(context.TODO(), types.NamespacedName{Name: "version"}, clusterVersion) 1571 if err != nil { 1572 if apierrors.IsNotFound(err) { 1573 return "", nil 1574 } 1575 return "", err 1576 } 1577 return clusterVersion.Status.Desired.Version, nil 1578 } 1579 1580 // generateRandomBytes returns a securely generated random bytes. 1581 func generateRandomBytes(n int) []byte { 1582 b := make([]byte, n) 1583 _, err := rand.Read(b) 1584 if err != nil { 1585 log.Error(err, "") 1586 } 1587 return b 1588 } 1589 1590 // generateRandomString returns a securely generated random string. 1591 func generateRandomString(s int) string { 1592 b := generateRandomBytes(s) 1593 return base64.URLEncoding.EncodeToString(b) 1594 } 1595 1596 // contains returns true if a string is part of the given slice. 1597 func contains(s []string, g string) bool { 1598 for _, a := range s { 1599 if a == g { 1600 return true 1601 } 1602 } 1603 return false 1604 } 1605 1606 // getApplicationSetHTTPServerHost will return the host for the given ArgoCD. 1607 func getApplicationSetHTTPServerHost(cr *argoproj.ArgoCD) (string, error) { 1608 host := cr.Name 1609 if len(cr.Spec.ApplicationSet.WebhookServer.Host) > 0 { 1610 hostname, err := shortenHostname(cr.Spec.ApplicationSet.WebhookServer.Host) 1611 if err != nil { 1612 return "", err 1613 } 1614 host = hostname 1615 } 1616 return host, nil 1617 }