Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 5 additions & 15 deletions cmd/argocd/commands/admin/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import (
"k8s.io/client-go/tools/clientcmd"
"sigs.k8s.io/yaml"

cmdutil "github.com/argoproj/argo-cd/v3/cmd/util"
"github.com/argoproj/argo-cd/v3/common"
applicationpkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/application"
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v3/util/argo/normalizers"
"github.com/argoproj/argo-cd/v3/util/cli"
Expand Down Expand Up @@ -582,20 +582,10 @@ argocd admin settings resource-overrides action /tmp/deploy.yaml restart --argoc
action := args[1]

// Parse resource action parameters
parsedParams := make([]*applicationpkg.ResourceActionParameters, 0)
if len(resourceActionParameters) > 0 {
for _, param := range resourceActionParameters {
parts := strings.SplitN(param, "=", 2)
if len(parts) != 2 {
log.Fatalf("Invalid parameter format: %s", param)
}
name := parts[0]
value := parts[1]
parsedParams = append(parsedParams, &applicationpkg.ResourceActionParameters{
Name: &name,
Value: &value,
})
}
parsedParams, err := cmdutil.ParseActionParameters(resourceActionParameters)
errors.CheckError(err)
if dupes := cmdutil.DuplicateActionParameterNames(parsedParams); len(dupes) > 0 {
log.Warnf("Duplicate parameter names provided (%s): the last value for each parameter will be used", strings.Join(dupes, ", "))
}

executeResourceOverrideCommand(ctx, cmdCtx, args, func(res unstructured.Unstructured, override v1alpha1.ResourceOverride, overrides map[string]v1alpha1.ResourceOverride) {
Expand Down
27 changes: 18 additions & 9 deletions cmd/argocd/commands/app_actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"os"
"strconv"
"strings"
"text/tabwriter"

log "github.com/sirupsen/logrus"
Expand Down Expand Up @@ -150,6 +151,7 @@ func NewApplicationResourceActionsRunCommand(clientOpts *argocdclient.ClientOpti
var kind string
var group string
var all bool
var params []string
command := &cobra.Command{
Use: "run APPNAME ACTION",
Short: "Runs an available action on resource(s) matching the specified filters.",
Expand All @@ -167,6 +169,7 @@ func NewApplicationResourceActionsRunCommand(clientOpts *argocdclient.ClientOpti
command.Flags().StringVar(&group, "group", "", "Group of the resource on which the action should be run")
errors.CheckError(command.MarkFlagRequired("kind"))
command.Flags().BoolVar(&all, "all", false, "Indicates whether to run the action on multiple matching resources")
command.Flags().StringArrayVar(&params, "param", []string{}, "Action parameter in key=value format (e.g. replicas=2). This flag may be repeated")
Comment thread
olavst-spk marked this conversation as resolved.

command.Run = func(c *cobra.Command, args []string) {
ctx := c.Context()
Expand All @@ -191,20 +194,26 @@ func NewApplicationResourceActionsRunCommand(clientOpts *argocdclient.ClientOpti
}
}

parsedParams, err := util.ParseActionParameters(params)
errors.CheckError(err)
if dupes := util.DuplicateActionParameterNames(parsedParams); len(dupes) > 0 {
log.Warnf("Duplicate parameter names provided (%s): the last value for each parameter will be used", strings.Join(dupes, ", "))
}

for i := range filteredObjects {
obj := filteredObjects[i]
gvk := obj.GroupVersionKind()
objResourceName := obj.GetName()
_, err := appIf.RunResourceActionV2(ctx, &applicationpkg.ResourceActionRunRequestV2{
Name: &appName,
AppNamespace: &appNs,
Namespace: new(obj.GetNamespace()),
ResourceName: new(objResourceName),
Group: new(gvk.Group),
Kind: new(gvk.Kind),
Version: new(gvk.GroupVersion().Version),
Action: new(actionName),
// TODO: add support for parameters
Name: &appName,
AppNamespace: &appNs,
Namespace: new(obj.GetNamespace()),
ResourceName: new(objResourceName),
Group: new(gvk.Group),
Kind: new(gvk.Kind),
Version: new(gvk.GroupVersion().Version),
Action: new(actionName),
ResourceActionParameters: parsedParams,
})
if err == nil {
continue
Expand Down
45 changes: 45 additions & 0 deletions cmd/util/actions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package util

import (
"fmt"
"strings"

applicationpkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/application"
)

// ParseActionParameters parses a slice of "name=value" strings into ResourceActionParameters.
func ParseActionParameters(params []string) ([]*applicationpkg.ResourceActionParameters, error) {
parsedParams := make([]*applicationpkg.ResourceActionParameters, 0, len(params))
for _, param := range params {
parts := strings.SplitN(param, "=", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("invalid parameter format %q: expected name=value", param)
}
name := parts[0]
value := parts[1]
Comment thread
olavst-spk marked this conversation as resolved.
if name == "" {
return nil, fmt.Errorf("invalid parameter format %q: parameter name cannot be empty", param)
}
parsedParams = append(parsedParams, &applicationpkg.ResourceActionParameters{
Name: &name,
Value: &value,
})
}
return parsedParams, nil
}

// DuplicateActionParameterNames returns the names of any parameters specified more than once.
func DuplicateActionParameterNames(params []*applicationpkg.ResourceActionParameters) []string {
seen := make(map[string]int, len(params))
var duplicates []string
for _, p := range params {
if p == nil || p.Name == nil {
continue
}
seen[*p.Name]++
if seen[*p.Name] == 2 {
duplicates = append(duplicates, *p.Name)
}
}
return duplicates
}
161 changes: 161 additions & 0 deletions cmd/util/actions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package util

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

applicationpkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/application"
)

func strPtr(s string) *string { return &s }

func TestParseActionParameters(t *testing.T) {
testCases := []struct {
name string
params []string
expectedParams []*applicationpkg.ResourceActionParameters
expectError bool
}{
{
name: "empty",
params: []string{},
expectedParams: []*applicationpkg.ResourceActionParameters{},
},
{
name: "single parameter",
params: []string{"replicas=2"},
expectedParams: []*applicationpkg.ResourceActionParameters{
{Name: strPtr("replicas"), Value: strPtr("2")},
},
},
{
name: "multiple parameters",
params: []string{"replicas=2", "image=foo"},
expectedParams: []*applicationpkg.ResourceActionParameters{
{Name: strPtr("replicas"), Value: strPtr("2")},
{Name: strPtr("image"), Value: strPtr("foo")},
},
},
{
name: "value containing equals sign",
params: []string{"url=http://example.com?foo=bar"},
expectedParams: []*applicationpkg.ResourceActionParameters{
{Name: strPtr("url"), Value: strPtr("http://example.com?foo=bar")},
},
},
{
name: "empty key",
params: []string{"=value"},
expectError: true,
},
{
name: "empty value",
params: []string{"key="},
expectedParams: []*applicationpkg.ResourceActionParameters{
{Name: strPtr("key"), Value: strPtr("")},
},
},
{
name: "missing equals sign",
params: []string{"replicas"},
expectError: true,
},
{
name: "duplicate keys",
params: []string{"replicas=2", "replicas=3"},
expectedParams: []*applicationpkg.ResourceActionParameters{
{Name: strPtr("replicas"), Value: strPtr("2")},
{Name: strPtr("replicas"), Value: strPtr("3")},
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, err := ParseActionParameters(tc.params)
if tc.expectError {
assert.Error(t, err)
return
}
require.NoError(t, err)
assert.Len(t, result, len(tc.expectedParams))
for i, p := range result {
assert.Equal(t, *tc.expectedParams[i].Name, *p.Name)
assert.Equal(t, *tc.expectedParams[i].Value, *p.Value)
}
})
}
}

func TestDuplicateActionParameterNames(t *testing.T) {
testCases := []struct {
name string
params []*applicationpkg.ResourceActionParameters
expectedDupes []string
}{
{
name: "empty",
params: []*applicationpkg.ResourceActionParameters{},
expectedDupes: nil,
},
{
name: "no duplicates",
params: []*applicationpkg.ResourceActionParameters{
{Name: strPtr("replicas"), Value: strPtr("2")},
},
expectedDupes: nil,
},
{
name: "single duplicate",
params: []*applicationpkg.ResourceActionParameters{
{Name: strPtr("replicas"), Value: strPtr("2")},
{Name: strPtr("replicas"), Value: strPtr("3")},
},
expectedDupes: []string{"replicas"},
},
{
name: "multiple duplicates",
params: []*applicationpkg.ResourceActionParameters{
{Name: strPtr("replicas"), Value: strPtr("2")},
{Name: strPtr("replicas"), Value: strPtr("3")},
{Name: strPtr("replicas"), Value: strPtr("4")},
},
expectedDupes: []string{"replicas"},
},
{
name: "multiple distinct duplicates",
params: []*applicationpkg.ResourceActionParameters{
{Name: strPtr("replicas"), Value: strPtr("2")},
{Name: strPtr("replicas"), Value: strPtr("3")},
{Name: strPtr("replicas"), Value: strPtr("4")},
{Name: strPtr("image"), Value: strPtr("foo")},
{Name: strPtr("image"), Value: strPtr("bar")},
{Name: strPtr("image"), Value: strPtr("baz")},
},
expectedDupes: []string{"replicas", "image"},
},
{
name: "nil element in slice",
params: []*applicationpkg.ResourceActionParameters{
nil,
},
expectedDupes: nil,
},
{
name: "nil name field",
params: []*applicationpkg.ResourceActionParameters{
{Name: nil, Value: nil},
},
expectedDupes: nil,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := DuplicateActionParameterNames(tc.params)
assert.ElementsMatch(t, tc.expectedDupes, result)
})
}
}
1 change: 1 addition & 0 deletions docs/user-guide/commands/argocd_app_actions_run.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions docs/user-guide/scale_application_resources.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,12 @@ This enables users to scale resources directly from the Argo CD UI. Users will b
> [!NOTE]
> If you use HPA (Horizontal Pod Autoscaling) or enabled Argo CD auto-sync, changing the replica count in scale actions would be overwritten.
> Ensure that invalid values (e.g., `non-numeric` characters, `negative` numbers, or values beyond the `max integer limit`) cannot be entered.

## CLI Usage

You can also scale resources from the CLI using the `--param` flag:

```bash
argocd app actions run APPNAME scale --kind Deployment --resource-name RESOURCE_NAME --param replicas=2
argocd app actions run APPNAME scale --kind StatefulSet --resource-name RESOURCE_NAME --param replicas=3
```
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,10 @@ actionTests:

- action: scale
inputPath: testdata/deployment.yaml
expectedErrorMessage: "invalid number: not_a_number"
expectedErrorMessage: "replicas parameter is required"

- action: scale
inputPath: testdata/deployment.yaml
expectedErrorMessage: "invalid number: not_a_number (must be >= 0)"
parameters:
replicas: "not_a_number"

Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
local os = require("os")

local replicas = tonumber(actionParams["replicas"])
local replicas = actionParams["replicas"]
if not replicas then
error("invalid number: " .. actionParams["replicas"], 0)
error("replicas parameter is required", 0)
end

obj.spec.replicas = replicas
local replicasNum = tonumber(replicas)
if not replicasNum or replicasNum < 0 then
error("invalid number: " .. replicas .. " (must be >= 0)", 0)
end

obj.spec.replicas = replicasNum
return obj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ actionTests:
replicas: '6'
- action: scale
inputPath: testdata/statefulset.yaml
expectedErrorMessage: 'invalid number: not_a_number'
expectedErrorMessage: 'replicas parameter is required'
- action: scale
inputPath: testdata/statefulset.yaml
expectedErrorMessage: 'invalid number: not_a_number (must be >= 0)'
parameters:
replicas: 'not_a_number'
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
local os = require("os")

local replicas = tonumber(actionParams["replicas"])
local replicas = actionParams["replicas"]
if not replicas then
error("invalid number: " .. actionParams["replicas"], 0)
error("replicas parameter is required", 0)
end

obj.spec.replicas = replicas
local replicasNum = tonumber(replicas)
if not replicasNum or replicasNum < 0 then
error("invalid number: " .. replicas .. " (must be >= 0)", 0)
end

obj.spec.replicas = replicasNum
return obj
Loading
Loading