github.com/argoproj/argo-cd/v2@v2.10.5/util/settings/settings.go (about) 1 package settings 2 3 import ( 4 "context" 5 "crypto/rand" 6 "crypto/sha256" 7 "crypto/tls" 8 "crypto/x509" 9 "encoding/base64" 10 "fmt" 11 "math/big" 12 "net/url" 13 "path" 14 "reflect" 15 "strconv" 16 "strings" 17 "sync" 18 "time" 19 20 log "github.com/sirupsen/logrus" 21 apiv1 "k8s.io/api/core/v1" 22 apierr "k8s.io/apimachinery/pkg/api/errors" 23 apierrors "k8s.io/apimachinery/pkg/api/errors" 24 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 "k8s.io/apimachinery/pkg/fields" 26 "k8s.io/apimachinery/pkg/labels" 27 v1 "k8s.io/client-go/informers/core/v1" 28 "k8s.io/client-go/kubernetes" 29 v1listers "k8s.io/client-go/listers/core/v1" 30 "k8s.io/client-go/tools/cache" 31 "sigs.k8s.io/yaml" 32 33 enginecache "github.com/argoproj/gitops-engine/pkg/cache" 34 timeutil "github.com/argoproj/pkg/time" 35 36 "github.com/argoproj/argo-cd/v2/common" 37 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" 38 "github.com/argoproj/argo-cd/v2/server/settings/oidc" 39 "github.com/argoproj/argo-cd/v2/util" 40 "github.com/argoproj/argo-cd/v2/util/crypto" 41 "github.com/argoproj/argo-cd/v2/util/kube" 42 "github.com/argoproj/argo-cd/v2/util/password" 43 tlsutil "github.com/argoproj/argo-cd/v2/util/tls" 44 ) 45 46 // ArgoCDSettings holds in-memory runtime configuration options. 47 type ArgoCDSettings struct { 48 // URL is the externally facing URL users will visit to reach Argo CD. 49 // The value here is used when configuring SSO. Omitting this value will disable SSO. 50 URL string `json:"url,omitempty"` 51 // Indicates if status badge is enabled or not. 52 StatusBadgeEnabled bool `json:"statusBadgeEnable"` 53 // Indicates if status badge custom root URL should be used. 54 StatusBadgeRootUrl string `json:"statusBadgeRootUrl,omitempty"` 55 // DexConfig contains portions of a dex config yaml 56 DexConfig string `json:"dexConfig,omitempty"` 57 // OIDCConfigRAW holds OIDC configuration as a raw string 58 OIDCConfigRAW string `json:"oidcConfig,omitempty"` 59 // ServerSignature holds the key used to generate JWT tokens. 60 ServerSignature []byte `json:"serverSignature,omitempty"` 61 // Certificate holds the certificate/private key for the Argo CD API server. 62 // If nil, will run insecure without TLS. 63 Certificate *tls.Certificate `json:"-"` 64 // CertificateIsExternal indicates whether Certificate was loaded from external secret 65 CertificateIsExternal bool `json:"-"` 66 // WebhookGitLabSecret holds the shared secret for authenticating GitHub webhook events 67 WebhookGitHubSecret string `json:"webhookGitHubSecret,omitempty"` 68 // WebhookGitLabSecret holds the shared secret for authenticating GitLab webhook events 69 WebhookGitLabSecret string `json:"webhookGitLabSecret,omitempty"` 70 // WebhookBitbucketUUID holds the UUID for authenticating Bitbucket webhook events 71 WebhookBitbucketUUID string `json:"webhookBitbucketUUID,omitempty"` 72 // WebhookBitbucketServerSecret holds the shared secret for authenticating BitbucketServer webhook events 73 WebhookBitbucketServerSecret string `json:"webhookBitbucketServerSecret,omitempty"` 74 // WebhookGogsSecret holds the shared secret for authenticating Gogs webhook events 75 WebhookGogsSecret string `json:"webhookGogsSecret,omitempty"` 76 // WebhookAzureDevOpsUsername holds the username for authenticating Azure DevOps webhook events 77 WebhookAzureDevOpsUsername string `json:"webhookAzureDevOpsUsername,omitempty"` 78 // WebhookAzureDevOpsPassword holds the password for authenticating Azure DevOps webhook events 79 WebhookAzureDevOpsPassword string `json:"webhookAzureDevOpsPassword,omitempty"` 80 // Secrets holds all secrets in argocd-secret as a map[string]string 81 Secrets map[string]string `json:"secrets,omitempty"` 82 // KustomizeBuildOptions is a string of kustomize build parameters 83 KustomizeBuildOptions string `json:"kustomizeBuildOptions,omitempty"` 84 // Indicates if anonymous user is enabled or not 85 AnonymousUserEnabled bool `json:"anonymousUserEnabled,omitempty"` 86 // Specifies token expiration duration 87 UserSessionDuration time.Duration `json:"userSessionDuration,omitempty"` 88 // UiCssURL local or remote path to user-defined CSS to customize ArgoCD UI 89 UiCssURL string `json:"uiCssURL,omitempty"` 90 // Content of UI Banner 91 UiBannerContent string `json:"uiBannerContent,omitempty"` 92 // URL for UI Banner 93 UiBannerURL string `json:"uiBannerURL,omitempty"` 94 // Make Banner permanent and not closeable 95 UiBannerPermanent bool `json:"uiBannerPermanent,omitempty"` 96 // Position of UI Banner 97 UiBannerPosition string `json:"uiBannerPosition,omitempty"` 98 // PasswordPattern for password regular expression 99 PasswordPattern string `json:"passwordPattern,omitempty"` 100 // BinaryUrls contains the URLs for downloading argocd binaries 101 BinaryUrls map[string]string `json:"binaryUrls,omitempty"` 102 // InClusterEnabled indicates whether to allow in-cluster server address 103 InClusterEnabled bool `json:"inClusterEnabled"` 104 // ServerRBACLogEnforceEnable temporary var indicates whether rbac will be enforced on logs 105 ServerRBACLogEnforceEnable bool `json:"serverRBACLogEnforceEnable"` 106 // ExecEnabled indicates whether the UI exec feature is enabled 107 ExecEnabled bool `json:"execEnabled"` 108 // ExecShells restricts which shells are allowed for `exec` and in which order they are tried 109 ExecShells []string `json:"execShells"` 110 // TrackingMethod defines the resource tracking method to be used 111 TrackingMethod string `json:"application.resourceTrackingMethod,omitempty"` 112 // OIDCTLSInsecureSkipVerify determines whether certificate verification is skipped when verifying tokens with the 113 // configured OIDC provider (either external or the bundled Dex instance). Setting this to `true` will cause JWT 114 // token verification to pass despite the OIDC provider having an invalid certificate. Only set to `true` if you 115 // understand the risks. 116 OIDCTLSInsecureSkipVerify bool `json:"oidcTLSInsecureSkipVerify"` 117 // AppsInAnyNamespaceEnabled indicates whether applications are allowed to be created in any namespace 118 AppsInAnyNamespaceEnabled bool `json:"appsInAnyNamespaceEnabled"` 119 // ExtensionConfig configurations related to ArgoCD proxy extensions. The value 120 // is a yaml string defined in extension.ExtensionConfigs struct. 121 ExtensionConfig string `json:"extensionConfig,omitempty"` 122 } 123 124 type GoogleAnalytics struct { 125 TrackingID string `json:"trackingID,omitempty"` 126 AnonymizeUsers bool `json:"anonymizeUsers,omitempty"` 127 } 128 129 type GlobalProjectSettings struct { 130 ProjectName string `json:"projectName,omitempty"` 131 LabelSelector metav1.LabelSelector `json:"labelSelector,omitempty"` 132 } 133 134 // Help settings 135 type Help struct { 136 // the URL for getting chat help, this will typically be your Slack channel for support 137 ChatURL string `json:"chatUrl,omitempty"` 138 // the text for getting chat help, defaults to "Chat now!" 139 ChatText string `json:"chatText,omitempty"` 140 // the URLs for downloading argocd binaries 141 BinaryURLs map[string]string `json:"binaryUrl,omitempty"` 142 } 143 144 // oidcConfig is the same as the public OIDCConfig, except the public one excludes the AllowedAudiences and the 145 // SkipAudienceCheckWhenTokenHasNoAudience fields. 146 // AllowedAudiences should be accessed via ArgoCDSettings.OAuth2AllowedAudiences. 147 // SkipAudienceCheckWhenTokenHasNoAudience should be accessed via ArgoCDSettings.SkipAudienceCheckWhenTokenHasNoAudience. 148 type oidcConfig struct { 149 OIDCConfig 150 AllowedAudiences []string `json:"allowedAudiences,omitempty"` 151 SkipAudienceCheckWhenTokenHasNoAudience *bool `json:"skipAudienceCheckWhenTokenHasNoAudience,omitempty"` 152 } 153 154 func (o *oidcConfig) toExported() *OIDCConfig { 155 if o == nil { 156 return nil 157 } 158 return &OIDCConfig{ 159 Name: o.Name, 160 Issuer: o.Issuer, 161 ClientID: o.ClientID, 162 ClientSecret: o.ClientSecret, 163 CLIClientID: o.CLIClientID, 164 UserInfoPath: o.UserInfoPath, 165 EnableUserInfoGroups: o.EnableUserInfoGroups, 166 UserInfoCacheExpiration: o.UserInfoCacheExpiration, 167 RequestedScopes: o.RequestedScopes, 168 RequestedIDTokenClaims: o.RequestedIDTokenClaims, 169 LogoutURL: o.LogoutURL, 170 RootCA: o.RootCA, 171 EnablePKCEAuthentication: o.EnablePKCEAuthentication, 172 } 173 } 174 175 type OIDCConfig struct { 176 Name string `json:"name,omitempty"` 177 Issuer string `json:"issuer,omitempty"` 178 ClientID string `json:"clientID,omitempty"` 179 ClientSecret string `json:"clientSecret,omitempty"` 180 CLIClientID string `json:"cliClientID,omitempty"` 181 EnableUserInfoGroups bool `json:"enableUserInfoGroups,omitempty"` 182 UserInfoPath string `json:"userInfoPath,omitempty"` 183 UserInfoCacheExpiration string `json:"userInfoCacheExpiration,omitempty"` 184 RequestedScopes []string `json:"requestedScopes,omitempty"` 185 RequestedIDTokenClaims map[string]*oidc.Claim `json:"requestedIDTokenClaims,omitempty"` 186 LogoutURL string `json:"logoutURL,omitempty"` 187 RootCA string `json:"rootCA,omitempty"` 188 EnablePKCEAuthentication bool `json:"enablePKCEAuthentication,omitempty"` 189 } 190 191 // DEPRECATED. Helm repository credentials are now managed using RepoCredentials 192 type HelmRepoCredentials struct { 193 URL string `json:"url,omitempty"` 194 Name string `json:"name,omitempty"` 195 UsernameSecret *apiv1.SecretKeySelector `json:"usernameSecret,omitempty"` 196 PasswordSecret *apiv1.SecretKeySelector `json:"passwordSecret,omitempty"` 197 CertSecret *apiv1.SecretKeySelector `json:"certSecret,omitempty"` 198 KeySecret *apiv1.SecretKeySelector `json:"keySecret,omitempty"` 199 } 200 201 // KustomizeVersion holds information about additional Kustomize version 202 type KustomizeVersion struct { 203 // Name holds Kustomize version name 204 Name string 205 // Path holds corresponding binary path 206 Path string 207 // BuildOptions that are specific to Kustomize version 208 BuildOptions string 209 } 210 211 // KustomizeSettings holds kustomize settings 212 type KustomizeSettings struct { 213 BuildOptions string 214 Versions []KustomizeVersion 215 } 216 217 var ( 218 ByClusterURLIndexer = "byClusterURL" 219 byClusterURLIndexerFunc = func(obj interface{}) ([]string, error) { 220 s, ok := obj.(*apiv1.Secret) 221 if !ok { 222 return nil, nil 223 } 224 if s.Labels == nil || s.Labels[common.LabelKeySecretType] != common.LabelValueSecretTypeCluster { 225 return nil, nil 226 } 227 if s.Data == nil { 228 return nil, nil 229 } 230 if url, ok := s.Data["server"]; ok { 231 return []string{strings.TrimRight(string(url), "/")}, nil 232 } 233 return nil, nil 234 } 235 ByClusterNameIndexer = "byClusterName" 236 byClusterNameIndexerFunc = func(obj interface{}) ([]string, error) { 237 s, ok := obj.(*apiv1.Secret) 238 if !ok { 239 return nil, nil 240 } 241 if s.Labels == nil || s.Labels[common.LabelKeySecretType] != common.LabelValueSecretTypeCluster { 242 return nil, nil 243 } 244 if s.Data == nil { 245 return nil, nil 246 } 247 if name, ok := s.Data["name"]; ok { 248 return []string{string(name)}, nil 249 } 250 return nil, nil 251 } 252 ByProjectClusterIndexer = "byProjectCluster" 253 ByProjectRepoIndexer = "byProjectRepo" 254 byProjectIndexerFunc = func(secretType string) func(obj interface{}) ([]string, error) { 255 return func(obj interface{}) ([]string, error) { 256 s, ok := obj.(*apiv1.Secret) 257 if !ok { 258 return nil, nil 259 } 260 if s.Labels == nil || s.Labels[common.LabelKeySecretType] != secretType { 261 return nil, nil 262 } 263 if s.Data == nil { 264 return nil, nil 265 } 266 if project, ok := s.Data["project"]; ok { 267 return []string{string(project)}, nil 268 } 269 return nil, nil 270 } 271 } 272 ) 273 274 func (ks *KustomizeSettings) GetOptions(source v1alpha1.ApplicationSource) (*v1alpha1.KustomizeOptions, error) { 275 binaryPath := "" 276 buildOptions := "" 277 if source.Kustomize != nil && source.Kustomize.Version != "" { 278 for _, ver := range ks.Versions { 279 if ver.Name == source.Kustomize.Version { 280 // add version specific path and build options 281 binaryPath = ver.Path 282 buildOptions = ver.BuildOptions 283 break 284 } 285 } 286 if binaryPath == "" { 287 return nil, fmt.Errorf("kustomize version %s is not registered", source.Kustomize.Version) 288 } 289 } else { 290 // add build options for the default version 291 buildOptions = ks.BuildOptions 292 } 293 return &v1alpha1.KustomizeOptions{ 294 BuildOptions: buildOptions, 295 BinaryPath: binaryPath, 296 }, nil 297 } 298 299 // Credentials for accessing a Git repository 300 type Repository struct { 301 // The URL to the repository 302 URL string `json:"url,omitempty"` 303 // the type of the repo, "git" or "helm", assumed to be "git" if empty or absent 304 Type string `json:"type,omitempty"` 305 // helm only 306 Name string `json:"name,omitempty"` 307 // Name of the secret storing the username used to access the repo 308 UsernameSecret *apiv1.SecretKeySelector `json:"usernameSecret,omitempty"` 309 // Name of the secret storing the password used to access the repo 310 PasswordSecret *apiv1.SecretKeySelector `json:"passwordSecret,omitempty"` 311 // Name of the secret storing the SSH private key used to access the repo. Git only 312 SSHPrivateKeySecret *apiv1.SecretKeySelector `json:"sshPrivateKeySecret,omitempty"` 313 // Whether to connect the repository in an insecure way (deprecated) 314 InsecureIgnoreHostKey bool `json:"insecureIgnoreHostKey,omitempty"` 315 // Whether to connect the repository in an insecure way 316 Insecure bool `json:"insecure,omitempty"` 317 // Whether the repo is git-lfs enabled. Git only. 318 EnableLFS bool `json:"enableLfs,omitempty"` 319 // Name of the secret storing the TLS client cert data 320 TLSClientCertDataSecret *apiv1.SecretKeySelector `json:"tlsClientCertDataSecret,omitempty"` 321 // Name of the secret storing the TLS client cert's key data 322 TLSClientCertKeySecret *apiv1.SecretKeySelector `json:"tlsClientCertKeySecret,omitempty"` 323 // Whether the repo is helm-oci enabled. Git only. 324 EnableOci bool `json:"enableOci,omitempty"` 325 // Github App Private Key PEM data 326 GithubAppPrivateKeySecret *apiv1.SecretKeySelector `json:"githubAppPrivateKeySecret,omitempty"` 327 // Github App ID of the app used to access the repo 328 GithubAppId int64 `json:"githubAppID,omitempty"` 329 // Github App Installation ID of the installed GitHub App 330 GithubAppInstallationId int64 `json:"githubAppInstallationID,omitempty"` 331 // Github App Enterprise base url if empty will default to https://api.github.com 332 GithubAppEnterpriseBaseURL string `json:"githubAppEnterpriseBaseUrl,omitempty"` 333 // Proxy specifies the HTTP/HTTPS proxy used to access the repo 334 Proxy string `json:"proxy,omitempty"` 335 // GCPServiceAccountKey specifies the service account key in JSON format to be used for getting credentials to Google Cloud Source repos 336 GCPServiceAccountKey *apiv1.SecretKeySelector `json:"gcpServiceAccountKey,omitempty"` 337 // ForceHttpBasicAuth determines whether Argo CD should force use of basic auth for HTTP connected repositories 338 ForceHttpBasicAuth bool `json:"forceHttpBasicAuth,omitempty"` 339 } 340 341 // Credential template for accessing repositories 342 type RepositoryCredentials struct { 343 // The URL pattern the repository URL has to match 344 URL string `json:"url,omitempty"` 345 // Name of the secret storing the username used to access the repo 346 UsernameSecret *apiv1.SecretKeySelector `json:"usernameSecret,omitempty"` 347 // Name of the secret storing the password used to access the repo 348 PasswordSecret *apiv1.SecretKeySelector `json:"passwordSecret,omitempty"` 349 // Name of the secret storing the SSH private key used to access the repo. Git only 350 SSHPrivateKeySecret *apiv1.SecretKeySelector `json:"sshPrivateKeySecret,omitempty"` 351 // Name of the secret storing the TLS client cert data 352 TLSClientCertDataSecret *apiv1.SecretKeySelector `json:"tlsClientCertDataSecret,omitempty"` 353 // Name of the secret storing the TLS client cert's key data 354 TLSClientCertKeySecret *apiv1.SecretKeySelector `json:"tlsClientCertKeySecret,omitempty"` 355 // Github App Private Key PEM data 356 GithubAppPrivateKeySecret *apiv1.SecretKeySelector `json:"githubAppPrivateKeySecret,omitempty"` 357 // Github App ID of the app used to access the repo 358 GithubAppId int64 `json:"githubAppID,omitempty"` 359 // Github App Installation ID of the installed GitHub App 360 GithubAppInstallationId int64 `json:"githubAppInstallationID,omitempty"` 361 // Github App Enterprise base url if empty will default to https://api.github.com 362 GithubAppEnterpriseBaseURL string `json:"githubAppEnterpriseBaseUrl,omitempty"` 363 // EnableOCI specifies whether helm-oci support should be enabled for this repo 364 EnableOCI bool `json:"enableOCI,omitempty"` 365 // the type of the repositoryCredentials, "git" or "helm", assumed to be "git" if empty or absent 366 Type string `json:"type,omitempty"` 367 // GCPServiceAccountKey specifies the service account key in JSON format to be used for getting credentials to Google Cloud Source repos 368 GCPServiceAccountKey *apiv1.SecretKeySelector `json:"gcpServiceAccountKey,omitempty"` 369 // ForceHttpBasicAuth determines whether Argo CD should force use of basic auth for HTTP connected repositories 370 ForceHttpBasicAuth bool `json:"forceHttpBasicAuth,omitempty"` 371 } 372 373 // DeepLink structure 374 type DeepLink struct { 375 // URL that the deep link will redirect to 376 URL string `json:"url"` 377 // Title that will be displayed in the UI corresponding to that link 378 Title string `json:"title"` 379 // Description (optional) a description for what the deep link is about 380 Description *string `json:"description,omitempty"` 381 // IconClass (optional) a font-awesome icon class to be used when displaying the links in dropdown menus. 382 IconClass *string `json:"icon.class,omitempty"` 383 // Condition (optional) a conditional statement depending on which the deep link shall be rendered 384 Condition *string `json:"if,omitempty"` 385 } 386 387 const ( 388 // settingServerSignatureKey designates the key for a server secret key inside a Kubernetes secret. 389 settingServerSignatureKey = "server.secretkey" 390 // gaTrackingID holds Google Analytics tracking id 391 gaTrackingID = "ga.trackingid" 392 // the URL for getting chat help, this will typically be your Slack channel for support 393 helpChatURL = "help.chatUrl" 394 // the text for getting chat help, defaults to "Chat now!" 395 helpChatText = "help.chatText" 396 // gaAnonymizeUsers specifies if user ids should be anonymized (hashed) before sending to Google Analytics. True unless value is set to 'false' 397 gaAnonymizeUsers = "ga.anonymizeusers" 398 // settingServerCertificate designates the key for the public cert used in TLS 399 settingServerCertificate = "tls.crt" 400 // settingServerPrivateKey designates the key for the private key used in TLS 401 settingServerPrivateKey = "tls.key" 402 // settingURLKey designates the key where Argo CD's external URL is set 403 settingURLKey = "url" 404 // repositoriesKey designates the key where ArgoCDs repositories list is set 405 repositoriesKey = "repositories" 406 // repositoryCredentialsKey designates the key where ArgoCDs repositories credentials list is set 407 repositoryCredentialsKey = "repository.credentials" 408 // helmRepositoriesKey designates the key where list of helm repositories is set 409 helmRepositoriesKey = "helm.repositories" 410 // settingDexConfigKey designates the key for the dex config 411 settingDexConfigKey = "dex.config" 412 // settingsOIDCConfigKey designates the key for OIDC config 413 settingsOIDCConfigKey = "oidc.config" 414 // statusBadgeEnabledKey holds the key which enables of disables status badge feature 415 statusBadgeEnabledKey = "statusbadge.enabled" 416 // statusBadgeRootUrlKey holds the key for the root badge URL override 417 statusBadgeRootUrlKey = "statusbadge.url" 418 // settingsWebhookGitHubSecret is the key for the GitHub shared webhook secret 419 settingsWebhookGitHubSecretKey = "webhook.github.secret" 420 // settingsWebhookGitLabSecret is the key for the GitLab shared webhook secret 421 settingsWebhookGitLabSecretKey = "webhook.gitlab.secret" 422 // settingsWebhookBitbucketUUID is the key for Bitbucket webhook UUID 423 settingsWebhookBitbucketUUIDKey = "webhook.bitbucket.uuid" 424 // settingsWebhookBitbucketServerSecret is the key for BitbucketServer webhook secret 425 settingsWebhookBitbucketServerSecretKey = "webhook.bitbucketserver.secret" 426 // settingsWebhookGogsSecret is the key for Gogs webhook secret 427 settingsWebhookGogsSecretKey = "webhook.gogs.secret" 428 // settingsWebhookAzureDevOpsUsernameKey is the key for Azure DevOps webhook username 429 settingsWebhookAzureDevOpsUsernameKey = "webhook.azuredevops.username" 430 // settingsWebhookAzureDevOpsPasswordKey is the key for Azure DevOps webhook password 431 settingsWebhookAzureDevOpsPasswordKey = "webhook.azuredevops.password" 432 // settingsApplicationInstanceLabelKey is the key to configure injected app instance label key 433 settingsApplicationInstanceLabelKey = "application.instanceLabelKey" 434 // settingsResourceTrackingMethodKey is the key to configure tracking method for application resources 435 settingsResourceTrackingMethodKey = "application.resourceTrackingMethod" 436 // resourcesCustomizationsKey is the key to the map of resource overrides 437 resourceCustomizationsKey = "resource.customizations" 438 // resourceExclusions is the key to the list of excluded resources 439 resourceExclusionsKey = "resource.exclusions" 440 // resourceInclusions is the key to the list of explicitly watched resources 441 resourceInclusionsKey = "resource.inclusions" 442 // resourceIgnoreResourceUpdatesEnabledKey is the key to a boolean determining whether the resourceIgnoreUpdates feature is enabled 443 resourceIgnoreResourceUpdatesEnabledKey = "resource.ignoreResourceUpdatesEnabled" 444 // resourceCustomLabelKey is the key to a custom label to show in node info, if present 445 resourceCustomLabelsKey = "resource.customLabels" 446 // kustomizeBuildOptionsKey is a string of kustomize build parameters 447 kustomizeBuildOptionsKey = "kustomize.buildOptions" 448 // kustomizeVersionKeyPrefix is a kustomize version key prefix 449 kustomizeVersionKeyPrefix = "kustomize.version" 450 // kustomizePathPrefixKey is a kustomize path for a specific version 451 kustomizePathPrefixKey = "kustomize.path" 452 // anonymousUserEnabledKey is the key which enables or disables anonymous user 453 anonymousUserEnabledKey = "users.anonymous.enabled" 454 // userSessionDurationKey is the key which specifies token expiration duration 455 userSessionDurationKey = "users.session.duration" 456 // diffOptions is the key where diff options are configured 457 resourceCompareOptionsKey = "resource.compareoptions" 458 // settingUiCssURLKey designates the key for user-defined CSS URL for UI customization 459 settingUiCssURLKey = "ui.cssurl" 460 // settingUiBannerContentKey designates the key for content of user-defined info banner for UI 461 settingUiBannerContentKey = "ui.bannercontent" 462 // settingUiBannerURLKey designates the key for the link for user-defined info banner for UI 463 settingUiBannerURLKey = "ui.bannerurl" 464 // settingUiBannerPermanentKey designates the key for whether the banner is permanent and not closeable 465 settingUiBannerPermanentKey = "ui.bannerpermanent" 466 // settingUiBannerPositionKey designates the key for the position of the banner 467 settingUiBannerPositionKey = "ui.bannerposition" 468 // settingsBinaryUrlsKey designates the key for the argocd binary URLs 469 settingsBinaryUrlsKey = "help.download" 470 // globalProjectsKey designates the key for global project settings 471 globalProjectsKey = "globalProjects" 472 // initialPasswordSecretName is the name of the secret that will hold the initial admin password 473 initialPasswordSecretName = "argocd-initial-admin-secret" 474 // initialPasswordSecretField is the name of the field in initialPasswordSecretName to store the password 475 initialPasswordSecretField = "password" 476 // initialPasswordLength defines the length of the generated initial password 477 initialPasswordLength = 16 478 // externalServerTLSSecretName defines the name of the external secret holding the server's TLS certificate 479 externalServerTLSSecretName = "argocd-server-tls" 480 // partOfArgoCDSelector holds label selector that should be applied to config maps and secrets used to manage Argo CD 481 partOfArgoCDSelector = "app.kubernetes.io/part-of=argocd" 482 // settingsPasswordPatternKey is the key to configure user password regular expression 483 settingsPasswordPatternKey = "passwordPattern" 484 // inClusterEnabledKey is the key to configure whether to allow in-cluster server address 485 inClusterEnabledKey = "cluster.inClusterEnabled" 486 // settingsServerRBACLogEnforceEnable is the key to configure whether logs RBAC enforcement is enabled 487 settingsServerRBACLogEnforceEnableKey = "server.rbac.log.enforce.enable" 488 // helmValuesFileSchemesKey is the key to configure the list of supported helm values file schemas 489 helmValuesFileSchemesKey = "helm.valuesFileSchemes" 490 // execEnabledKey is the key to configure whether the UI exec feature is enabled 491 execEnabledKey = "exec.enabled" 492 // execShellsKey is the key to configure which shells are allowed for `exec` and in what order they are tried 493 execShellsKey = "exec.shells" 494 // oidcTLSInsecureSkipVerifyKey is the key to configure whether TLS cert verification is skipped for OIDC connections 495 oidcTLSInsecureSkipVerifyKey = "oidc.tls.insecure.skip.verify" 496 // ApplicationDeepLinks is the application deep link key 497 ApplicationDeepLinks = "application.links" 498 // ProjectDeepLinks is the project deep link key 499 ProjectDeepLinks = "project.links" 500 // ResourceDeepLinks is the resource deep link key 501 ResourceDeepLinks = "resource.links" 502 extensionConfig = "extension.config" 503 // RespectRBAC is the key to configure argocd to respect rbac while watching for resources 504 RespectRBAC = "resource.respectRBAC" 505 RespectRBACValueStrict = "strict" 506 RespectRBACValueNormal = "normal" 507 ) 508 509 var ( 510 sourceTypeToEnableGenerationKey = map[v1alpha1.ApplicationSourceType]string{ 511 v1alpha1.ApplicationSourceTypeKustomize: "kustomize.enable", 512 v1alpha1.ApplicationSourceTypeHelm: "helm.enable", 513 v1alpha1.ApplicationSourceTypeDirectory: "jsonnet.enable", 514 } 515 ) 516 517 // SettingsManager holds config info for a new manager with which to access Kubernetes ConfigMaps. 518 type SettingsManager struct { 519 ctx context.Context 520 clientset kubernetes.Interface 521 secrets v1listers.SecretLister 522 secretsInformer cache.SharedIndexInformer 523 configmaps v1listers.ConfigMapLister 524 namespace string 525 // subscribers is a list of subscribers to settings updates 526 subscribers []chan<- *ArgoCDSettings 527 // mutex protects concurrency sensitive parts of settings manager: access to subscribers list and initialization flag 528 mutex *sync.Mutex 529 initContextCancel func() 530 reposCache []Repository 531 repoCredsCache []RepositoryCredentials 532 reposOrClusterChanged func() 533 } 534 535 type incompleteSettingsError struct { 536 message string 537 } 538 539 type IgnoreStatus string 540 541 const ( 542 // IgnoreResourceStatusInCRD ignores status changes for all CRDs 543 IgnoreResourceStatusInCRD IgnoreStatus = "crd" 544 // IgnoreResourceStatusInAll ignores status changes for all resources 545 IgnoreResourceStatusInAll IgnoreStatus = "all" 546 // IgnoreResourceStatusInNone ignores status changes for no resources 547 IgnoreResourceStatusInNone IgnoreStatus = "off" 548 ) 549 550 type ArgoCDDiffOptions struct { 551 IgnoreAggregatedRoles bool `json:"ignoreAggregatedRoles,omitempty"` 552 553 // If set to true then differences caused by status are ignored. 554 IgnoreResourceStatusField IgnoreStatus `json:"ignoreResourceStatusField,omitempty"` 555 556 // If set to true then ignoreDifferences are applied to ignore application refresh on resource updates. 557 IgnoreDifferencesOnResourceUpdates bool `json:"ignoreDifferencesOnResourceUpdates,omitempty"` 558 } 559 560 func (e *incompleteSettingsError) Error() string { 561 return e.message 562 } 563 564 func (mgr *SettingsManager) onRepoOrClusterChanged() { 565 if mgr.reposOrClusterChanged != nil { 566 go mgr.reposOrClusterChanged() 567 } 568 } 569 570 func (mgr *SettingsManager) RespectRBAC() (int, error) { 571 cm, err := mgr.getConfigMap() 572 if err != nil { 573 return enginecache.RespectRbacDisabled, err 574 } 575 if cm.Data[RespectRBAC] != "" { 576 switch cm.Data[RespectRBAC] { 577 case RespectRBACValueNormal: 578 return enginecache.RespectRbacNormal, nil 579 case RespectRBACValueStrict: 580 return enginecache.RespectRbacStrict, nil 581 default: 582 return enginecache.RespectRbacDisabled, fmt.Errorf("invalid value for %s: %s", RespectRBAC, cm.Data[RespectRBAC]) 583 } 584 } 585 return enginecache.RespectRbacDisabled, nil 586 } 587 588 func (mgr *SettingsManager) GetSecretsLister() (v1listers.SecretLister, error) { 589 err := mgr.ensureSynced(false) 590 if err != nil { 591 return nil, err 592 } 593 return mgr.secrets, nil 594 } 595 596 func (mgr *SettingsManager) GetSecretsInformer() (cache.SharedIndexInformer, error) { 597 err := mgr.ensureSynced(false) 598 if err != nil { 599 return nil, fmt.Errorf("error ensuring that the secrets manager is synced: %w", err) 600 } 601 return mgr.secretsInformer, nil 602 } 603 604 func (mgr *SettingsManager) updateSecret(callback func(*apiv1.Secret) error) error { 605 err := mgr.ensureSynced(false) 606 if err != nil { 607 return err 608 } 609 argoCDSecret, err := mgr.secrets.Secrets(mgr.namespace).Get(common.ArgoCDSecretName) 610 createSecret := false 611 if err != nil { 612 if !apierr.IsNotFound(err) { 613 return err 614 } 615 argoCDSecret = &apiv1.Secret{ 616 ObjectMeta: metav1.ObjectMeta{ 617 Name: common.ArgoCDSecretName, 618 }, 619 Data: make(map[string][]byte), 620 } 621 createSecret = true 622 } 623 if argoCDSecret.Data == nil { 624 argoCDSecret.Data = make(map[string][]byte) 625 } 626 627 updatedSecret := argoCDSecret.DeepCopy() 628 err = callback(updatedSecret) 629 if err != nil { 630 return err 631 } 632 633 if !createSecret && reflect.DeepEqual(argoCDSecret.Data, updatedSecret.Data) { 634 return nil 635 } 636 637 if createSecret { 638 _, err = mgr.clientset.CoreV1().Secrets(mgr.namespace).Create(context.Background(), updatedSecret, metav1.CreateOptions{}) 639 } else { 640 _, err = mgr.clientset.CoreV1().Secrets(mgr.namespace).Update(context.Background(), updatedSecret, metav1.UpdateOptions{}) 641 } 642 if err != nil { 643 return err 644 } 645 646 return mgr.ResyncInformers() 647 } 648 649 func (mgr *SettingsManager) updateConfigMap(callback func(*apiv1.ConfigMap) error) error { 650 argoCDCM, err := mgr.getConfigMap() 651 createCM := false 652 if err != nil { 653 if !apierr.IsNotFound(err) { 654 return err 655 } 656 argoCDCM = &apiv1.ConfigMap{ 657 ObjectMeta: metav1.ObjectMeta{ 658 Name: common.ArgoCDConfigMapName, 659 }, 660 } 661 createCM = true 662 } 663 if argoCDCM.Data == nil { 664 argoCDCM.Data = make(map[string]string) 665 } 666 beforeUpdate := argoCDCM.DeepCopy() 667 err = callback(argoCDCM) 668 if err != nil { 669 return err 670 } 671 if reflect.DeepEqual(beforeUpdate.Data, argoCDCM.Data) { 672 return nil 673 } 674 675 if createCM { 676 _, err = mgr.clientset.CoreV1().ConfigMaps(mgr.namespace).Create(context.Background(), argoCDCM, metav1.CreateOptions{}) 677 } else { 678 _, err = mgr.clientset.CoreV1().ConfigMaps(mgr.namespace).Update(context.Background(), argoCDCM, metav1.UpdateOptions{}) 679 } 680 681 if err != nil { 682 return err 683 } 684 685 mgr.invalidateCache() 686 687 return mgr.ResyncInformers() 688 } 689 690 func (mgr *SettingsManager) getConfigMap() (*apiv1.ConfigMap, error) { 691 err := mgr.ensureSynced(false) 692 if err != nil { 693 return nil, err 694 } 695 argoCDCM, err := mgr.configmaps.ConfigMaps(mgr.namespace).Get(common.ArgoCDConfigMapName) 696 if err != nil { 697 return nil, err 698 } 699 if argoCDCM.Data == nil { 700 argoCDCM.Data = make(map[string]string) 701 } 702 return argoCDCM, err 703 } 704 705 // Returns the ConfigMap with the given name from the cluster. 706 // The ConfigMap must be labeled with "app.kubernetes.io/part-of: argocd" in 707 // order to be retrievable. 708 func (mgr *SettingsManager) GetConfigMapByName(configMapName string) (*apiv1.ConfigMap, error) { 709 err := mgr.ensureSynced(false) 710 if err != nil { 711 return nil, err 712 } 713 configMap, err := mgr.configmaps.ConfigMaps(mgr.namespace).Get(configMapName) 714 if err != nil { 715 return nil, err 716 } 717 return configMap, err 718 } 719 720 func (mgr *SettingsManager) GetResourcesFilter() (*ResourcesFilter, error) { 721 argoCDCM, err := mgr.getConfigMap() 722 if err != nil { 723 return nil, fmt.Errorf("error retrieving argocd-cm: %w", err) 724 } 725 rf := &ResourcesFilter{} 726 if value, ok := argoCDCM.Data[resourceInclusionsKey]; ok { 727 includedResources := make([]FilteredResource, 0) 728 err := yaml.Unmarshal([]byte(value), &includedResources) 729 if err != nil { 730 return nil, fmt.Errorf("error unmarshalling included resources %w", err) 731 } 732 rf.ResourceInclusions = includedResources 733 } 734 735 if value, ok := argoCDCM.Data[resourceExclusionsKey]; ok { 736 excludedResources := make([]FilteredResource, 0) 737 err := yaml.Unmarshal([]byte(value), &excludedResources) 738 if err != nil { 739 return nil, fmt.Errorf("error unmarshalling excluded resources %w", err) 740 } 741 rf.ResourceExclusions = excludedResources 742 } 743 return rf, nil 744 } 745 746 func (mgr *SettingsManager) GetAppInstanceLabelKey() (string, error) { 747 argoCDCM, err := mgr.getConfigMap() 748 if err != nil { 749 return "", err 750 } 751 label := argoCDCM.Data[settingsApplicationInstanceLabelKey] 752 if label == "" { 753 return common.LabelKeyAppInstance, nil 754 } 755 return label, nil 756 } 757 758 func (mgr *SettingsManager) GetTrackingMethod() (string, error) { 759 argoCDCM, err := mgr.getConfigMap() 760 if err != nil { 761 return "", err 762 } 763 return argoCDCM.Data[settingsResourceTrackingMethodKey], nil 764 } 765 766 func (mgr *SettingsManager) GetPasswordPattern() (string, error) { 767 argoCDCM, err := mgr.getConfigMap() 768 if err != nil { 769 return "", err 770 } 771 label := argoCDCM.Data[settingsPasswordPatternKey] 772 if label == "" { 773 return common.PasswordPatten, nil 774 } 775 return label, nil 776 } 777 778 func (mgr *SettingsManager) GetServerRBACLogEnforceEnable() (bool, error) { 779 argoCDCM, err := mgr.getConfigMap() 780 if err != nil { 781 return false, err 782 } 783 784 if argoCDCM.Data[settingsServerRBACLogEnforceEnableKey] == "" { 785 return false, nil 786 } 787 788 return strconv.ParseBool(argoCDCM.Data[settingsServerRBACLogEnforceEnableKey]) 789 } 790 791 func (mgr *SettingsManager) GetDeepLinks(deeplinkType string) ([]DeepLink, error) { 792 argoCDCM, err := mgr.getConfigMap() 793 if err != nil { 794 return nil, fmt.Errorf("error retrieving argocd-cm: %w", err) 795 } 796 deepLinks := make([]DeepLink, 0) 797 if value, ok := argoCDCM.Data[deeplinkType]; ok { 798 err := yaml.Unmarshal([]byte(value), &deepLinks) 799 if err != nil { 800 return nil, fmt.Errorf("error unmarshalling deep links %w", err) 801 } 802 } 803 return deepLinks, nil 804 } 805 806 func (mgr *SettingsManager) GetEnabledSourceTypes() (map[string]bool, error) { 807 argoCDCM, err := mgr.getConfigMap() 808 if err != nil { 809 return nil, fmt.Errorf("failed to get argo-cd config map: %w", err) 810 } 811 res := map[string]bool{} 812 for sourceType := range sourceTypeToEnableGenerationKey { 813 res[string(sourceType)] = true 814 } 815 for sourceType, key := range sourceTypeToEnableGenerationKey { 816 if val, ok := argoCDCM.Data[key]; ok && val != "" { 817 res[string(sourceType)] = val == "true" 818 } 819 } 820 // plugin based manifest generation cannot be disabled 821 res[string(v1alpha1.ApplicationSourceTypePlugin)] = true 822 return res, nil 823 } 824 825 func (mgr *SettingsManager) GetIgnoreResourceUpdatesOverrides() (map[string]v1alpha1.ResourceOverride, error) { 826 compareOptions, err := mgr.GetResourceCompareOptions() 827 if err != nil { 828 return nil, fmt.Errorf("failed to get compare options: %w", err) 829 } 830 831 resourceOverrides, err := mgr.GetResourceOverrides() 832 if err != nil { 833 return nil, fmt.Errorf("failed to get resource overrides: %w", err) 834 } 835 836 for k, v := range resourceOverrides { 837 resourceUpdates := v.IgnoreResourceUpdates 838 if compareOptions.IgnoreDifferencesOnResourceUpdates { 839 resourceUpdates.JQPathExpressions = append(resourceUpdates.JQPathExpressions, v.IgnoreDifferences.JQPathExpressions...) 840 resourceUpdates.JSONPointers = append(resourceUpdates.JSONPointers, v.IgnoreDifferences.JSONPointers...) 841 resourceUpdates.ManagedFieldsManagers = append(resourceUpdates.ManagedFieldsManagers, v.IgnoreDifferences.ManagedFieldsManagers...) 842 } 843 // Set the IgnoreDifferences because these are the overrides used by Normalizers 844 v.IgnoreDifferences = resourceUpdates 845 v.IgnoreResourceUpdates = v1alpha1.OverrideIgnoreDiff{} 846 resourceOverrides[k] = v 847 } 848 849 if compareOptions.IgnoreDifferencesOnResourceUpdates { 850 log.Info("Using diffing customizations to ignore resource updates") 851 } 852 853 addIgnoreDiffItemOverrideToGK(resourceOverrides, "*/*", "/metadata/resourceVersion") 854 addIgnoreDiffItemOverrideToGK(resourceOverrides, "*/*", "/metadata/generation") 855 addIgnoreDiffItemOverrideToGK(resourceOverrides, "*/*", "/metadata/managedFields") 856 857 return resourceOverrides, nil 858 } 859 860 func (mgr *SettingsManager) GetIsIgnoreResourceUpdatesEnabled() (bool, error) { 861 argoCDCM, err := mgr.getConfigMap() 862 if err != nil { 863 return false, fmt.Errorf("error retrieving config map: %w", err) 864 } 865 866 if argoCDCM.Data[resourceIgnoreResourceUpdatesEnabledKey] == "" { 867 return false, nil 868 } 869 870 return strconv.ParseBool(argoCDCM.Data[resourceIgnoreResourceUpdatesEnabledKey]) 871 } 872 873 // GetResourceOverrides loads Resource Overrides from argocd-cm ConfigMap 874 func (mgr *SettingsManager) GetResourceOverrides() (map[string]v1alpha1.ResourceOverride, error) { 875 argoCDCM, err := mgr.getConfigMap() 876 if err != nil { 877 return nil, fmt.Errorf("error retrieving config map: %w", err) 878 } 879 resourceOverrides := map[string]v1alpha1.ResourceOverride{} 880 if value, ok := argoCDCM.Data[resourceCustomizationsKey]; ok && value != "" { 881 err := yaml.Unmarshal([]byte(value), &resourceOverrides) 882 if err != nil { 883 return nil, err 884 } 885 } 886 887 err = mgr.appendResourceOverridesFromSplitKeys(argoCDCM.Data, resourceOverrides) 888 if err != nil { 889 return nil, err 890 } 891 892 var diffOptions ArgoCDDiffOptions 893 if value, ok := argoCDCM.Data[resourceCompareOptionsKey]; ok { 894 err := yaml.Unmarshal([]byte(value), &diffOptions) 895 if err != nil { 896 return nil, err 897 } 898 } 899 900 crdGK := "apiextensions.k8s.io/CustomResourceDefinition" 901 crdPrsvUnkn := "/spec/preserveUnknownFields" 902 903 switch diffOptions.IgnoreResourceStatusField { 904 case "", "crd": 905 addStatusOverrideToGK(resourceOverrides, crdGK) 906 addIgnoreDiffItemOverrideToGK(resourceOverrides, crdGK, crdPrsvUnkn) 907 case "all": 908 addStatusOverrideToGK(resourceOverrides, "*/*") 909 log.Info("Ignore status for all objects") 910 911 case "off", "false": 912 log.Info("Not ignoring status for any object") 913 914 default: 915 addStatusOverrideToGK(resourceOverrides, crdGK) 916 log.Warnf("Unrecognized value for ignoreResourceStatusField - %s, ignore status for CustomResourceDefinitions", diffOptions.IgnoreResourceStatusField) 917 } 918 919 return resourceOverrides, nil 920 } 921 922 func addStatusOverrideToGK(resourceOverrides map[string]v1alpha1.ResourceOverride, groupKind string) { 923 if val, ok := resourceOverrides[groupKind]; ok { 924 val.IgnoreDifferences.JSONPointers = append(val.IgnoreDifferences.JSONPointers, "/status") 925 resourceOverrides[groupKind] = val 926 } else { 927 resourceOverrides[groupKind] = v1alpha1.ResourceOverride{ 928 IgnoreDifferences: v1alpha1.OverrideIgnoreDiff{JSONPointers: []string{"/status"}}, 929 } 930 } 931 } 932 933 func addIgnoreDiffItemOverrideToGK(resourceOverrides map[string]v1alpha1.ResourceOverride, groupKind, ignoreItem string) { 934 if val, ok := resourceOverrides[groupKind]; ok { 935 val.IgnoreDifferences.JSONPointers = append(val.IgnoreDifferences.JSONPointers, ignoreItem) 936 resourceOverrides[groupKind] = val 937 } else { 938 resourceOverrides[groupKind] = v1alpha1.ResourceOverride{ 939 IgnoreDifferences: v1alpha1.OverrideIgnoreDiff{JSONPointers: []string{ignoreItem}}, 940 } 941 } 942 } 943 944 func (mgr *SettingsManager) appendResourceOverridesFromSplitKeys(cmData map[string]string, resourceOverrides map[string]v1alpha1.ResourceOverride) error { 945 for k, v := range cmData { 946 if !strings.HasPrefix(k, resourceCustomizationsKey) { 947 continue 948 } 949 950 // config map key should be of format resource.customizations.<type>.<group-kind> 951 parts := strings.SplitN(k, ".", 4) 952 if len(parts) < 4 { 953 continue 954 } 955 956 overrideKey, err := convertToOverrideKey(parts[3]) 957 if err != nil { 958 return err 959 } 960 961 if overrideKey == "all" { 962 overrideKey = "*/*" 963 } 964 965 overrideVal, ok := resourceOverrides[overrideKey] 966 if !ok { 967 overrideVal = v1alpha1.ResourceOverride{} 968 } 969 970 customizationType := parts[2] 971 switch customizationType { 972 case "health": 973 overrideVal.HealthLua = v 974 case "useOpenLibs": 975 useOpenLibs, err := strconv.ParseBool(v) 976 if err != nil { 977 return err 978 } 979 overrideVal.UseOpenLibs = useOpenLibs 980 case "actions": 981 overrideVal.Actions = v 982 case "ignoreDifferences": 983 overrideIgnoreDiff := v1alpha1.OverrideIgnoreDiff{} 984 err := yaml.Unmarshal([]byte(v), &overrideIgnoreDiff) 985 if err != nil { 986 return err 987 } 988 overrideVal.IgnoreDifferences = overrideIgnoreDiff 989 case "ignoreResourceUpdates": 990 overrideIgnoreUpdate := v1alpha1.OverrideIgnoreDiff{} 991 err := yaml.Unmarshal([]byte(v), &overrideIgnoreUpdate) 992 if err != nil { 993 return err 994 } 995 overrideVal.IgnoreResourceUpdates = overrideIgnoreUpdate 996 case "knownTypeFields": 997 var knownTypeFields []v1alpha1.KnownTypeField 998 err := yaml.Unmarshal([]byte(v), &knownTypeFields) 999 if err != nil { 1000 return err 1001 } 1002 overrideVal.KnownTypeFields = knownTypeFields 1003 default: 1004 return fmt.Errorf("resource customization type %s not supported", customizationType) 1005 } 1006 resourceOverrides[overrideKey] = overrideVal 1007 } 1008 return nil 1009 } 1010 1011 // Convert group-kind format to <group/kind>, allowed key format examples 1012 // resource.customizations.health.cert-manager.io_Certificate 1013 // resource.customizations.health.Certificate 1014 func convertToOverrideKey(groupKind string) (string, error) { 1015 parts := strings.Split(groupKind, "_") 1016 if len(parts) == 2 { 1017 return fmt.Sprintf("%s/%s", parts[0], parts[1]), nil 1018 } else if len(parts) == 1 && groupKind != "" { 1019 return groupKind, nil 1020 } 1021 return "", fmt.Errorf("group kind should be in format `resource.customizations.<type>.<group_kind>` or resource.customizations.<type>.<kind>`, got group kind: '%s'", groupKind) 1022 } 1023 1024 func GetDefaultDiffOptions() ArgoCDDiffOptions { 1025 return ArgoCDDiffOptions{IgnoreAggregatedRoles: false, IgnoreDifferencesOnResourceUpdates: false} 1026 } 1027 1028 // GetResourceCompareOptions loads the resource compare options settings from the ConfigMap 1029 func (mgr *SettingsManager) GetResourceCompareOptions() (ArgoCDDiffOptions, error) { 1030 // We have a sane set of default diff options 1031 diffOptions := GetDefaultDiffOptions() 1032 1033 argoCDCM, err := mgr.getConfigMap() 1034 if err != nil { 1035 return diffOptions, err 1036 } 1037 1038 if value, ok := argoCDCM.Data[resourceCompareOptionsKey]; ok { 1039 err := yaml.Unmarshal([]byte(value), &diffOptions) 1040 if err != nil { 1041 return diffOptions, err 1042 } 1043 } 1044 1045 return diffOptions, nil 1046 } 1047 1048 // GetHelmSettings returns helm settings 1049 func (mgr *SettingsManager) GetHelmSettings() (*v1alpha1.HelmOptions, error) { 1050 argoCDCM, err := mgr.getConfigMap() 1051 if err != nil { 1052 return nil, fmt.Errorf("failed to get argo-cd config map: %v", err) 1053 } 1054 helmOptions := &v1alpha1.HelmOptions{} 1055 if value, ok := argoCDCM.Data[helmValuesFileSchemesKey]; ok { 1056 for _, item := range strings.Split(value, ",") { 1057 if item := strings.TrimSpace(item); item != "" { 1058 helmOptions.ValuesFileSchemes = append(helmOptions.ValuesFileSchemes, item) 1059 } 1060 } 1061 } else { 1062 helmOptions.ValuesFileSchemes = []string{"https", "http"} 1063 } 1064 return helmOptions, nil 1065 } 1066 1067 // GetKustomizeSettings loads the kustomize settings from argocd-cm ConfigMap 1068 func (mgr *SettingsManager) GetKustomizeSettings() (*KustomizeSettings, error) { 1069 argoCDCM, err := mgr.getConfigMap() 1070 if err != nil { 1071 return nil, fmt.Errorf("error retrieving argocd-cm: %w", err) 1072 } 1073 kustomizeVersionsMap := map[string]KustomizeVersion{} 1074 buildOptions := map[string]string{} 1075 settings := &KustomizeSettings{} 1076 1077 // extract build options for the default version 1078 if options, ok := argoCDCM.Data[kustomizeBuildOptionsKey]; ok { 1079 settings.BuildOptions = options 1080 } 1081 1082 // extract per-version binary paths and build options 1083 for k, v := range argoCDCM.Data { 1084 // extract version and path from kustomize.version.<version> 1085 if strings.HasPrefix(k, kustomizeVersionKeyPrefix) { 1086 err = addKustomizeVersion(kustomizeVersionKeyPrefix, k, v, kustomizeVersionsMap) 1087 if err != nil { 1088 return nil, fmt.Errorf("failed to add kustomize version from %q: %w", k, err) 1089 } 1090 } 1091 1092 // extract version and path from kustomize.path.<version> 1093 if strings.HasPrefix(k, kustomizePathPrefixKey) { 1094 err = addKustomizeVersion(kustomizePathPrefixKey, k, v, kustomizeVersionsMap) 1095 if err != nil { 1096 return nil, fmt.Errorf("failed to add kustomize version from %q: %w", k, err) 1097 } 1098 } 1099 1100 // extract version and build options from kustomize.buildOptions.<version> 1101 if strings.HasPrefix(k, kustomizeBuildOptionsKey) && k != kustomizeBuildOptionsKey { 1102 buildOptions[k[len(kustomizeBuildOptionsKey)+1:]] = v 1103 } 1104 } 1105 1106 for _, v := range kustomizeVersionsMap { 1107 if _, ok := buildOptions[v.Name]; ok { 1108 v.BuildOptions = buildOptions[v.Name] 1109 } 1110 settings.Versions = append(settings.Versions, v) 1111 } 1112 return settings, nil 1113 } 1114 1115 func addKustomizeVersion(prefix, name, path string, kvMap map[string]KustomizeVersion) error { 1116 version := name[len(prefix)+1:] 1117 if _, ok := kvMap[version]; ok { 1118 return fmt.Errorf("found duplicate kustomize version: %s", version) 1119 } 1120 kvMap[version] = KustomizeVersion{ 1121 Name: version, 1122 Path: path, 1123 } 1124 return nil 1125 } 1126 1127 // DEPRECATED. Helm repository credentials are now managed using RepoCredentials 1128 func (mgr *SettingsManager) GetHelmRepositories() ([]HelmRepoCredentials, error) { 1129 argoCDCM, err := mgr.getConfigMap() 1130 if err != nil { 1131 return nil, fmt.Errorf("error retrieving config map: %w", err) 1132 } 1133 helmRepositories := make([]HelmRepoCredentials, 0) 1134 helmRepositoriesStr := argoCDCM.Data[helmRepositoriesKey] 1135 if helmRepositoriesStr != "" { 1136 err := yaml.Unmarshal([]byte(helmRepositoriesStr), &helmRepositories) 1137 if err != nil { 1138 return nil, fmt.Errorf("error unmarshalling helm repositories: %w", err) 1139 } 1140 } 1141 return helmRepositories, nil 1142 } 1143 1144 func (mgr *SettingsManager) GetRepositories() ([]Repository, error) { 1145 1146 mgr.mutex.Lock() 1147 reposCache := mgr.reposCache 1148 mgr.mutex.Unlock() 1149 if reposCache != nil { 1150 return reposCache, nil 1151 } 1152 1153 // Get the config map outside of the lock 1154 argoCDCM, err := mgr.getConfigMap() 1155 if err != nil { 1156 return nil, fmt.Errorf("failed to get argo-cd config map: %w", err) 1157 } 1158 1159 mgr.mutex.Lock() 1160 defer mgr.mutex.Unlock() 1161 repositories := make([]Repository, 0) 1162 repositoriesStr := argoCDCM.Data[repositoriesKey] 1163 if repositoriesStr != "" { 1164 err := yaml.Unmarshal([]byte(repositoriesStr), &repositories) 1165 if err != nil { 1166 return nil, fmt.Errorf("failed to unmarshal repositories from config map key %q: %w", repositoriesKey, err) 1167 } 1168 } 1169 mgr.reposCache = repositories 1170 1171 return mgr.reposCache, nil 1172 } 1173 1174 func (mgr *SettingsManager) SaveRepositories(repos []Repository) error { 1175 return mgr.updateConfigMap(func(argoCDCM *apiv1.ConfigMap) error { 1176 if len(repos) > 0 { 1177 yamlStr, err := yaml.Marshal(repos) 1178 if err != nil { 1179 return err 1180 } 1181 argoCDCM.Data[repositoriesKey] = string(yamlStr) 1182 } else { 1183 delete(argoCDCM.Data, repositoriesKey) 1184 } 1185 return nil 1186 }) 1187 } 1188 1189 func (mgr *SettingsManager) SaveRepositoryCredentials(creds []RepositoryCredentials) error { 1190 return mgr.updateConfigMap(func(argoCDCM *apiv1.ConfigMap) error { 1191 if len(creds) > 0 { 1192 yamlStr, err := yaml.Marshal(creds) 1193 if err != nil { 1194 return err 1195 } 1196 argoCDCM.Data[repositoryCredentialsKey] = string(yamlStr) 1197 } else { 1198 delete(argoCDCM.Data, repositoryCredentialsKey) 1199 } 1200 return nil 1201 }) 1202 } 1203 1204 func (mgr *SettingsManager) GetRepositoryCredentials() ([]RepositoryCredentials, error) { 1205 1206 mgr.mutex.Lock() 1207 repoCredsCache := mgr.repoCredsCache 1208 mgr.mutex.Unlock() 1209 if repoCredsCache != nil { 1210 return repoCredsCache, nil 1211 } 1212 1213 // Get the config map outside of the lock 1214 argoCDCM, err := mgr.getConfigMap() 1215 if err != nil { 1216 return nil, fmt.Errorf("error retrieving config map: %w", err) 1217 } 1218 1219 mgr.mutex.Lock() 1220 defer mgr.mutex.Unlock() 1221 creds := make([]RepositoryCredentials, 0) 1222 credsStr := argoCDCM.Data[repositoryCredentialsKey] 1223 if credsStr != "" { 1224 err := yaml.Unmarshal([]byte(credsStr), &creds) 1225 if err != nil { 1226 return nil, err 1227 } 1228 } 1229 mgr.repoCredsCache = creds 1230 1231 return mgr.repoCredsCache, nil 1232 } 1233 1234 func (mgr *SettingsManager) GetGoogleAnalytics() (*GoogleAnalytics, error) { 1235 argoCDCM, err := mgr.getConfigMap() 1236 if err != nil { 1237 return nil, fmt.Errorf("error retrieving config map: %w", err) 1238 } 1239 return &GoogleAnalytics{ 1240 TrackingID: argoCDCM.Data[gaTrackingID], 1241 AnonymizeUsers: argoCDCM.Data[gaAnonymizeUsers] != "false", 1242 }, nil 1243 } 1244 1245 func (mgr *SettingsManager) GetHelp() (*Help, error) { 1246 argoCDCM, err := mgr.getConfigMap() 1247 if err != nil { 1248 return nil, fmt.Errorf("error retrieving config map: %w", err) 1249 } 1250 chatText, ok := argoCDCM.Data[helpChatText] 1251 if !ok { 1252 chatText = "Chat now!" 1253 } 1254 chatURL, ok := argoCDCM.Data[helpChatURL] 1255 if !ok { 1256 chatText = "" 1257 } 1258 return &Help{ 1259 ChatURL: chatURL, 1260 ChatText: chatText, 1261 BinaryURLs: getDownloadBinaryUrlsFromConfigMap(argoCDCM), 1262 }, nil 1263 } 1264 1265 // GetSettings retrieves settings from the ArgoCDConfigMap and secret. 1266 func (mgr *SettingsManager) GetSettings() (*ArgoCDSettings, error) { 1267 err := mgr.ensureSynced(false) 1268 if err != nil { 1269 return nil, err 1270 } 1271 argoCDCM, err := mgr.configmaps.ConfigMaps(mgr.namespace).Get(common.ArgoCDConfigMapName) 1272 if err != nil { 1273 return nil, fmt.Errorf("error retrieving argocd-cm: %w", err) 1274 } 1275 argoCDSecret, err := mgr.secrets.Secrets(mgr.namespace).Get(common.ArgoCDSecretName) 1276 if err != nil { 1277 return nil, fmt.Errorf("error retrieving argocd-secret: %w", err) 1278 } 1279 selector, err := labels.Parse(partOfArgoCDSelector) 1280 if err != nil { 1281 return nil, fmt.Errorf("error parsing Argo CD selector %w", err) 1282 } 1283 secrets, err := mgr.secrets.Secrets(mgr.namespace).List(selector) 1284 if err != nil { 1285 return nil, err 1286 } 1287 var settings ArgoCDSettings 1288 var errs []error 1289 updateSettingsFromConfigMap(&settings, argoCDCM) 1290 if err := mgr.updateSettingsFromSecret(&settings, argoCDSecret, secrets); err != nil { 1291 errs = append(errs, err) 1292 } 1293 if len(errs) > 0 { 1294 return &settings, errs[0] 1295 } 1296 1297 return &settings, nil 1298 } 1299 1300 // Clears cached settings on configmap/secret change 1301 func (mgr *SettingsManager) invalidateCache() { 1302 mgr.mutex.Lock() 1303 defer mgr.mutex.Unlock() 1304 1305 mgr.reposCache = nil 1306 mgr.repoCredsCache = nil 1307 } 1308 1309 func (mgr *SettingsManager) initialize(ctx context.Context) error { 1310 tweakConfigMap := func(options *metav1.ListOptions) { 1311 cmLabelSelector := fields.ParseSelectorOrDie(partOfArgoCDSelector) 1312 options.LabelSelector = cmLabelSelector.String() 1313 } 1314 1315 eventHandler := cache.ResourceEventHandlerFuncs{ 1316 UpdateFunc: func(oldObj, newObj interface{}) { 1317 mgr.invalidateCache() 1318 mgr.onRepoOrClusterChanged() 1319 }, 1320 AddFunc: func(obj interface{}) { 1321 mgr.onRepoOrClusterChanged() 1322 }, 1323 DeleteFunc: func(obj interface{}) { 1324 mgr.onRepoOrClusterChanged() 1325 }, 1326 } 1327 indexers := cache.Indexers{ 1328 cache.NamespaceIndex: cache.MetaNamespaceIndexFunc, 1329 ByClusterURLIndexer: byClusterURLIndexerFunc, 1330 ByClusterNameIndexer: byClusterNameIndexerFunc, 1331 ByProjectClusterIndexer: byProjectIndexerFunc(common.LabelValueSecretTypeCluster), 1332 ByProjectRepoIndexer: byProjectIndexerFunc(common.LabelValueSecretTypeRepository), 1333 } 1334 cmInformer := v1.NewFilteredConfigMapInformer(mgr.clientset, mgr.namespace, 3*time.Minute, indexers, tweakConfigMap) 1335 secretsInformer := v1.NewSecretInformer(mgr.clientset, mgr.namespace, 3*time.Minute, indexers) 1336 _, err := cmInformer.AddEventHandler(eventHandler) 1337 if err != nil { 1338 log.Error(err) 1339 } 1340 1341 _, err = secretsInformer.AddEventHandler(eventHandler) 1342 if err != nil { 1343 log.Error(err) 1344 } 1345 1346 log.Info("Starting configmap/secret informers") 1347 go func() { 1348 cmInformer.Run(ctx.Done()) 1349 log.Info("configmap informer cancelled") 1350 }() 1351 go func() { 1352 secretsInformer.Run(ctx.Done()) 1353 log.Info("secrets informer cancelled") 1354 }() 1355 1356 if !cache.WaitForCacheSync(ctx.Done(), cmInformer.HasSynced, secretsInformer.HasSynced) { 1357 return fmt.Errorf("Timed out waiting for settings cache to sync") 1358 } 1359 log.Info("Configmap/secret informer synced") 1360 1361 tryNotify := func() { 1362 newSettings, err := mgr.GetSettings() 1363 if err != nil { 1364 log.Warnf("Unable to parse updated settings: %v", err) 1365 } else { 1366 mgr.notifySubscribers(newSettings) 1367 } 1368 } 1369 now := time.Now() 1370 handler := cache.ResourceEventHandlerFuncs{ 1371 AddFunc: func(obj interface{}) { 1372 if metaObj, ok := obj.(metav1.Object); ok { 1373 if metaObj.GetCreationTimestamp().After(now) { 1374 tryNotify() 1375 } 1376 } 1377 1378 }, 1379 UpdateFunc: func(oldObj, newObj interface{}) { 1380 oldMeta, oldOk := oldObj.(metav1.Common) 1381 newMeta, newOk := newObj.(metav1.Common) 1382 if oldOk && newOk && oldMeta.GetResourceVersion() != newMeta.GetResourceVersion() { 1383 tryNotify() 1384 } 1385 }, 1386 } 1387 _, err = secretsInformer.AddEventHandler(handler) 1388 if err != nil { 1389 log.Error(err) 1390 } 1391 _, err = cmInformer.AddEventHandler(handler) 1392 if err != nil { 1393 log.Error(err) 1394 } 1395 mgr.secrets = v1listers.NewSecretLister(secretsInformer.GetIndexer()) 1396 mgr.secretsInformer = secretsInformer 1397 mgr.configmaps = v1listers.NewConfigMapLister(cmInformer.GetIndexer()) 1398 return nil 1399 } 1400 1401 func (mgr *SettingsManager) ensureSynced(forceResync bool) error { 1402 mgr.mutex.Lock() 1403 defer mgr.mutex.Unlock() 1404 if !forceResync && mgr.secrets != nil && mgr.configmaps != nil { 1405 return nil 1406 } 1407 1408 if mgr.initContextCancel != nil { 1409 mgr.initContextCancel() 1410 } 1411 ctx, cancel := context.WithCancel(mgr.ctx) 1412 mgr.initContextCancel = cancel 1413 return mgr.initialize(ctx) 1414 } 1415 1416 func getDownloadBinaryUrlsFromConfigMap(argoCDCM *apiv1.ConfigMap) map[string]string { 1417 binaryUrls := map[string]string{} 1418 for _, archType := range []string{"darwin-amd64", "darwin-arm64", "windows-amd64", "linux-amd64", "linux-arm64", "linux-ppc64le", "linux-s390x"} { 1419 if val, ok := argoCDCM.Data[settingsBinaryUrlsKey+"."+archType]; ok { 1420 binaryUrls[archType] = val 1421 } 1422 } 1423 return binaryUrls 1424 } 1425 1426 // updateSettingsFromConfigMap transfers settings from a Kubernetes configmap into an ArgoCDSettings struct. 1427 func updateSettingsFromConfigMap(settings *ArgoCDSettings, argoCDCM *apiv1.ConfigMap) { 1428 settings.DexConfig = argoCDCM.Data[settingDexConfigKey] 1429 settings.OIDCConfigRAW = argoCDCM.Data[settingsOIDCConfigKey] 1430 settings.KustomizeBuildOptions = argoCDCM.Data[kustomizeBuildOptionsKey] 1431 settings.StatusBadgeEnabled = argoCDCM.Data[statusBadgeEnabledKey] == "true" 1432 settings.StatusBadgeRootUrl = argoCDCM.Data[statusBadgeRootUrlKey] 1433 settings.AnonymousUserEnabled = argoCDCM.Data[anonymousUserEnabledKey] == "true" 1434 settings.UiCssURL = argoCDCM.Data[settingUiCssURLKey] 1435 settings.UiBannerContent = argoCDCM.Data[settingUiBannerContentKey] 1436 settings.UiBannerPermanent = argoCDCM.Data[settingUiBannerPermanentKey] == "true" 1437 settings.UiBannerPosition = argoCDCM.Data[settingUiBannerPositionKey] 1438 settings.ServerRBACLogEnforceEnable = argoCDCM.Data[settingsServerRBACLogEnforceEnableKey] == "true" 1439 settings.BinaryUrls = getDownloadBinaryUrlsFromConfigMap(argoCDCM) 1440 if err := validateExternalURL(argoCDCM.Data[settingURLKey]); err != nil { 1441 log.Warnf("Failed to validate URL in configmap: %v", err) 1442 } 1443 settings.URL = argoCDCM.Data[settingURLKey] 1444 if err := validateExternalURL(argoCDCM.Data[settingUiBannerURLKey]); err != nil { 1445 log.Warnf("Failed to validate UI banner URL in configmap: %v", err) 1446 } 1447 settings.UiBannerURL = argoCDCM.Data[settingUiBannerURLKey] 1448 settings.UserSessionDuration = time.Hour * 24 1449 if userSessionDurationStr, ok := argoCDCM.Data[userSessionDurationKey]; ok { 1450 if val, err := timeutil.ParseDuration(userSessionDurationStr); err != nil { 1451 log.Warnf("Failed to parse '%s' key: %v", userSessionDurationKey, err) 1452 } else { 1453 settings.UserSessionDuration = *val 1454 } 1455 } 1456 settings.PasswordPattern = argoCDCM.Data[settingsPasswordPatternKey] 1457 if settings.PasswordPattern == "" { 1458 settings.PasswordPattern = common.PasswordPatten 1459 } 1460 settings.InClusterEnabled = argoCDCM.Data[inClusterEnabledKey] != "false" 1461 settings.ExecEnabled = argoCDCM.Data[execEnabledKey] == "true" 1462 execShells := argoCDCM.Data[execShellsKey] 1463 if execShells != "" { 1464 settings.ExecShells = strings.Split(execShells, ",") 1465 } else { 1466 // Fall back to default. If you change this list, also change docs/operator-manual/argocd-cm.yaml. 1467 settings.ExecShells = []string{"bash", "sh", "powershell", "cmd"} 1468 } 1469 settings.TrackingMethod = argoCDCM.Data[settingsResourceTrackingMethodKey] 1470 settings.OIDCTLSInsecureSkipVerify = argoCDCM.Data[oidcTLSInsecureSkipVerifyKey] == "true" 1471 settings.ExtensionConfig = argoCDCM.Data[extensionConfig] 1472 } 1473 1474 // validateExternalURL ensures the external URL that is set on the configmap is valid 1475 func validateExternalURL(u string) error { 1476 if u == "" { 1477 return nil 1478 } 1479 URL, err := url.Parse(u) 1480 if err != nil { 1481 return fmt.Errorf("Failed to parse URL: %v", err) 1482 } 1483 if URL.Scheme != "http" && URL.Scheme != "https" { 1484 return fmt.Errorf("URL must include http or https protocol") 1485 } 1486 return nil 1487 } 1488 1489 // updateSettingsFromSecret transfers settings from a Kubernetes secret into an ArgoCDSettings struct. 1490 func (mgr *SettingsManager) updateSettingsFromSecret(settings *ArgoCDSettings, argoCDSecret *apiv1.Secret, secrets []*apiv1.Secret) error { 1491 var errs []error 1492 secretKey, ok := argoCDSecret.Data[settingServerSignatureKey] 1493 if ok { 1494 settings.ServerSignature = secretKey 1495 } else { 1496 errs = append(errs, &incompleteSettingsError{message: "server.secretkey is missing"}) 1497 } 1498 if githubWebhookSecret := argoCDSecret.Data[settingsWebhookGitHubSecretKey]; len(githubWebhookSecret) > 0 { 1499 settings.WebhookGitHubSecret = string(githubWebhookSecret) 1500 } 1501 if gitlabWebhookSecret := argoCDSecret.Data[settingsWebhookGitLabSecretKey]; len(gitlabWebhookSecret) > 0 { 1502 settings.WebhookGitLabSecret = string(gitlabWebhookSecret) 1503 } 1504 if bitbucketWebhookUUID := argoCDSecret.Data[settingsWebhookBitbucketUUIDKey]; len(bitbucketWebhookUUID) > 0 { 1505 settings.WebhookBitbucketUUID = string(bitbucketWebhookUUID) 1506 } 1507 if bitbucketserverWebhookSecret := argoCDSecret.Data[settingsWebhookBitbucketServerSecretKey]; len(bitbucketserverWebhookSecret) > 0 { 1508 settings.WebhookBitbucketServerSecret = string(bitbucketserverWebhookSecret) 1509 } 1510 if gogsWebhookSecret := argoCDSecret.Data[settingsWebhookGogsSecretKey]; len(gogsWebhookSecret) > 0 { 1511 settings.WebhookGogsSecret = string(gogsWebhookSecret) 1512 } 1513 if azureDevOpsUsername := argoCDSecret.Data[settingsWebhookAzureDevOpsUsernameKey]; len(azureDevOpsUsername) > 0 { 1514 settings.WebhookAzureDevOpsUsername = string(azureDevOpsUsername) 1515 } 1516 if azureDevOpsPassword := argoCDSecret.Data[settingsWebhookAzureDevOpsPasswordKey]; len(azureDevOpsPassword) > 0 { 1517 settings.WebhookAzureDevOpsPassword = string(azureDevOpsPassword) 1518 } 1519 1520 // The TLS certificate may be externally managed. We try to load it from an 1521 // external secret first. If the external secret doesn't exist, we either 1522 // load it from argocd-secret or generate (and persist) a self-signed one. 1523 cert, err := mgr.externalServerTLSCertificate() 1524 if err != nil { 1525 errs = append(errs, &incompleteSettingsError{message: fmt.Sprintf("could not read from secret %s/%s: %v", mgr.namespace, externalServerTLSSecretName, err)}) 1526 } else { 1527 if cert != nil { 1528 settings.Certificate = cert 1529 settings.CertificateIsExternal = true 1530 log.Infof("Loading TLS configuration from secret %s/%s", mgr.namespace, externalServerTLSSecretName) 1531 } else { 1532 serverCert, certOk := argoCDSecret.Data[settingServerCertificate] 1533 serverKey, keyOk := argoCDSecret.Data[settingServerPrivateKey] 1534 if certOk && keyOk { 1535 cert, err := tls.X509KeyPair(serverCert, serverKey) 1536 if err != nil { 1537 errs = append(errs, &incompleteSettingsError{message: fmt.Sprintf("invalid x509 key pair %s/%s in secret: %s", settingServerCertificate, settingServerPrivateKey, err)}) 1538 } else { 1539 settings.Certificate = &cert 1540 settings.CertificateIsExternal = false 1541 } 1542 } 1543 } 1544 } 1545 secretValues := make(map[string]string, len(argoCDSecret.Data)) 1546 for _, s := range secrets { 1547 for k, v := range s.Data { 1548 secretValues[fmt.Sprintf("%s:%s", s.Name, k)] = string(v) 1549 } 1550 } 1551 for k, v := range argoCDSecret.Data { 1552 secretValues[k] = string(v) 1553 } 1554 settings.Secrets = secretValues 1555 if len(errs) > 0 { 1556 return errs[0] 1557 } 1558 return nil 1559 } 1560 1561 // externalServerTLSCertificate will try and load a TLS certificate from an 1562 // external secret, instead of tls.crt and tls.key in argocd-secret. If both 1563 // return values are nil, no external secret has been configured. 1564 func (mgr *SettingsManager) externalServerTLSCertificate() (*tls.Certificate, error) { 1565 var cert tls.Certificate 1566 secret, err := mgr.secrets.Secrets(mgr.namespace).Get(externalServerTLSSecretName) 1567 if err != nil { 1568 if apierr.IsNotFound(err) { 1569 return nil, nil 1570 } 1571 } 1572 tlsCert, certOK := secret.Data[settingServerCertificate] 1573 tlsKey, keyOK := secret.Data[settingServerPrivateKey] 1574 if certOK && keyOK { 1575 cert, err = tls.X509KeyPair(tlsCert, tlsKey) 1576 if err != nil { 1577 return nil, err 1578 } 1579 } 1580 return &cert, nil 1581 } 1582 1583 // SaveSettings serializes ArgoCDSettings and upserts it into K8s secret/configmap 1584 func (mgr *SettingsManager) SaveSettings(settings *ArgoCDSettings) error { 1585 err := mgr.updateConfigMap(func(argoCDCM *apiv1.ConfigMap) error { 1586 if settings.URL != "" { 1587 argoCDCM.Data[settingURLKey] = settings.URL 1588 } else { 1589 delete(argoCDCM.Data, settingURLKey) 1590 } 1591 if settings.DexConfig != "" { 1592 argoCDCM.Data[settingDexConfigKey] = settings.DexConfig 1593 } else { 1594 delete(argoCDCM.Data, settings.DexConfig) 1595 } 1596 if settings.OIDCConfigRAW != "" { 1597 argoCDCM.Data[settingsOIDCConfigKey] = settings.OIDCConfigRAW 1598 } else { 1599 delete(argoCDCM.Data, settingsOIDCConfigKey) 1600 } 1601 if settings.UiCssURL != "" { 1602 argoCDCM.Data[settingUiCssURLKey] = settings.UiCssURL 1603 } 1604 if settings.UiBannerContent != "" { 1605 argoCDCM.Data[settingUiBannerContentKey] = settings.UiBannerContent 1606 } else { 1607 delete(argoCDCM.Data, settingUiBannerContentKey) 1608 } 1609 if settings.UiBannerURL != "" { 1610 argoCDCM.Data[settingUiBannerURLKey] = settings.UiBannerURL 1611 } else { 1612 delete(argoCDCM.Data, settingUiBannerURLKey) 1613 } 1614 return nil 1615 }) 1616 1617 if err != nil { 1618 return err 1619 } 1620 1621 return mgr.updateSecret(func(argoCDSecret *apiv1.Secret) error { 1622 argoCDSecret.Data[settingServerSignatureKey] = settings.ServerSignature 1623 if settings.WebhookGitHubSecret != "" { 1624 argoCDSecret.Data[settingsWebhookGitHubSecretKey] = []byte(settings.WebhookGitHubSecret) 1625 } 1626 if settings.WebhookGitLabSecret != "" { 1627 argoCDSecret.Data[settingsWebhookGitLabSecretKey] = []byte(settings.WebhookGitLabSecret) 1628 } 1629 if settings.WebhookBitbucketUUID != "" { 1630 argoCDSecret.Data[settingsWebhookBitbucketUUIDKey] = []byte(settings.WebhookBitbucketUUID) 1631 } 1632 if settings.WebhookBitbucketServerSecret != "" { 1633 argoCDSecret.Data[settingsWebhookBitbucketServerSecretKey] = []byte(settings.WebhookBitbucketServerSecret) 1634 } 1635 if settings.WebhookGogsSecret != "" { 1636 argoCDSecret.Data[settingsWebhookGogsSecretKey] = []byte(settings.WebhookGogsSecret) 1637 } 1638 if settings.WebhookAzureDevOpsUsername != "" { 1639 argoCDSecret.Data[settingsWebhookAzureDevOpsUsernameKey] = []byte(settings.WebhookAzureDevOpsUsername) 1640 } 1641 if settings.WebhookAzureDevOpsPassword != "" { 1642 argoCDSecret.Data[settingsWebhookAzureDevOpsPasswordKey] = []byte(settings.WebhookAzureDevOpsPassword) 1643 } 1644 // we only write the certificate to the secret if it's not externally 1645 // managed. 1646 if settings.Certificate != nil && !settings.CertificateIsExternal { 1647 cert, key := tlsutil.EncodeX509KeyPair(*settings.Certificate) 1648 argoCDSecret.Data[settingServerCertificate] = cert 1649 argoCDSecret.Data[settingServerPrivateKey] = key 1650 } else { 1651 delete(argoCDSecret.Data, settingServerCertificate) 1652 delete(argoCDSecret.Data, settingServerPrivateKey) 1653 } 1654 return nil 1655 }) 1656 } 1657 1658 // Save the SSH known host data into the corresponding ConfigMap 1659 func (mgr *SettingsManager) SaveSSHKnownHostsData(ctx context.Context, knownHostsList []string) error { 1660 err := mgr.ensureSynced(false) 1661 if err != nil { 1662 return err 1663 } 1664 1665 certCM, err := mgr.GetConfigMapByName(common.ArgoCDKnownHostsConfigMapName) 1666 if err != nil { 1667 return err 1668 } 1669 1670 if certCM.Data == nil { 1671 certCM.Data = make(map[string]string) 1672 } 1673 1674 sshKnownHostsData := strings.Join(knownHostsList, "\n") + "\n" 1675 certCM.Data["ssh_known_hosts"] = sshKnownHostsData 1676 _, err = mgr.clientset.CoreV1().ConfigMaps(mgr.namespace).Update(ctx, certCM, metav1.UpdateOptions{}) 1677 if err != nil { 1678 return err 1679 } 1680 1681 return mgr.ResyncInformers() 1682 } 1683 1684 func (mgr *SettingsManager) SaveTLSCertificateData(ctx context.Context, tlsCertificates map[string]string) error { 1685 err := mgr.ensureSynced(false) 1686 if err != nil { 1687 return err 1688 } 1689 1690 certCM, err := mgr.GetConfigMapByName(common.ArgoCDTLSCertsConfigMapName) 1691 if err != nil { 1692 return err 1693 } 1694 1695 certCM.Data = tlsCertificates 1696 _, err = mgr.clientset.CoreV1().ConfigMaps(mgr.namespace).Update(ctx, certCM, metav1.UpdateOptions{}) 1697 if err != nil { 1698 return err 1699 } 1700 1701 return mgr.ResyncInformers() 1702 } 1703 1704 func (mgr *SettingsManager) SaveGPGPublicKeyData(ctx context.Context, gpgPublicKeys map[string]string) error { 1705 err := mgr.ensureSynced(false) 1706 if err != nil { 1707 return err 1708 } 1709 1710 keysCM, err := mgr.GetConfigMapByName(common.ArgoCDGPGKeysConfigMapName) 1711 if err != nil { 1712 return err 1713 } 1714 1715 keysCM.Data = gpgPublicKeys 1716 _, err = mgr.clientset.CoreV1().ConfigMaps(mgr.namespace).Update(ctx, keysCM, metav1.UpdateOptions{}) 1717 if err != nil { 1718 return err 1719 } 1720 1721 return mgr.ResyncInformers() 1722 1723 } 1724 1725 type SettingsManagerOpts func(mgs *SettingsManager) 1726 1727 func WithRepoOrClusterChangedHandler(handler func()) SettingsManagerOpts { 1728 return func(mgr *SettingsManager) { 1729 mgr.reposOrClusterChanged = handler 1730 } 1731 } 1732 1733 // NewSettingsManager generates a new SettingsManager pointer and returns it 1734 func NewSettingsManager(ctx context.Context, clientset kubernetes.Interface, namespace string, opts ...SettingsManagerOpts) *SettingsManager { 1735 1736 mgr := &SettingsManager{ 1737 ctx: ctx, 1738 clientset: clientset, 1739 namespace: namespace, 1740 mutex: &sync.Mutex{}, 1741 } 1742 for i := range opts { 1743 opts[i](mgr) 1744 } 1745 1746 return mgr 1747 } 1748 1749 func (mgr *SettingsManager) ResyncInformers() error { 1750 return mgr.ensureSynced(true) 1751 } 1752 1753 // IsSSOConfigured returns whether or not single-sign-on is configured 1754 func (a *ArgoCDSettings) IsSSOConfigured() bool { 1755 if a.IsDexConfigured() { 1756 return true 1757 } 1758 if a.OIDCConfig() != nil { 1759 return true 1760 } 1761 return false 1762 } 1763 1764 func (a *ArgoCDSettings) IsDexConfigured() bool { 1765 if a.URL == "" { 1766 return false 1767 } 1768 dexCfg, err := UnmarshalDexConfig(a.DexConfig) 1769 if err != nil { 1770 log.Warnf("invalid dex yaml config: %s", err.Error()) 1771 return false 1772 } 1773 return len(dexCfg) > 0 1774 } 1775 1776 // GetServerEncryptionKey generates a new server encryption key using the server signature as a passphrase 1777 func (a *ArgoCDSettings) GetServerEncryptionKey() ([]byte, error) { 1778 return crypto.KeyFromPassphrase(string(a.ServerSignature)) 1779 } 1780 1781 func UnmarshalDexConfig(config string) (map[string]interface{}, error) { 1782 var dexCfg map[string]interface{} 1783 err := yaml.Unmarshal([]byte(config), &dexCfg) 1784 return dexCfg, err 1785 } 1786 1787 func (a *ArgoCDSettings) oidcConfig() *oidcConfig { 1788 if a.OIDCConfigRAW == "" { 1789 return nil 1790 } 1791 configMap := map[string]interface{}{} 1792 err := yaml.Unmarshal([]byte(a.OIDCConfigRAW), &configMap) 1793 if err != nil { 1794 log.Warnf("invalid oidc config: %v", err) 1795 return nil 1796 } 1797 1798 configMap = ReplaceMapSecrets(configMap, a.Secrets) 1799 data, err := yaml.Marshal(configMap) 1800 if err != nil { 1801 log.Warnf("invalid oidc config: %v", err) 1802 return nil 1803 } 1804 1805 config, err := unmarshalOIDCConfig(string(data)) 1806 if err != nil { 1807 log.Warnf("invalid oidc config: %v", err) 1808 return nil 1809 } 1810 1811 return &config 1812 } 1813 1814 func (a *ArgoCDSettings) OIDCConfig() *OIDCConfig { 1815 config := a.oidcConfig() 1816 if config == nil { 1817 return nil 1818 } 1819 return config.toExported() 1820 } 1821 1822 func unmarshalOIDCConfig(configStr string) (oidcConfig, error) { 1823 var config oidcConfig 1824 err := yaml.Unmarshal([]byte(configStr), &config) 1825 return config, err 1826 } 1827 1828 func ValidateOIDCConfig(configStr string) error { 1829 _, err := unmarshalOIDCConfig(configStr) 1830 return err 1831 } 1832 1833 // TLSConfig returns a tls.Config with the configured certificates 1834 func (a *ArgoCDSettings) TLSConfig() *tls.Config { 1835 if a.Certificate == nil { 1836 return nil 1837 } 1838 certPool := x509.NewCertPool() 1839 pemCertBytes, _ := tlsutil.EncodeX509KeyPair(*a.Certificate) 1840 ok := certPool.AppendCertsFromPEM(pemCertBytes) 1841 if !ok { 1842 panic("bad certs") 1843 } 1844 return &tls.Config{ 1845 RootCAs: certPool, 1846 } 1847 } 1848 1849 func (a *ArgoCDSettings) IssuerURL() string { 1850 if oidcConfig := a.OIDCConfig(); oidcConfig != nil { 1851 return oidcConfig.Issuer 1852 } 1853 if a.DexConfig != "" { 1854 return a.URL + common.DexAPIEndpoint 1855 } 1856 return "" 1857 } 1858 1859 // UserInfoGroupsEnabled returns whether group claims should be fetch from UserInfo endpoint 1860 func (a *ArgoCDSettings) UserInfoGroupsEnabled() bool { 1861 if oidcConfig := a.OIDCConfig(); oidcConfig != nil { 1862 return oidcConfig.EnableUserInfoGroups 1863 } 1864 return false 1865 } 1866 1867 // UserInfoPath returns the sub-path on which the IDP exposes the UserInfo endpoint 1868 func (a *ArgoCDSettings) UserInfoPath() string { 1869 if oidcConfig := a.OIDCConfig(); oidcConfig != nil { 1870 return oidcConfig.UserInfoPath 1871 } 1872 return "" 1873 } 1874 1875 // UserInfoCacheExpiration returns the expiry time of the UserInfo cache 1876 func (a *ArgoCDSettings) UserInfoCacheExpiration() time.Duration { 1877 if oidcConfig := a.OIDCConfig(); oidcConfig != nil && oidcConfig.UserInfoCacheExpiration != "" { 1878 userInfoCacheExpiration, err := time.ParseDuration(oidcConfig.UserInfoCacheExpiration) 1879 if err != nil { 1880 log.Warnf("Failed to parse 'oidc.config.userInfoCacheExpiration' key: %v", err) 1881 } 1882 return userInfoCacheExpiration 1883 } 1884 return 0 1885 } 1886 1887 func (a *ArgoCDSettings) OAuth2ClientID() string { 1888 if oidcConfig := a.OIDCConfig(); oidcConfig != nil { 1889 return oidcConfig.ClientID 1890 } 1891 if a.DexConfig != "" { 1892 return common.ArgoCDClientAppID 1893 } 1894 return "" 1895 } 1896 1897 // OAuth2AllowedAudiences returns a list of audiences that are allowed for the OAuth2 client. If the user has not 1898 // explicitly configured the list of audiences (or has configured an empty list), then the OAuth2 client ID is returned 1899 // as the only allowed audience. When using the bundled Dex, that client ID is always "argo-cd". 1900 func (a *ArgoCDSettings) OAuth2AllowedAudiences() []string { 1901 if config := a.oidcConfig(); config != nil { 1902 if len(config.AllowedAudiences) == 0 { 1903 allowedAudiences := []string{config.ClientID} 1904 if config.CLIClientID != "" { 1905 allowedAudiences = append(allowedAudiences, config.CLIClientID) 1906 } 1907 return allowedAudiences 1908 } 1909 return config.AllowedAudiences 1910 } 1911 if a.DexConfig != "" { 1912 return []string{common.ArgoCDClientAppID, common.ArgoCDCLIClientAppID} 1913 } 1914 return nil 1915 } 1916 1917 func (a *ArgoCDSettings) SkipAudienceCheckWhenTokenHasNoAudience() bool { 1918 if config := a.oidcConfig(); config != nil { 1919 if config.SkipAudienceCheckWhenTokenHasNoAudience != nil { 1920 return *config.SkipAudienceCheckWhenTokenHasNoAudience 1921 } 1922 return false 1923 } 1924 // When using the bundled Dex, the audience check is required. Dex will always send JWTs with an audience. 1925 return false 1926 } 1927 1928 func (a *ArgoCDSettings) OAuth2ClientSecret() string { 1929 if oidcConfig := a.OIDCConfig(); oidcConfig != nil { 1930 return oidcConfig.ClientSecret 1931 } 1932 if a.DexConfig != "" { 1933 return a.DexOAuth2ClientSecret() 1934 } 1935 return "" 1936 } 1937 1938 // OIDCTLSConfig returns the TLS config for the OIDC provider. If an external provider is configured, returns a TLS 1939 // config using the root CAs (if any) specified in the OIDC config. If an external OIDC provider is not configured, 1940 // returns the API server TLS config, because the API server proxies requests to Dex. 1941 func (a *ArgoCDSettings) OIDCTLSConfig() *tls.Config { 1942 var tlsConfig *tls.Config 1943 1944 oidcConfig := a.OIDCConfig() 1945 if oidcConfig != nil { 1946 tlsConfig = &tls.Config{} 1947 if oidcConfig.RootCA != "" { 1948 certPool := x509.NewCertPool() 1949 ok := certPool.AppendCertsFromPEM([]byte(oidcConfig.RootCA)) 1950 if !ok { 1951 log.Warn("failed to append certificates from PEM: proceeding without custom rootCA") 1952 } else { 1953 tlsConfig.RootCAs = certPool 1954 } 1955 } 1956 } else { 1957 tlsConfig = a.TLSConfig() 1958 } 1959 if tlsConfig != nil && a.OIDCTLSInsecureSkipVerify { 1960 tlsConfig.InsecureSkipVerify = true 1961 } 1962 return tlsConfig 1963 } 1964 1965 func appendURLPath(inputURL string, inputPath string) (string, error) { 1966 u, err := url.Parse(inputURL) 1967 if err != nil { 1968 return "", err 1969 } 1970 u.Path = path.Join(u.Path, inputPath) 1971 return u.String(), nil 1972 } 1973 1974 func (a *ArgoCDSettings) RedirectURL() (string, error) { 1975 return appendURLPath(a.URL, common.CallbackEndpoint) 1976 } 1977 1978 func (a *ArgoCDSettings) DexRedirectURL() (string, error) { 1979 return appendURLPath(a.URL, common.DexCallbackEndpoint) 1980 } 1981 1982 // DexOAuth2ClientSecret calculates an arbitrary, but predictable OAuth2 client secret string derived 1983 // from the server secret. This is called by the dex startup wrapper (argocd-dex rundex), as well 1984 // as the API server, such that they both independently come to the same conclusion of what the 1985 // OAuth2 shared client secret should be. 1986 func (a *ArgoCDSettings) DexOAuth2ClientSecret() string { 1987 h := sha256.New() 1988 _, err := h.Write(a.ServerSignature) 1989 if err != nil { 1990 panic(err) 1991 } 1992 sha := h.Sum(nil) 1993 return base64.URLEncoding.EncodeToString(sha)[:40] 1994 } 1995 1996 // Subscribe registers a channel in which to subscribe to settings updates 1997 func (mgr *SettingsManager) Subscribe(subCh chan<- *ArgoCDSettings) { 1998 mgr.mutex.Lock() 1999 defer mgr.mutex.Unlock() 2000 mgr.subscribers = append(mgr.subscribers, subCh) 2001 log.Infof("%v subscribed to settings updates", subCh) 2002 } 2003 2004 // Unsubscribe unregisters a channel from receiving of settings updates 2005 func (mgr *SettingsManager) Unsubscribe(subCh chan<- *ArgoCDSettings) { 2006 mgr.mutex.Lock() 2007 defer mgr.mutex.Unlock() 2008 for i, ch := range mgr.subscribers { 2009 if ch == subCh { 2010 mgr.subscribers = append(mgr.subscribers[:i], mgr.subscribers[i+1:]...) 2011 log.Infof("%v unsubscribed from settings updates", subCh) 2012 return 2013 } 2014 } 2015 } 2016 2017 func (mgr *SettingsManager) notifySubscribers(newSettings *ArgoCDSettings) { 2018 mgr.mutex.Lock() 2019 defer mgr.mutex.Unlock() 2020 if len(mgr.subscribers) > 0 { 2021 subscribers := make([]chan<- *ArgoCDSettings, len(mgr.subscribers)) 2022 copy(subscribers, mgr.subscribers) 2023 // make sure subscribes are notified in a separate thread to avoid potential deadlock 2024 go func() { 2025 log.Infof("Notifying %d settings subscribers: %v", len(subscribers), subscribers) 2026 for _, sub := range subscribers { 2027 sub <- newSettings 2028 } 2029 }() 2030 } 2031 } 2032 2033 func isIncompleteSettingsError(err error) bool { 2034 _, ok := err.(*incompleteSettingsError) 2035 return ok 2036 } 2037 2038 // InitializeSettings is used to initialize empty admin password, signature, certificate etc if missing 2039 func (mgr *SettingsManager) InitializeSettings(insecureModeEnabled bool) (*ArgoCDSettings, error) { 2040 const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-" 2041 2042 cdSettings, err := mgr.GetSettings() 2043 if err != nil && !isIncompleteSettingsError(err) { 2044 return nil, err 2045 } 2046 if cdSettings == nil { 2047 cdSettings = &ArgoCDSettings{} 2048 } 2049 if cdSettings.ServerSignature == nil { 2050 // set JWT signature 2051 signature, err := util.MakeSignature(32) 2052 if err != nil { 2053 return nil, fmt.Errorf("error setting JWT signature: %w", err) 2054 } 2055 cdSettings.ServerSignature = signature 2056 log.Info("Initialized server signature") 2057 } 2058 err = mgr.UpdateAccount(common.ArgoCDAdminUsername, func(adminAccount *Account) error { 2059 if adminAccount.Enabled { 2060 now := time.Now().UTC() 2061 if adminAccount.PasswordHash == "" { 2062 randBytes := make([]byte, initialPasswordLength) 2063 for i := 0; i < initialPasswordLength; i++ { 2064 num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters)))) 2065 if err != nil { 2066 return err 2067 } 2068 randBytes[i] = letters[num.Int64()] 2069 } 2070 initialPassword := string(randBytes) 2071 2072 hashedPassword, err := password.HashPassword(initialPassword) 2073 if err != nil { 2074 return err 2075 } 2076 ku := kube.NewKubeUtil(mgr.clientset, mgr.ctx) 2077 err = ku.CreateOrUpdateSecretField(mgr.namespace, initialPasswordSecretName, initialPasswordSecretField, initialPassword) 2078 if err != nil { 2079 return err 2080 } 2081 adminAccount.PasswordHash = hashedPassword 2082 adminAccount.PasswordMtime = &now 2083 log.Info("Initialized admin password") 2084 } 2085 if adminAccount.PasswordMtime == nil || adminAccount.PasswordMtime.IsZero() { 2086 adminAccount.PasswordMtime = &now 2087 log.Info("Initialized admin mtime") 2088 } 2089 } else { 2090 log.Info("admin disabled") 2091 } 2092 return nil 2093 }) 2094 if err != nil { 2095 return nil, err 2096 } 2097 2098 if cdSettings.Certificate == nil && !insecureModeEnabled { 2099 // generate TLS cert 2100 hosts := []string{ 2101 "localhost", 2102 "argocd-server", 2103 fmt.Sprintf("argocd-server.%s", mgr.namespace), 2104 fmt.Sprintf("argocd-server.%s.svc", mgr.namespace), 2105 fmt.Sprintf("argocd-server.%s.svc.cluster.local", mgr.namespace), 2106 } 2107 certOpts := tlsutil.CertOptions{ 2108 Hosts: hosts, 2109 Organization: "Argo CD", 2110 IsCA: false, 2111 } 2112 cert, err := tlsutil.GenerateX509KeyPair(certOpts) 2113 if err != nil { 2114 return nil, err 2115 } 2116 cdSettings.Certificate = cert 2117 log.Info("Initialized TLS certificate") 2118 } 2119 2120 err = mgr.SaveSettings(cdSettings) 2121 if apierrors.IsConflict(err) { 2122 // assume settings are initialized by another instance of api server 2123 log.Warnf("conflict when initializing settings. assuming updated by another replica") 2124 return mgr.GetSettings() 2125 } 2126 return cdSettings, nil 2127 } 2128 2129 // ReplaceMapSecrets takes a json object and recursively looks for any secret key references in the 2130 // object and replaces the value with the secret value 2131 func ReplaceMapSecrets(obj map[string]interface{}, secretValues map[string]string) map[string]interface{} { 2132 newObj := make(map[string]interface{}) 2133 for k, v := range obj { 2134 switch val := v.(type) { 2135 case map[string]interface{}: 2136 newObj[k] = ReplaceMapSecrets(val, secretValues) 2137 case []interface{}: 2138 newObj[k] = replaceListSecrets(val, secretValues) 2139 case string: 2140 newObj[k] = ReplaceStringSecret(val, secretValues) 2141 default: 2142 newObj[k] = val 2143 } 2144 } 2145 return newObj 2146 } 2147 2148 func replaceListSecrets(obj []interface{}, secretValues map[string]string) []interface{} { 2149 newObj := make([]interface{}, len(obj)) 2150 for i, v := range obj { 2151 switch val := v.(type) { 2152 case map[string]interface{}: 2153 newObj[i] = ReplaceMapSecrets(val, secretValues) 2154 case []interface{}: 2155 newObj[i] = replaceListSecrets(val, secretValues) 2156 case string: 2157 newObj[i] = ReplaceStringSecret(val, secretValues) 2158 default: 2159 newObj[i] = val 2160 } 2161 } 2162 return newObj 2163 } 2164 2165 // ReplaceStringSecret checks if given string is a secret key reference ( starts with $ ) and returns corresponding value from provided map 2166 func ReplaceStringSecret(val string, secretValues map[string]string) string { 2167 if val == "" || !strings.HasPrefix(val, "$") { 2168 return val 2169 } 2170 secretKey := val[1:] 2171 secretVal, ok := secretValues[secretKey] 2172 if !ok { 2173 log.Warnf("config referenced '%s', but key does not exist in secret", val) 2174 return val 2175 } 2176 return strings.TrimSpace(secretVal) 2177 } 2178 2179 // GetGlobalProjectsSettings loads the global project settings from argocd-cm ConfigMap 2180 func (mgr *SettingsManager) GetGlobalProjectsSettings() ([]GlobalProjectSettings, error) { 2181 argoCDCM, err := mgr.getConfigMap() 2182 if err != nil { 2183 return nil, fmt.Errorf("error retrieving argocd-cm: %w", err) 2184 } 2185 globalProjectSettings := make([]GlobalProjectSettings, 0) 2186 if value, ok := argoCDCM.Data[globalProjectsKey]; ok { 2187 if value != "" { 2188 err := yaml.Unmarshal([]byte(value), &globalProjectSettings) 2189 if err != nil { 2190 return nil, fmt.Errorf("error unmarshalling global project settings: %w", err) 2191 } 2192 } 2193 } 2194 return globalProjectSettings, nil 2195 } 2196 2197 func (mgr *SettingsManager) GetNamespace() string { 2198 return mgr.namespace 2199 } 2200 2201 func (mgr *SettingsManager) GetResourceCustomLabels() ([]string, error) { 2202 argoCDCM, err := mgr.getConfigMap() 2203 if err != nil { 2204 return []string{}, fmt.Errorf("failed getting configmap: %v", err) 2205 } 2206 labels := argoCDCM.Data[resourceCustomLabelsKey] 2207 if labels != "" { 2208 return strings.Split(labels, ","), nil 2209 } 2210 return []string{}, nil 2211 }