mirror of
https://github.com/argoproj/argo-cd.git
synced 2026-02-20 01:28:45 +01:00
Signed-off-by: Christopher Coco <chriscoco1205@gmail.com>
This commit is contained in:
@@ -95,6 +95,7 @@ func NewApplicationCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comman
|
||||
command.AddCommand(NewApplicationTerminateOpCommand(clientOpts))
|
||||
command.AddCommand(NewApplicationEditCommand(clientOpts))
|
||||
command.AddCommand(NewApplicationPatchCommand(clientOpts))
|
||||
command.AddCommand(NewApplicationGetResourceCommand(clientOpts))
|
||||
command.AddCommand(NewApplicationPatchResourceCommand(clientOpts))
|
||||
command.AddCommand(NewApplicationDeleteResourceCommand(clientOpts))
|
||||
command.AddCommand(NewApplicationResourceActionsCommand(clientOpts))
|
||||
|
||||
@@ -2,11 +2,14 @@ package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
|
||||
)
|
||||
@@ -117,3 +120,561 @@ func TestPrintResourcesTree(t *testing.T) {
|
||||
|
||||
assert.Equal(t, expectation, output)
|
||||
}
|
||||
|
||||
func TestFilterFieldsFromObject(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
obj unstructured.Unstructured
|
||||
filteredFields []string
|
||||
expectedFields []string
|
||||
unexpectedFields []string
|
||||
}{
|
||||
{
|
||||
name: "filter nested field",
|
||||
obj: unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": "vX",
|
||||
"kind": "kind",
|
||||
"metadata": map[string]any{
|
||||
"name": "test",
|
||||
},
|
||||
"spec": map[string]any{
|
||||
"testfield": map[string]any{
|
||||
"nestedtest": "test",
|
||||
},
|
||||
"testfield2": "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
filteredFields: []string{"spec.testfield.nestedtest"},
|
||||
expectedFields: []string{"spec.testfield.nestedtest"},
|
||||
unexpectedFields: []string{"spec.testfield2"},
|
||||
},
|
||||
{
|
||||
name: "filter multiple fields",
|
||||
obj: unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": "vX",
|
||||
"kind": "kind",
|
||||
"metadata": map[string]any{
|
||||
"name": "test",
|
||||
},
|
||||
"spec": map[string]any{
|
||||
"testfield": map[string]any{
|
||||
"nestedtest": "test",
|
||||
},
|
||||
"testfield2": "test",
|
||||
"testfield3": "deleteme",
|
||||
},
|
||||
},
|
||||
},
|
||||
filteredFields: []string{"spec.testfield.nestedtest", "spec.testfield3"},
|
||||
expectedFields: []string{"spec.testfield.nestedtest"},
|
||||
unexpectedFields: []string{"spec.testfield2"},
|
||||
},
|
||||
{
|
||||
name: "filter nested list object",
|
||||
obj: unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": "vX",
|
||||
"kind": "kind",
|
||||
"metadata": map[string]any{
|
||||
"name": "test",
|
||||
},
|
||||
"spec": map[string]any{
|
||||
"testfield": map[string]any{
|
||||
"nestedtest": "test",
|
||||
},
|
||||
"testfield2": "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
filteredFields: []string{"spec.testfield.nestedtest"},
|
||||
expectedFields: []string{"spec.testfield.nestedtest"},
|
||||
unexpectedFields: []string{"spec.testfield2"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.obj.SetName("test-object")
|
||||
|
||||
filtered := filterFieldsFromObject(&tt.obj, tt.filteredFields)
|
||||
|
||||
for _, field := range tt.expectedFields {
|
||||
fieldPath := strings.Split(field, ".")
|
||||
_, exists, err := unstructured.NestedFieldCopy(filtered.Object, fieldPath...)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, exists, "Expected field %s to exist", field)
|
||||
}
|
||||
|
||||
for _, field := range tt.unexpectedFields {
|
||||
fieldPath := strings.Split(field, ".")
|
||||
_, exists, err := unstructured.NestedFieldCopy(filtered.Object, fieldPath...)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, exists, "Expected field %s to not exist", field)
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.obj.GetName(), filtered.GetName())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractNestedItem(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
obj map[string]any
|
||||
fields []string
|
||||
depth int
|
||||
expected map[string]any
|
||||
}{
|
||||
{
|
||||
name: "extract simple nested item",
|
||||
obj: map[string]any{
|
||||
"listofitems": []any{
|
||||
map[string]any{
|
||||
"extract": "123",
|
||||
"dontextract": "abc",
|
||||
},
|
||||
map[string]any{
|
||||
"extract": "456",
|
||||
"dontextract": "def",
|
||||
},
|
||||
map[string]any{
|
||||
"extract": "789",
|
||||
"dontextract": "ghi",
|
||||
},
|
||||
},
|
||||
},
|
||||
fields: []string{"listofitems", "extract"},
|
||||
depth: 0,
|
||||
expected: map[string]any{
|
||||
"listofitems": []any{
|
||||
map[string]any{
|
||||
"extract": "123",
|
||||
},
|
||||
map[string]any{
|
||||
"extract": "456",
|
||||
},
|
||||
map[string]any{
|
||||
"extract": "789",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "double nested list of objects",
|
||||
obj: map[string]any{
|
||||
"listofitems": []any{
|
||||
map[string]any{
|
||||
"doublenested": []any{
|
||||
map[string]any{
|
||||
"extract": "123",
|
||||
},
|
||||
},
|
||||
"dontextract": "abc",
|
||||
},
|
||||
map[string]any{
|
||||
"doublenested": []any{
|
||||
map[string]any{
|
||||
"extract": "456",
|
||||
},
|
||||
},
|
||||
"dontextract": "def",
|
||||
},
|
||||
map[string]any{
|
||||
"doublenested": []any{
|
||||
map[string]any{
|
||||
"extract": "789",
|
||||
},
|
||||
},
|
||||
"dontextract": "ghi",
|
||||
},
|
||||
},
|
||||
},
|
||||
fields: []string{"listofitems", "doublenested", "extract"},
|
||||
depth: 0,
|
||||
expected: map[string]any{
|
||||
"listofitems": []any{
|
||||
map[string]any{
|
||||
"doublenested": []any{
|
||||
map[string]any{
|
||||
"extract": "123",
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"doublenested": []any{
|
||||
map[string]any{
|
||||
"extract": "456",
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"doublenested": []any{
|
||||
map[string]any{
|
||||
"extract": "789",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "depth is greater then list of field size",
|
||||
obj: map[string]any{"test1": "1234567890"},
|
||||
fields: []string{"test1"},
|
||||
depth: 4,
|
||||
expected: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
filteredObj := extractNestedItem(tt.obj, tt.fields, tt.depth)
|
||||
assert.Equal(t, tt.expected, filteredObj, "Did not get the correct filtered obj")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractItemsFromList(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
list []any
|
||||
fields []string
|
||||
expected []any
|
||||
}{
|
||||
{
|
||||
name: "test simple field",
|
||||
list: []any{
|
||||
map[string]any{"extract": "value1", "dontextract": "valueA"},
|
||||
map[string]any{"extract": "value2", "dontextract": "valueB"},
|
||||
map[string]any{"extract": "value3", "dontextract": "valueC"},
|
||||
},
|
||||
fields: []string{"extract"},
|
||||
expected: []any{
|
||||
map[string]any{"extract": "value1"},
|
||||
map[string]any{"extract": "value2"},
|
||||
map[string]any{"extract": "value3"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "test simple field with some depth",
|
||||
list: []any{
|
||||
map[string]any{
|
||||
"test1": map[string]any{
|
||||
"test2": map[string]any{
|
||||
"extract": "123",
|
||||
"dontextract": "abc",
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"test1": map[string]any{
|
||||
"test2": map[string]any{
|
||||
"extract": "456",
|
||||
"dontextract": "def",
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"test1": map[string]any{
|
||||
"test2": map[string]any{
|
||||
"extract": "789",
|
||||
"dontextract": "ghi",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
fields: []string{"test1", "test2", "extract"},
|
||||
expected: []any{
|
||||
map[string]any{
|
||||
"test1": map[string]any{
|
||||
"test2": map[string]any{
|
||||
"extract": "123",
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"test1": map[string]any{
|
||||
"test2": map[string]any{
|
||||
"extract": "456",
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"test1": map[string]any{
|
||||
"test2": map[string]any{
|
||||
"extract": "789",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "test a missing field",
|
||||
list: []any{
|
||||
map[string]any{"test1": "123"},
|
||||
map[string]any{"test1": "456"},
|
||||
map[string]any{"test1": "789"},
|
||||
},
|
||||
fields: []string{"test2"},
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "test getting an object",
|
||||
list: []any{
|
||||
map[string]any{
|
||||
"extract": map[string]any{
|
||||
"keyA": "valueA",
|
||||
"keyB": "valueB",
|
||||
"keyC": "valueC",
|
||||
},
|
||||
"dontextract": map[string]any{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
"key3": "value3",
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"extract": map[string]any{
|
||||
"keyD": "valueD",
|
||||
"keyE": "valueE",
|
||||
"keyF": "valueF",
|
||||
},
|
||||
"dontextract": map[string]any{
|
||||
"key4": "value4",
|
||||
"key5": "value5",
|
||||
"key6": "value6",
|
||||
},
|
||||
},
|
||||
},
|
||||
fields: []string{"extract"},
|
||||
expected: []any{
|
||||
map[string]any{
|
||||
"extract": map[string]any{
|
||||
"keyA": "valueA",
|
||||
"keyB": "valueB",
|
||||
"keyC": "valueC",
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"extract": map[string]any{
|
||||
"keyD": "valueD",
|
||||
"keyE": "valueE",
|
||||
"keyF": "valueF",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
extractedList := extractItemsFromList(tt.list, tt.fields)
|
||||
assert.Equal(t, tt.expected, extractedList, "Lists were not equal")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconstructObject(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
extracted []any
|
||||
fields []string
|
||||
depth int
|
||||
expected map[string]any
|
||||
}{
|
||||
{
|
||||
name: "simple single field at depth 0",
|
||||
extracted: []any{"value1", "value2"},
|
||||
fields: []string{"items"},
|
||||
depth: 0,
|
||||
expected: map[string]any{
|
||||
"items": []any{"value1", "value2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "object nested at depth 1",
|
||||
extracted: []any{map[string]any{"key": "value"}},
|
||||
fields: []string{"test1", "test2"},
|
||||
depth: 1,
|
||||
expected: map[string]any{
|
||||
"test1": map[string]any{
|
||||
"test2": []any{map[string]any{"key": "value"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty list of extracted items",
|
||||
extracted: []any{},
|
||||
fields: []string{"test1"},
|
||||
depth: 0,
|
||||
expected: map[string]any{
|
||||
"test1": []any{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "complex object nesteed at depth 2",
|
||||
extracted: []any{map[string]any{
|
||||
"obj1": map[string]any{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
},
|
||||
"obj2": map[string]any{
|
||||
"keyA": "valueA",
|
||||
"keyB": "valueB",
|
||||
},
|
||||
}},
|
||||
fields: []string{"test1", "test2", "test3"},
|
||||
depth: 2,
|
||||
expected: map[string]any{
|
||||
"test1": map[string]any{
|
||||
"test2": map[string]any{
|
||||
"test3": []any{
|
||||
map[string]any{
|
||||
"obj1": map[string]any{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
},
|
||||
"obj2": map[string]any{
|
||||
"keyA": "valueA",
|
||||
"keyB": "valueB",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
filteredObj := reconstructObject(tt.extracted, tt.fields, tt.depth)
|
||||
assert.Equal(t, tt.expected, filteredObj, "objects were not equal")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintManifests(t *testing.T) {
|
||||
obj := unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": "vX",
|
||||
"kind": "test",
|
||||
"metadata": map[string]any{
|
||||
"name": "unit-test",
|
||||
},
|
||||
"spec": map[string]any{
|
||||
"testfield": "testvalue",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expectedYAML := `apiVersion: vX
|
||||
kind: test
|
||||
metadata:
|
||||
name: unit-test
|
||||
spec:
|
||||
testfield: testvalue
|
||||
`
|
||||
|
||||
output, _ := captureOutput(func() error {
|
||||
printManifests(&[]unstructured.Unstructured{obj}, false, true, "yaml")
|
||||
return nil
|
||||
})
|
||||
assert.Equal(t, expectedYAML+"\n", output, "Incorrect yaml output for printManifests")
|
||||
|
||||
output, _ = captureOutput(func() error {
|
||||
printManifests(&[]unstructured.Unstructured{obj, obj}, false, true, "yaml")
|
||||
return nil
|
||||
})
|
||||
assert.Equal(t, expectedYAML+"\n---\n"+expectedYAML+"\n", output, "Incorrect yaml output with multiple objs.")
|
||||
|
||||
expectedJSON := `{
|
||||
"apiVersion": "vX",
|
||||
"kind": "test",
|
||||
"metadata": {
|
||||
"name": "unit-test"
|
||||
},
|
||||
"spec": {
|
||||
"testfield": "testvalue"
|
||||
}
|
||||
}`
|
||||
|
||||
output, _ = captureOutput(func() error {
|
||||
printManifests(&[]unstructured.Unstructured{obj}, false, true, "json")
|
||||
return nil
|
||||
})
|
||||
assert.Equal(t, expectedJSON+"\n", output, "Incorrect json output.")
|
||||
|
||||
output, _ = captureOutput(func() error {
|
||||
printManifests(&[]unstructured.Unstructured{obj, obj}, false, true, "json")
|
||||
return nil
|
||||
})
|
||||
assert.Equal(t, expectedJSON+"\n---\n"+expectedJSON+"\n", output, "Incorrect json output with multiple objs.")
|
||||
|
||||
output, _ = captureOutput(func() error {
|
||||
printManifests(&[]unstructured.Unstructured{obj}, true, true, "wide")
|
||||
return nil
|
||||
})
|
||||
assert.Contains(t, output, "FIELD RESOURCE NAME VALUE", "Missing or incorrect header line for table print with showing names.")
|
||||
assert.Contains(t, output, "apiVersion unit-test vX", "Missing or incorrect row in table related to apiVersion with showing names.")
|
||||
assert.Contains(t, output, "kind unit-test test", "Missing or incorrect line in the table related to kind with showing names.")
|
||||
assert.Contains(t, output, "spec.testfield unit-test testvalue", "Missing or incorrect line in the table related to spec.testfield with showing names.")
|
||||
assert.NotContains(t, output, "metadata.name unit-test testvalue", "Missing or incorrect line in the table related to metadata.name with showing names.")
|
||||
|
||||
output, _ = captureOutput(func() error {
|
||||
printManifests(&[]unstructured.Unstructured{obj}, true, false, "wide")
|
||||
return nil
|
||||
})
|
||||
assert.Contains(t, output, "FIELD VALUE", "Missing or incorrect header line for table print with not showing names.")
|
||||
assert.Contains(t, output, "apiVersion vX", "Missing or incorrect row in table related to apiVersion with not showing names.")
|
||||
assert.Contains(t, output, "kind test", "Missing or incorrect row in the table related to kind with not showing names.")
|
||||
assert.Contains(t, output, "spec.testfield testvalue", "Missing or incorrect row in the table related to spec.testefield with not showing names.")
|
||||
assert.NotContains(t, output, "metadata.name testvalue", "Missing or incorrect row in the tbale related to metadata.name with not showing names.")
|
||||
}
|
||||
|
||||
func TestPrintManifests_FilterNestedListObject_Wide(t *testing.T) {
|
||||
obj := unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": "vX",
|
||||
"kind": "kind",
|
||||
"metadata": map[string]any{
|
||||
"name": "unit-test",
|
||||
},
|
||||
"status": map[string]any{
|
||||
"podIPs": []map[string]any{
|
||||
{
|
||||
"IP": "127.0.0.1",
|
||||
},
|
||||
{
|
||||
"IP": "127.0.0.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
output, _ := captureOutput(func() error {
|
||||
v, err := json.Marshal(&obj)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var obj2 *unstructured.Unstructured
|
||||
err = json.Unmarshal([]byte(v), &obj2)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
printManifests(&[]unstructured.Unstructured{*obj2}, false, true, "wide")
|
||||
return nil
|
||||
})
|
||||
|
||||
// Verify table header
|
||||
assert.Contains(t, output, "FIELD RESOURCE NAME VALUE", "Missing a line in the table")
|
||||
assert.Contains(t, output, "apiVersion unit-test vX", "Test for apiVersion field failed for wide output")
|
||||
assert.Contains(t, output, "kind unit-test kind", "Test for kind field failed for wide output")
|
||||
assert.Contains(t, output, "status.podIPs[0].IP unit-test 127.0.0.1", "Test for podIP array index 0 field failed for wide output")
|
||||
assert.Contains(t, output, "status.podIPs[1].IP unit-test 127.0.0.2", "Test for podIP array index 1 field failed for wide output")
|
||||
}
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/cmd/argocd/commands/utils"
|
||||
"github.com/argoproj/argo-cd/v3/cmd/util"
|
||||
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/utils/ptr"
|
||||
|
||||
@@ -22,15 +28,273 @@ import (
|
||||
utilio "github.com/argoproj/argo-cd/v3/util/io"
|
||||
)
|
||||
|
||||
// NewApplicationGetResourceCommand returns a new instance of the `app get-resource` command
|
||||
func NewApplicationGetResourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
resourceName string
|
||||
kind string
|
||||
project string
|
||||
filteredFields []string
|
||||
showManagedFields bool
|
||||
output string
|
||||
)
|
||||
command := &cobra.Command{
|
||||
Use: "get-resource APPNAME",
|
||||
Short: "Get details about the live Kubernetes manifests of a resource in an application. The filter-fields flag can be used to only display fields you want to see.",
|
||||
Example: `
|
||||
# Get a specific resource, Pod my-app-pod, in 'my-app' by name in wide format
|
||||
argocd app get-resource my-app --kind Pod --resource-name my-app-pod
|
||||
|
||||
# Get a specific resource, Pod my-app-pod, in 'my-app' by name in yaml format
|
||||
argocd app get-resource my-app --kind Pod --resource-name my-app-pod -o yaml
|
||||
|
||||
# Get a specific resource, Pod my-app-pod, in 'my-app' by name in json format
|
||||
argocd app get-resource my-app --kind Pod --resource-name my-app-pod -o json
|
||||
|
||||
# Get details about all Pods in the application
|
||||
argocd app get-resource my-app --kind Pod
|
||||
|
||||
# Get a specific resource with managed fields, Pod my-app-pod, in 'my-app' by name in wide format
|
||||
argocd app get-resource my-app --kind Pod --resource-name my-app-pod --show-managed-fields
|
||||
|
||||
# Get the the details of a specific field in a resource in 'my-app' in the wide format
|
||||
argocd app get-resource my-app --kind Pod --filter-fields status.podIP
|
||||
|
||||
# Get the details of multiple specific fields in a specific resource in 'my-app' in the wide format
|
||||
argocd app get-resource my-app --kind Pod --resource-name my-app-pod --filter-fields status.podIP,status.hostIP`,
|
||||
}
|
||||
|
||||
command.Run = func(c *cobra.Command, args []string) {
|
||||
ctx := c.Context()
|
||||
|
||||
if len(args) != 1 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
appName, appNs := argo.ParseFromQualifiedName(args[0], "")
|
||||
|
||||
conn, appIf := headless.NewClientOrDie(clientOpts, c).NewApplicationClientOrDie()
|
||||
defer utilio.Close(conn)
|
||||
|
||||
tree, err := appIf.ResourceTree(ctx, &applicationpkg.ResourcesQuery{
|
||||
ApplicationName: &appName,
|
||||
AppNamespace: &appNs,
|
||||
})
|
||||
errors.CheckError(err)
|
||||
|
||||
// Get manifests of resources
|
||||
// If resource name is "" find all resources of that kind
|
||||
var resources []unstructured.Unstructured
|
||||
var fetchedStr string
|
||||
for _, r := range tree.Nodes {
|
||||
if (resourceName != "" && r.Name != resourceName) || r.Kind != kind {
|
||||
continue
|
||||
}
|
||||
resource, err := appIf.GetResource(ctx, &applicationpkg.ApplicationResourceRequest{
|
||||
Name: &appName,
|
||||
AppNamespace: &appNs,
|
||||
Group: &r.Group,
|
||||
Kind: &r.Kind,
|
||||
Namespace: &r.Namespace,
|
||||
Project: &project,
|
||||
ResourceName: &r.Name,
|
||||
Version: &r.Version,
|
||||
})
|
||||
errors.CheckError(err)
|
||||
manifest := resource.GetManifest()
|
||||
|
||||
var obj *unstructured.Unstructured
|
||||
err = json.Unmarshal([]byte(manifest), &obj)
|
||||
errors.CheckError(err)
|
||||
|
||||
if !showManagedFields {
|
||||
unstructured.RemoveNestedField(obj.Object, "metadata", "managedFields")
|
||||
}
|
||||
|
||||
if len(filteredFields) != 0 {
|
||||
obj = filterFieldsFromObject(obj, filteredFields)
|
||||
}
|
||||
|
||||
fetchedStr += obj.GetName() + ", "
|
||||
resources = append(resources, *obj)
|
||||
}
|
||||
printManifests(&resources, len(filteredFields) > 0, resourceName == "", output)
|
||||
|
||||
if fetchedStr != "" {
|
||||
fetchedStr = strings.TrimSuffix(fetchedStr, ", ")
|
||||
}
|
||||
log.Infof("Resources '%s' fetched", fetchedStr)
|
||||
}
|
||||
|
||||
command.Flags().StringVar(&resourceName, "resource-name", "", "Name of resource, if none is included will output details of all resources with specified kind")
|
||||
command.Flags().StringVar(&kind, "kind", "", "Kind of resource [REQUIRED]")
|
||||
err := command.MarkFlagRequired("kind")
|
||||
errors.CheckError(err)
|
||||
command.Flags().StringVar(&project, "project", "", "Project of resource")
|
||||
command.Flags().StringSliceVar(&filteredFields, "filter-fields", nil, "A comma separated list of fields to display, if not provided will output the entire manifest")
|
||||
command.Flags().BoolVar(&showManagedFields, "show-managed-fields", false, "Show managed fields in the output manifest")
|
||||
command.Flags().StringVarP(&output, "output", "o", "wide", "Format of the output, wide, yaml, or json")
|
||||
return command
|
||||
}
|
||||
|
||||
// filterFieldsFromObject creates a new unstructured object containing only the specified fields from the source object.
|
||||
func filterFieldsFromObject(obj *unstructured.Unstructured, filteredFields []string) *unstructured.Unstructured {
|
||||
var filteredObj unstructured.Unstructured
|
||||
filteredObj.Object = make(map[string]any)
|
||||
|
||||
for _, f := range filteredFields {
|
||||
fields := strings.Split(f, ".")
|
||||
|
||||
value, exists, err := unstructured.NestedFieldCopy(obj.Object, fields...)
|
||||
if exists {
|
||||
errors.CheckError(err)
|
||||
err = unstructured.SetNestedField(filteredObj.Object, value, fields...)
|
||||
errors.CheckError(err)
|
||||
} else {
|
||||
// If doesn't exist assume its a nested inside a list of objects
|
||||
value := extractNestedItem(obj.Object, fields, 0)
|
||||
filteredObj.Object = value
|
||||
}
|
||||
}
|
||||
filteredObj.SetName(obj.GetName())
|
||||
return &filteredObj
|
||||
}
|
||||
|
||||
// extractNestedItem recursively extracts an item that may be nested inside a list of objects.
|
||||
func extractNestedItem(obj map[string]any, fields []string, depth int) map[string]any {
|
||||
if depth >= len(fields) {
|
||||
return nil
|
||||
}
|
||||
|
||||
value, exists, _ := unstructured.NestedFieldCopy(obj, fields[:depth+1]...)
|
||||
list, ok := value.([]any)
|
||||
if !exists || !ok {
|
||||
return extractNestedItem(obj, fields, depth+1)
|
||||
}
|
||||
|
||||
extractedItems := extractItemsFromList(list, fields[depth+1:])
|
||||
if len(extractedItems) == 0 {
|
||||
for _, e := range list {
|
||||
if o, ok := e.(map[string]any); ok {
|
||||
result := extractNestedItem(o, fields[depth+1:], 0)
|
||||
extractedItems = append(extractedItems, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filteredObj := reconstructObject(extractedItems, fields, depth)
|
||||
return filteredObj
|
||||
}
|
||||
|
||||
// extractItemsFromList processes a list of objects and extracts specific fields from each item.
|
||||
func extractItemsFromList(list []any, fields []string) []any {
|
||||
var extratedObjs []any
|
||||
for _, e := range list {
|
||||
extractedObj := make(map[string]any)
|
||||
if o, ok := e.(map[string]any); ok {
|
||||
value, exists, _ := unstructured.NestedFieldCopy(o, fields...)
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
err := unstructured.SetNestedField(extractedObj, value, fields...)
|
||||
errors.CheckError(err)
|
||||
extratedObjs = append(extratedObjs, extractedObj)
|
||||
}
|
||||
}
|
||||
return extratedObjs
|
||||
}
|
||||
|
||||
// reconstructObject rebuilds the original object structure by placing extracted items back into their proper nested location.
|
||||
func reconstructObject(extracted []any, fields []string, depth int) map[string]any {
|
||||
obj := make(map[string]any)
|
||||
err := unstructured.SetNestedField(obj, extracted, fields[:depth+1]...)
|
||||
errors.CheckError(err)
|
||||
return obj
|
||||
}
|
||||
|
||||
// printManifests outputs resource manifests in the specified format (wide, JSON, or YAML).
|
||||
func printManifests(objs *[]unstructured.Unstructured, filteredFields bool, showName bool, output string) {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
if showName {
|
||||
fmt.Fprintf(w, "FIELD\tRESOURCE NAME\tVALUE\n")
|
||||
} else {
|
||||
fmt.Fprintf(w, "FIELD\tVALUE\n")
|
||||
}
|
||||
|
||||
for i, o := range *objs {
|
||||
if output == "json" || output == "yaml" {
|
||||
var formattedManifest []byte
|
||||
var err error
|
||||
if output == "json" {
|
||||
formattedManifest, err = json.MarshalIndent(o.Object, "", " ")
|
||||
} else {
|
||||
formattedManifest, err = yaml.Marshal(o.Object)
|
||||
}
|
||||
errors.CheckError(err)
|
||||
|
||||
fmt.Println(string(formattedManifest))
|
||||
if len(*objs) > 1 && i != len(*objs)-1 {
|
||||
fmt.Println("---")
|
||||
}
|
||||
} else {
|
||||
name := o.GetName()
|
||||
if filteredFields {
|
||||
unstructured.RemoveNestedField(o.Object, "metadata", "name")
|
||||
}
|
||||
|
||||
printManifestAsTable(w, name, showName, o.Object, "")
|
||||
}
|
||||
}
|
||||
|
||||
if output != "json" && output != "yaml" {
|
||||
err := w.Flush()
|
||||
errors.CheckError(err)
|
||||
}
|
||||
}
|
||||
|
||||
// printManifestAsTable recursively prints a manifest object as a tabular view with nested fields flattened.
|
||||
func printManifestAsTable(w *tabwriter.Writer, name string, showName bool, obj map[string]any, parentField string) {
|
||||
for key, value := range obj {
|
||||
field := parentField + key
|
||||
switch v := value.(type) {
|
||||
case map[string]any:
|
||||
printManifestAsTable(w, name, showName, v, field+".")
|
||||
case []any:
|
||||
for i, e := range v {
|
||||
index := "[" + strconv.Itoa(i) + "]"
|
||||
|
||||
if innerObj, ok := e.(map[string]any); ok {
|
||||
printManifestAsTable(w, name, showName, innerObj, field+index+".")
|
||||
} else {
|
||||
if showName {
|
||||
fmt.Fprintf(w, "%v\t%v\t%v\n", field+index, name, e)
|
||||
} else {
|
||||
fmt.Fprintf(w, "%v\t%v\n", field+index, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
if showName {
|
||||
fmt.Fprintf(w, "%v\t%v\t%v\n", field, name, v)
|
||||
} else {
|
||||
fmt.Fprintf(w, "%v\t%v\n", field, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func NewApplicationPatchResourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var patch string
|
||||
var patchType string
|
||||
var resourceName string
|
||||
var namespace string
|
||||
var kind string
|
||||
var group string
|
||||
var all bool
|
||||
var project string
|
||||
var (
|
||||
patch string
|
||||
patchType string
|
||||
resourceName string
|
||||
namespace string
|
||||
kind string
|
||||
group string
|
||||
all bool
|
||||
project string
|
||||
)
|
||||
command := &cobra.Command{
|
||||
Use: "patch-resource APPNAME",
|
||||
Short: "Patch resource in an application",
|
||||
@@ -90,14 +354,16 @@ func NewApplicationPatchResourceCommand(clientOpts *argocdclient.ClientOptions)
|
||||
}
|
||||
|
||||
func NewApplicationDeleteResourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var resourceName string
|
||||
var namespace string
|
||||
var kind string
|
||||
var group string
|
||||
var force bool
|
||||
var orphan bool
|
||||
var all bool
|
||||
var project string
|
||||
var (
|
||||
resourceName string
|
||||
namespace string
|
||||
kind string
|
||||
group string
|
||||
force bool
|
||||
orphan bool
|
||||
all bool
|
||||
project string
|
||||
)
|
||||
command := &cobra.Command{
|
||||
Use: "delete-resource APPNAME",
|
||||
Short: "Delete resource in an application",
|
||||
@@ -253,13 +519,16 @@ func printResources(listAll bool, orphaned bool, appResourceTree *v1alpha1.Appli
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = w.Flush()
|
||||
err := w.Flush()
|
||||
errors.CheckError(err)
|
||||
}
|
||||
|
||||
func NewApplicationListResourcesCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var orphaned bool
|
||||
var output string
|
||||
var project string
|
||||
var (
|
||||
orphaned bool
|
||||
output string
|
||||
project string
|
||||
)
|
||||
command := &cobra.Command{
|
||||
Use: "resources APPNAME",
|
||||
Short: "List resource of application",
|
||||
|
||||
1
docs/user-guide/commands/argocd_app.md
generated
1
docs/user-guide/commands/argocd_app.md
generated
@@ -89,6 +89,7 @@ argocd app [flags]
|
||||
* [argocd app diff](argocd_app_diff.md) - Perform a diff against the target and live state.
|
||||
* [argocd app edit](argocd_app_edit.md) - Edit application
|
||||
* [argocd app get](argocd_app_get.md) - Get application details
|
||||
* [argocd app get-resource](argocd_app_get-resource.md) - Get details about the live Kubernetes manifests of a resource in an application. The filter-fields flag can be used to only display fields you want to see.
|
||||
* [argocd app history](argocd_app_history.md) - Show application deployment history
|
||||
* [argocd app list](argocd_app_list.md) - List applications
|
||||
* [argocd app logs](argocd_app_logs.md) - Get logs of application pods
|
||||
|
||||
83
docs/user-guide/commands/argocd_app_get-resource.md
generated
Normal file
83
docs/user-guide/commands/argocd_app_get-resource.md
generated
Normal file
@@ -0,0 +1,83 @@
|
||||
# `argocd app get-resource` Command Reference
|
||||
|
||||
## argocd app get-resource
|
||||
|
||||
Get details about the live Kubernetes manifests of a resource in an application. The filter-fields flag can be used to only display fields you want to see.
|
||||
|
||||
```
|
||||
argocd app get-resource APPNAME [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
|
||||
# Get a specific resource, Pod my-app-pod, in 'my-app' by name in wide format
|
||||
argocd app get-resource my-app --kind Pod --resource-name my-app-pod
|
||||
|
||||
# Get a specific resource, Pod my-app-pod, in 'my-app' by name in yaml format
|
||||
argocd app get-resource my-app --kind Pod --resource-name my-app-pod -o yaml
|
||||
|
||||
# Get a specific resource, Pod my-app-pod, in 'my-app' by name in json format
|
||||
argocd app get-resource my-app --kind Pod --resource-name my-app-pod -o json
|
||||
|
||||
# Get details about all Pods in the application
|
||||
argocd app get-resource my-app --kind Pod
|
||||
|
||||
# Get a specific resource with managed fields, Pod my-app-pod, in 'my-app' by name in wide format
|
||||
argocd app get-resource my-app --kind Pod --resource-name my-app-pod --show-managed-fields
|
||||
|
||||
# Get the the details of a specific field in a resource in 'my-app' in the wide format
|
||||
argocd app get-resource my-app --kind Pod --filter-fields status.podIP
|
||||
|
||||
# Get the details of multiple specific fields in a specific resource in 'my-app' in the wide format
|
||||
argocd app get-resource my-app --kind Pod --resource-name my-app-pod --filter-fields status.podIP,status.hostIP
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
--filter-fields strings A comma separated list of fields to display, if not provided will output the entire manifest
|
||||
-h, --help help for get-resource
|
||||
--kind string Kind of resource [REQUIRED]
|
||||
-o, --output string Format of the output, wide, yaml, or json (default "wide")
|
||||
--project string Project of resource
|
||||
--resource-name string Name of resource, if none is included will output details of all resources with specified kind
|
||||
--show-managed-fields Show managed fields in the output manifest
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
--argocd-context string The name of the Argo-CD server context to use
|
||||
--auth-token string Authentication token; set this or the ARGOCD_AUTH_TOKEN environment variable
|
||||
--client-crt string Client certificate file
|
||||
--client-crt-key string Client certificate key file
|
||||
--config string Path to Argo CD config (default "/home/user/.config/argocd/config")
|
||||
--controller-name string Name of the Argo CD Application controller; set this or the ARGOCD_APPLICATION_CONTROLLER_NAME environment variable when the controller's name label differs from the default, for example when installing via the Helm chart (default "argocd-application-controller")
|
||||
--core If set to true then CLI talks directly to Kubernetes instead of talking to Argo CD API server
|
||||
--grpc-web Enables gRPC-web protocol. Useful if Argo CD server is behind proxy which does not support HTTP2.
|
||||
--grpc-web-root-path string Enables gRPC-web protocol. Useful if Argo CD server is behind proxy which does not support HTTP2. Set web root.
|
||||
-H, --header strings Sets additional header to all requests made by Argo CD CLI. (Can be repeated multiple times to add multiple headers, also supports comma separated headers)
|
||||
--http-retry-max int Maximum number of retries to establish http connection to Argo CD server
|
||||
--insecure Skip server certificate and domain verification
|
||||
--kube-context string Directs the command to the given kube-context
|
||||
--logformat string Set the logging format. One of: json|text (default "json")
|
||||
--loglevel string Set the logging level. One of: debug|info|warn|error (default "info")
|
||||
--plaintext Disable TLS
|
||||
--port-forward Connect to a random argocd-server port using port forwarding
|
||||
--port-forward-namespace string Namespace name which should be used for port forwarding
|
||||
--prompts-enabled Force optional interactive prompts to be enabled or disabled, overriding local configuration. If not specified, the local configuration value will be used, which is false by default.
|
||||
--redis-compress string Enable this if the application controller is configured with redis compression enabled. (possible values: gzip, none) (default "gzip")
|
||||
--redis-haproxy-name string Name of the Redis HA Proxy; set this or the ARGOCD_REDIS_HAPROXY_NAME environment variable when the HA Proxy's name label differs from the default, for example when installing via the Helm chart (default "argocd-redis-ha-haproxy")
|
||||
--redis-name string Name of the Redis deployment; set this or the ARGOCD_REDIS_NAME environment variable when the Redis's name label differs from the default, for example when installing via the Helm chart (default "argocd-redis")
|
||||
--repo-server-name string Name of the Argo CD Repo server; set this or the ARGOCD_REPO_SERVER_NAME environment variable when the server's name label differs from the default, for example when installing via the Helm chart (default "argocd-repo-server")
|
||||
--server string Argo CD server address
|
||||
--server-crt string Server certificate file
|
||||
--server-name string Name of the Argo CD API server; set this or the ARGOCD_SERVER_NAME environment variable when the server's name label differs from the default, for example when installing via the Helm chart (default "argocd-server")
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [argocd app](argocd_app.md) - Manage applications
|
||||
|
||||
Reference in New Issue
Block a user