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