github.com/argoproj/argo-cd/v3@v3.2.1/cmd/util/app.go (about) 1 package util 2 3 import ( 4 "bufio" 5 stderrors "errors" 6 "fmt" 7 "io" 8 "net/url" 9 "os" 10 "strings" 11 "time" 12 13 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 14 15 "github.com/argoproj/gitops-engine/pkg/utils/kube" 16 17 log "github.com/sirupsen/logrus" 18 "github.com/spf13/cobra" 19 "github.com/spf13/pflag" 20 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 "k8s.io/utils/ptr" 22 23 "github.com/argoproj/argo-cd/v3/pkg/apis/application" 24 argoappv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" 25 "github.com/argoproj/argo-cd/v3/util/argo" 26 "github.com/argoproj/argo-cd/v3/util/config" 27 "github.com/argoproj/argo-cd/v3/util/errors" 28 "github.com/argoproj/argo-cd/v3/util/text/label" 29 ) 30 31 type AppOptions struct { 32 repoURL string 33 appPath string 34 chart string 35 env string 36 revision string 37 revisionHistoryLimit int 38 destName string 39 destServer string 40 destNamespace string 41 Parameters []string 42 valuesFiles []string 43 ignoreMissingValueFiles bool 44 values string 45 releaseName string 46 helmSets []string 47 helmSetStrings []string 48 helmSetFiles []string 49 helmVersion string 50 helmPassCredentials bool 51 helmSkipCrds bool 52 helmSkipSchemaValidation bool 53 helmSkipTests bool 54 helmNamespace string 55 helmKubeVersion string 56 helmApiVersions []string //nolint:revive //FIXME(var-naming) 57 project string 58 syncPolicy string 59 syncOptions []string 60 autoPrune bool 61 selfHeal bool 62 allowEmpty bool 63 namePrefix string 64 nameSuffix string 65 directoryRecurse bool 66 configManagementPlugin string 67 jsonnetTlaStr []string 68 jsonnetTlaCode []string 69 jsonnetExtVarStr []string 70 jsonnetExtVarCode []string 71 jsonnetLibs []string 72 kustomizeImages []string 73 kustomizeReplicas []string 74 kustomizeVersion string 75 kustomizeCommonLabels []string 76 kustomizeCommonAnnotations []string 77 kustomizeLabelWithoutSelector bool 78 kustomizeLabelIncludeTemplates bool 79 kustomizeForceCommonLabels bool 80 kustomizeForceCommonAnnotations bool 81 kustomizeNamespace string 82 kustomizeKubeVersion string 83 kustomizeApiVersions []string //nolint:revive //FIXME(var-naming) 84 ignoreMissingComponents bool 85 pluginEnvs []string 86 Validate bool 87 directoryExclude string 88 directoryInclude string 89 retryLimit int64 90 retryBackoffDuration time.Duration 91 retryBackoffMaxDuration time.Duration 92 retryBackoffFactor int64 93 retryRefresh bool 94 ref string 95 SourceName string 96 drySourceRepo string 97 drySourceRevision string 98 drySourcePath string 99 syncSourceBranch string 100 syncSourcePath string 101 hydrateToBranch string 102 } 103 104 func AddAppFlags(command *cobra.Command, opts *AppOptions) { 105 command.Flags().StringVar(&opts.repoURL, "repo", "", "Repository URL, ignored if a file is set") 106 command.Flags().StringVar(&opts.appPath, "path", "", "Path in repository to the app directory, ignored if a file is set") 107 command.Flags().StringVar(&opts.chart, "helm-chart", "", "Helm Chart name") 108 command.Flags().StringVar(&opts.env, "env", "", "Application environment to monitor") 109 command.Flags().StringVar(&opts.revision, "revision", "", "The tracking source branch, tag, commit or Helm chart version the application will sync to") 110 command.Flags().StringVar(&opts.drySourceRepo, "dry-source-repo", "", "Repository URL of the app dry source") 111 command.Flags().StringVar(&opts.drySourceRevision, "dry-source-revision", "", "Revision of the app dry source") 112 command.Flags().StringVar(&opts.drySourcePath, "dry-source-path", "", "Path in repository to the app directory for the dry source") 113 command.Flags().StringVar(&opts.syncSourceBranch, "sync-source-branch", "", "The branch from which the app will sync") 114 command.Flags().StringVar(&opts.syncSourcePath, "sync-source-path", "", "The path in the repository from which the app will sync") 115 command.Flags().StringVar(&opts.hydrateToBranch, "hydrate-to-branch", "", "The branch to hydrate the app to") 116 command.Flags().IntVar(&opts.revisionHistoryLimit, "revision-history-limit", argoappv1.RevisionHistoryLimit, "How many items to keep in revision history") 117 command.Flags().StringVar(&opts.destServer, "dest-server", "", "K8s cluster URL (e.g. https://kubernetes.default.svc)") 118 command.Flags().StringVar(&opts.destName, "dest-name", "", "K8s cluster Name (e.g. minikube)") 119 command.Flags().StringVar(&opts.destNamespace, "dest-namespace", "", "K8s target namespace") 120 command.Flags().StringArrayVarP(&opts.Parameters, "parameter", "p", []string{}, "set a parameter override (e.g. -p guestbook=image=example/guestbook:latest)") 121 command.Flags().StringArrayVar(&opts.valuesFiles, "values", []string{}, "Helm values file(s) to use") 122 command.Flags().BoolVar(&opts.ignoreMissingValueFiles, "ignore-missing-value-files", false, "Ignore locally missing valueFiles when setting helm template --values") 123 command.Flags().StringVar(&opts.values, "values-literal-file", "", "Filename or URL to import as a literal Helm values block") 124 command.Flags().StringVar(&opts.releaseName, "release-name", "", "Helm release-name") 125 command.Flags().StringVar(&opts.helmVersion, "helm-version", "", "Helm version") 126 command.Flags().BoolVar(&opts.helmPassCredentials, "helm-pass-credentials", false, "Pass credentials to all domain") 127 command.Flags().StringArrayVar(&opts.helmSets, "helm-set", []string{}, "Helm set values on the command line (can be repeated to set several values: --helm-set key1=val1 --helm-set key2=val2)") 128 command.Flags().StringArrayVar(&opts.helmSetStrings, "helm-set-string", []string{}, "Helm set STRING values on the command line (can be repeated to set several values: --helm-set-string key1=val1 --helm-set-string key2=val2)") 129 command.Flags().StringArrayVar(&opts.helmSetFiles, "helm-set-file", []string{}, "Helm set values from respective files specified via the command line (can be repeated to set several values: --helm-set-file key1=path1 --helm-set-file key2=path2)") 130 command.Flags().BoolVar(&opts.helmSkipCrds, "helm-skip-crds", false, "Skip helm crd installation step") 131 command.Flags().BoolVar(&opts.helmSkipSchemaValidation, "helm-skip-schema-validation", false, "Skip helm schema validation step") 132 command.Flags().BoolVar(&opts.helmSkipTests, "helm-skip-tests", false, "Skip helm test manifests installation step") 133 command.Flags().StringVar(&opts.helmNamespace, "helm-namespace", "", "Helm namespace to use when running helm template. If not set, use app.spec.destination.namespace") 134 command.Flags().StringVar(&opts.helmKubeVersion, "helm-kube-version", "", "Helm kube-version to use when running helm template. If not set, use the kube version from the destination cluster") 135 command.Flags().StringArrayVar(&opts.helmApiVersions, "helm-api-versions", []string{}, "Helm api-versions (in format [group/]version/kind) to use when running helm template (Can be repeated to set several values: --helm-api-versions traefik.io/v1alpha1/TLSOption --helm-api-versions v1/Service). If not set, use the api-versions from the destination cluster") 136 command.Flags().StringVar(&opts.project, "project", "", "Application project name") 137 command.Flags().StringVar(&opts.syncPolicy, "sync-policy", "", "Set the sync policy (one of: manual (aliases of manual: none), automated (aliases of automated: auto, automatic))") 138 command.Flags().StringArrayVar(&opts.syncOptions, "sync-option", []string{}, "Add or remove a sync option, e.g add `Prune=false`. Remove using `!` prefix, e.g. `!Prune=false`") 139 command.Flags().BoolVar(&opts.autoPrune, "auto-prune", false, "Set automatic pruning when sync is automated") 140 command.Flags().BoolVar(&opts.selfHeal, "self-heal", false, "Set self healing when sync is automated") 141 command.Flags().BoolVar(&opts.allowEmpty, "allow-empty", false, "Set allow zero live resources when sync is automated") 142 command.Flags().StringVar(&opts.namePrefix, "nameprefix", "", "Kustomize nameprefix") 143 command.Flags().StringVar(&opts.nameSuffix, "namesuffix", "", "Kustomize namesuffix") 144 command.Flags().StringVar(&opts.kustomizeVersion, "kustomize-version", "", "Kustomize version") 145 command.Flags().BoolVar(&opts.directoryRecurse, "directory-recurse", false, "Recurse directory") 146 command.Flags().StringVar(&opts.configManagementPlugin, "config-management-plugin", "", "Config management plugin name") 147 command.Flags().StringArrayVar(&opts.jsonnetTlaStr, "jsonnet-tla-str", []string{}, "Jsonnet top level string arguments") 148 command.Flags().StringArrayVar(&opts.jsonnetTlaCode, "jsonnet-tla-code", []string{}, "Jsonnet top level code arguments") 149 command.Flags().StringArrayVar(&opts.jsonnetExtVarStr, "jsonnet-ext-var-str", []string{}, "Jsonnet string ext var") 150 command.Flags().StringArrayVar(&opts.jsonnetExtVarCode, "jsonnet-ext-var-code", []string{}, "Jsonnet ext var") 151 command.Flags().StringArrayVar(&opts.jsonnetLibs, "jsonnet-libs", []string{}, "Additional jsonnet libs (prefixed by repoRoot)") 152 command.Flags().StringArrayVar(&opts.kustomizeImages, "kustomize-image", []string{}, "Kustomize images (e.g. --kustomize-image node:8.15.0 --kustomize-image mysql=mariadb,alpine@sha256:24a0c4b4a4c0eb97a1aabb8e29f18e917d05abfe1b7a7c07857230879ce7d3d)") 153 command.Flags().StringArrayVar(&opts.kustomizeReplicas, "kustomize-replica", []string{}, "Kustomize replicas (e.g. --kustomize-replica my-development=2 --kustomize-replica my-statefulset=4)") 154 command.Flags().BoolVar(&opts.ignoreMissingComponents, "ignore-missing-components", false, "Ignore locally missing component directories when setting Kustomize components") 155 command.Flags().StringArrayVar(&opts.pluginEnvs, "plugin-env", []string{}, "Additional plugin envs") 156 command.Flags().BoolVar(&opts.Validate, "validate", true, "Validation of repo and cluster") 157 command.Flags().StringArrayVar(&opts.kustomizeCommonLabels, "kustomize-common-label", []string{}, "Set common labels in Kustomize") 158 command.Flags().StringArrayVar(&opts.kustomizeCommonAnnotations, "kustomize-common-annotation", []string{}, "Set common labels in Kustomize") 159 command.Flags().BoolVar(&opts.kustomizeLabelWithoutSelector, "kustomize-label-without-selector", false, "Do not apply common label to selectors. Also do not apply label to templates unless --kustomize-label-include-templates is set") 160 command.Flags().BoolVar(&opts.kustomizeLabelIncludeTemplates, "kustomize-label-include-templates", false, "Apply common label to resource templates") 161 command.Flags().BoolVar(&opts.kustomizeForceCommonLabels, "kustomize-force-common-label", false, "Force common labels in Kustomize") 162 command.Flags().BoolVar(&opts.kustomizeForceCommonAnnotations, "kustomize-force-common-annotation", false, "Force common annotations in Kustomize") 163 command.Flags().StringVar(&opts.kustomizeNamespace, "kustomize-namespace", "", "Kustomize namespace") 164 command.Flags().StringVar(&opts.kustomizeKubeVersion, "kustomize-kube-version", "", "kube-version to use when running helm template. If not set, use the kube version from the destination cluster. Only applicable when Helm is enabled for Kustomize builds") 165 command.Flags().StringArrayVar(&opts.kustomizeApiVersions, "kustomize-api-versions", nil, "api-versions (in format [group/]version/kind) to use when running helm template (Can be repeated to set several values: --helm-api-versions traefik.io/v1alpha1/TLSOption --helm-api-versions v1/Service). If not set, use the api-versions from the destination cluster. Only applicable when Helm is enabled for Kustomize builds") 166 command.Flags().StringVar(&opts.directoryExclude, "directory-exclude", "", "Set glob expression used to exclude files from application source path") 167 command.Flags().StringVar(&opts.directoryInclude, "directory-include", "", "Set glob expression used to include files from application source path") 168 command.Flags().Int64Var(&opts.retryLimit, "sync-retry-limit", 0, "Max number of allowed sync retries") 169 command.Flags().DurationVar(&opts.retryBackoffDuration, "sync-retry-backoff-duration", argoappv1.DefaultSyncRetryDuration, "Sync retry backoff base duration. Input needs to be a duration (e.g. 2m, 1h)") 170 command.Flags().DurationVar(&opts.retryBackoffMaxDuration, "sync-retry-backoff-max-duration", argoappv1.DefaultSyncRetryMaxDuration, "Max sync retry backoff duration. Input needs to be a duration (e.g. 2m, 1h)") 171 command.Flags().Int64Var(&opts.retryBackoffFactor, "sync-retry-backoff-factor", argoappv1.DefaultSyncRetryFactor, "Factor multiplies the base duration after each failed sync retry") 172 command.Flags().BoolVar(&opts.retryRefresh, "sync-retry-refresh", false, "Indicates if the latest revision should be used on retry instead of the initial one") 173 command.Flags().StringVar(&opts.ref, "ref", "", "Ref is reference to another source within sources field") 174 command.Flags().StringVar(&opts.SourceName, "source-name", "", "Name of the source from the list of sources of the app.") 175 } 176 177 func SetAppSpecOptions(flags *pflag.FlagSet, spec *argoappv1.ApplicationSpec, appOpts *AppOptions, sourcePosition int) int { 178 visited := 0 179 if flags == nil { 180 return visited 181 } 182 var h *argoappv1.SourceHydrator 183 h, hasHydratorFlag := constructSourceHydrator(spec.SourceHydrator, *appOpts, flags) 184 if hasHydratorFlag { 185 spec.SourceHydrator = h 186 } else { 187 source := spec.GetSourcePtrByPosition(sourcePosition) 188 if source == nil { 189 source = &argoappv1.ApplicationSource{} 190 } 191 source, visited = ConstructSource(source, *appOpts, flags) 192 if spec.HasMultipleSources() { 193 switch { 194 case sourcePosition == 0: 195 spec.Sources[sourcePosition] = *source 196 case sourcePosition > 0: 197 spec.Sources[sourcePosition-1] = *source 198 default: 199 spec.Sources = append(spec.Sources, *source) 200 } 201 } else { 202 spec.Source = source 203 } 204 } 205 flags.Visit(func(f *pflag.Flag) { 206 visited++ 207 208 switch f.Name { 209 case "revision-history-limit": 210 i := int64(appOpts.revisionHistoryLimit) 211 spec.RevisionHistoryLimit = &i 212 case "dest-name": 213 spec.Destination.Name = appOpts.destName 214 case "dest-server": 215 spec.Destination.Server = appOpts.destServer 216 case "dest-namespace": 217 spec.Destination.Namespace = appOpts.destNamespace 218 case "project": 219 spec.Project = appOpts.project 220 case "sync-policy": 221 switch appOpts.syncPolicy { 222 case "none", "manual": 223 if spec.SyncPolicy != nil { 224 spec.SyncPolicy.Automated = nil 225 } 226 if spec.SyncPolicy.IsZero() { 227 spec.SyncPolicy = nil 228 } 229 case "automated", "automatic", "auto": 230 if spec.SyncPolicy == nil { 231 spec.SyncPolicy = &argoappv1.SyncPolicy{} 232 } 233 spec.SyncPolicy.Automated = &argoappv1.SyncPolicyAutomated{} 234 default: 235 log.Fatalf("Invalid sync-policy: %s", appOpts.syncPolicy) 236 } 237 case "sync-option": 238 if spec.SyncPolicy == nil { 239 spec.SyncPolicy = &argoappv1.SyncPolicy{} 240 } 241 for _, option := range appOpts.syncOptions { 242 // `!` means remove the option 243 if strings.HasPrefix(option, "!") { 244 option = strings.TrimPrefix(option, "!") 245 spec.SyncPolicy.SyncOptions = spec.SyncPolicy.SyncOptions.RemoveOption(option) 246 } else { 247 spec.SyncPolicy.SyncOptions = spec.SyncPolicy.SyncOptions.AddOption(option) 248 } 249 } 250 if spec.SyncPolicy.IsZero() { 251 spec.SyncPolicy = nil 252 } 253 case "sync-retry-limit": 254 switch { 255 case appOpts.retryLimit > 0: 256 if spec.SyncPolicy == nil { 257 spec.SyncPolicy = &argoappv1.SyncPolicy{} 258 } 259 spec.SyncPolicy.Retry = &argoappv1.RetryStrategy{ 260 Limit: appOpts.retryLimit, 261 Backoff: &argoappv1.Backoff{ 262 Duration: appOpts.retryBackoffDuration.String(), 263 MaxDuration: appOpts.retryBackoffMaxDuration.String(), 264 Factor: ptr.To(appOpts.retryBackoffFactor), 265 }, 266 Refresh: appOpts.retryRefresh, 267 } 268 case appOpts.retryLimit == 0: 269 if spec.SyncPolicy.IsZero() { 270 spec.SyncPolicy = nil 271 } else { 272 spec.SyncPolicy.Retry = nil 273 } 274 default: 275 log.Fatalf("Invalid sync-retry-limit [%d]", appOpts.retryLimit) 276 } 277 case "sync-retry-refresh": 278 if spec.SyncPolicy == nil { 279 spec.SyncPolicy = &argoappv1.SyncPolicy{} 280 } 281 if spec.SyncPolicy.Retry == nil { 282 spec.SyncPolicy.Retry = &argoappv1.RetryStrategy{} 283 } 284 spec.SyncPolicy.Retry.Refresh = appOpts.retryRefresh 285 } 286 }) 287 if flags.Changed("auto-prune") { 288 if spec.SyncPolicy == nil || !spec.SyncPolicy.IsAutomatedSyncEnabled() { 289 log.Fatal("Cannot set --auto-prune: application not configured with automatic sync") 290 } 291 spec.SyncPolicy.Automated.Prune = appOpts.autoPrune 292 } 293 if flags.Changed("self-heal") { 294 if spec.SyncPolicy == nil || !spec.SyncPolicy.IsAutomatedSyncEnabled() { 295 log.Fatal("Cannot set --self-heal: application not configured with automatic sync") 296 } 297 spec.SyncPolicy.Automated.SelfHeal = appOpts.selfHeal 298 } 299 if flags.Changed("allow-empty") { 300 if spec.SyncPolicy == nil || !spec.SyncPolicy.IsAutomatedSyncEnabled() { 301 log.Fatal("Cannot set --allow-empty: application not configured with automatic sync") 302 } 303 spec.SyncPolicy.Automated.AllowEmpty = appOpts.allowEmpty 304 } 305 306 return visited 307 } 308 309 type kustomizeOpts struct { 310 namePrefix string 311 nameSuffix string 312 images []string 313 replicas []string 314 version string 315 commonLabels map[string]string 316 commonAnnotations map[string]string 317 labelWithoutSelector bool 318 labelIncludeTemplates bool 319 forceCommonLabels bool 320 forceCommonAnnotations bool 321 namespace string 322 kubeVersion string 323 apiVersions []string 324 ignoreMissingComponents bool 325 } 326 327 func setKustomizeOpt(src *argoappv1.ApplicationSource, opts kustomizeOpts) { 328 if src.Kustomize == nil { 329 src.Kustomize = &argoappv1.ApplicationSourceKustomize{} 330 } 331 if opts.version != "" { 332 src.Kustomize.Version = opts.version 333 } 334 if opts.namePrefix != "" { 335 src.Kustomize.NamePrefix = opts.namePrefix 336 } 337 if opts.nameSuffix != "" { 338 src.Kustomize.NameSuffix = opts.nameSuffix 339 } 340 if opts.namespace != "" { 341 src.Kustomize.Namespace = opts.namespace 342 } 343 if opts.kubeVersion != "" { 344 src.Kustomize.KubeVersion = opts.kubeVersion 345 } 346 if len(opts.apiVersions) > 0 { 347 src.Kustomize.APIVersions = opts.apiVersions 348 } 349 if opts.commonLabels != nil { 350 src.Kustomize.CommonLabels = opts.commonLabels 351 } 352 if opts.commonAnnotations != nil { 353 src.Kustomize.CommonAnnotations = opts.commonAnnotations 354 } 355 if opts.labelWithoutSelector { 356 src.Kustomize.LabelWithoutSelector = opts.labelWithoutSelector 357 } 358 if opts.labelIncludeTemplates { 359 src.Kustomize.LabelIncludeTemplates = opts.labelIncludeTemplates 360 } 361 if opts.forceCommonLabels { 362 src.Kustomize.ForceCommonLabels = opts.forceCommonLabels 363 } 364 if opts.forceCommonAnnotations { 365 src.Kustomize.ForceCommonAnnotations = opts.forceCommonAnnotations 366 } 367 if opts.ignoreMissingComponents { 368 src.Kustomize.IgnoreMissingComponents = opts.ignoreMissingComponents 369 } 370 for _, image := range opts.images { 371 src.Kustomize.MergeImage(argoappv1.KustomizeImage(image)) 372 } 373 for _, replica := range opts.replicas { 374 r, err := argoappv1.NewKustomizeReplica(replica) 375 if err != nil { 376 log.Fatal(err) 377 } 378 src.Kustomize.MergeReplica(*r) 379 } 380 381 if src.Kustomize.IsZero() { 382 src.Kustomize = nil 383 } 384 } 385 386 func setPluginOptEnvs(src *argoappv1.ApplicationSource, envs []string) { 387 if src.Plugin == nil { 388 src.Plugin = &argoappv1.ApplicationSourcePlugin{} 389 } 390 391 for _, text := range envs { 392 e, err := argoappv1.NewEnvEntry(text) 393 if err != nil { 394 log.Fatal(err) 395 } 396 src.Plugin.AddEnvEntry(e) 397 } 398 } 399 400 type helmOpts struct { 401 valueFiles []string 402 ignoreMissingValueFiles bool 403 values string 404 releaseName string 405 version string 406 helmSets []string 407 helmSetStrings []string 408 helmSetFiles []string 409 passCredentials bool 410 skipCrds bool 411 skipSchemaValidation bool 412 skipTests bool 413 namespace string 414 kubeVersion string 415 apiVersions []string 416 } 417 418 func setHelmOpt(src *argoappv1.ApplicationSource, opts helmOpts) { 419 if src.Helm == nil { 420 src.Helm = &argoappv1.ApplicationSourceHelm{} 421 } 422 if len(opts.valueFiles) > 0 { 423 src.Helm.ValueFiles = opts.valueFiles 424 } 425 if opts.ignoreMissingValueFiles { 426 src.Helm.IgnoreMissingValueFiles = opts.ignoreMissingValueFiles 427 } 428 if opts.values != "" { 429 err := src.Helm.SetValuesString(opts.values) 430 if err != nil { 431 log.Fatal(err) 432 } 433 } 434 if opts.releaseName != "" { 435 src.Helm.ReleaseName = opts.releaseName 436 } 437 if opts.version != "" { 438 src.Helm.Version = opts.version 439 } 440 if opts.passCredentials { 441 src.Helm.PassCredentials = opts.passCredentials 442 } 443 if opts.skipCrds { 444 src.Helm.SkipCrds = opts.skipCrds 445 } 446 if opts.skipSchemaValidation { 447 src.Helm.SkipSchemaValidation = opts.skipSchemaValidation 448 } 449 if opts.skipTests { 450 src.Helm.SkipTests = opts.skipTests 451 } 452 if opts.namespace != "" { 453 src.Helm.Namespace = opts.namespace 454 } 455 if opts.kubeVersion != "" { 456 src.Helm.KubeVersion = opts.kubeVersion 457 } 458 if len(opts.apiVersions) > 0 { 459 src.Helm.APIVersions = opts.apiVersions 460 } 461 for _, text := range opts.helmSets { 462 p, err := argoappv1.NewHelmParameter(text, false) 463 if err != nil { 464 log.Fatal(err) 465 } 466 src.Helm.AddParameter(*p) 467 } 468 for _, text := range opts.helmSetStrings { 469 p, err := argoappv1.NewHelmParameter(text, true) 470 if err != nil { 471 log.Fatal(err) 472 } 473 src.Helm.AddParameter(*p) 474 } 475 for _, text := range opts.helmSetFiles { 476 p, err := argoappv1.NewHelmFileParameter(text) 477 if err != nil { 478 log.Fatal(err) 479 } 480 src.Helm.AddFileParameter(*p) 481 } 482 if src.Helm.IsZero() { 483 src.Helm = nil 484 } 485 } 486 487 func setJsonnetOpt(src *argoappv1.ApplicationSource, tlaParameters []string, code bool) { 488 if src.Directory == nil { 489 src.Directory = &argoappv1.ApplicationSourceDirectory{} 490 } 491 for _, j := range tlaParameters { 492 src.Directory.Jsonnet.TLAs = append(src.Directory.Jsonnet.TLAs, argoappv1.NewJsonnetVar(j, code)) 493 } 494 } 495 496 func setJsonnetOptExtVar(src *argoappv1.ApplicationSource, jsonnetExtVar []string, code bool) { 497 if src.Directory == nil { 498 src.Directory = &argoappv1.ApplicationSourceDirectory{} 499 } 500 for _, j := range jsonnetExtVar { 501 src.Directory.Jsonnet.ExtVars = append(src.Directory.Jsonnet.ExtVars, argoappv1.NewJsonnetVar(j, code)) 502 } 503 } 504 505 func setJsonnetOptLibs(src *argoappv1.ApplicationSource, libs []string) { 506 if src.Directory == nil { 507 src.Directory = &argoappv1.ApplicationSourceDirectory{} 508 } 509 src.Directory.Jsonnet.Libs = append(src.Directory.Jsonnet.Libs, libs...) 510 } 511 512 // SetParameterOverrides updates an existing or appends a new parameter override in the application 513 // The app is assumed to be a helm app and is expected to be in the form: 514 // param=value 515 func SetParameterOverrides(app *argoappv1.Application, parameters []string, index int) { 516 if len(parameters) == 0 { 517 return 518 } 519 source := app.Spec.GetSourcePtrByIndex(index) 520 var sourceType argoappv1.ApplicationSourceType 521 if st, _ := source.ExplicitType(); st != nil { 522 sourceType = *st 523 } else if app.Status.SourceType != "" { 524 sourceType = app.Status.SourceType 525 } else if len(strings.SplitN(parameters[0], "=", 2)) == 2 { 526 sourceType = argoappv1.ApplicationSourceTypeHelm 527 } 528 529 switch sourceType { 530 case argoappv1.ApplicationSourceTypeHelm: 531 if source.Helm == nil { 532 source.Helm = &argoappv1.ApplicationSourceHelm{} 533 } 534 for _, p := range parameters { 535 newParam, err := argoappv1.NewHelmParameter(p, false) 536 if err != nil { 537 log.Error(err) 538 continue 539 } 540 source.Helm.AddParameter(*newParam) 541 } 542 default: 543 log.Fatalf("Parameters can only be set against Helm applications") 544 } 545 } 546 547 func readApps(yml []byte, apps *[]*argoappv1.Application) error { 548 yamls, _ := kube.SplitYAMLToString(yml) 549 550 var err error 551 552 for _, yml := range yamls { 553 var app argoappv1.Application 554 err = config.Unmarshal([]byte(yml), &app) 555 *apps = append(*apps, &app) 556 if err != nil { 557 return err 558 } 559 } 560 561 return err 562 } 563 564 func readAppsFromStdin(apps *[]*argoappv1.Application) error { 565 reader := bufio.NewReader(os.Stdin) 566 data, err := io.ReadAll(reader) 567 if err != nil { 568 return err 569 } 570 err = readApps(data, apps) 571 if err != nil { 572 return fmt.Errorf("unable to read manifest from stdin: %w", err) 573 } 574 return nil 575 } 576 577 func readAppsFromURI(fileURL string, apps *[]*argoappv1.Application) error { 578 readFilePayload := func() ([]byte, error) { 579 parsedURL, err := url.ParseRequestURI(fileURL) 580 if err != nil || (parsedURL.Scheme != "http" && parsedURL.Scheme != "https") { 581 return os.ReadFile(fileURL) 582 } 583 return config.ReadRemoteFile(fileURL) 584 } 585 586 yml, err := readFilePayload() 587 if err != nil { 588 return err 589 } 590 591 return readApps(yml, apps) 592 } 593 594 func constructAppsFromStdin() ([]*argoappv1.Application, error) { 595 apps := make([]*argoappv1.Application, 0) 596 // read stdin 597 err := readAppsFromStdin(&apps) 598 if err != nil { 599 return nil, err 600 } 601 return apps, nil 602 } 603 604 func constructAppsBaseOnName(appName string, labels, annotations, args []string, appOpts AppOptions, flags *pflag.FlagSet) ([]*argoappv1.Application, error) { 605 var app *argoappv1.Application 606 607 // read arguments 608 if len(args) == 1 { 609 if appName != "" && appName != args[0] { 610 return nil, fmt.Errorf("--name argument '%s' does not match app name %s", appName, args[0]) 611 } 612 appName = args[0] 613 } 614 appName, appNs := argo.ParseFromQualifiedName(appName, "") 615 app = &argoappv1.Application{ 616 TypeMeta: metav1.TypeMeta{ 617 Kind: application.ApplicationKind, 618 APIVersion: application.Group + "/v1alpha1", 619 }, 620 ObjectMeta: metav1.ObjectMeta{ 621 Name: appName, 622 Namespace: appNs, 623 }, 624 Spec: argoappv1.ApplicationSpec{}, 625 } 626 SetAppSpecOptions(flags, &app.Spec, &appOpts, 0) 627 SetParameterOverrides(app, appOpts.Parameters, 0) 628 mergeLabels(app, labels) 629 setAnnotations(app, annotations) 630 return []*argoappv1.Application{ 631 app, 632 }, nil 633 } 634 635 func constructAppsFromFileURL(fileURL, appName string, labels, annotations, args []string, appOpts AppOptions, flags *pflag.FlagSet) ([]*argoappv1.Application, error) { 636 apps := make([]*argoappv1.Application, 0) 637 // read uri 638 err := readAppsFromURI(fileURL, &apps) 639 if err != nil { 640 return nil, err 641 } 642 for _, app := range apps { 643 if len(args) == 1 && args[0] != app.Name { 644 return nil, fmt.Errorf("app name '%s' does not match app spec metadata.name '%s'", args[0], app.Name) 645 } 646 if appName != "" && appName != app.Name { 647 app.Name = appName 648 } 649 if app.Name == "" { 650 return nil, stderrors.New("app.Name is empty. --name argument can be used to provide app.Name") 651 } 652 653 mergeLabels(app, labels) 654 setAnnotations(app, annotations) 655 656 // do not allow overrides for applications with multiple sources 657 if !app.Spec.HasMultipleSources() { 658 SetAppSpecOptions(flags, &app.Spec, &appOpts, 0) 659 SetParameterOverrides(app, appOpts.Parameters, 0) 660 } 661 } 662 return apps, nil 663 } 664 665 func ConstructApps(fileURL, appName string, labels, annotations, args []string, appOpts AppOptions, flags *pflag.FlagSet) ([]*argoappv1.Application, error) { 666 if fileURL == "-" { 667 return constructAppsFromStdin() 668 } else if fileURL != "" { 669 return constructAppsFromFileURL(fileURL, appName, labels, annotations, args, appOpts, flags) 670 } 671 672 return constructAppsBaseOnName(appName, labels, annotations, args, appOpts, flags) 673 } 674 675 func ConstructSource(source *argoappv1.ApplicationSource, appOpts AppOptions, flags *pflag.FlagSet) (*argoappv1.ApplicationSource, int) { 676 visited := 0 677 flags.Visit(func(f *pflag.Flag) { 678 visited++ 679 switch f.Name { 680 case "repo": 681 source.RepoURL = appOpts.repoURL 682 case "path": 683 source.Path = appOpts.appPath 684 case "helm-chart": 685 source.Chart = appOpts.chart 686 case "revision": 687 source.TargetRevision = appOpts.revision 688 case "values": 689 setHelmOpt(source, helmOpts{valueFiles: appOpts.valuesFiles}) 690 case "ignore-missing-value-files": 691 setHelmOpt(source, helmOpts{ignoreMissingValueFiles: appOpts.ignoreMissingValueFiles}) 692 case "values-literal-file": 693 var data []byte 694 // read uri 695 parsedURL, err := url.ParseRequestURI(appOpts.values) 696 if err != nil || (parsedURL.Scheme != "http" && parsedURL.Scheme != "https") { 697 data, err = os.ReadFile(appOpts.values) 698 } else { 699 data, err = config.ReadRemoteFile(appOpts.values) 700 } 701 errors.CheckError(err) 702 setHelmOpt(source, helmOpts{values: string(data)}) 703 case "release-name": 704 setHelmOpt(source, helmOpts{releaseName: appOpts.releaseName}) 705 case "helm-version": 706 setHelmOpt(source, helmOpts{version: appOpts.helmVersion}) 707 case "helm-pass-credentials": 708 setHelmOpt(source, helmOpts{passCredentials: appOpts.helmPassCredentials}) 709 case "helm-set": 710 setHelmOpt(source, helmOpts{helmSets: appOpts.helmSets}) 711 case "helm-set-string": 712 setHelmOpt(source, helmOpts{helmSetStrings: appOpts.helmSetStrings}) 713 case "helm-set-file": 714 setHelmOpt(source, helmOpts{helmSetFiles: appOpts.helmSetFiles}) 715 case "helm-skip-crds": 716 setHelmOpt(source, helmOpts{skipCrds: appOpts.helmSkipCrds}) 717 case "helm-skip-schema-validation": 718 setHelmOpt(source, helmOpts{skipSchemaValidation: appOpts.helmSkipSchemaValidation}) 719 case "helm-skip-tests": 720 setHelmOpt(source, helmOpts{skipTests: appOpts.helmSkipTests}) 721 case "helm-namespace": 722 setHelmOpt(source, helmOpts{namespace: appOpts.helmNamespace}) 723 case "helm-kube-version": 724 setHelmOpt(source, helmOpts{kubeVersion: appOpts.helmKubeVersion}) 725 case "helm-api-versions": 726 setHelmOpt(source, helmOpts{apiVersions: appOpts.helmApiVersions}) 727 case "directory-recurse": 728 if source.Directory != nil { 729 source.Directory.Recurse = appOpts.directoryRecurse 730 } else { 731 source.Directory = &argoappv1.ApplicationSourceDirectory{Recurse: appOpts.directoryRecurse} 732 } 733 case "directory-exclude": 734 if source.Directory != nil { 735 source.Directory.Exclude = appOpts.directoryExclude 736 } else { 737 source.Directory = &argoappv1.ApplicationSourceDirectory{Exclude: appOpts.directoryExclude} 738 } 739 case "directory-include": 740 if source.Directory != nil { 741 source.Directory.Include = appOpts.directoryInclude 742 } else { 743 source.Directory = &argoappv1.ApplicationSourceDirectory{Include: appOpts.directoryInclude} 744 } 745 case "config-management-plugin": 746 source.Plugin = &argoappv1.ApplicationSourcePlugin{Name: appOpts.configManagementPlugin} 747 case "nameprefix": 748 setKustomizeOpt(source, kustomizeOpts{namePrefix: appOpts.namePrefix}) 749 case "namesuffix": 750 setKustomizeOpt(source, kustomizeOpts{nameSuffix: appOpts.nameSuffix}) 751 case "kustomize-image": 752 setKustomizeOpt(source, kustomizeOpts{images: appOpts.kustomizeImages}) 753 case "kustomize-replica": 754 setKustomizeOpt(source, kustomizeOpts{replicas: appOpts.kustomizeReplicas}) 755 case "kustomize-version": 756 setKustomizeOpt(source, kustomizeOpts{version: appOpts.kustomizeVersion}) 757 case "kustomize-namespace": 758 setKustomizeOpt(source, kustomizeOpts{namespace: appOpts.kustomizeNamespace}) 759 case "kustomize-kube-version": 760 setKustomizeOpt(source, kustomizeOpts{kubeVersion: appOpts.kustomizeKubeVersion}) 761 case "kustomize-api-versions": 762 setKustomizeOpt(source, kustomizeOpts{apiVersions: appOpts.kustomizeApiVersions}) 763 case "kustomize-common-label": 764 parsedLabels, err := label.Parse(appOpts.kustomizeCommonLabels) 765 errors.CheckError(err) 766 setKustomizeOpt(source, kustomizeOpts{commonLabels: parsedLabels}) 767 case "kustomize-common-annotation": 768 parsedAnnotations, err := label.Parse(appOpts.kustomizeCommonAnnotations) 769 errors.CheckError(err) 770 setKustomizeOpt(source, kustomizeOpts{commonAnnotations: parsedAnnotations}) 771 case "kustomize-label-without-selector": 772 setKustomizeOpt(source, kustomizeOpts{labelWithoutSelector: appOpts.kustomizeLabelWithoutSelector}) 773 case "kustomize-label-include-templates": 774 setKustomizeOpt(source, kustomizeOpts{labelIncludeTemplates: appOpts.kustomizeLabelIncludeTemplates}) 775 case "kustomize-force-common-label": 776 setKustomizeOpt(source, kustomizeOpts{forceCommonLabels: appOpts.kustomizeForceCommonLabels}) 777 case "kustomize-force-common-annotation": 778 setKustomizeOpt(source, kustomizeOpts{forceCommonAnnotations: appOpts.kustomizeForceCommonAnnotations}) 779 case "ignore-missing-components": 780 setKustomizeOpt(source, kustomizeOpts{ignoreMissingComponents: appOpts.ignoreMissingComponents}) 781 case "jsonnet-tla-str": 782 setJsonnetOpt(source, appOpts.jsonnetTlaStr, false) 783 case "jsonnet-tla-code": 784 setJsonnetOpt(source, appOpts.jsonnetTlaCode, true) 785 case "jsonnet-ext-var-str": 786 setJsonnetOptExtVar(source, appOpts.jsonnetExtVarStr, false) 787 case "jsonnet-ext-var-code": 788 setJsonnetOptExtVar(source, appOpts.jsonnetExtVarCode, true) 789 case "jsonnet-libs": 790 setJsonnetOptLibs(source, appOpts.jsonnetLibs) 791 case "plugin-env": 792 setPluginOptEnvs(source, appOpts.pluginEnvs) 793 case "ref": 794 source.Ref = appOpts.ref 795 case "source-name": 796 source.Name = appOpts.SourceName 797 } 798 }) 799 return source, visited 800 } 801 802 // constructSourceHydrator constructs a source hydrator from the command line flags. It returns the modified source 803 // hydrator and a boolean indicating if any hydrator flags were set. We return instead of just modifying the source 804 // hydrator in place because the given hydrator `h` might be nil. In that case, we need to create a new source hydrator 805 // and return it. 806 func constructSourceHydrator(h *argoappv1.SourceHydrator, appOpts AppOptions, flags *pflag.FlagSet) (*argoappv1.SourceHydrator, bool) { 807 hasHydratorFlag := false 808 ensureNotNil := func(notEmpty bool) { 809 hasHydratorFlag = true 810 if notEmpty && h == nil { 811 h = &argoappv1.SourceHydrator{} 812 } 813 } 814 flags.Visit(func(f *pflag.Flag) { 815 switch f.Name { 816 case "dry-source-repo": 817 ensureNotNil(appOpts.drySourceRepo != "") 818 h.DrySource.RepoURL = appOpts.drySourceRepo 819 case "dry-source-path": 820 ensureNotNil(appOpts.drySourcePath != "") 821 h.DrySource.Path = appOpts.drySourcePath 822 case "dry-source-revision": 823 ensureNotNil(appOpts.drySourceRevision != "") 824 h.DrySource.TargetRevision = appOpts.drySourceRevision 825 case "sync-source-branch": 826 ensureNotNil(appOpts.syncSourceBranch != "") 827 h.SyncSource.TargetBranch = appOpts.syncSourceBranch 828 case "sync-source-path": 829 ensureNotNil(appOpts.syncSourcePath != "") 830 h.SyncSource.Path = appOpts.syncSourcePath 831 case "hydrate-to-branch": 832 ensureNotNil(appOpts.hydrateToBranch != "") 833 if appOpts.hydrateToBranch == "" { 834 h.HydrateTo = nil 835 } else { 836 h.HydrateTo = &argoappv1.HydrateTo{TargetBranch: appOpts.hydrateToBranch} 837 } 838 } 839 }) 840 return h, hasHydratorFlag 841 } 842 843 func mergeLabels(app *argoappv1.Application, labels []string) { 844 mapLabels, err := label.Parse(labels) 845 errors.CheckError(err) 846 847 mergedLabels := make(map[string]string) 848 849 for name, value := range app.GetLabels() { 850 mergedLabels[name] = value 851 } 852 853 for name, value := range mapLabels { 854 mergedLabels[name] = value 855 } 856 857 app.SetLabels(mergedLabels) 858 } 859 860 func setAnnotations(app *argoappv1.Application, annotations []string) { 861 if len(annotations) > 0 && app.Annotations == nil { 862 app.Annotations = map[string]string{} 863 } 864 for _, a := range annotations { 865 annotation := strings.SplitN(a, "=", 2) 866 if len(annotation) == 2 { 867 app.Annotations[annotation[0]] = annotation[1] 868 } else { 869 app.Annotations[annotation[0]] = "" 870 } 871 } 872 } 873 874 // LiveObjects deserializes the list of live states into unstructured objects 875 func LiveObjects(resources []*argoappv1.ResourceDiff) ([]*unstructured.Unstructured, error) { 876 objs := make([]*unstructured.Unstructured, len(resources)) 877 for i, resState := range resources { 878 obj, err := resState.LiveObject() 879 if err != nil { 880 return nil, err 881 } 882 objs[i] = obj 883 } 884 return objs, nil 885 } 886 887 func FilterResources(groupChanged bool, resources []*argoappv1.ResourceDiff, group, kind, namespace, resourceName string, all bool) ([]*unstructured.Unstructured, error) { 888 liveObjs, err := LiveObjects(resources) 889 errors.CheckError(err) 890 filteredObjects := make([]*unstructured.Unstructured, 0) 891 for i := range liveObjs { 892 obj := liveObjs[i] 893 if obj == nil { 894 continue 895 } 896 gvk := obj.GroupVersionKind() 897 if groupChanged && group != gvk.Group { 898 continue 899 } 900 if namespace != "" && namespace != obj.GetNamespace() { 901 continue 902 } 903 if resourceName != "" && resourceName != obj.GetName() { 904 continue 905 } 906 if kind != "" && kind != gvk.Kind { 907 continue 908 } 909 deepCopy := obj.DeepCopy() 910 filteredObjects = append(filteredObjects, deepCopy) 911 } 912 if len(filteredObjects) == 0 { 913 return nil, stderrors.New("no matching resource found") 914 } 915 if len(filteredObjects) > 1 && !all { 916 return nil, stderrors.New("multiple resources match inputs, use the --all flag to patch multiple resources") 917 } 918 return filteredObjects, nil 919 }