diff --git a/.github/workflows/image-reuse.yaml b/.github/workflows/image-reuse.yaml index 1d509fed519a9..40b2b68a011d7 100644 --- a/.github/workflows/image-reuse.yaml +++ b/.github/workflows/image-reuse.yaml @@ -143,7 +143,7 @@ jobs: - name: Build and push container image id: image - uses: docker/build-push-action@af5a7ed5ba88268d5278f7203fb52cd833f66d6e #v5.2.0 + uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 #v5.3.0 with: context: . platforms: ${{ inputs.platforms }} diff --git a/cmd/argocd/commands/app.go b/cmd/argocd/commands/app.go index 4188e9d713c9d..2ed936e286a53 100644 --- a/cmd/argocd/commands/app.go +++ b/cmd/argocd/commands/app.go @@ -137,13 +137,15 @@ func NewApplicationCreateCommand(clientOpts *argocdclient.ClientOptions) *cobra. # Create a Kustomize app argocd app create kustomize-guestbook --repo https://github.com/argoproj/argocd-example-apps.git --path kustomize-guestbook --dest-namespace default --dest-server https://kubernetes.default.svc --kustomize-image gcr.io/heptio-images/ks-guestbook-demo:0.1 + # Create a MultiSource app while yaml file contains an application with multiple sources + argocd app create guestbook --file + # Create a app using a custom tool: argocd app create kasane --repo https://github.com/argoproj/argocd-example-apps.git --path plugins/kasane --dest-namespace default --dest-server https://kubernetes.default.svc --config-management-plugin kasane`, Run: func(c *cobra.Command, args []string) { ctx := c.Context() argocdClient := headless.NewClientOrDie(clientOpts, c) - apps, err := cmdutil.ConstructApps(fileURL, appName, labels, annotations, args, appOpts, c.Flags()) errors.CheckError(err) @@ -730,6 +732,7 @@ func NewApplicationSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com var ( appOpts cmdutil.AppOptions appNamespace string + sourceIndex int ) var command = &cobra.Command{ Use: "set APPNAME", @@ -747,6 +750,9 @@ func NewApplicationSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com # Set and override application parameters with a parameter file argocd app set my-app --parameter-file path/to/parameter-file.yaml + # Set and override application parameters for a source at index 1 under spec.sources of app my-app. source-index starts at 1. + argocd app set my-app --source-index 1 --repo https://github.com/argoproj/argocd-example-apps.git + # Set application parameters and specify the namespace argocd app set my-app --parameter key1=value1 --parameter key2=value2 --namespace my-namespace `), @@ -765,14 +771,25 @@ func NewApplicationSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com app, err := appIf.Get(ctx, &application.ApplicationQuery{Name: &appName, AppNamespace: &appNs}) errors.CheckError(err) - visited := cmdutil.SetAppSpecOptions(c.Flags(), &app.Spec, &appOpts) + if app.Spec.HasMultipleSources() { + if sourceIndex <= 0 { + errors.CheckError(fmt.Errorf("Source index should be specified and greater than 0 for applications with multiple sources")) + } + if len(app.Spec.GetSources()) < sourceIndex { + errors.CheckError(fmt.Errorf("Source index should be less than the number of sources in the application")) + } + } + + // sourceIndex startes with 1, thus, it needs to be decreased by 1 to find the correct index in the list of sources + sourceIndex = sourceIndex - 1 + visited := cmdutil.SetAppSpecOptions(c.Flags(), &app.Spec, &appOpts, sourceIndex) if visited == 0 { log.Error("Please set at least one option to update") c.HelpFunc()(c, args) os.Exit(1) } - setParameterOverrides(app, appOpts.Parameters) + setParameterOverrides(app, appOpts.Parameters, sourceIndex) _, err = appIf.UpdateSpec(ctx, &application.ApplicationUpdateSpecRequest{ Name: &app.Name, Spec: &app.Spec, @@ -782,6 +799,7 @@ func NewApplicationSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com errors.CheckError(err) }, } + command.Flags().IntVar(&sourceIndex, "source-index", -1, "Index of the source from the list of sources of the app. Index starts at 1.") cmdutil.AddAppFlags(command, &appOpts) command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Set application parameters in namespace") return command @@ -801,6 +819,7 @@ type unsetOpts struct { ignoreMissingValueFiles bool pluginEnvs []string passCredentials bool + ref bool } // IsZero returns true when the Application options for kustomize are considered empty @@ -816,6 +835,9 @@ func (o *unsetOpts) KustomizeIsZero() bool { // NewApplicationUnsetCommand returns a new instance of an `argocd app unset` command func NewApplicationUnsetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command { + var ( + sourceIndex int + ) appOpts := cmdutil.AppOptions{} opts := unsetOpts{} var appNamespace string @@ -825,9 +847,12 @@ func NewApplicationUnsetCommand(clientOpts *argocdclient.ClientOptions) *cobra.C Example: ` # Unset kustomize override kustomize image argocd app unset my-app --kustomize-image=alpine - # Unset kustomize override prefix + # Unset kustomize override suffix argocd app unset my-app --namesuffix + # Unset kustomize override suffix for source at index 1 under spec.sources of app my-app. source-index starts at 1. + argocd app unset my-app --source-index 1 --namesuffix + # Unset parameter override argocd app unset my-app -p COMPONENT=PARAM`, @@ -838,14 +863,25 @@ func NewApplicationUnsetCommand(clientOpts *argocdclient.ClientOptions) *cobra.C c.HelpFunc()(c, args) os.Exit(1) } + appName, appNs := argo.ParseFromQualifiedName(args[0], appNamespace) conn, appIf := headless.NewClientOrDie(clientOpts, c).NewApplicationClientOrDie() defer argoio.Close(conn) app, err := appIf.Get(ctx, &application.ApplicationQuery{Name: &appName, AppNamespace: &appNs}) errors.CheckError(err) - source := app.Spec.GetSource() - updated, nothingToUnset := unset(&source, opts) + if app.Spec.HasMultipleSources() { + if sourceIndex <= 0 { + errors.CheckError(fmt.Errorf("Source index should be specified and greater than 0 for applications with multiple sources")) + } + if len(app.Spec.GetSources()) < sourceIndex { + errors.CheckError(fmt.Errorf("Source index should be less than the number of sources in the application")) + } + } + + source := app.Spec.GetSourcePtr(sourceIndex) + + updated, nothingToUnset := unset(source, opts) if nothingToUnset { c.HelpFunc()(c, args) os.Exit(1) @@ -854,7 +890,7 @@ func NewApplicationUnsetCommand(clientOpts *argocdclient.ClientOptions) *cobra.C return } - cmdutil.SetAppSpecOptions(c.Flags(), &app.Spec, &appOpts) + cmdutil.SetAppSpecOptions(c.Flags(), &app.Spec, &appOpts, sourceIndex) _, err = appIf.UpdateSpec(ctx, &application.ApplicationUpdateSpecRequest{ Name: &app.Name, Spec: &app.Spec, @@ -877,13 +913,22 @@ func NewApplicationUnsetCommand(clientOpts *argocdclient.ClientOptions) *cobra.C command.Flags().StringArrayVar(&opts.kustomizeReplicas, "kustomize-replica", []string{}, "Kustomize replicas name (e.g. --kustomize-replica my-deployment --kustomize-replica my-statefulset)") command.Flags().StringArrayVar(&opts.pluginEnvs, "plugin-env", []string{}, "Unset plugin env variables (e.g --plugin-env name)") command.Flags().BoolVar(&opts.passCredentials, "pass-credentials", false, "Unset passCredentials") + command.Flags().BoolVar(&opts.ref, "ref", false, "Unset ref on the source") + command.Flags().IntVar(&sourceIndex, "source-index", -1, "Index of the source from the list of sources of the app. Index starts at 1.") return command } func unset(source *argoappv1.ApplicationSource, opts unsetOpts) (updated bool, nothingToUnset bool) { + needToUnsetRef := false + if opts.ref && source.Ref != "" { + source.Ref = "" + updated = true + needToUnsetRef = true + } + if source.Kustomize != nil { if opts.KustomizeIsZero() { - return false, true + return updated, !needToUnsetRef } if opts.namePrefix && source.Kustomize.NamePrefix != "" { @@ -933,7 +978,7 @@ func unset(source *argoappv1.ApplicationSource, opts unsetOpts) (updated bool, n } if source.Helm != nil { if len(opts.parameters) == 0 && len(opts.valuesFiles) == 0 && !opts.valuesLiteral && !opts.ignoreMissingValueFiles && !opts.passCredentials { - return false, true + return updated, !needToUnsetRef } for _, paramStr := range opts.parameters { helmParams := source.Helm.Parameters @@ -970,9 +1015,10 @@ func unset(source *argoappv1.ApplicationSource, opts unsetOpts) (updated bool, n updated = true } } + if source.Plugin != nil { if len(opts.pluginEnvs) == 0 { - return false, true + return false, !needToUnsetRef } for _, env := range opts.pluginEnvs { err := source.Plugin.RemoveEnvEntry(env) @@ -1084,7 +1130,7 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co var command = &cobra.Command{ Use: "diff APPNAME", Short: shortDesc, - Long: shortDesc + "\nUses 'diff' to render the difference. KUBECTL_EXTERNAL_DIFF environment variable can be used to select your own diff tool.\nReturns the following exit codes: 2 on general errors, 1 when a diff is found, and 0 when no diff is found", + Long: shortDesc + "\nUses 'diff' to render the difference. KUBECTL_EXTERNAL_DIFF environment variable can be used to select your own diff tool.\nReturns the following exit codes: 2 on general errors, 1 when a diff is found, and 0 when no diff is found\nKubernetes Secrets are ignored from this diff.", Run: func(c *cobra.Command, args []string) { ctx := c.Context() @@ -2419,11 +2465,11 @@ func waitOnApplicationStatus(ctx context.Context, acdClient argocdclient.Client, // setParameterOverrides updates an existing or appends a new parameter override in the application // the app is assumed to be a helm app and is expected to be in the form: // param=value -func setParameterOverrides(app *argoappv1.Application, parameters []string) { +func setParameterOverrides(app *argoappv1.Application, parameters []string, index int) { if len(parameters) == 0 { return } - source := app.Spec.GetSource() + source := app.Spec.GetSourcePtr(index) var sourceType argoappv1.ApplicationSourceType if st, _ := source.ExplicitType(); st != nil { sourceType = *st @@ -2763,7 +2809,9 @@ func NewApplicationTerminateOpCommand(clientOpts *argocdclient.ClientOptions) *c } func NewApplicationEditCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command { - var appNamespace string + var ( + appNamespace string + ) var command = &cobra.Command{ Use: "edit APPNAME", Short: "Edit application", @@ -2774,6 +2822,7 @@ func NewApplicationEditCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co c.HelpFunc()(c, args) os.Exit(1) } + appName, appNs := argo.ParseFromQualifiedName(args[0], appNamespace) conn, appIf := headless.NewClientOrDie(clientOpts, c).NewApplicationClientOrDie() defer argoio.Close(conn) @@ -2800,7 +2849,11 @@ func NewApplicationEditCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co } var appOpts cmdutil.AppOptions - cmdutil.SetAppSpecOptions(c.Flags(), &app.Spec, &appOpts) + + // do not allow overrides for applications with multiple sources + if !app.Spec.HasMultipleSources() { + cmdutil.SetAppSpecOptions(c.Flags(), &app.Spec, &appOpts, 0) + } _, err = appIf.UpdateSpec(ctx, &application.ApplicationUpdateSpecRequest{ Name: &appName, Spec: &updatedSpec, @@ -2903,8 +2956,12 @@ func NewApplicationAddSourceCommand(clientOpts *argocdclient.ClientOptions) *cob if len(app.Spec.Sources) > 0 { appSource, _ := cmdutil.ConstructSource(&argoappv1.ApplicationSource{}, appOpts, c.Flags()) + // sourceIndex is the index at which new source will be appended to spec.Sources + sourceIndex := len(app.Spec.GetSources()) app.Spec.Sources = append(app.Spec.Sources, *appSource) + setParameterOverrides(app, appOpts.Parameters, sourceIndex) + _, err = appIf.UpdateSpec(ctx, &application.ApplicationUpdateSpecRequest{ Name: &app.Name, Spec: &app.Spec, @@ -2927,13 +2984,13 @@ func NewApplicationAddSourceCommand(clientOpts *argocdclient.ClientOptions) *cob // NewApplicationRemoveSourceCommand returns a new instance of an `argocd app remove-source` command func NewApplicationRemoveSourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command { var ( - source_index int + sourceIndex int appNamespace string ) command := &cobra.Command{ Use: "remove-source APPNAME", - Short: "Remove a source from multiple sources application. Index starts with 0.", - Example: ` # Remove the source at index 1 from application's sources + Short: "Remove a source from multiple sources application. Index starts with 1. Default value is -1.", + Example: ` # Remove the source at index 1 from application's sources. Index starts at 1. argocd app remove-source myapplication --source-index 1`, Run: func(c *cobra.Command, args []string) { ctx := c.Context() @@ -2943,8 +3000,8 @@ func NewApplicationRemoveSourceCommand(clientOpts *argocdclient.ClientOptions) * os.Exit(1) } - if source_index < 0 { - errors.CheckError(fmt.Errorf("Index value of source cannot be less than 0")) + if sourceIndex <= 0 { + errors.CheckError(fmt.Errorf("Index value of source must be greater than 0")) } argocdClient := headless.NewClientOrDie(clientOpts, c) @@ -2968,11 +3025,11 @@ func NewApplicationRemoveSourceCommand(clientOpts *argocdclient.ClientOptions) * errors.CheckError(fmt.Errorf("Cannot remove the only source remaining in the app")) } - if len(app.Spec.GetSources()) <= source_index { - errors.CheckError(fmt.Errorf("Application does not have source at %d\n", source_index)) + if len(app.Spec.GetSources()) < sourceIndex { + errors.CheckError(fmt.Errorf("Application does not have source at %d\n", sourceIndex)) } - app.Spec.Sources = append(app.Spec.Sources[:source_index], app.Spec.Sources[source_index+1:]...) + app.Spec.Sources = append(app.Spec.Sources[:sourceIndex-1], app.Spec.Sources[sourceIndex:]...) _, err = appIf.UpdateSpec(ctx, &application.ApplicationUpdateSpecRequest{ Name: &app.Name, @@ -2985,6 +3042,6 @@ func NewApplicationRemoveSourceCommand(clientOpts *argocdclient.ClientOptions) * }, } command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Namespace of the target application where the source will be appended") - command.Flags().IntVar(&source_index, "source-index", -1, "Index of the source from the list of sources of the app. Index starts from 0.") + command.Flags().IntVar(&sourceIndex, "source-index", -1, "Index of the source from the list of sources of the app. Index starts from 1.") return command } diff --git a/cmd/util/app.go b/cmd/util/app.go index 9a284b56ce38b..b1693689004c4 100644 --- a/cmd/util/app.go +++ b/cmd/util/app.go @@ -139,16 +139,27 @@ func AddAppFlags(command *cobra.Command, opts *AppOptions) { command.Flags().StringVar(&opts.ref, "ref", "", "Ref is reference to another source within sources field") } -func SetAppSpecOptions(flags *pflag.FlagSet, spec *argoappv1.ApplicationSpec, appOpts *AppOptions) int { +func SetAppSpecOptions(flags *pflag.FlagSet, spec *argoappv1.ApplicationSpec, appOpts *AppOptions, index int) int { visited := 0 if flags == nil { return visited } - source := spec.GetSourcePtr() + source := spec.GetSourcePtr(index) if source == nil { source = &argoappv1.ApplicationSource{} } source, visited = ConstructSource(source, *appOpts, flags) + if spec.HasMultipleSources() { + if index == 0 { + spec.Sources[index] = *source + } else if index > 0 { + spec.Sources[index-1] = *source + } else { + spec.Sources = append(spec.Sources, *source) + } + } else { + spec.Source = source + } flags.Visit(func(f *pflag.Flag) { visited++ @@ -220,7 +231,6 @@ func SetAppSpecOptions(flags *pflag.FlagSet, spec *argoappv1.ApplicationSpec, ap log.Fatalf("Invalid sync-retry-limit [%d]", appOpts.retryLimit) } } - spec.Source = source }) if flags.Changed("auto-prune") { if spec.SyncPolicy == nil || spec.SyncPolicy.Automated == nil { @@ -414,11 +424,11 @@ func setJsonnetOptLibs(src *argoappv1.ApplicationSource, libs []string) { // SetParameterOverrides updates an existing or appends a new parameter override in the application // The app is assumed to be a helm app and is expected to be in the form: // param=value -func SetParameterOverrides(app *argoappv1.Application, parameters []string) { +func SetParameterOverrides(app *argoappv1.Application, parameters []string, index int) { if len(parameters) == 0 { return } - source := app.Spec.GetSource() + source := app.Spec.GetSourcePtr(index) var sourceType argoappv1.ApplicationSourceType if st, _ := source.ExplicitType(); st != nil { sourceType = *st @@ -530,8 +540,8 @@ func constructAppsBaseOnName(appName string, labels, annotations, args []string, Source: &argoappv1.ApplicationSource{}, }, } - SetAppSpecOptions(flags, &app.Spec, &appOpts) - SetParameterOverrides(app, appOpts.Parameters) + SetAppSpecOptions(flags, &app.Spec, &appOpts, 0) + SetParameterOverrides(app, appOpts.Parameters, 0) mergeLabels(app, labels) setAnnotations(app, annotations) return []*argoappv1.Application{ @@ -557,10 +567,14 @@ func constructAppsFromFileUrl(fileURL, appName string, labels, annotations, args return nil, fmt.Errorf("app.Name is empty. --name argument can be used to provide app.Name") } - SetAppSpecOptions(flags, &app.Spec, &appOpts) - SetParameterOverrides(app, appOpts.Parameters) mergeLabels(app, labels) setAnnotations(app, annotations) + + // do not allow overrides for applications with multiple sources + if !app.Spec.HasMultipleSources() { + SetAppSpecOptions(flags, &app.Spec, &appOpts, 0) + SetParameterOverrides(app, appOpts.Parameters, 0) + } } return apps, nil } diff --git a/cmd/util/app_test.go b/cmd/util/app_test.go index b5fce9c1e663e..5e95eeb388634 100644 --- a/cmd/util/app_test.go +++ b/cmd/util/app_test.go @@ -170,7 +170,16 @@ func (f *appOptionsFixture) SetFlag(key, value string) error { if err != nil { return err } - _ = SetAppSpecOptions(f.command.Flags(), f.spec, f.options) + _ = SetAppSpecOptions(f.command.Flags(), f.spec, f.options, 0) + return err +} + +func (f *appOptionsFixture) SetFlagWithSourceIndex(key, value string, index int) error { + err := f.command.Flags().Set(key, value) + if err != nil { + return err + } + _ = SetAppSpecOptions(f.command.Flags(), f.spec, f.options, index) return err } @@ -225,6 +234,54 @@ func Test_setAppSpecOptions(t *testing.T) { }) } +func newMultiSourceAppOptionsFixture() *appOptionsFixture { + fixture := &appOptionsFixture{ + spec: &v1alpha1.ApplicationSpec{ + Sources: v1alpha1.ApplicationSources{ + v1alpha1.ApplicationSource{}, + v1alpha1.ApplicationSource{}, + }, + }, + command: &cobra.Command{}, + options: &AppOptions{}, + } + AddAppFlags(fixture.command, fixture.options) + return fixture +} + +func Test_setAppSpecOptionsMultiSourceApp(t *testing.T) { + f := newMultiSourceAppOptionsFixture() + index := 0 + index1 := 1 + index2 := 2 + t.Run("SyncPolicy", func(t *testing.T) { + assert.NoError(t, f.SetFlagWithSourceIndex("sync-policy", "automated", index1)) + assert.NotNil(t, f.spec.SyncPolicy.Automated) + + f.spec.SyncPolicy = nil + assert.NoError(t, f.SetFlagWithSourceIndex("sync-policy", "automatic", index1)) + assert.NotNil(t, f.spec.SyncPolicy.Automated) + }) + t.Run("Helm - Index 0", func(t *testing.T) { + assert.NoError(t, f.SetFlagWithSourceIndex("helm-version", "v2", index)) + assert.Equal(t, len(f.spec.GetSources()), 2) + assert.Equal(t, f.spec.GetSources()[index].Helm.Version, "v2") + }) + t.Run("Kustomize", func(t *testing.T) { + assert.NoError(t, f.SetFlagWithSourceIndex("kustomize-replica", "my-deployment=2", index1)) + assert.Equal(t, f.spec.Sources[index1-1].Kustomize.Replicas, v1alpha1.KustomizeReplicas{{Name: "my-deployment", Count: intstr.FromInt(2)}}) + assert.NoError(t, f.SetFlagWithSourceIndex("kustomize-replica", "my-deployment=4", index2)) + assert.Equal(t, f.spec.Sources[index2-1].Kustomize.Replicas, v1alpha1.KustomizeReplicas{{Name: "my-deployment", Count: intstr.FromInt(4)}}) + }) + t.Run("Helm", func(t *testing.T) { + assert.NoError(t, f.SetFlagWithSourceIndex("helm-version", "v2", index1)) + assert.NoError(t, f.SetFlagWithSourceIndex("helm-version", "v3", index2)) + assert.Equal(t, len(f.spec.GetSources()), 2) + assert.Equal(t, f.spec.GetSources()[index1-1].Helm.Version, "v2") + assert.Equal(t, f.spec.GetSources()[index2-1].Helm.Version, "v3") + }) +} + func Test_setAnnotations(t *testing.T) { t.Run("Annotations", func(t *testing.T) { app := v1alpha1.Application{} diff --git a/docs/operator-manual/app-any-namespace.md b/docs/operator-manual/app-any-namespace.md index 21bfa5c4f5a0b..5f4a76d610afd 100644 --- a/docs/operator-manual/app-any-namespace.md +++ b/docs/operator-manual/app-any-namespace.md @@ -1,7 +1,5 @@ # Applications in any namespace -**Current feature state**: Beta - !!! warning Please read this documentation carefully before you enable this feature. Misconfiguration could lead to potential security issues. diff --git a/docs/user-guide/commands/argocd_app.md b/docs/user-guide/commands/argocd_app.md index ff8fe0d4a01b6..a5878502ce5c7 100644 --- a/docs/user-guide/commands/argocd_app.md +++ b/docs/user-guide/commands/argocd_app.md @@ -91,7 +91,7 @@ argocd app [flags] * [argocd app manifests](argocd_app_manifests.md) - Print manifests of an application * [argocd app patch](argocd_app_patch.md) - Patch application * [argocd app patch-resource](argocd_app_patch-resource.md) - Patch resource in an application -* [argocd app remove-source](argocd_app_remove-source.md) - Remove a source from multiple sources application. Index starts with 0. +* [argocd app remove-source](argocd_app_remove-source.md) - Remove a source from multiple sources application. Index starts with 1. Default value is -1. * [argocd app resources](argocd_app_resources.md) - List resource of application * [argocd app rollback](argocd_app_rollback.md) - Rollback application to a previous deployed version by History ID, omitted will Rollback to the previous version * [argocd app set](argocd_app_set.md) - Set application parameters diff --git a/docs/user-guide/commands/argocd_app_create.md b/docs/user-guide/commands/argocd_app_create.md index 00b4949f7993b..fb147b8e4aa9f 100644 --- a/docs/user-guide/commands/argocd_app_create.md +++ b/docs/user-guide/commands/argocd_app_create.md @@ -26,6 +26,9 @@ argocd app create APPNAME [flags] # Create a Kustomize app argocd app create kustomize-guestbook --repo https://github.com/argoproj/argocd-example-apps.git --path kustomize-guestbook --dest-namespace default --dest-server https://kubernetes.default.svc --kustomize-image gcr.io/heptio-images/ks-guestbook-demo:0.1 + # Create a MultiSource app while yaml file contains an application with multiple sources + argocd app create guestbook --file + # Create a app using a custom tool: argocd app create kasane --repo https://github.com/argoproj/argocd-example-apps.git --path plugins/kasane --dest-namespace default --dest-server https://kubernetes.default.svc --config-management-plugin kasane ``` diff --git a/docs/user-guide/commands/argocd_app_diff.md b/docs/user-guide/commands/argocd_app_diff.md index 18cc8f4751324..b352c30123eca 100644 --- a/docs/user-guide/commands/argocd_app_diff.md +++ b/docs/user-guide/commands/argocd_app_diff.md @@ -9,6 +9,7 @@ Perform a diff against the target and live state. Perform a diff against the target and live state. Uses 'diff' to render the difference. KUBECTL_EXTERNAL_DIFF environment variable can be used to select your own diff tool. Returns the following exit codes: 2 on general errors, 1 when a diff is found, and 0 when no diff is found +Kubernetes Secrets are ignored from this diff. ``` argocd app diff APPNAME [flags] diff --git a/docs/user-guide/commands/argocd_app_remove-source.md b/docs/user-guide/commands/argocd_app_remove-source.md index b7bd0df09823d..b9f29d8c6eb45 100644 --- a/docs/user-guide/commands/argocd_app_remove-source.md +++ b/docs/user-guide/commands/argocd_app_remove-source.md @@ -2,7 +2,7 @@ ## argocd app remove-source -Remove a source from multiple sources application. Index starts with 0. +Remove a source from multiple sources application. Index starts with 1. Default value is -1. ``` argocd app remove-source APPNAME [flags] @@ -11,7 +11,7 @@ argocd app remove-source APPNAME [flags] ### Examples ``` - # Remove the source at index 1 from application's sources + # Remove the source at index 1 from application's sources. Index starts at 1. argocd app remove-source myapplication --source-index 1 ``` @@ -20,7 +20,7 @@ argocd app remove-source APPNAME [flags] ``` -N, --app-namespace string Namespace of the target application where the source will be appended -h, --help help for remove-source - --source-index int Index of the source from the list of sources of the app. Index starts from 0. (default -1) + --source-index int Index of the source from the list of sources of the app. Index starts from 1. (default -1) ``` ### Options inherited from parent commands diff --git a/docs/user-guide/commands/argocd_app_set.md b/docs/user-guide/commands/argocd_app_set.md index 1c6cc40bd5c27..97288ad775345 100644 --- a/docs/user-guide/commands/argocd_app_set.md +++ b/docs/user-guide/commands/argocd_app_set.md @@ -23,6 +23,9 @@ argocd app set APPNAME [flags] # Set and override application parameters with a parameter file argocd app set my-app --parameter-file path/to/parameter-file.yaml + # Set and override application parameters for a source at index 1 under spec.sources of app my-app. source-index starts at 1. + argocd app set my-app --source-index 1 --repo https://github.com/argoproj/argocd-example-apps.git + # Set application parameters and specify the namespace argocd app set my-app --parameter key1=value1 --parameter key2=value2 --namespace my-namespace ``` @@ -76,6 +79,7 @@ argocd app set APPNAME [flags] --revision string The tracking source branch, tag, commit or Helm chart version the application will sync to --revision-history-limit int How many items to keep in revision history (default 10) --self-heal Set self healing when sync is automated + --source-index int Index of the source from the list of sources of the app. Index starts at 1. (default -1) --sync-option Prune=false Add or remove a sync option, e.g add Prune=false. Remove using `!` prefix, e.g. `!Prune=false` --sync-policy string Set the sync policy (one of: manual (aliases of manual: none), automated (aliases of automated: auto, automatic)) --sync-retry-backoff-duration duration Sync retry backoff base duration. Input needs to be a duration (e.g. 2m, 1h) (default 5s) diff --git a/docs/user-guide/commands/argocd_app_unset.md b/docs/user-guide/commands/argocd_app_unset.md index 34194b02d447c..0c3bf25d7fa91 100644 --- a/docs/user-guide/commands/argocd_app_unset.md +++ b/docs/user-guide/commands/argocd_app_unset.md @@ -14,9 +14,12 @@ argocd app unset APPNAME parameters [flags] # Unset kustomize override kustomize image argocd app unset my-app --kustomize-image=alpine - # Unset kustomize override prefix + # Unset kustomize override suffix argocd app unset my-app --namesuffix + # Unset kustomize override suffix for source at index 1 under spec.sources of app my-app. source-index starts at 1. + argocd app unset my-app --source-index 1 --namesuffix + # Unset parameter override argocd app unset my-app -p COMPONENT=PARAM ``` @@ -36,6 +39,8 @@ argocd app unset APPNAME parameters [flags] -p, --parameter stringArray Unset a parameter override (e.g. -p guestbook=image) --pass-credentials Unset passCredentials --plugin-env stringArray Unset plugin env variables (e.g --plugin-env name) + --ref Unset ref on the source + --source-index int Index of the source from the list of sources of the app. Index starts at 1. (default -1) --values stringArray Unset one or more Helm values files --values-literal Unset literal Helm values block ``` diff --git a/pkg/apis/application/v1alpha1/types.go b/pkg/apis/application/v1alpha1/types.go index fc2908c4643dc..abd2735710e72 100644 --- a/pkg/apis/application/v1alpha1/types.go +++ b/pkg/apis/application/v1alpha1/types.go @@ -230,9 +230,12 @@ func (a *ApplicationSpec) HasMultipleSources() bool { return a.Sources != nil && len(a.Sources) > 0 } -func (a *ApplicationSpec) GetSourcePtr() *ApplicationSource { +func (a *ApplicationSpec) GetSourcePtr(index int) *ApplicationSource { // if Application has multiple sources, return the first source in sources if a.HasMultipleSources() { + if index > 0 { + return &a.Sources[index-1] + } return &a.Sources[0] } return a.Source diff --git a/server/application/terminal.go b/server/application/terminal.go index 5cd0602fc1f21..53784fc5ffcc1 100644 --- a/server/application/terminal.go +++ b/server/application/terminal.go @@ -38,12 +38,12 @@ type terminalHandler struct { allowedShells []string namespace string enabledNamespaces []string - sessionManager util_session.SessionManager + sessionManager *util_session.SessionManager } // NewHandler returns a new terminal handler. func NewHandler(appLister applisters.ApplicationLister, namespace string, enabledNamespaces []string, db db.ArgoDB, enf *rbac.Enforcer, cache *servercache.Cache, - appResourceTree AppResourceTreeFn, allowedShells []string, sessionManager util_session.SessionManager) *terminalHandler { + appResourceTree AppResourceTreeFn, allowedShells []string, sessionManager *util_session.SessionManager) *terminalHandler { return &terminalHandler{ appLister: appLister, db: db, diff --git a/server/application/websocket.go b/server/application/websocket.go index faee91c4f47e4..b04330c45c3d7 100644 --- a/server/application/websocket.go +++ b/server/application/websocket.go @@ -37,7 +37,7 @@ type terminalSession struct { tty bool readLock sync.Mutex writeLock sync.Mutex - sessionManager util_session.SessionManager + sessionManager *util_session.SessionManager token *string } @@ -48,7 +48,7 @@ func getToken(r *http.Request) (string, error) { } // newTerminalSession create terminalSession -func newTerminalSession(w http.ResponseWriter, r *http.Request, responseHeader http.Header, sessionManager util_session.SessionManager) (*terminalSession, error) { +func newTerminalSession(w http.ResponseWriter, r *http.Request, responseHeader http.Header, sessionManager *util_session.SessionManager) (*terminalSession, error) { token, err := getToken(r) if err != nil { return nil, err diff --git a/server/repository/repository_test.go b/server/repository/repository_test.go index 9c294b5a332b9..55bf7ab7220ac 100644 --- a/server/repository/repository_test.go +++ b/server/repository/repository_test.go @@ -654,7 +654,7 @@ func TestRepositoryServerGetAppDetails(t *testing.T) { s := NewServer(&repoServerClientset, db, enforcer, newFixtures().Cache, appLister, projLister, testNamespace, settingsMgr) resp, err := s.GetAppDetails(context.TODO(), &repository.RepoAppDetailsQuery{ - Source: guestbookApp.Spec.GetSourcePtr(), + Source: guestbookApp.Spec.GetSourcePtr(0), AppName: "guestbook", AppProject: "default", }) @@ -752,7 +752,7 @@ func TestRepositoryServerGetAppDetails(t *testing.T) { s := NewServer(&repoServerClientset, db, enforcer, newFixtures().Cache, appLister, projLister, testNamespace, settingsMgr) resp, err := s.GetAppDetails(context.TODO(), &repository.RepoAppDetailsQuery{ - Source: guestbookApp.Spec.GetSourcePtr(), + Source: guestbookApp.Spec.GetSourcePtr(0), AppName: "guestbook", AppProject: "mismatch", }) diff --git a/server/server.go b/server/server.go index e42e6f59a49a3..625fa2053023e 100644 --- a/server/server.go +++ b/server/server.go @@ -1011,7 +1011,7 @@ func (a *ArgoCDServer) newHTTPServer(ctx context.Context, port int, grpcWebHandl } mux.Handle("/api/", handler) - terminal := application.NewHandler(a.appLister, a.Namespace, a.ApplicationNamespaces, a.db, a.enf, a.Cache, appResourceTreeFn, a.settings.ExecShells, *a.sessionMgr). + terminal := application.NewHandler(a.appLister, a.Namespace, a.ApplicationNamespaces, a.db, a.enf, a.Cache, appResourceTreeFn, a.settings.ExecShells, a.sessionMgr). WithFeatureFlagMiddleware(a.settingsMgr.GetSettings) th := util_session.WithAuthMiddleware(a.DisableAuth, a.sessionMgr, terminal) mux.Handle("/terminal", th) diff --git a/test/container/Dockerfile b/test/container/Dockerfile index 1f30c79b64bef..c5936361cd5d9 100644 --- a/test/container/Dockerfile +++ b/test/container/Dockerfile @@ -10,7 +10,7 @@ FROM docker.io/library/node:21.7.1@sha256:b9ccc4aca32eebf124e0ca0fd573dacffba2b9 FROM docker.io/library/golang:1.22.1@sha256:0b55ab82ac2a54a6f8f85ec8b943b9e470c39e32c109b766bbc1b801f3fa8d3b as golang -FROM docker.io/library/registry:2.8@sha256:f4e1b878d4bc40a1f65532d68c94dcfbab56aa8cba1f00e355a206e7f6cc9111 as registry +FROM docker.io/library/registry:2.8@sha256:fb9c9aef62af3955f6014613456551c92e88a67dcf1fc51f5f91bcbd1832813f as registry FROM docker.io/bitnami/kubectl:1.27@sha256:14ab746e857d96c105df4989cc2bf841292f2d143f7c60f9d7f549ae660eab43 as kubectl diff --git a/ui/package.json b/ui/package.json index 828d0c6e7f97d..16f717c4e8a69 100644 --- a/ui/package.json +++ b/ui/package.json @@ -102,7 +102,7 @@ "jest-junit": "^6.4.0", "jest-transform-css": "^2.0.0", "monaco-editor-webpack-plugin": "^7.0.0", - "postcss": "^8.4.35", + "postcss": "^8.4.37", "prettier": "1.19", "raw-loader": "^0.5.1", "react-test-renderer": "16.8.3", diff --git a/ui/src/app/applications/components/utils.tsx b/ui/src/app/applications/components/utils.tsx index cd39470bfb25b..a75b1a62adc80 100644 --- a/ui/src/app/applications/components/utils.tsx +++ b/ui/src/app/applications/components/utils.tsx @@ -325,7 +325,12 @@ export const deletePodAction = async (pod: appModels.Pod, appContext: AppContext }; export const deletePopup = async (ctx: ContextApis, resource: ResourceTreeNode, application: appModels.Application, appChanged?: BehaviorSubject) => { - const isManaged = !!resource.status; + function isTopLevelResource(res: ResourceTreeNode, app: appModels.Application): boolean { + const uniqRes = `/${res.namespace}/${res.group}/${res.kind}/${res.name}`; + return app.status.resources.some(resStatus => `/${resStatus.namespace}/${resStatus.group}/${resStatus.kind}/${resStatus.name}` === uniqRes); + } + + const isManaged = isTopLevelResource(resource, application); const deleteOptions = { option: 'foreground' }; diff --git a/ui/src/app/settings/components/clusters-list/cluster-list.scss b/ui/src/app/settings/components/clusters-list/cluster-list.scss new file mode 100644 index 0000000000000..d221263f84b28 --- /dev/null +++ b/ui/src/app/settings/components/clusters-list/cluster-list.scss @@ -0,0 +1,25 @@ +@import 'node_modules/argo-ui/src/styles/config'; +@import 'node_modules/argo-ui/src/styles/theme'; + + +.help-text { + color: $argo-color-gray-8; + @include themify($themes) { + color: themed('text-2'); + } + a { + color: #007bff; /* Blue color for the link */ + @include themify($themes) { + color: themed('light-argo-teal-7'); + } + text-decoration: none; /* Remove default underline */ + transition: color 0.3s ease; /* Smooth transition for color change */ + + &:hover { + color: #0056b3; /* Darker blue color on hover */ + @include themify($themes) { + color: themed('light-argo-teal-5'); + } + } + } +} diff --git a/ui/src/app/settings/components/clusters-list/clusters-list.tsx b/ui/src/app/settings/components/clusters-list/clusters-list.tsx index c6dea9ab372aa..e5c7c0682e88e 100644 --- a/ui/src/app/settings/components/clusters-list/clusters-list.tsx +++ b/ui/src/app/settings/components/clusters-list/clusters-list.tsx @@ -1,11 +1,50 @@ import {DropDownMenu, ErrorNotification, NotificationType} from 'argo-ui'; -import {Tooltip} from 'argo-ui'; +import {Tooltip, Toolbar} from 'argo-ui'; import * as React from 'react'; import {RouteComponentProps} from 'react-router-dom'; import {clusterName, ConnectionStateIcon, DataLoader, EmptyState, Page} from '../../../shared/components'; -import {Consumer} from '../../../shared/context'; +import {Consumer, Context} from '../../../shared/context'; import * as models from '../../../shared/models'; import {services} from '../../../shared/services'; +import {AddAuthToToolbar} from '../../../shared/components'; +import {Observable} from 'rxjs'; + +import './cluster-list.scss'; + +// CustomTopBar component similar to FlexTopBar in application-list panel +const CustomTopBar = (props: {toolbar?: Toolbar | Observable}) => { + const ctx = React.useContext(Context); + const loadToolbar = AddAuthToToolbar(props.toolbar, ctx); + return ( + +
+ loadToolbar}> + {toolbar => ( + +
+
+
+ + Refer to CLI{' '} + + Documentation{' '} + {' '} + for adding clusters. + +
+
+
+
{toolbar.tools}
+
+ )} +
+
+
+ ); +}; export const ClustersList = (props: RouteComponentProps<{}>) => { const clustersLoaderRef = React.useRef(); @@ -13,7 +52,8 @@ export const ClustersList = (props: RouteComponentProps<{}>) => { {ctx => ( - + +
=0.6.2 <2.0.0", source-map-js@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" - integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== +"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" + integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== source-map-loader@^0.2.3: version "0.2.4" diff --git a/util/notification/expression/repo/repo.go b/util/notification/expression/repo/repo.go index a782c0b7c1725..8456774f0869a 100644 --- a/util/notification/expression/repo/repo.go +++ b/util/notification/expression/repo/repo.go @@ -33,7 +33,7 @@ func getApplicationSourceAndName(obj *unstructured.Unstructured) (*v1alpha1.Appl if err != nil { return nil, "", err } - return application.Spec.GetSourcePtr(), application.GetName(), nil + return application.Spec.GetSourcePtr(0), application.GetName(), nil } func getAppDetails(app *unstructured.Unstructured, argocdService service.Service) (*shared.AppDetail, error) { diff --git a/util/session/sessionmanager.go b/util/session/sessionmanager.go index 8999009b9bdd7..af22ca0f2502e 100644 --- a/util/session/sessionmanager.go +++ b/util/session/sessionmanager.go @@ -10,6 +10,7 @@ import ( "net/http" "os" "strings" + "sync" "time" "github.com/coreos/go-oidc/v3/oidc" @@ -41,6 +42,7 @@ type SessionManager struct { storage UserStateStorage sleep func(d time.Duration) verificationDelayNoiseEnabled bool + failedLock sync.RWMutex } // LoginAttempts is a timestamped counter for failed login attempts @@ -69,7 +71,7 @@ const ( // Maximum length of username, too keep the cache's memory signature low maxUsernameLength = 32 // The default maximum session cache size - defaultMaxCacheSize = 1000 + defaultMaxCacheSize = 10000 // The default number of maximum login failures before delay kicks in defaultMaxLoginFailures = 5 // The default time in seconds for the failure window @@ -284,7 +286,7 @@ func (mgr *SessionManager) Parse(tokenString string) (jwt.Claims, string, error) return token.Claims, newToken, nil } -// GetLoginFailures retrieves the login failure information from the cache +// GetLoginFailures retrieves the login failure information from the cache. Any modifications to the LoginAttemps map must be done in a thread-safe manner. func (mgr *SessionManager) GetLoginFailures() map[string]LoginAttempts { // Get failures from the cache var failures map[string]LoginAttempts @@ -299,25 +301,43 @@ func (mgr *SessionManager) GetLoginFailures() map[string]LoginAttempts { return failures } -func expireOldFailedAttempts(maxAge time.Duration, failures *map[string]LoginAttempts) int { +func expireOldFailedAttempts(maxAge time.Duration, failures map[string]LoginAttempts) int { expiredCount := 0 - for key, attempt := range *failures { + for key, attempt := range failures { if time.Since(attempt.LastFailed) > maxAge*time.Second { expiredCount += 1 - delete(*failures, key) + delete(failures, key) } } return expiredCount } +// Protect admin user from login attempt reset caused by attempts to overflow cache in a brute force attack. Instead remove random non-admin to make room in cache. +func pickRandomNonAdminLoginFailure(failures map[string]LoginAttempts, username string) *string { + idx := rand.Intn(len(failures) - 1) + i := 0 + for key := range failures { + if i == idx { + if key == common.ArgoCDAdminUsername || key == username { + return pickRandomNonAdminLoginFailure(failures, username) + } + return &key + } + i++ + } + return nil +} + // Updates the failure count for a given username. If failed is true, increases the counter. Otherwise, sets counter back to 0. func (mgr *SessionManager) updateFailureCount(username string, failed bool) { + mgr.failedLock.Lock() + defer mgr.failedLock.Unlock() failures := mgr.GetLoginFailures() // Expire old entries in the cache if we have a failure window defined. if window := getLoginFailureWindow(); window > 0 { - count := expireOldFailedAttempts(window, &failures) + count := expireOldFailedAttempts(window, failures) if count > 0 { log.Infof("Expired %d entries from session cache due to max age reached", count) } @@ -327,23 +347,13 @@ func (mgr *SessionManager) updateFailureCount(username string, failed bool) { // prevent overbloating the cache with fake entries, as this could lead to // memory exhaustion and ultimately in a DoS. We remove a single entry to // replace it with the new one. - // - // Chances are that we remove the one that is under active attack, but this - // chance is low (1:cache_size) if failed && len(failures) >= getMaximumCacheSize() { log.Warnf("Session cache size exceeds %d entries, removing random entry", getMaximumCacheSize()) - idx := rand.Intn(len(failures) - 1) - var rmUser string - i := 0 - for key := range failures { - if i == idx { - rmUser = key - delete(failures, key) - break - } - i++ + rmUser := pickRandomNonAdminLoginFailure(failures, username) + if rmUser != nil { + delete(failures, *rmUser) + log.Infof("Deleted entry for user %s from cache", *rmUser) } - log.Infof("Deleted entry for user %s from cache", rmUser) } attempt, ok := failures[username] @@ -374,6 +384,8 @@ func (mgr *SessionManager) updateFailureCount(username string, failed bool) { // Get the current login failure attempts for given username func (mgr *SessionManager) getFailureCount(username string) LoginAttempts { + mgr.failedLock.RLock() + defer mgr.failedLock.RUnlock() failures := mgr.GetLoginFailures() attempt, ok := failures[username] if !ok { diff --git a/util/session/sessionmanager_test.go b/util/session/sessionmanager_test.go index d01ba3ef5f32d..817966376daa3 100644 --- a/util/session/sessionmanager_test.go +++ b/util/session/sessionmanager_test.go @@ -1173,3 +1173,42 @@ requestedScopes: ["oidc"]`, oidcTestServer.URL), assert.ErrorIs(t, err, common.TokenVerificationErr) }) } + +func Test_PickFailureAttemptWhenOverflowed(t *testing.T) { + t.Run("Not pick admin user from the queue", func(t *testing.T) { + failures := map[string]LoginAttempts{ + "admin": { + FailCount: 1, + }, + "test2": { + FailCount: 1, + }, + } + + // inside pickRandomNonAdminLoginFailure, it uses random, so we need to test it multiple times + for i := 0; i < 1000; i++ { + user := pickRandomNonAdminLoginFailure(failures, "test") + assert.Equal(t, "test2", *user) + } + }) + + t.Run("Not pick admin user and current user from the queue", func(t *testing.T) { + failures := map[string]LoginAttempts{ + "test": { + FailCount: 1, + }, + "admin": { + FailCount: 1, + }, + "test2": { + FailCount: 1, + }, + } + + // inside pickRandomNonAdminLoginFailure, it uses random, so we need to test it multiple times + for i := 0; i < 1000; i++ { + user := pickRandomNonAdminLoginFailure(failures, "test") + assert.Equal(t, "test2", *user) + } + }) +}