Files
argo-cd/cmd/argocd/commands/app.go
2026-02-12 09:29:40 -05:00

3737 lines
141 KiB
Go

package commands
import (
"context"
"encoding/json"
stderrors "errors"
"fmt"
"io"
"os"
"reflect"
"slices"
"sort"
"strconv"
"strings"
"sync"
"text/tabwriter"
"time"
"unicode/utf8"
"golang.org/x/sync/errgroup"
"github.com/argoproj/argo-cd/gitops-engine/pkg/health"
"github.com/argoproj/argo-cd/gitops-engine/pkg/sync/common"
"github.com/argoproj/argo-cd/gitops-engine/pkg/sync/hook"
"github.com/argoproj/argo-cd/gitops-engine/pkg/sync/ignore"
"github.com/argoproj/argo-cd/gitops-engine/pkg/utils/kube"
grpc_retry "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/retry"
"github.com/mattn/go-isatty"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
k8swatch "k8s.io/apimachinery/pkg/watch"
"k8s.io/utils/ptr"
"sigs.k8s.io/yaml"
"github.com/argoproj/argo-cd/v3/cmd/argocd/commands/headless"
"github.com/argoproj/argo-cd/v3/cmd/argocd/commands/utils"
cmdutil "github.com/argoproj/argo-cd/v3/cmd/util"
argocommon "github.com/argoproj/argo-cd/v3/common"
"github.com/argoproj/argo-cd/v3/controller"
argocdclient "github.com/argoproj/argo-cd/v3/pkg/apiclient"
"github.com/argoproj/argo-cd/v3/pkg/apiclient/application"
resourceutil "github.com/argoproj/argo-cd/gitops-engine/pkg/sync/resource"
clusterpkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/cluster"
projectpkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/project"
"github.com/argoproj/argo-cd/v3/pkg/apiclient/settings"
argoappv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
repoapiclient "github.com/argoproj/argo-cd/v3/reposerver/apiclient"
"github.com/argoproj/argo-cd/v3/reposerver/repository"
"github.com/argoproj/argo-cd/v3/util/argo"
argodiff "github.com/argoproj/argo-cd/v3/util/argo/diff"
"github.com/argoproj/argo-cd/v3/util/argo/normalizers"
"github.com/argoproj/argo-cd/v3/util/cli"
"github.com/argoproj/argo-cd/v3/util/errors"
"github.com/argoproj/argo-cd/v3/util/git"
"github.com/argoproj/argo-cd/v3/util/grpc"
utilio "github.com/argoproj/argo-cd/v3/util/io"
logutils "github.com/argoproj/argo-cd/v3/util/log"
"github.com/argoproj/argo-cd/v3/util/manifeststream"
"github.com/argoproj/argo-cd/v3/util/templates"
"github.com/argoproj/argo-cd/v3/util/text/label"
)
// NewApplicationCommand returns a new instance of an `argocd app` command
func NewApplicationCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
command := &cobra.Command{
Use: "app",
Short: "Manage applications",
Example: ` # List all the applications.
argocd app list
# Get the details of a application
argocd app get my-app
# Set an override parameter
argocd app set my-app -p image.tag=v1.0.1`,
Run: func(c *cobra.Command, args []string) {
c.HelpFunc()(c, args)
os.Exit(1)
},
}
command.AddCommand(NewApplicationCreateCommand(clientOpts))
command.AddCommand(NewApplicationGetCommand(clientOpts))
command.AddCommand(NewApplicationDiffCommand(clientOpts))
command.AddCommand(NewApplicationSetCommand(clientOpts))
command.AddCommand(NewApplicationUnsetCommand(clientOpts))
command.AddCommand(NewApplicationSyncCommand(clientOpts))
command.AddCommand(NewApplicationHistoryCommand(clientOpts))
command.AddCommand(NewApplicationRollbackCommand(clientOpts))
command.AddCommand(NewApplicationListCommand(clientOpts))
command.AddCommand(NewApplicationDeleteCommand(clientOpts))
command.AddCommand(NewApplicationWaitCommand(clientOpts))
command.AddCommand(NewApplicationManifestsCommand(clientOpts))
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))
command.AddCommand(NewApplicationListResourcesCommand(clientOpts))
command.AddCommand(NewApplicationLogsCommand(clientOpts))
command.AddCommand(NewApplicationAddSourceCommand(clientOpts))
command.AddCommand(NewApplicationRemoveSourceCommand(clientOpts))
command.AddCommand(NewApplicationConfirmDeletionCommand(clientOpts))
return command
}
type watchOpts struct {
sync bool
health bool
operation bool
suspended bool
degraded bool
delete bool
hydrated bool
}
// NewApplicationCreateCommand returns a new instance of an `argocd app create` command
func NewApplicationCreateCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
appOpts cmdutil.AppOptions
fileURL string
appName string
upsert bool
labels []string
annotations []string
setFinalizer bool
appNamespace string
)
command := &cobra.Command{
Use: "create APPNAME",
Short: "Create an application",
Example: ` # Create a directory app
argocd app create guestbook --repo https://github.com/argoproj/argocd-example-apps.git --path guestbook --dest-namespace default --dest-server https://kubernetes.default.svc --directory-recurse
# Create a Jsonnet app
argocd app create jsonnet-guestbook --repo https://github.com/argoproj/argocd-example-apps.git --path jsonnet-guestbook --dest-namespace default --dest-server https://kubernetes.default.svc --jsonnet-ext-str replicas=2
# Create a Helm app
argocd app create helm-guestbook --repo https://github.com/argoproj/argocd-example-apps.git --path helm-guestbook --dest-namespace default --dest-server https://kubernetes.default.svc --helm-set replicaCount=2
# Create a Helm app from a Helm repo
argocd app create nginx-ingress --repo https://charts.helm.sh/stable --helm-chart nginx-ingress --revision 1.24.3 --dest-namespace default --dest-server https://kubernetes.default.svc
# 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 quay.io/argoprojlabs/argocd-e2e-container:0.1
# Create a MultiSource app while yaml file contains an application with multiple sources
argocd app create guestbook --file <path-to-yaml-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)
for _, app := range apps {
if app.Name == "" {
c.HelpFunc()(c, args)
os.Exit(1)
}
if appNamespace != "" {
app.Namespace = appNamespace
}
if setFinalizer {
app.Finalizers = append(app.Finalizers, argoappv1.ResourcesFinalizerName)
}
conn, appIf := argocdClient.NewApplicationClientOrDie()
defer utilio.Close(conn)
appCreateRequest := application.ApplicationCreateRequest{
Application: app,
Upsert: &upsert,
Validate: &appOpts.Validate,
}
// Get app before creating to see if it is being updated or no change
existing, err := appIf.Get(ctx, &application.ApplicationQuery{Name: &app.Name})
unwrappedError := grpc.UnwrapGRPCStatus(err).Code()
// As part of the fix for CVE-2022-41354, the API will return Permission Denied when an app does not exist.
if unwrappedError != codes.NotFound && unwrappedError != codes.PermissionDenied {
errors.CheckError(err)
}
created, err := appIf.Create(ctx, &appCreateRequest)
errors.CheckError(err)
var action string
switch {
case existing == nil:
action = "created"
case !hasAppChanged(existing, created, upsert):
action = "unchanged"
default:
action = "updated"
}
fmt.Printf("application '%s' %s\n", created.Name, action)
}
},
}
command.Flags().StringVar(&appName, "name", "", "A name for the app, ignored if a file is set (DEPRECATED)")
command.Flags().BoolVar(&upsert, "upsert", false, "Allows to override application with the same name even if supplied application spec is different from existing spec")
command.Flags().StringVarP(&fileURL, "file", "f", "", "Filename or URL to Kubernetes manifests for the app")
command.Flags().StringArrayVarP(&labels, "label", "l", []string{}, "Labels to apply to the app")
command.Flags().StringArrayVarP(&annotations, "annotations", "", []string{}, "Set metadata annotations (e.g. example=value)")
command.Flags().BoolVar(&setFinalizer, "set-finalizer", false, "Sets deletion finalizer on the application, application resources will be cascaded on deletion")
// Only complete files with appropriate extension.
err := command.Flags().SetAnnotation("file", cobra.BashCompFilenameExt, []string{"json", "yaml", "yml"})
if err != nil {
log.Fatal(err)
}
command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Namespace where the application will be created in")
cmdutil.AddAppFlags(command, &appOpts)
return command
}
// getInfos converts a list of string key=value pairs to a list of Info objects.
func getInfos(infos []string) []*argoappv1.Info {
mapInfos, err := label.Parse(infos)
errors.CheckError(err)
sliceInfos := make([]*argoappv1.Info, len(mapInfos))
i := 0
for key, element := range mapInfos {
sliceInfos[i] = &argoappv1.Info{Name: key, Value: element}
i++
}
return sliceInfos
}
func getRefreshType(refresh bool, hardRefresh bool) *string {
if hardRefresh {
refreshType := string(argoappv1.RefreshTypeHard)
return &refreshType
}
if refresh {
refreshType := string(argoappv1.RefreshTypeNormal)
return &refreshType
}
return nil
}
func hasAppChanged(appReq, appRes *argoappv1.Application, upsert bool) bool {
// upsert==false, no change occurred from create command
if !upsert {
return false
}
// If no project, assume default project
if appReq.Spec.Project == "" {
appReq.Spec.Project = "default"
}
// Server will return nils for empty labels, annotations, finalizers
if len(appReq.Labels) == 0 {
appReq.Labels = nil
}
if len(appReq.Annotations) == 0 {
appReq.Annotations = nil
}
if len(appReq.Finalizers) == 0 {
appReq.Finalizers = nil
}
if reflect.DeepEqual(appRes.Spec, appReq.Spec) &&
reflect.DeepEqual(appRes.Labels, appReq.Labels) &&
reflect.DeepEqual(appRes.Annotations, appReq.Annotations) &&
reflect.DeepEqual(appRes.Finalizers, appReq.Finalizers) {
return false
}
return true
}
func parentChildDetails(ctx context.Context, appIf application.ApplicationServiceClient, appName string, appNs string) (map[string]argoappv1.ResourceNode, map[string][]string, map[string]struct{}) {
mapUIDToNode := make(map[string]argoappv1.ResourceNode)
mapParentToChild := make(map[string][]string)
parentNode := make(map[string]struct{})
resourceTree, err := appIf.ResourceTree(ctx, &application.ResourcesQuery{Name: &appName, AppNamespace: &appNs, ApplicationName: &appName})
errors.CheckError(err)
for _, node := range resourceTree.Nodes {
mapUIDToNode[node.UID] = node
if len(node.ParentRefs) > 0 {
_, ok := mapParentToChild[node.ParentRefs[0].UID]
if !ok {
var temp []string
mapParentToChild[node.ParentRefs[0].UID] = temp
}
mapParentToChild[node.ParentRefs[0].UID] = append(mapParentToChild[node.ParentRefs[0].UID], node.UID)
} else {
parentNode[node.UID] = struct{}{}
}
}
return mapUIDToNode, mapParentToChild, parentNode
}
func printHeader(ctx context.Context, acdClient argocdclient.Client, app *argoappv1.Application, windows *argoappv1.SyncWindows, showOperation bool, showParams bool, sourcePosition int) {
appURL := getAppURL(ctx, acdClient, app.Name)
printAppSummaryTable(app, appURL, windows)
if len(app.Status.Conditions) > 0 {
fmt.Println()
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
printAppConditions(w, app)
_ = w.Flush()
fmt.Println()
}
if showOperation && app.Status.OperationState != nil {
fmt.Println()
printOperationResult(app.Status.OperationState)
}
if showParams {
printParams(app, sourcePosition)
}
}
// getSourceNameToPositionMap returns a map of source name to position
func getSourceNameToPositionMap(app *argoappv1.Application) map[string]int64 {
sourceNameToPosition := make(map[string]int64)
for i, s := range app.Spec.Sources {
if s.Name != "" {
sourceNameToPosition[s.Name] = int64(i + 1)
}
}
return sourceNameToPosition
}
// NewApplicationGetCommand returns a new instance of an `argocd app get` command
func NewApplicationGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
refresh bool
hardRefresh bool
output string
timeout uint
showParams bool
showOperation bool
appNamespace string
sourcePosition int
sourceName string
)
command := &cobra.Command{
Use: "get APPNAME",
Short: "Get application details",
Example: templates.Examples(`
# Get basic details about the application "my-app" in wide format
argocd app get my-app -o wide
# Get detailed information about the application "my-app" in YAML format
argocd app get my-app -o yaml
# Get details of the application "my-app" in JSON format
argocd get my-app -o json
# Get application details and include information about the current operation
argocd app get my-app --show-operation
# Show application parameters and overrides
argocd app get my-app --show-params
# Show application parameters and overrides for a source at position 1 under spec.sources of app my-app
argocd app get my-app --show-params --source-position 1
# Show application parameters and overrides for a source named "test"
argocd app get my-app --show-params --source-name test
# Refresh application data when retrieving
argocd app get my-app --refresh
# Perform a hard refresh, including refreshing application data and target manifests cache
argocd app get my-app --hard-refresh
# Get application details and display them in a tree format
argocd app get my-app --output tree
# Get application details and display them in a detailed tree format
argocd app get my-app --output tree=detailed
`),
Run: func(c *cobra.Command, args []string) {
ctx, cancel := context.WithCancel(c.Context())
defer cancel()
if len(args) == 0 {
c.HelpFunc()(c, args)
os.Exit(1)
}
acdClient := headless.NewClientOrDie(clientOpts, c)
conn, appIf := acdClient.NewApplicationClientOrDie()
defer utilio.Close(conn)
appName, appNs := argo.ParseFromQualifiedName(args[0], appNamespace)
if timeout != 0 {
time.AfterFunc(time.Duration(timeout)*time.Second, func() {
if ctx.Err() != nil {
fmt.Println("Timeout function: context already cancelled:", ctx.Err())
} else {
fmt.Println("Timeout function: cancelling context manually")
cancel()
}
})
}
getAppStateWithRetry := func() (*argoappv1.Application, error) {
type getResponse struct {
app *argoappv1.Application
err error
}
ch := make(chan getResponse, 1)
go func() {
app, err := appIf.Get(ctx, &application.ApplicationQuery{
Name: &appName,
Refresh: getRefreshType(refresh, hardRefresh),
AppNamespace: &appNs,
})
ch <- getResponse{app: app, err: err}
}()
select {
case result := <-ch:
return result.app, result.err
case <-ctx.Done():
// Timeout occurred, try again without refresh flag
// Create new context for retry request
ctx := context.Background()
app, err := appIf.Get(ctx, &application.ApplicationQuery{
Name: &appName,
AppNamespace: &appNs,
})
return app, err
}
}
app, err := getAppStateWithRetry()
errors.CheckError(err)
if ctx.Err() != nil {
ctx = context.Background() // Reset context for subsequent requests
}
if sourceName != "" && sourcePosition != -1 {
errors.Fatal(errors.ErrorGeneric, "Only one of source-position and source-name can be specified.")
}
if sourceName != "" {
sourceNameToPosition := getSourceNameToPositionMap(app)
pos, ok := sourceNameToPosition[sourceName]
if !ok {
log.Fatalf("Unknown source name '%s'", sourceName)
}
sourcePosition = int(pos)
}
// check for source position if --show-params is set
if app.Spec.HasMultipleSources() && showParams {
if sourcePosition <= 0 {
errors.Fatal(errors.ErrorGeneric, "Source position should be specified and must be greater than 0 for applications with multiple sources")
}
if len(app.Spec.GetSources()) < sourcePosition {
errors.Fatal(errors.ErrorGeneric, "Source position should be less than the number of sources in the application")
}
}
pConn, projIf := headless.NewClientOrDie(clientOpts, c).NewProjectClientOrDie()
defer utilio.Close(pConn)
proj, err := projIf.Get(ctx, &projectpkg.ProjectQuery{Name: app.Spec.Project})
errors.CheckError(err)
windows := proj.Spec.SyncWindows.Matches(app)
switch output {
case "yaml", "json":
err := PrintResource(app, output)
errors.CheckError(err)
case "wide", "":
printHeader(ctx, acdClient, app, windows, showOperation, showParams, sourcePosition)
if len(app.Status.Resources) > 0 {
fmt.Println()
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
printAppResources(w, app)
_ = w.Flush()
}
case "tree":
printHeader(ctx, acdClient, app, windows, showOperation, showParams, sourcePosition)
mapUIDToNode, mapParentToChild, parentNode, mapNodeNameToResourceState := resourceParentChild(ctx, acdClient, appName, appNs)
if len(mapUIDToNode) > 0 {
fmt.Println()
printTreeView(mapUIDToNode, mapParentToChild, parentNode, mapNodeNameToResourceState)
}
case "tree=detailed":
printHeader(ctx, acdClient, app, windows, showOperation, showParams, sourcePosition)
mapUIDToNode, mapParentToChild, parentNode, mapNodeNameToResourceState := resourceParentChild(ctx, acdClient, appName, appNs)
if len(mapUIDToNode) > 0 {
fmt.Println()
printTreeViewDetailed(mapUIDToNode, mapParentToChild, parentNode, mapNodeNameToResourceState)
}
default:
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
}
},
}
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|tree")
command.Flags().UintVar(&timeout, "timeout", defaultCheckTimeoutSeconds, "Time out after this many seconds")
command.Flags().BoolVar(&showOperation, "show-operation", false, "Show application operation")
command.Flags().BoolVar(&showParams, "show-params", false, "Show application parameters and overrides")
command.Flags().BoolVar(&refresh, "refresh", false, "Refresh application data when retrieving")
command.Flags().BoolVar(&hardRefresh, "hard-refresh", false, "Refresh application data as well as target manifests cache")
command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Only get application from namespace")
command.Flags().IntVar(&sourcePosition, "source-position", -1, "Position of the source from the list of sources of the app. Counting starts at 1.")
command.Flags().StringVar(&sourceName, "source-name", "", "Name of the source from the list of sources of the app.")
return command
}
// NewApplicationLogsCommand returns logs of application pods
func NewApplicationLogsCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
group string
kind string
namespace string
resourceName string
follow bool
tail int64
sinceSeconds int64
untilTime string
filter string
container string
previous bool
matchCase bool
)
command := &cobra.Command{
Use: "logs APPNAME",
Short: "Get logs of application pods",
Example: templates.Examples(`
# Get logs of pods associated with the application "my-app"
argocd app logs my-app
# Get logs of pods associated with the application "my-app" in a specific resource group
argocd app logs my-app --group my-group
# Get logs of pods associated with the application "my-app" in a specific resource kind
argocd app logs my-app --kind my-kind
# Get logs of pods associated with the application "my-app" in a specific namespace
argocd app logs my-app --namespace my-namespace
# Get logs of pods associated with the application "my-app" for a specific resource name
argocd app logs my-app --name my-resource
# Stream logs in real-time for the application "my-app"
argocd app logs my-app -f
# Get the last N lines of logs for the application "my-app"
argocd app logs my-app --tail 100
# Get logs since a specified number of seconds ago
argocd app logs my-app --since-seconds 3600
# Get logs until a specified time (format: "2023-10-10T15:30:00Z")
argocd app logs my-app --until-time "2023-10-10T15:30:00Z"
# Filter logs to show only those containing a specific string
argocd app logs my-app --filter "error"
# Filter logs to show only those containing a specific string and match case
argocd app logs my-app --filter "error" --match-case
# Get logs for a specific container within the pods
argocd app logs my-app -c my-container
# Get previously terminated container logs
argocd app logs my-app -p
`),
Run: func(c *cobra.Command, args []string) {
ctx := c.Context()
if len(args) == 0 {
c.HelpFunc()(c, args)
os.Exit(1)
}
acdClient := headless.NewClientOrDie(clientOpts, c)
conn, appIf := acdClient.NewApplicationClientOrDie()
defer utilio.Close(conn)
appName, appNs := argo.ParseFromQualifiedName(args[0], "")
retry := true
for retry {
retry = false
stream, err := appIf.PodLogs(ctx, &application.ApplicationPodLogsQuery{
Name: &appName,
Group: &group,
Namespace: ptr.To(namespace),
Kind: &kind,
ResourceName: &resourceName,
Follow: ptr.To(follow),
TailLines: ptr.To(tail),
SinceSeconds: ptr.To(sinceSeconds),
UntilTime: &untilTime,
Filter: &filter,
MatchCase: ptr.To(matchCase),
Container: ptr.To(container),
Previous: ptr.To(previous),
AppNamespace: &appNs,
})
if err != nil {
log.Fatalf("failed to get pod logs: %v", err)
}
for {
msg, err := stream.Recv()
if err != nil {
if stderrors.Is(err, io.EOF) {
return
}
st, ok := status.FromError(err)
if !ok {
log.Fatalf("stream read failed: %v", err)
}
if st.Code() == codes.Unavailable && follow {
retry = true
sinceSeconds = 1
break
}
log.Fatalf("stream read failed: %v", err)
}
if msg.GetLast() {
return
}
fmt.Println(msg.GetContent())
} // Done with receive message
} // Done with retry
},
}
command.Flags().StringVar(&group, "group", "", "Resource group")
command.Flags().StringVar(&kind, "kind", "", "Resource kind")
command.Flags().StringVar(&namespace, "namespace", "", "Resource namespace")
command.Flags().StringVar(&resourceName, "name", "", "Resource name")
command.Flags().BoolVarP(&follow, "follow", "f", false, "Specify if the logs should be streamed")
command.Flags().Int64Var(&tail, "tail", 0, "The number of lines from the end of the logs to show")
command.Flags().Int64Var(&sinceSeconds, "since-seconds", 0, "A relative time in seconds before the current time from which to show logs")
command.Flags().StringVar(&untilTime, "until-time", "", "Show logs until this time")
command.Flags().StringVar(&filter, "filter", "", "Show logs contain this string")
command.Flags().StringVarP(&container, "container", "c", "", "Optional container name")
command.Flags().BoolVarP(&previous, "previous", "p", false, "Specify if the previously terminated container logs should be returned")
command.Flags().BoolVarP(&matchCase, "match-case", "m", false, "Specify if the filter should be case-sensitive")
return command
}
func printAppSummaryTable(app *argoappv1.Application, appURL string, windows *argoappv1.SyncWindows) {
fmt.Printf(printOpFmtStr, "Name:", app.QualifiedName())
fmt.Printf(printOpFmtStr, "Project:", app.Spec.GetProject())
fmt.Printf(printOpFmtStr, "Server:", getServer(app))
fmt.Printf(printOpFmtStr, "Namespace:", app.Spec.Destination.Namespace)
fmt.Printf(printOpFmtStr, "URL:", appURL)
if !app.Spec.HasMultipleSources() {
fmt.Println("Source:")
} else {
fmt.Println("Sources:")
}
for _, source := range app.Spec.GetSources() {
printAppSourceDetails(&source)
}
var wds []string
var status string
var allow, deny, inactiveAllows bool
if windows.HasWindows() {
active, err := windows.Active()
if err == nil && active.HasWindows() {
for _, w := range *active {
if w.Kind == "deny" {
deny = true
} else {
allow = true
}
}
}
inactiveAllowWindows, err := windows.InactiveAllows()
if err == nil && inactiveAllowWindows.HasWindows() {
inactiveAllows = true
}
if deny || !deny && !allow && inactiveAllows {
s, err := windows.CanSync(true)
if err == nil && s {
status = "Manual Allowed"
} else {
status = "Sync Denied"
}
} else {
status = "Sync Allowed"
}
for _, w := range *windows {
s := w.Kind + ":" + w.Schedule + ":" + w.Duration
wds = append(wds, s)
}
} else {
status = "Sync Allowed"
}
fmt.Printf(printOpFmtStr, "SyncWindow:", status)
if len(wds) > 0 {
fmt.Printf(printOpFmtStr, "Assigned Windows:", strings.Join(wds, ","))
}
var syncPolicy string
if app.Spec.SyncPolicy != nil && app.Spec.SyncPolicy.IsAutomatedSyncEnabled() {
syncPolicy = "Automated"
if app.Spec.SyncPolicy.Automated.Prune {
syncPolicy += " (Prune)"
}
} else {
syncPolicy = "Manual"
}
fmt.Printf(printOpFmtStr, "Sync Policy:", syncPolicy)
syncStatusStr := string(app.Status.Sync.Status)
switch app.Status.Sync.Status {
case argoappv1.SyncStatusCodeSynced:
syncStatusStr += " to " + app.Spec.GetSource().TargetRevision
case argoappv1.SyncStatusCodeOutOfSync:
syncStatusStr += " from " + app.Spec.GetSource().TargetRevision
}
if !git.IsCommitSHA(app.Spec.GetSource().TargetRevision) && !git.IsTruncatedCommitSHA(app.Spec.GetSource().TargetRevision) && len(app.Status.Sync.Revision) > 7 {
syncStatusStr += fmt.Sprintf(" (%s)", app.Status.Sync.Revision[0:7])
}
fmt.Printf(printOpFmtStr, "Sync Status:", syncStatusStr)
healthStr := string(app.Status.Health.Status)
fmt.Printf(printOpFmtStr, "Health Status:", healthStr)
}
func printAppSourceDetails(appSrc *argoappv1.ApplicationSource) {
fmt.Printf(printOpFmtStr, "- Repo:", appSrc.RepoURL)
fmt.Printf(printOpFmtStr, " Target:", appSrc.TargetRevision)
if appSrc.Path != "" {
fmt.Printf(printOpFmtStr, " Path:", appSrc.Path)
}
if appSrc.IsRef() {
fmt.Printf(printOpFmtStr, " Ref:", appSrc.Ref)
}
if appSrc.Helm != nil && len(appSrc.Helm.ValueFiles) > 0 {
fmt.Printf(printOpFmtStr, " Helm Values:", strings.Join(appSrc.Helm.ValueFiles, ","))
}
if appSrc.Kustomize != nil && appSrc.Kustomize.NamePrefix != "" {
fmt.Printf(printOpFmtStr, " Name Prefix:", appSrc.Kustomize.NamePrefix)
}
}
func printAppConditions(w io.Writer, app *argoappv1.Application) {
_, _ = fmt.Fprintf(w, "CONDITION\tMESSAGE\tLAST TRANSITION\n")
for _, item := range app.Status.Conditions {
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\n", item.Type, item.Message, item.LastTransitionTime)
}
}
// appURLDefault returns the default URL of an application
func appURLDefault(acdClient argocdclient.Client, appName string) string {
var scheme string
opts := acdClient.ClientOptions()
server := opts.ServerAddr
if opts.PlainText {
scheme = "http"
} else {
scheme = "https"
if strings.HasSuffix(opts.ServerAddr, ":443") {
server = server[0 : len(server)-4]
}
}
return fmt.Sprintf("%s://%s/applications/%s", scheme, server, appName)
}
// getAppURL returns the URL of an application
func getAppURL(ctx context.Context, acdClient argocdclient.Client, appName string) string {
conn, settingsIf := acdClient.NewSettingsClientOrDie()
defer utilio.Close(conn)
argoSettings, err := settingsIf.Get(ctx, &settings.SettingsQuery{})
errors.CheckError(err)
if argoSettings.URL != "" {
return fmt.Sprintf("%s/applications/%s", argoSettings.URL, appName)
}
return appURLDefault(acdClient, appName)
}
func truncateString(str string, num int) string {
bnoden := str
if utf8.RuneCountInString(str) > num {
if num > 3 {
num -= 3
}
bnoden = string([]rune(str)[0:num]) + "..."
}
return bnoden
}
// printParams prints parameters and overrides
func printParams(app *argoappv1.Application, sourcePosition int) {
var source *argoappv1.ApplicationSource
if app.Spec.HasMultipleSources() {
// Get the source by the sourcePosition whose params you'd like to print
source = app.Spec.GetSourcePtrByPosition(sourcePosition)
if source == nil {
source = &argoappv1.ApplicationSource{}
}
} else {
src := app.Spec.GetSource()
source = &src
}
if source.Helm != nil {
printHelmParams(source.Helm)
}
}
func printHelmParams(helm *argoappv1.ApplicationSourceHelm) {
paramLenLimit := 80
fmt.Println()
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
if helm != nil {
fmt.Println()
_, _ = fmt.Fprintf(w, "NAME\tVALUE\n")
for _, p := range helm.Parameters {
_, _ = fmt.Fprintf(w, "%s\t%s\n", p.Name, truncateString(p.Value, paramLenLimit))
}
}
_ = w.Flush()
}
func getServer(app *argoappv1.Application) string {
if app.Spec.Destination.Server == "" {
return app.Spec.Destination.Name
}
return app.Spec.Destination.Server
}
// NewApplicationSetCommand returns a new instance of an `argocd app set` command
func NewApplicationSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
appOpts cmdutil.AppOptions
appNamespace string
sourcePosition int
sourceName string
)
command := &cobra.Command{
Use: "set APPNAME",
Short: "Set application parameters",
Example: templates.Examples(`
# Set application parameters for the application "my-app"
argocd app set my-app --parameter key1=value1 --parameter key2=value2
# Set and validate application parameters for "my-app"
argocd app set my-app --parameter key1=value1 --parameter key2=value2 --validate
# Set and override application parameters for a source at position 1 under spec.sources of app my-app. source-position starts at 1.
argocd app set my-app --source-position 1 --repo https://github.com/argoproj/argocd-example-apps.git
# Set and override application parameters for a source named "test" under spec.sources of app my-app.
argocd app set my-app --source-name test --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
`),
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], appNamespace)
argocdClient := headless.NewClientOrDie(clientOpts, c)
conn, appIf := argocdClient.NewApplicationClientOrDie()
defer utilio.Close(conn)
app, err := appIf.Get(ctx, &application.ApplicationQuery{Name: &appName, AppNamespace: &appNs})
errors.CheckError(err)
sourceName = appOpts.SourceName
if sourceName != "" && sourcePosition != -1 {
errors.Fatal(errors.ErrorGeneric, "Only one of source-position and source-name can be specified.")
}
if sourceName != "" {
sourceNameToPosition := getSourceNameToPositionMap(app)
pos, ok := sourceNameToPosition[sourceName]
if !ok {
log.Fatalf("Unknown source name '%s'", sourceName)
}
sourcePosition = int(pos)
}
if app.Spec.HasMultipleSources() {
if sourcePosition <= 0 {
errors.Fatal(errors.ErrorGeneric, "Source position should be specified and must be greater than 0 for applications with multiple sources")
}
if len(app.Spec.GetSources()) < sourcePosition {
errors.Fatal(errors.ErrorGeneric, "Source position should be less than the number of sources in the application")
}
}
visited := cmdutil.SetAppSpecOptions(c.Flags(), &app.Spec, &appOpts, sourcePosition)
if visited == 0 {
log.Error("Please set at least one option to update")
c.HelpFunc()(c, args)
os.Exit(1)
}
setParameterOverrides(app, appOpts.Parameters, sourcePosition)
_, err = appIf.UpdateSpec(ctx, &application.ApplicationUpdateSpecRequest{
Name: &app.Name,
Spec: &app.Spec,
Validate: &appOpts.Validate,
AppNamespace: &appNs,
})
errors.CheckError(err)
},
}
cmdutil.AddAppFlags(command, &appOpts)
command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Set application parameters in namespace")
command.Flags().IntVar(&sourcePosition, "source-position", -1, "Position of the source from the list of sources of the app. Counting starts at 1.")
return command
}
// unsetOpts describe what to unset in an Application.
type unsetOpts struct {
namePrefix bool
nameSuffix bool
kustomizeVersion bool
kustomizeNamespace bool
kustomizeImages []string
kustomizeReplicas []string
ignoreMissingComponents bool
parameters []string
valuesFiles []string
valuesLiteral bool
ignoreMissingValueFiles bool
pluginEnvs []string
passCredentials bool
ref bool
}
// IsZero returns true when the Application options for kustomize are considered empty
func (o *unsetOpts) KustomizeIsZero() bool {
return o == nil ||
!o.namePrefix &&
!o.nameSuffix &&
!o.kustomizeVersion &&
!o.kustomizeNamespace &&
!o.ignoreMissingComponents &&
len(o.kustomizeImages) == 0 &&
len(o.kustomizeReplicas) == 0
}
// NewApplicationUnsetCommand returns a new instance of an `argocd app unset` command
func NewApplicationUnsetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var sourcePosition int
var sourceName string
appOpts := cmdutil.AppOptions{}
opts := unsetOpts{}
var appNamespace string
command := &cobra.Command{
Use: "unset APPNAME parameters",
Short: "Unset application parameters",
Example: ` # Unset kustomize override kustomize image
argocd app unset my-app --kustomize-image=alpine
# Unset kustomize override suffix
argocd app unset my-app --namesuffix
# Unset kustomize override suffix for source at position 1 under spec.sources of app my-app. source-position starts at 1.
argocd app unset my-app --source-position 1 --namesuffix
# Unset kustomize override suffix for source named "test" under spec.sources of app my-app.
argocd app unset my-app --source-name test --namesuffix
# Unset parameter override
argocd app unset my-app -p COMPONENT=PARAM`,
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], appNamespace)
conn, appIf := headless.NewClientOrDie(clientOpts, c).NewApplicationClientOrDie()
defer utilio.Close(conn)
app, err := appIf.Get(ctx, &application.ApplicationQuery{Name: &appName, AppNamespace: &appNs})
errors.CheckError(err)
sourceName = appOpts.SourceName
if sourceName != "" && sourcePosition != -1 {
errors.Fatal(errors.ErrorGeneric, "Only one of source-position and source-name can be specified.")
}
if sourceName != "" {
sourceNameToPosition := getSourceNameToPositionMap(app)
pos, ok := sourceNameToPosition[sourceName]
if !ok {
log.Fatalf("Unknown source name '%s'", sourceName)
}
sourcePosition = int(pos)
}
if app.Spec.HasMultipleSources() {
if sourcePosition <= 0 {
errors.Fatal(errors.ErrorGeneric, "Source position should be specified and must be greater than 0 for applications with multiple sources")
}
if len(app.Spec.GetSources()) < sourcePosition {
errors.Fatal(errors.ErrorGeneric, "Source position should be less than the number of sources in the application")
}
}
source := app.Spec.GetSourcePtrByPosition(sourcePosition)
updated, nothingToUnset := unset(source, opts)
if nothingToUnset {
c.HelpFunc()(c, args)
os.Exit(1)
}
if !updated {
return
}
cmdutil.SetAppSpecOptions(c.Flags(), &app.Spec, &appOpts, sourcePosition)
promptUtil := utils.NewPrompt(clientOpts.PromptsEnabled)
canUnset := promptUtil.Confirm("Are you sure you want to unset the parameters? [y/n]")
if canUnset {
_, err = appIf.UpdateSpec(ctx, &application.ApplicationUpdateSpecRequest{
Name: &app.Name,
Spec: &app.Spec,
Validate: &appOpts.Validate,
AppNamespace: &appNs,
})
errors.CheckError(err)
} else {
fmt.Println("The command to unset the parameters has been cancelled.")
}
},
}
command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Unset application parameters in namespace")
command.Flags().StringArrayVarP(&opts.parameters, "parameter", "p", []string{}, "Unset a parameter override (e.g. -p guestbook=image)")
command.Flags().StringArrayVar(&opts.valuesFiles, "values", []string{}, "Unset one or more Helm values files")
command.Flags().BoolVar(&opts.valuesLiteral, "values-literal", false, "Unset literal Helm values block")
command.Flags().BoolVar(&opts.ignoreMissingValueFiles, "ignore-missing-value-files", false, "Unset the helm ignore-missing-value-files option (revert to false)")
command.Flags().BoolVar(&opts.nameSuffix, "namesuffix", false, "Kustomize namesuffix")
command.Flags().BoolVar(&opts.namePrefix, "nameprefix", false, "Kustomize nameprefix")
command.Flags().BoolVar(&opts.kustomizeVersion, "kustomize-version", false, "Kustomize version")
command.Flags().BoolVar(&opts.kustomizeNamespace, "kustomize-namespace", false, "Kustomize namespace")
command.Flags().StringArrayVar(&opts.kustomizeImages, "kustomize-image", []string{}, "Kustomize images name (e.g. --kustomize-image node --kustomize-image mysql)")
command.Flags().StringArrayVar(&opts.kustomizeReplicas, "kustomize-replica", []string{}, "Kustomize replicas name (e.g. --kustomize-replica my-deployment --kustomize-replica my-statefulset)")
command.Flags().BoolVar(&opts.ignoreMissingComponents, "ignore-missing-components", false, "Unset the kustomize ignore-missing-components option (revert to false)")
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(&sourcePosition, "source-position", -1, "Position of the source from the list of sources of the app. Counting starts at 1.")
return command
}
func unset(source *argoappv1.ApplicationSource, opts unsetOpts) (updated bool, nothingToUnset bool) {
needToUnsetRef := false
if opts.ref && source.IsRef() {
source.Ref = ""
updated = true
needToUnsetRef = true
}
if source.Kustomize != nil {
if opts.KustomizeIsZero() {
return updated, !needToUnsetRef
}
if opts.namePrefix && source.Kustomize.NamePrefix != "" {
updated = true
source.Kustomize.NamePrefix = ""
}
if opts.nameSuffix && source.Kustomize.NameSuffix != "" {
updated = true
source.Kustomize.NameSuffix = ""
}
if opts.kustomizeVersion && source.Kustomize.Version != "" {
updated = true
source.Kustomize.Version = ""
}
if opts.kustomizeNamespace && source.Kustomize.Namespace != "" {
updated = true
source.Kustomize.Namespace = ""
}
if opts.ignoreMissingComponents && source.Kustomize.IgnoreMissingComponents {
source.Kustomize.IgnoreMissingComponents = false
updated = true
}
for _, kustomizeImage := range opts.kustomizeImages {
for i, item := range source.Kustomize.Images {
if !argoappv1.KustomizeImage(kustomizeImage).Match(item) {
continue
}
updated = true
// remove i
a := source.Kustomize.Images
copy(a[i:], a[i+1:]) // Shift a[i+1:] left one index.
a[len(a)-1] = "" // Erase last element (write zero value).
a = a[:len(a)-1] // Truncate slice.
source.Kustomize.Images = a
}
}
for _, kustomizeReplica := range opts.kustomizeReplicas {
kustomizeReplicas := source.Kustomize.Replicas
for i, item := range kustomizeReplicas {
if kustomizeReplica == item.Name {
source.Kustomize.Replicas = append(kustomizeReplicas[0:i], kustomizeReplicas[i+1:]...)
updated = true
break
}
}
}
}
if source.Helm != nil {
if len(opts.parameters) == 0 && len(opts.valuesFiles) == 0 && !opts.valuesLiteral && !opts.ignoreMissingValueFiles && !opts.passCredentials {
return updated, !needToUnsetRef
}
for _, paramStr := range opts.parameters {
helmParams := source.Helm.Parameters
for i, p := range helmParams {
if p.Name == paramStr {
source.Helm.Parameters = append(helmParams[0:i], helmParams[i+1:]...)
updated = true
break
}
}
}
if opts.valuesLiteral && !source.Helm.ValuesIsEmpty() {
err := source.Helm.SetValuesString("")
if err == nil {
updated = true
}
}
for _, valuesFile := range opts.valuesFiles {
specValueFiles := source.Helm.ValueFiles
for i, vf := range specValueFiles {
if vf == valuesFile {
source.Helm.ValueFiles = append(specValueFiles[0:i], specValueFiles[i+1:]...)
updated = true
break
}
}
}
if opts.ignoreMissingValueFiles && source.Helm.IgnoreMissingValueFiles {
source.Helm.IgnoreMissingValueFiles = false
updated = true
}
if opts.passCredentials && source.Helm.PassCredentials {
source.Helm.PassCredentials = false
updated = true
}
}
if source.Plugin != nil {
if len(opts.pluginEnvs) == 0 {
return false, !needToUnsetRef
}
for _, env := range opts.pluginEnvs {
err := source.Plugin.RemoveEnvEntry(env)
if err == nil {
updated = true
}
}
}
return updated, false
}
// targetObjects deserializes the list of target states into unstructured objects
func targetObjects(resources []*argoappv1.ResourceDiff) ([]*unstructured.Unstructured, error) {
objs := make([]*unstructured.Unstructured, len(resources))
for i, resState := range resources {
obj, err := resState.TargetObject()
if err != nil {
return nil, err
}
objs[i] = obj
}
return objs, nil
}
func getLocalObjects(ctx context.Context, app *argoappv1.Application, proj *argoappv1.AppProject, local, localRepoRoot, appLabelKey, kubeVersion string, apiVersions []string, kustomizeOptions *argoappv1.KustomizeOptions,
trackingMethod string,
) []*unstructured.Unstructured {
manifestStrings := getLocalObjectsString(ctx, app, proj, local, localRepoRoot, appLabelKey, kubeVersion, apiVersions, kustomizeOptions, trackingMethod)
objs := make([]*unstructured.Unstructured, len(manifestStrings))
for i := range manifestStrings {
obj := unstructured.Unstructured{}
err := json.Unmarshal([]byte(manifestStrings[i]), &obj)
errors.CheckError(err)
objs[i] = &obj
}
return objs
}
func getLocalObjectsString(ctx context.Context, app *argoappv1.Application, proj *argoappv1.AppProject, local, localRepoRoot, appLabelKey, kubeVersion string, apiVersions []string, kustomizeOptions *argoappv1.KustomizeOptions,
trackingMethod string,
) []string {
source := app.Spec.GetSource()
res, err := repository.GenerateManifests(ctx, local, localRepoRoot, source.TargetRevision, &repoapiclient.ManifestRequest{
Repo: &argoappv1.Repository{Repo: source.RepoURL},
AppLabelKey: appLabelKey,
AppName: app.Name,
Namespace: app.Spec.Destination.Namespace,
ApplicationSource: &source,
KustomizeOptions: kustomizeOptions,
KubeVersion: kubeVersion,
ApiVersions: apiVersions,
TrackingMethod: trackingMethod,
ProjectName: proj.Name,
ProjectSourceRepos: proj.Spec.SourceRepos,
AnnotationManifestGeneratePaths: app.GetAnnotation(argoappv1.AnnotationKeyManifestGeneratePaths),
}, true, &git.NoopCredsStore{}, resource.MustParse("0"), nil)
errors.CheckError(err)
return res.Manifests
}
type resourceInfoProvider struct {
namespacedByGk map[schema.GroupKind]bool
}
// Infer if obj is namespaced or not from corresponding live objects list. If corresponding live object has namespace then target object is also namespaced.
// If live object is missing then it does not matter if target is namespaced or not.
func (p *resourceInfoProvider) IsNamespaced(gk schema.GroupKind) (bool, error) {
return p.namespacedByGk[gk], nil
}
func groupObjsByKey(localObs []*unstructured.Unstructured, liveObjs []*unstructured.Unstructured, appNamespace string) map[kube.ResourceKey]*unstructured.Unstructured {
namespacedByGk := make(map[schema.GroupKind]bool)
for i := range liveObjs {
if liveObjs[i] != nil {
key := kube.GetResourceKey(liveObjs[i])
namespacedByGk[schema.GroupKind{Group: key.Group, Kind: key.Kind}] = key.Namespace != ""
}
}
localObs, _, err := controller.DeduplicateTargetObjects(appNamespace, localObs, &resourceInfoProvider{namespacedByGk: namespacedByGk})
errors.CheckError(err)
objByKey := make(map[kube.ResourceKey]*unstructured.Unstructured)
for i := range localObs {
obj := localObs[i]
if !hook.IsHook(obj) && !ignore.Ignore(obj) {
objByKey[kube.GetResourceKey(obj)] = obj
}
}
return objByKey
}
type objKeyLiveTarget struct {
key kube.ResourceKey
live *unstructured.Unstructured
target *unstructured.Unstructured
}
// addServerSideDiffPerfFlags adds server-side diff performance tuning flags to a command
func addServerSideDiffPerfFlags(command *cobra.Command, serverSideDiffConcurrency *int, serverSideDiffMaxBatchKB *int) {
command.Flags().IntVar(serverSideDiffConcurrency, "server-side-diff-concurrency", -1, "Max concurrent batches for server-side diff. -1 = unlimited, 1 = sequential, 2+ = concurrent (0 = invalid)")
command.Flags().IntVar(serverSideDiffMaxBatchKB, "server-side-diff-max-batch-kb", 250, "Max batch size in KB for server-side diff. Smaller values are safer for proxies")
}
// NewApplicationDiffCommand returns a new instance of an `argocd app diff` command
func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
refresh bool
hardRefresh bool
exitCode bool
diffExitCode int
local string
revision string
localRepoRoot string
serverSideGenerate bool
serverSideDiff bool
serverSideDiffConcurrency int
serverSideDiffMaxBatchKB int
localIncludes []string
appNamespace string
revisions []string
sourcePositions []int64
sourceNames []string
ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts
)
shortDesc := "Perform a diff against the target and live state."
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\nKubernetes Secrets are ignored from this diff.",
Run: func(c *cobra.Command, args []string) {
ctx := c.Context()
if len(args) != 1 {
c.HelpFunc()(c, args)
os.Exit(2)
}
if len(sourceNames) > 0 && len(sourcePositions) > 0 {
errors.Fatal(errors.ErrorGeneric, "Only one of source-positions and source-names can be specified.")
}
if len(sourcePositions) > 0 && len(revisions) != len(sourcePositions) {
errors.Fatal(errors.ErrorGeneric, "While using --revisions and --source-positions, length of values for both flags should be same.")
}
if len(sourceNames) > 0 && len(revisions) != len(sourceNames) {
errors.Fatal(errors.ErrorGeneric, "While using --revisions and --source-names, length of values for both flags should be same.")
}
clientset := headless.NewClientOrDie(clientOpts, c)
conn, appIf := clientset.NewApplicationClientOrDie()
defer utilio.Close(conn)
appName, appNs := argo.ParseFromQualifiedName(args[0], appNamespace)
app, err := appIf.Get(ctx, &application.ApplicationQuery{
Name: &appName,
Refresh: getRefreshType(refresh, hardRefresh),
AppNamespace: &appNs,
})
errors.CheckError(err)
if len(sourceNames) > 0 {
sourceNameToPosition := getSourceNameToPositionMap(app)
for _, name := range sourceNames {
pos, ok := sourceNameToPosition[name]
if !ok {
log.Fatalf("Unknown source name '%s'", name)
}
sourcePositions = append(sourcePositions, pos)
}
}
resources, err := appIf.ManagedResources(ctx, &application.ResourcesQuery{ApplicationName: &appName, AppNamespace: &appNs})
errors.CheckError(err)
conn, settingsIf := clientset.NewSettingsClientOrDie()
defer utilio.Close(conn)
argoSettings, err := settingsIf.Get(ctx, &settings.SettingsQuery{})
errors.CheckError(err)
diffOption := &DifferenceOption{}
hasServerSideDiffAnnotation := resourceutil.HasAnnotationOption(app, argocommon.AnnotationCompareOptions, "ServerSideDiff=true")
// Use annotation if flag not explicitly set
if !c.Flags().Changed("server-side-diff") {
serverSideDiff = hasServerSideDiffAnnotation
} else if serverSideDiff && !hasServerSideDiffAnnotation {
// Flag explicitly set to true, but app annotation is not set
fmt.Fprintf(os.Stderr, "Warning: Application does not have ServerSideDiff=true annotation.\n")
}
// Server side diff with local requires server side generate to be set as there will be a mismatch with client-generated manifests.
if serverSideDiff && local != "" && !serverSideGenerate {
log.Fatal("--server-side-diff with --local requires --server-side-generate.")
}
switch {
case app.Spec.HasMultipleSources() && len(revisions) > 0 && len(sourcePositions) > 0:
numOfSources := int64(len(app.Spec.GetSources()))
for _, pos := range sourcePositions {
if pos <= 0 || pos > numOfSources {
log.Fatal("source-position cannot be less than 1 or more than number of sources in the app. Counting starts at 1.")
}
}
q := application.ApplicationManifestQuery{
Name: &appName,
AppNamespace: &appNs,
Revisions: revisions,
SourcePositions: sourcePositions,
NoCache: &hardRefresh,
}
res, err := appIf.GetManifests(ctx, &q)
errors.CheckError(err)
diffOption.res = res
diffOption.revisions = revisions
case revision != "":
q := application.ApplicationManifestQuery{
Name: &appName,
Revision: &revision,
AppNamespace: &appNs,
NoCache: &hardRefresh,
}
res, err := appIf.GetManifests(ctx, &q)
errors.CheckError(err)
diffOption.res = res
diffOption.revision = revision
case local != "":
if serverSideGenerate {
client, err := appIf.GetManifestsWithFiles(ctx, grpc_retry.Disable())
errors.CheckError(err)
err = manifeststream.SendApplicationManifestQueryWithFiles(ctx, client, appName, appNs, local, localIncludes)
errors.CheckError(err)
res, err := client.CloseAndRecv()
errors.CheckError(err)
diffOption.serversideRes = res
} else {
fmt.Fprintf(os.Stderr, "Warning: local diff without --server-side-generate is deprecated and does not work with plugins. Server-side generation will be the default in v2.7.")
conn, clusterIf := clientset.NewClusterClientOrDie()
defer utilio.Close(conn)
cluster, err := clusterIf.Get(ctx, &clusterpkg.ClusterQuery{Name: app.Spec.Destination.Name, Server: app.Spec.Destination.Server})
errors.CheckError(err)
diffOption.local = local
diffOption.localRepoRoot = localRepoRoot
diffOption.cluster = cluster
}
}
proj := getProject(ctx, c, clientOpts, app.Spec.Project)
foundDiffs := findAndPrintDiff(ctx, app, proj.Project, resources, argoSettings, diffOption, ignoreNormalizerOpts, serverSideDiff, appIf, app.GetName(), app.GetNamespace(), serverSideDiffConcurrency, serverSideDiffMaxBatchKB)
if foundDiffs && exitCode {
os.Exit(diffExitCode)
}
},
}
command.Flags().BoolVar(&refresh, "refresh", false, "Refresh application data when retrieving")
command.Flags().BoolVar(&hardRefresh, "hard-refresh", false, "Refresh application data as well as target manifests cache")
command.Flags().BoolVar(&exitCode, "exit-code", true, "Return non-zero exit code when there is a diff. May also return non-zero exit code if there is an error.")
command.Flags().IntVar(&diffExitCode, "diff-exit-code", 1, "Return specified exit code when there is a diff. Typical error code is 20 but use another exit code if you want to differentiate from the generic exit code (20) returned by all CLI commands.")
command.Flags().StringVar(&local, "local", "", "Compare live app to a local manifests")
command.Flags().StringVar(&revision, "revision", "", "Compare live app to a particular revision")
command.Flags().StringVar(&localRepoRoot, "local-repo-root", "/", "Path to the repository root. Used together with --local allows setting the repository root")
command.Flags().BoolVar(&serverSideGenerate, "server-side-generate", false, "Used with --local, this will send your manifests to the server for diffing")
command.Flags().BoolVar(&serverSideDiff, "server-side-diff", false, "Use server-side diff to calculate the diff. This will default to true if the ServerSideDiff annotation is set on the application.")
addServerSideDiffPerfFlags(command, &serverSideDiffConcurrency, &serverSideDiffMaxBatchKB)
command.Flags().StringArrayVar(&localIncludes, "local-include", []string{"*.yaml", "*.yml", "*.json"}, "Used with --server-side-generate, specify patterns of filenames to send. Matching is based on filename and not path.")
command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Only render the difference in namespace")
command.Flags().StringArrayVar(&revisions, "revisions", []string{}, "Show manifests at specific revisions for source position in source-positions")
command.Flags().Int64SliceVar(&sourcePositions, "source-positions", []int64{}, "List of source positions. Default is empty array. Counting start at 1.")
command.Flags().StringArrayVar(&sourceNames, "source-names", []string{}, "List of source names. Default is an empty array.")
command.Flags().DurationVar(&ignoreNormalizerOpts.JQExecutionTimeout, "ignore-normalizer-jq-execution-timeout", normalizers.DefaultJQExecutionTimeout, "Set ignore normalizer JQ execution timeout")
return command
}
// printResourceDiff prints the diff header and calls cli.PrintDiff for a resource
func printResourceDiff(group, kind, namespace, name string, live, target *unstructured.Unstructured) {
fmt.Printf("\n===== %s/%s %s/%s ======\n", group, kind, namespace, name)
_ = cli.PrintDiff(name, live, target)
}
// findAndPrintServerSideDiff performs a server-side diff by making requests to the api server and prints the response
func findAndPrintServerSideDiff(ctx context.Context, app *argoappv1.Application, items []objKeyLiveTarget, resources *application.ManagedResourcesResponse, appIf application.ApplicationServiceClient, appName, appNs string, maxConcurrency int, maxBatchSizeKB int) bool {
if maxConcurrency == 0 {
errors.CheckError(stderrors.New("invalid value for --server-side-diff-concurrency: 0 is not allowed (use -1 for unlimited, or a positive number to limit concurrency)"))
}
liveResources := make([]*argoappv1.ResourceDiff, 0, len(items))
targetManifests := make([]string, 0, len(items))
// Process each item for server-side diff
foundDiffs := false
for _, item := range items {
if item.target != nil && hook.IsHook(item.target) || item.live != nil && hook.IsHook(item.live) {
continue
}
// For server-side diff, we need to create aligned arrays for this specific resource
var liveResource *argoappv1.ResourceDiff
var targetManifest string
if item.live != nil {
for _, res := range resources.Items {
if res.Group == item.key.Group && res.Kind == item.key.Kind &&
res.Namespace == item.key.Namespace && res.Name == item.key.Name {
liveResource = res
break
}
}
}
if liveResource == nil {
// Create empty live resource for creation case
liveResource = &argoappv1.ResourceDiff{
Group: item.key.Group,
Kind: item.key.Kind,
Namespace: item.key.Namespace,
Name: item.key.Name,
LiveState: "",
TargetState: "",
Modified: true,
}
}
liveResources = append(liveResources, liveResource)
if item.target != nil {
jsonBytes, err := json.Marshal(item.target)
if err != nil {
errors.CheckError(fmt.Errorf("error marshaling target object: %w", err))
}
targetManifest = string(jsonBytes)
}
targetManifests = append(targetManifests, targetManifest)
}
if len(liveResources) == 0 {
return false
}
// Batch by size to avoid proxy limits
maxBatchSize := maxBatchSizeKB * 1024
var batches []struct{ start, end int }
for i := 0; i < len(liveResources); {
start := i
size := 0
for i < len(liveResources) {
resourceSize := len(liveResources[i].LiveState) + len(targetManifests[i])
if size+resourceSize > maxBatchSize && i > start {
break
}
size += resourceSize
i++
}
batches = append(batches, struct{ start, end int }{start, i})
}
// Process batches in parallel
g, errGroupCtx := errgroup.WithContext(ctx)
g.SetLimit(maxConcurrency)
results := make([][]*argoappv1.ResourceDiff, len(batches))
for idx, batch := range batches {
i := idx
b := batch
g.Go(func() error {
// Call server-side diff for this batch of resources
serverSideDiffQuery := &application.ApplicationServerSideDiffQuery{
AppName: &appName,
AppNamespace: &appNs,
Project: &app.Spec.Project,
LiveResources: liveResources[b.start:b.end],
TargetManifests: targetManifests[b.start:b.end],
}
serverSideDiffRes, err := appIf.ServerSideDiff(errGroupCtx, serverSideDiffQuery)
if err != nil {
return err
}
results[i] = serverSideDiffRes.Items
return nil
})
}
if err := g.Wait(); err != nil {
errors.CheckError(err)
}
for _, items := range results {
for _, resultItem := range items {
if resultItem.Hook || (!resultItem.Modified && resultItem.TargetState != "" && resultItem.LiveState != "") {
continue
}
if resultItem.Modified || resultItem.TargetState == "" || resultItem.LiveState == "" {
var live, target *unstructured.Unstructured
if resultItem.TargetState != "" && resultItem.TargetState != "null" {
target = &unstructured.Unstructured{}
err := json.Unmarshal([]byte(resultItem.TargetState), target)
errors.CheckError(err)
}
if resultItem.LiveState != "" && resultItem.LiveState != "null" {
live = &unstructured.Unstructured{}
err := json.Unmarshal([]byte(resultItem.LiveState), live)
errors.CheckError(err)
}
// Print resulting diff for this resource
foundDiffs = true
printResourceDiff(resultItem.Group, resultItem.Kind, resultItem.Namespace, resultItem.Name, live, target)
}
}
}
return foundDiffs
}
// DifferenceOption struct to store diff options
type DifferenceOption struct {
local string
localRepoRoot string
revision string
cluster *argoappv1.Cluster
res *repoapiclient.ManifestResponse
serversideRes *repoapiclient.ManifestResponse
revisions []string
}
// findAndPrintDiff ... Prints difference between application current state and state stored in git or locally, returns boolean as true if difference is found else returns false
func findAndPrintDiff(ctx context.Context, app *argoappv1.Application, proj *argoappv1.AppProject, resources *application.ManagedResourcesResponse, argoSettings *settings.Settings, diffOptions *DifferenceOption, ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts, useServerSideDiff bool, appIf application.ApplicationServiceClient, appName, appNs string, serverSideDiffConcurrency int, serverSideDiffMaxBatchKB int) bool {
var foundDiffs bool
items, err := prepareObjectsForDiff(ctx, app, proj, resources, argoSettings, diffOptions)
errors.CheckError(err)
if useServerSideDiff {
return findAndPrintServerSideDiff(ctx, app, items, resources, appIf, appName, appNs, serverSideDiffConcurrency, serverSideDiffMaxBatchKB)
}
for _, item := range items {
if item.target != nil && hook.IsHook(item.target) || item.live != nil && hook.IsHook(item.live) {
continue
}
overrides := make(map[string]argoappv1.ResourceOverride)
for k := range argoSettings.ResourceOverrides {
val := argoSettings.ResourceOverrides[k]
overrides[k] = *val
}
// TODO remove hardcoded IgnoreAggregatedRoles and retrieve the
// compareOptions in the protobuf
ignoreAggregatedRoles := false
diffConfig, err := argodiff.NewDiffConfigBuilder().
WithDiffSettings(app.Spec.IgnoreDifferences, overrides, ignoreAggregatedRoles, ignoreNormalizerOpts).
WithTracking(argoSettings.AppLabelKey, argoSettings.TrackingMethod).
WithNoCache().
WithLogger(logutils.NewLogrusLogger(logutils.NewWithCurrentConfig())).
Build()
errors.CheckError(err)
diffRes, err := argodiff.StateDiff(item.live, item.target, diffConfig)
errors.CheckError(err)
if diffRes.Modified || item.target == nil || item.live == nil {
var live *unstructured.Unstructured
var target *unstructured.Unstructured
if item.target != nil && item.live != nil {
target = &unstructured.Unstructured{}
live = item.live
err = json.Unmarshal(diffRes.PredictedLive, target)
errors.CheckError(err)
} else {
live = item.live
target = item.target
}
foundDiffs = true
printResourceDiff(item.key.Group, item.key.Kind, item.key.Namespace, item.key.Name, live, target)
}
}
return foundDiffs
}
func groupObjsForDiff(resources *application.ManagedResourcesResponse, objs map[kube.ResourceKey]*unstructured.Unstructured, items []objKeyLiveTarget, argoSettings *settings.Settings, appName, namespace string) []objKeyLiveTarget {
resourceTracking := argo.NewResourceTracking()
for _, res := range resources.Items {
live := &unstructured.Unstructured{}
err := json.Unmarshal([]byte(res.NormalizedLiveState), &live)
errors.CheckError(err)
key := kube.ResourceKey{Name: res.Name, Namespace: res.Namespace, Group: res.Group, Kind: res.Kind}
if key.Kind == kube.SecretKind && key.Group == "" {
// Don't bother comparing secrets, argo-cd doesn't have access to k8s secret data
delete(objs, key)
continue
}
if local, ok := objs[key]; ok || live != nil {
if local != nil && !kube.IsCRD(local) {
err = resourceTracking.SetAppInstance(local, argoSettings.AppLabelKey, appName, namespace, argoappv1.TrackingMethod(argoSettings.GetTrackingMethod()), argoSettings.GetInstallationID())
errors.CheckError(err)
}
items = append(items, objKeyLiveTarget{key, live, local})
delete(objs, key)
}
}
for key, local := range objs {
if key.Kind == kube.SecretKind && key.Group == "" {
// Don't bother comparing secrets, argo-cd doesn't have access to k8s secret data
delete(objs, key)
continue
}
items = append(items, objKeyLiveTarget{key, nil, local})
}
return items
}
// NewApplicationDeleteCommand returns a new instance of an `argocd app delete` command
func NewApplicationDeleteCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
cascade bool
noPrompt bool
propagationPolicy string
selector string
wait bool
appNamespace string
)
command := &cobra.Command{
Use: "delete APPNAME",
Short: "Delete an application",
Example: ` # Delete an app
argocd app delete my-app
# Delete multiple apps
argocd app delete my-app other-app
# Delete apps by label
argocd app delete -l app.kubernetes.io/instance=my-app
argocd app delete -l app.kubernetes.io/instance!=my-app
argocd app delete -l app.kubernetes.io/instance
argocd app delete -l '!app.kubernetes.io/instance'
argocd app delete -l 'app.kubernetes.io/instance notin (my-app,other-app)'`,
Run: func(c *cobra.Command, args []string) {
ctx := c.Context()
if len(args) == 0 && selector == "" {
c.HelpFunc()(c, args)
os.Exit(1)
}
acdClient := headless.NewClientOrDie(clientOpts, c)
conn, appIf := acdClient.NewApplicationClientOrDie()
defer utilio.Close(conn)
isTerminal := isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd())
promptFlag := c.Flag("yes")
if promptFlag.Changed && promptFlag.Value.String() == "true" {
noPrompt = true
}
appNames, err := getAppNamesBySelector(ctx, appIf, selector)
errors.CheckError(err)
if len(appNames) == 0 {
appNames = args
}
numOfApps := len(appNames)
// This is for backward compatibility,
// before we showed the prompts only when condition cascade && isTerminal && !noPrompt is true
promptUtil := utils.NewPrompt(cascade && isTerminal && !noPrompt)
var (
confirmAll = false
confirm = false
)
for _, appFullName := range appNames {
appName, appNs := argo.ParseFromQualifiedName(appFullName, appNamespace)
appDeleteReq := application.ApplicationDeleteRequest{
Name: &appName,
AppNamespace: &appNs,
}
if c.Flag("cascade").Changed {
appDeleteReq.Cascade = &cascade
}
if c.Flag("propagation-policy").Changed {
appDeleteReq.PropagationPolicy = &propagationPolicy
}
messageForSingle := "Are you sure you want to delete '" + appFullName + "' and all its resources? [y/n] "
messageForAll := "Are you sure you want to delete '" + appFullName + "' and all its resources? [y/n/a] where 'a' is to delete all specified apps and their resources without prompting "
if !confirmAll {
confirm, confirmAll = promptUtil.ConfirmBaseOnCount(messageForSingle, messageForAll, numOfApps)
}
if confirm || confirmAll {
_, err := appIf.Delete(ctx, &appDeleteReq)
errors.CheckError(err)
if wait {
checkForDeleteEvent(ctx, acdClient, appFullName)
}
fmt.Printf("application '%s' deleted\n", appFullName)
} else {
fmt.Println("The command to delete '" + appFullName + "' was cancelled.")
}
}
},
}
command.Flags().BoolVar(&cascade, "cascade", true, "Perform a cascaded deletion of all application resources")
command.Flags().StringVarP(&propagationPolicy, "propagation-policy", "p", "foreground", "Specify propagation policy for deletion of application's resources. One of: foreground|background")
command.Flags().BoolVarP(&noPrompt, "yes", "y", false, "Turn off prompting to confirm cascaded deletion of application resources")
command.Flags().StringVarP(&selector, "selector", "l", "", "Delete all apps with matching label. Supports '=', '==', '!=', in, notin, exists & not exists. Matching apps must satisfy all of the specified label constraints.")
command.Flags().BoolVar(&wait, "wait", false, "Wait until deletion of the application(s) completes")
command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Namespace where the application will be deleted from")
return command
}
func checkForDeleteEvent(ctx context.Context, acdClient argocdclient.Client, appFullName string) {
appEventCh := acdClient.WatchApplicationWithRetry(ctx, appFullName, "")
for appEvent := range appEventCh {
if appEvent.Type == k8swatch.Deleted {
return
}
}
}
// Print simple list of application names
func printApplicationNames(apps []argoappv1.Application) {
for _, app := range apps {
fmt.Println(app.QualifiedName())
}
}
// Print table of application data
func printApplicationTable(apps []argoappv1.Application, output *string) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
var fmtStr string
headers := []any{"NAME", "CLUSTER", "NAMESPACE", "PROJECT", "STATUS", "HEALTH", "SYNCPOLICY", "CONDITIONS"}
if *output == "wide" {
fmtStr = "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n"
headers = append(headers, "REPO", "PATH", "TARGET")
} else {
fmtStr = "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n"
}
_, _ = fmt.Fprintf(w, fmtStr, headers...)
for _, app := range apps {
vals := []any{
app.QualifiedName(),
getServer(&app),
app.Spec.Destination.Namespace,
app.Spec.GetProject(),
app.Status.Sync.Status,
app.Status.Health.Status,
formatSyncPolicy(app),
formatConditionsSummary(app),
}
if *output == "wide" {
vals = append(vals, app.Spec.GetSource().RepoURL, app.Spec.GetSource().Path, app.Spec.GetSource().TargetRevision)
}
_, _ = fmt.Fprintf(w, fmtStr, vals...)
}
_ = w.Flush()
}
// NewApplicationListCommand returns a new instance of an `argocd app list` command
func NewApplicationListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
output string
selector string
projects []string
repo string
appNamespace string
cluster string
path string
)
command := &cobra.Command{
Use: "list",
Short: "List applications",
Example: ` # List all apps
argocd app list
# List apps by label, in this example we listing apps that are children of another app (aka app-of-apps)
argocd app list -l app.kubernetes.io/instance=my-app
argocd app list -l app.kubernetes.io/instance!=my-app
argocd app list -l app.kubernetes.io/instance
argocd app list -l '!app.kubernetes.io/instance'
argocd app list -l 'app.kubernetes.io/instance notin (my-app,other-app)'`,
Run: func(c *cobra.Command, _ []string) {
ctx := c.Context()
conn, appIf := headless.NewClientOrDie(clientOpts, c).NewApplicationClientOrDie()
defer utilio.Close(conn)
apps, err := appIf.List(ctx, &application.ApplicationQuery{
Selector: ptr.To(selector),
AppNamespace: &appNamespace,
})
errors.CheckError(err)
appList := apps.Items
if len(projects) != 0 {
appList = argo.FilterByProjects(appList, projects)
}
if repo != "" {
appList = argo.FilterByRepo(appList, repo)
}
if cluster != "" {
appList = argo.FilterByCluster(appList, cluster)
}
if path != "" {
appList = argo.FilterByPath(appList, path)
}
switch output {
case "yaml", "json":
err := PrintResourceList(appList, output, false)
errors.CheckError(err)
case "name":
printApplicationNames(appList)
case "wide", "":
printApplicationTable(appList, &output)
default:
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
}
},
}
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: wide|name|json|yaml")
command.Flags().StringVarP(&selector, "selector", "l", "", "List apps by label. Supports '=', '==', '!=', in, notin, exists & not exists. Matching apps must satisfy all of the specified label constraints.")
command.Flags().StringArrayVarP(&projects, "project", "p", []string{}, "Filter by project name")
command.Flags().StringVarP(&repo, "repo", "r", "", "List apps by source repo URL")
command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Only list applications in namespace")
command.Flags().StringVarP(&cluster, "cluster", "c", "", "List apps by cluster name or url")
command.Flags().StringVarP(&path, "path", "P", "", "List apps by path")
return command
}
func formatSyncPolicy(app argoappv1.Application) string {
if app.Spec.SyncPolicy == nil || !app.Spec.SyncPolicy.IsAutomatedSyncEnabled() {
return "Manual"
}
policy := "Auto"
if app.Spec.SyncPolicy.Automated.Prune {
policy = policy + "-Prune"
}
return policy
}
func formatConditionsSummary(app argoappv1.Application) string {
typeToCnt := make(map[string]int)
for i := range app.Status.Conditions {
condition := app.Status.Conditions[i]
if cnt, ok := typeToCnt[condition.Type]; ok {
typeToCnt[condition.Type] = cnt + 1
} else {
typeToCnt[condition.Type] = 1
}
}
items := make([]string, 0)
for cndType, cnt := range typeToCnt {
if cnt > 1 {
items = append(items, fmt.Sprintf("%s(%d)", cndType, cnt))
} else {
items = append(items, cndType)
}
}
// Sort the keys by name
sort.Strings(items)
summary := "<none>"
if len(items) > 0 {
slices.Sort(items)
summary = strings.Join(items, ",")
}
return summary
}
const (
resourceFieldDelimiter = ":"
resourceFieldCount = 3
resourceFieldNamespaceDelimiter = "/"
resourceFieldNameWithNamespaceCount = 2
resourceExcludeIndicator = "!"
)
// resource is GROUP:KIND:NAMESPACE/NAME or GROUP:KIND:NAME
func parseSelectedResources(resources []string) ([]*argoappv1.SyncOperationResource, error) {
// retrieve name and namespace in case if format is GROUP:KIND:NAMESPACE/NAME, otherwise return name and empty namespace
nameRetriever := func(resourceName, resource string) (string, string, error) {
if !strings.Contains(resourceName, resourceFieldNamespaceDelimiter) {
return resourceName, "", nil
}
nameFields := strings.Split(resourceName, resourceFieldNamespaceDelimiter)
if len(nameFields) != resourceFieldNameWithNamespaceCount {
return "", "", fmt.Errorf("resource with namespace should have GROUP%sKIND%sNAMESPACE%sNAME, but instead got: %s", resourceFieldDelimiter, resourceFieldDelimiter, resourceFieldNamespaceDelimiter, resource)
}
namespace := nameFields[0]
name := nameFields[1]
return name, namespace, nil
}
var selectedResources []*argoappv1.SyncOperationResource
if resources == nil {
return selectedResources, nil
}
for _, resource := range resources {
isExcluded := false
// check if the resource flag starts with a '!'
if after, ok := strings.CutPrefix(resource, resourceExcludeIndicator); ok {
resource = after
isExcluded = true
}
fields := strings.Split(resource, resourceFieldDelimiter)
if len(fields) != resourceFieldCount {
return nil, fmt.Errorf("resource should have GROUP%sKIND%sNAME, but instead got: %s", resourceFieldDelimiter, resourceFieldDelimiter, resource)
}
name, namespace, err := nameRetriever(fields[2], resource)
if err != nil {
return nil, err
}
selectedResources = append(selectedResources, &argoappv1.SyncOperationResource{
Group: fields[0],
Kind: fields[1],
Name: name,
Namespace: namespace,
Exclude: isExcluded,
})
}
return selectedResources, nil
}
func getWatchOpts(watch watchOpts) watchOpts {
// if no opts are defined should wait for sync,health,operation
if (watch == watchOpts{}) {
return watchOpts{
sync: true,
health: true,
operation: true,
}
}
return watch
}
// NewApplicationWaitCommand returns a new instance of an `argocd app wait` command
func NewApplicationWaitCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
watch watchOpts
timeout uint
selector string
resources []string
output string
appNamespace string
)
command := &cobra.Command{
Use: "wait [APPNAME.. | -l selector]",
Short: "Wait for an application to reach a synced and healthy state",
Example: ` # Wait for an app
argocd app wait my-app
# Wait for multiple apps
argocd app wait my-app other-app
# Wait for apps by resource
# Resource should be formatted as GROUP:KIND:NAME. If no GROUP is specified then :KIND:NAME.
argocd app wait my-app --resource :Service:my-service
argocd app wait my-app --resource argoproj.io:Rollout:my-rollout
argocd app wait my-app --resource '!apps:Deployment:my-service'
argocd app wait my-app --resource apps:Deployment:my-service --resource :Service:my-service
argocd app wait my-app --resource '!*:Service:*'
# Specify namespace if the application has resources with the same name in different namespaces
argocd app wait my-app --resource argoproj.io:Rollout:my-namespace/my-rollout
# Wait for apps by label, in this example we waiting for apps that are children of another app (aka app-of-apps)
argocd app wait -l app.kubernetes.io/instance=my-app
argocd app wait -l app.kubernetes.io/instance!=my-app
argocd app wait -l app.kubernetes.io/instance
argocd app wait -l '!app.kubernetes.io/instance'
argocd app wait -l 'app.kubernetes.io/instance notin (my-app,other-app)'`,
Run: func(c *cobra.Command, args []string) {
ctx := c.Context()
if len(args) == 0 && selector == "" {
c.HelpFunc()(c, args)
os.Exit(1)
}
watch = getWatchOpts(watch)
selectedResources, err := parseSelectedResources(resources)
errors.CheckError(err)
appNames := args
acdClient := headless.NewClientOrDie(clientOpts, c)
closer, appIf := acdClient.NewApplicationClientOrDie()
defer utilio.Close(closer)
if selector != "" {
list, err := appIf.List(ctx, &application.ApplicationQuery{Selector: ptr.To(selector)})
errors.CheckError(err)
for _, i := range list.Items {
appNames = append(appNames, i.QualifiedName())
}
}
for _, appName := range appNames {
// Construct QualifiedName
if appNamespace != "" && !strings.Contains(appName, "/") {
appName = appNamespace + "/" + appName
}
_, _, err := waitOnApplicationStatus(ctx, acdClient, appName, timeout, watch, selectedResources, output)
errors.CheckError(err)
}
},
}
command.Flags().BoolVar(&watch.sync, "sync", false, "Wait for sync")
command.Flags().BoolVar(&watch.health, "health", false, "Wait for health")
command.Flags().BoolVar(&watch.suspended, "suspended", false, "Wait for suspended")
command.Flags().BoolVar(&watch.degraded, "degraded", false, "Wait for degraded")
command.Flags().BoolVar(&watch.delete, "delete", false, "Wait for delete")
command.Flags().BoolVar(&watch.hydrated, "hydrated", false, "Wait for hydration operations")
command.Flags().StringVarP(&selector, "selector", "l", "", "Wait for apps by label. Supports '=', '==', '!=', in, notin, exists & not exists. Matching apps must satisfy all of the specified label constraints.")
command.Flags().StringArrayVar(&resources, "resource", []string{}, fmt.Sprintf("Sync only specific resources as GROUP%[1]sKIND%[1]sNAME or %[2]sGROUP%[1]sKIND%[1]sNAME. Fields may be blank and '*' can be used. This option may be specified repeatedly", resourceFieldDelimiter, resourceExcludeIndicator))
command.Flags().BoolVar(&watch.operation, "operation", false, "Wait for pending operations")
command.Flags().UintVar(&timeout, "timeout", defaultCheckTimeoutSeconds, "Time out after this many seconds")
command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Only wait for an application in namespace")
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|tree|tree=detailed")
return command
}
// printAppResources prints the resources of an application in a tabwriter table
func printAppResources(w io.Writer, app *argoappv1.Application) {
_, _ = fmt.Fprintf(w, "GROUP\tKIND\tNAMESPACE\tNAME\tSTATUS\tHEALTH\tHOOK\tMESSAGE\n")
for _, res := range getResourceStates(app, nil) {
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", res.Group, res.Kind, res.Namespace, res.Name, res.Status, res.Health, res.Hook, res.Message)
}
}
func printTreeView(nodeMapping map[string]argoappv1.ResourceNode, parentChildMapping map[string][]string, parentNodes map[string]struct{}, mapNodeNameToResourceState map[string]*resourceState) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
_, _ = fmt.Fprintf(w, "KIND/NAME\tSTATUS\tHEALTH\tMESSAGE\n")
for uid := range parentNodes {
treeViewAppGet("", nodeMapping, parentChildMapping, nodeMapping[uid], mapNodeNameToResourceState, w)
}
_ = w.Flush()
}
func printTreeViewDetailed(nodeMapping map[string]argoappv1.ResourceNode, parentChildMapping map[string][]string, parentNodes map[string]struct{}, mapNodeNameToResourceState map[string]*resourceState) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "KIND/NAME\tSTATUS\tHEALTH\tAGE\tMESSAGE\tREASON\n")
for uid := range parentNodes {
detailedTreeViewAppGet("", nodeMapping, parentChildMapping, nodeMapping[uid], mapNodeNameToResourceState, w)
}
_ = w.Flush()
}
// NewApplicationSyncCommand returns a new instance of an `argocd app sync` command
func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
revision string
revisions []string
sourcePositions []int64
sourceNames []string
resources []string
labels []string
selector string
prune bool
dryRun bool
timeout uint
strategy string
force bool
replace bool
serverSideApply bool
applyOutOfSyncOnly bool
async bool
retryLimit int64
retryRefresh bool
retryBackoffDuration time.Duration
retryBackoffMaxDuration time.Duration
retryBackoffFactor int64
local string
localRepoRoot string
infos []string
diffChanges bool
diffChangesConfirm bool
projects []string
output string
appNamespace string
ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts
serverSideDiffConcurrency int
serverSideDiffMaxBatchKB int
)
command := &cobra.Command{
Use: "sync [APPNAME... | -l selector | --project project-name]",
Short: "Sync an application to its target state",
Example: ` # Sync an app
argocd app sync my-app
# Sync multiples apps
argocd app sync my-app other-app
# Sync apps by label, in this example we sync apps that are children of another app (aka app-of-apps)
argocd app sync -l app.kubernetes.io/instance=my-app
argocd app sync -l app.kubernetes.io/instance!=my-app
argocd app sync -l app.kubernetes.io/instance
argocd app sync -l '!app.kubernetes.io/instance'
argocd app sync -l 'app.kubernetes.io/instance notin (my-app,other-app)'
# Sync a multi-source application for specific revision of specific sources
argocd app sync my-app --revisions 0.0.1 --source-positions 1 --revisions 0.0.2 --source-positions 2
argocd app sync my-app --revisions 0.0.1 --source-names my-chart --revisions 0.0.2 --source-names my-values
# Sync a specific resource
# Resource should be formatted as GROUP:KIND:NAME. If no GROUP is specified then :KIND:NAME
argocd app sync my-app --resource :Service:my-service
argocd app sync my-app --resource argoproj.io:Rollout:my-rollout
argocd app sync my-app --resource '!apps:Deployment:my-service'
argocd app sync my-app --resource apps:Deployment:my-service --resource :Service:my-service
argocd app sync my-app --resource '!*:Service:*'
# Specify namespace if the application has resources with the same name in different namespaces
argocd app sync my-app --resource argoproj.io:Rollout:my-namespace/my-rollout`,
Run: func(c *cobra.Command, args []string) {
ctx := c.Context()
if len(args) == 0 && selector == "" && len(projects) == 0 {
c.HelpFunc()(c, args)
os.Exit(1)
}
if len(args) > 1 && selector != "" {
log.Fatal("Cannot use selector option when application name(s) passed as argument(s)")
}
if len(args) != 1 && (len(revisions) > 0 || len(sourcePositions) > 0) {
log.Fatal("Cannot use --revisions and --source-positions options when 0 or more than 1 application names are passed as argument(s)")
}
if len(args) != 1 && (len(revisions) > 0 || len(sourceNames) > 0) {
log.Fatal("Cannot use --revisions and --source-names options when 0 or more than 1 application names are passed as argument(s)")
}
if len(sourceNames) > 0 && len(sourcePositions) > 0 {
log.Fatal("Only one of source-positions and source-names can be specified.")
}
if len(sourcePositions) > 0 && len(revisions) != len(sourcePositions) {
log.Fatal("While using --revisions and --source-positions, length of values for both flags should be same.")
}
if len(sourceNames) > 0 && len(revisions) != len(sourceNames) {
log.Fatal("While using --revisions and --source-names, length of values for both flags should be same.")
}
for _, pos := range sourcePositions {
if pos <= 0 {
log.Fatal("source-position cannot be less than or equal to 0, Counting starts at 1")
}
}
acdClient := headless.NewClientOrDie(clientOpts, c)
conn, appIf := acdClient.NewApplicationClientOrDie()
defer utilio.Close(conn)
selectedLabels, err := label.Parse(labels)
errors.CheckError(err)
if len(args) == 1 && len(sourceNames) > 0 {
appName, _ := argo.ParseFromQualifiedName(args[0], appNamespace)
app, err := appIf.Get(context.Background(), &application.ApplicationQuery{Name: &appName})
errors.CheckError(err)
sourceNameToPosition := getSourceNameToPositionMap(app)
for _, name := range sourceNames {
pos, ok := sourceNameToPosition[name]
if !ok {
log.Fatalf("Unknown source name '%s'", name)
}
sourcePositions = append(sourcePositions, pos)
}
}
appNames := args
if selector != "" || len(projects) > 0 {
list, err := appIf.List(ctx, &application.ApplicationQuery{
Selector: ptr.To(selector),
AppNamespace: &appNamespace,
Projects: projects,
})
errors.CheckError(err)
// unlike list, we'd want to fail if nothing was found
if len(list.Items) == 0 {
errMsg := "No matching apps found for filter:"
if selector != "" {
errMsg += " selector " + selector
}
if len(projects) != 0 {
errMsg += fmt.Sprintf(" projects %v", projects)
}
log.Fatal(errMsg)
}
for _, i := range list.Items {
appNames = append(appNames, i.QualifiedName())
}
}
for _, appQualifiedName := range appNames {
// Construct QualifiedName
if appNamespace != "" && !strings.Contains(appQualifiedName, "/") {
appQualifiedName = appNamespace + "/" + appQualifiedName
}
appName, appNs := argo.ParseFromQualifiedName(appQualifiedName, "")
if len(selectedLabels) > 0 {
q := application.ApplicationManifestQuery{
Name: &appName,
AppNamespace: &appNs,
Revision: &revision,
Revisions: revisions,
SourcePositions: sourcePositions,
}
res, err := appIf.GetManifests(ctx, &q)
if err != nil {
log.Fatal(err)
}
fmt.Println("The name of the app is ", appName)
for _, mfst := range res.Manifests {
obj, err := argoappv1.UnmarshalToUnstructured(mfst)
errors.CheckError(err)
for key, selectedValue := range selectedLabels {
if objectValue, ok := obj.GetLabels()[key]; ok && selectedValue == objectValue {
gvk := obj.GroupVersionKind()
resources = append(resources, fmt.Sprintf("%s:%s:%s", gvk.Group, gvk.Kind, obj.GetName()))
}
}
}
// If labels are provided and none are found return error only if specific resources were also not
// specified.
if len(resources) == 0 {
log.Fatalf("No matching resources found for labels: %v", labels)
return
}
}
selectedResources, err := parseSelectedResources(resources)
errors.CheckError(err)
var localObjsStrings []string
diffOption := &DifferenceOption{}
app, err := appIf.Get(ctx, &application.ApplicationQuery{
Name: &appName,
AppNamespace: &appNs,
})
errors.CheckError(err)
if app.Spec.HasMultipleSources() {
if revision != "" {
log.Fatal("argocd cli does not work on multi-source app with --revision flag. Use --revisions and --source-position instead.")
return
}
if local != "" {
log.Fatal("argocd cli does not work on multi-source app with --local flag")
return
}
}
// filters out only those resources that needs to be synced
filteredResources := filterAppResources(app, selectedResources)
// if resources are provided and no app resources match, then return error
if len(resources) > 0 && len(filteredResources) == 0 {
log.Fatalf("No matching app resources found for resource filter: %v", strings.Join(resources, ", "))
}
if local != "" {
if app.Spec.SyncPolicy != nil && app.Spec.SyncPolicy.IsAutomatedSyncEnabled() && !dryRun {
log.Fatal("Cannot use local sync when Automatic Sync Policy is enabled except with --dry-run")
}
errors.CheckError(err)
conn, settingsIf := acdClient.NewSettingsClientOrDie()
argoSettings, err := settingsIf.Get(ctx, &settings.SettingsQuery{})
errors.CheckError(err)
utilio.Close(conn)
conn, clusterIf := acdClient.NewClusterClientOrDie()
defer utilio.Close(conn)
cluster, err := clusterIf.Get(ctx, &clusterpkg.ClusterQuery{Name: app.Spec.Destination.Name, Server: app.Spec.Destination.Server})
errors.CheckError(err)
utilio.Close(conn)
proj := getProject(ctx, c, clientOpts, app.Spec.Project)
localObjsStrings = getLocalObjectsString(ctx, app, proj.Project, local, localRepoRoot, argoSettings.AppLabelKey, cluster.Info.ServerVersion, cluster.Info.APIVersions, argoSettings.KustomizeOptions, argoSettings.TrackingMethod)
errors.CheckError(err)
diffOption.local = local
diffOption.localRepoRoot = localRepoRoot
diffOption.cluster = cluster
}
syncOptionsFactory := func() *application.SyncOptions {
syncOptions := application.SyncOptions{}
items := make([]string, 0)
if replace {
items = append(items, common.SyncOptionReplace)
}
if serverSideApply {
items = append(items, common.SyncOptionServerSideApply)
}
if applyOutOfSyncOnly {
items = append(items, common.SyncOptionApplyOutOfSyncOnly)
}
if len(items) == 0 {
// for prevent send even empty array if not need
return nil
}
syncOptions.Items = items
return &syncOptions
}
syncReq := application.ApplicationSyncRequest{
Name: &appName,
AppNamespace: &appNs,
DryRun: &dryRun,
Revision: &revision,
Resources: filteredResources,
Prune: &prune,
Manifests: localObjsStrings,
Infos: getInfos(infos),
SyncOptions: syncOptionsFactory(),
Revisions: revisions,
SourcePositions: sourcePositions,
}
switch strategy {
case "apply":
syncReq.Strategy = &argoappv1.SyncStrategy{Apply: &argoappv1.SyncStrategyApply{}}
syncReq.Strategy.Apply.Force = force
case "", "hook":
syncReq.Strategy = &argoappv1.SyncStrategy{Hook: &argoappv1.SyncStrategyHook{}}
syncReq.Strategy.Hook.Force = force
default:
log.Fatalf("Unknown sync strategy: '%s'", strategy)
}
if retryLimit != 0 {
syncReq.RetryStrategy = &argoappv1.RetryStrategy{
Limit: retryLimit,
Refresh: retryRefresh,
Backoff: &argoappv1.Backoff{
Duration: retryBackoffDuration.String(),
MaxDuration: retryBackoffMaxDuration.String(),
Factor: ptr.To(retryBackoffFactor),
},
}
}
if diffChanges {
resources, err := appIf.ManagedResources(ctx, &application.ResourcesQuery{
ApplicationName: &appName,
AppNamespace: &appNs,
})
errors.CheckError(err)
conn, settingsIf := acdClient.NewSettingsClientOrDie()
defer utilio.Close(conn)
argoSettings, err := settingsIf.Get(ctx, &settings.SettingsQuery{})
errors.CheckError(err)
foundDiffs := false
fmt.Printf("====== Previewing differences between live and desired state of application %s ======\n", appQualifiedName)
proj := getProject(ctx, c, clientOpts, app.Spec.Project)
// Check if application has ServerSideDiff annotation
serverSideDiff := resourceutil.HasAnnotationOption(app, argocommon.AnnotationCompareOptions, "ServerSideDiff=true")
foundDiffs = findAndPrintDiff(ctx, app, proj.Project, resources, argoSettings, diffOption, ignoreNormalizerOpts, serverSideDiff, appIf, appName, appNs, serverSideDiffConcurrency, serverSideDiffMaxBatchKB)
if !foundDiffs {
fmt.Printf("====== No Differences found ======\n")
// if no differences found, then no need to sync
return
}
if !diffChangesConfirm {
yesno := cli.AskToProceed(fmt.Sprintf("Please review changes to application %s shown above. Do you want to continue the sync process? (y/n): ", appQualifiedName))
if !yesno {
os.Exit(0)
}
}
}
_, err = appIf.Sync(ctx, &syncReq)
errors.CheckError(err)
if !async {
app, opState, err := waitOnApplicationStatus(ctx, acdClient, appQualifiedName, timeout, watchOpts{operation: true}, selectedResources, output)
errors.CheckError(err)
if !dryRun {
if !opState.Phase.Successful() {
log.Fatalf("Operation has completed with phase: %s", opState.Phase)
} else if len(selectedResources) == 0 && app.Status.Sync.Status != argoappv1.SyncStatusCodeSynced {
// Only get resources to be pruned if sync was application-wide and final status is not synced
pruningRequired := opState.SyncResult.Resources.PruningRequired()
if pruningRequired > 0 {
log.Fatalf("%d resources require pruning", pruningRequired)
}
}
}
}
}
},
}
command.Flags().BoolVar(&dryRun, "dry-run", false, "Preview apply without affecting cluster")
command.Flags().BoolVar(&prune, "prune", false, "Allow deleting unexpected resources")
command.Flags().StringVar(&revision, "revision", "", "Sync to a specific revision. Preserves parameter overrides")
command.Flags().StringArrayVar(&resources, "resource", []string{}, fmt.Sprintf("Sync only specific resources as GROUP%[1]sKIND%[1]sNAME or %[2]sGROUP%[1]sKIND%[1]sNAME. Fields may be blank and '*' can be used. This option may be specified repeatedly", resourceFieldDelimiter, resourceExcludeIndicator))
command.Flags().StringVarP(&selector, "selector", "l", "", "Sync apps that match this label. Supports '=', '==', '!=', in, notin, exists & not exists. Matching apps must satisfy all of the specified label constraints.")
command.Flags().StringArrayVar(&labels, "label", []string{}, "Sync only specific resources with a label. This option may be specified repeatedly.")
command.Flags().UintVar(&timeout, "timeout", defaultCheckTimeoutSeconds, "Time out after this many seconds")
command.Flags().Int64Var(&retryLimit, "retry-limit", 0, "Max number of allowed sync retries")
command.Flags().BoolVar(&retryRefresh, "retry-refresh", false, "Indicates if the latest revision should be used on retry instead of the initial one")
command.Flags().DurationVar(&retryBackoffDuration, "retry-backoff-duration", argoappv1.DefaultSyncRetryDuration, "Retry backoff base duration. Input needs to be a duration (e.g. 2m, 1h)")
command.Flags().DurationVar(&retryBackoffMaxDuration, "retry-backoff-max-duration", argoappv1.DefaultSyncRetryMaxDuration, "Max retry backoff duration. Input needs to be a duration (e.g. 2m, 1h)")
command.Flags().Int64Var(&retryBackoffFactor, "retry-backoff-factor", argoappv1.DefaultSyncRetryFactor, "Factor multiplies the base duration after each failed retry")
command.Flags().StringVar(&strategy, "strategy", "", "Sync strategy (one of: apply|hook)")
command.Flags().BoolVar(&force, "force", false, "Use a force apply")
command.Flags().BoolVar(&replace, "replace", false, "Use a kubectl create/replace instead apply")
command.Flags().BoolVar(&serverSideApply, "server-side", false, "Use server-side apply while syncing the application")
command.Flags().BoolVar(&applyOutOfSyncOnly, "apply-out-of-sync-only", false, "Sync only out-of-sync resources")
command.Flags().BoolVar(&async, "async", false, "Do not wait for application to sync before continuing")
command.Flags().StringVar(&local, "local", "", "Path to a local directory. When this flag is present no git queries will be made")
command.Flags().StringVar(&localRepoRoot, "local-repo-root", "/", "Path to the repository root. Used together with --local allows setting the repository root")
command.Flags().StringArrayVar(&infos, "info", []string{}, "A list of key-value pairs during sync process. These infos will be persisted in app.")
command.Flags().BoolVar(&diffChangesConfirm, "assumeYes", false, "Assume yes as answer for all user queries or prompts")
command.Flags().BoolVar(&diffChanges, "preview-changes", false, "Preview difference against the target and live state before syncing app and wait for user confirmation")
command.Flags().StringArrayVar(&projects, "project", []string{}, "Sync apps that belong to the specified projects. This option may be specified repeatedly.")
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|tree|tree=detailed")
command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Only sync an application in namespace")
command.Flags().DurationVar(&ignoreNormalizerOpts.JQExecutionTimeout, "ignore-normalizer-jq-execution-timeout", normalizers.DefaultJQExecutionTimeout, "Set ignore normalizer JQ execution timeout")
command.Flags().StringArrayVar(&revisions, "revisions", []string{}, "Show manifests at specific revisions for source position in source-positions")
command.Flags().Int64SliceVar(&sourcePositions, "source-positions", []int64{}, "List of source positions. Default is empty array. Counting start at 1.")
command.Flags().StringArrayVar(&sourceNames, "source-names", []string{}, "List of source names. Default is an empty array.")
addServerSideDiffPerfFlags(command, &serverSideDiffConcurrency, &serverSideDiffMaxBatchKB)
return command
}
func getAppNamesBySelector(ctx context.Context, appIf application.ApplicationServiceClient, selector string) ([]string, error) {
appNames := []string{}
if selector != "" {
list, err := appIf.List(ctx, &application.ApplicationQuery{Selector: ptr.To(selector)})
if err != nil {
return []string{}, err
}
// unlike list, we'd want to fail if nothing was found
if len(list.Items) == 0 {
return []string{}, fmt.Errorf("no apps match selector %v", selector)
}
for _, i := range list.Items {
appNames = append(appNames, i.QualifiedName())
}
}
return appNames, nil
}
// ResourceState tracks the state of a resource when waiting on an application status.
type resourceState struct {
Group string
Kind string
Namespace string
Name string
Status string
Health string
Hook string
Message string
}
// Key returns a unique-ish key for the resource.
func (rs *resourceState) Key() string {
return fmt.Sprintf("%s/%s/%s/%s", rs.Group, rs.Kind, rs.Namespace, rs.Name)
}
func (rs *resourceState) FormatItems() []any {
timeStr := time.Now().Format("2006-01-02T15:04:05-07:00")
return []any{timeStr, rs.Group, rs.Kind, rs.Namespace, rs.Name, rs.Status, rs.Health, rs.Hook, rs.Message}
}
// Merge merges the new state with any different contents from another resourceState.
// Blank fields in the receiver state will be updated to non-blank.
// Non-blank fields in the receiver state will never be updated to blank.
// Returns whether or not any keys were updated.
func (rs *resourceState) Merge(newState *resourceState) bool {
updated := false
for _, field := range []string{"Status", "Health", "Hook", "Message"} {
v := reflect.ValueOf(rs).Elem().FieldByName(field)
currVal := v.String()
newVal := reflect.ValueOf(newState).Elem().FieldByName(field).String()
if newVal != "" && currVal != newVal {
v.SetString(newVal)
updated = true
}
}
return updated
}
func getResourceStates(app *argoappv1.Application, selectedResources []*argoappv1.SyncOperationResource) []*resourceState {
var states []*resourceState
resourceByKey := make(map[kube.ResourceKey]argoappv1.ResourceStatus)
for i := range app.Status.Resources {
res := app.Status.Resources[i]
resourceByKey[kube.NewResourceKey(res.Group, res.Kind, res.Namespace, res.Name)] = res
}
// print most resources info along with most recent operation results
if app.Status.OperationState != nil && app.Status.OperationState.SyncResult != nil {
for _, res := range app.Status.OperationState.SyncResult.Resources {
sync := string(res.HookPhase)
health := string(res.Status)
key := kube.NewResourceKey(res.Group, res.Kind, res.Namespace, res.Name)
if resource, ok := resourceByKey[key]; ok && res.HookType == "" {
health = ""
if resource.Health != nil {
health = string(resource.Health.Status)
}
sync = string(resource.Status)
}
states = append(states, &resourceState{
Group: res.Group, Kind: res.Kind, Namespace: res.Namespace, Name: res.Name, Status: sync, Health: health, Hook: string(res.HookType), Message: res.Message,
})
delete(resourceByKey, kube.NewResourceKey(res.Group, res.Kind, res.Namespace, res.Name))
}
}
resKeys := make([]kube.ResourceKey, 0)
for k := range resourceByKey {
resKeys = append(resKeys, k)
}
sort.Slice(resKeys, func(i, j int) bool {
return resKeys[i].String() < resKeys[j].String()
})
// print rest of resources which were not part of most recent operation
for _, resKey := range resKeys {
res := resourceByKey[resKey]
health := ""
if res.Health != nil {
health = string(res.Health.Status)
}
states = append(states, &resourceState{
Group: res.Group, Kind: res.Kind, Namespace: res.Namespace, Name: res.Name, Status: string(res.Status), Health: health, Hook: "", Message: "",
})
}
// filter out not selected resources
if len(selectedResources) > 0 {
for i := len(states) - 1; i >= 0; i-- {
res := states[i]
if !argo.IncludeResource(res.Name, res.Namespace, schema.GroupVersionKind{Group: res.Group, Kind: res.Kind}, selectedResources) {
states = append(states[:i], states[i+1:]...)
}
}
}
return states
}
// filterAppResources selects the app resources that match atleast one of the resource filters.
func filterAppResources(app *argoappv1.Application, selectedResources []*argoappv1.SyncOperationResource) []*argoappv1.SyncOperationResource {
var filteredResources []*argoappv1.SyncOperationResource
if app != nil && len(selectedResources) > 0 {
for i := range app.Status.Resources {
appResource := app.Status.Resources[i]
if (argo.IncludeResource(appResource.Name, appResource.Namespace,
schema.GroupVersionKind{Group: appResource.Group, Kind: appResource.Kind}, selectedResources)) {
filteredResources = append(filteredResources, &argoappv1.SyncOperationResource{
Group: appResource.Group,
Kind: appResource.Kind,
Name: appResource.Name,
Namespace: appResource.Namespace,
})
}
}
}
return filteredResources
}
func groupResourceStates(app *argoappv1.Application, selectedResources []*argoappv1.SyncOperationResource) map[string]*resourceState {
resStates := make(map[string]*resourceState)
for _, result := range getResourceStates(app, selectedResources) {
key := result.Key()
if prev, ok := resStates[key]; ok {
prev.Merge(result)
} else {
resStates[key] = result
}
}
return resStates
}
// check if resource health, sync and operation statuses matches watch options
func checkResourceStatus(watch watchOpts, healthStatus string, syncStatus string, operationStatus *argoappv1.Operation, hydrationFinished bool) bool {
if watch.delete {
return false
}
healthBeingChecked := watch.suspended || watch.health || watch.degraded
healthCheckPassed := true
if healthBeingChecked {
healthCheckPassed = false
if watch.health {
healthCheckPassed = healthCheckPassed || healthStatus == string(health.HealthStatusHealthy)
}
if watch.suspended {
healthCheckPassed = healthCheckPassed || healthStatus == string(health.HealthStatusSuspended)
}
if watch.degraded {
healthCheckPassed = healthCheckPassed || healthStatus == string(health.HealthStatusDegraded)
}
}
synced := !watch.sync || syncStatus == string(argoappv1.SyncStatusCodeSynced)
operational := !watch.operation || operationStatus == nil
hydrated := !watch.hydrated || hydrationFinished
return synced && healthCheckPassed && operational && hydrated
}
// resourceParentChild gets the latest state of the app and the latest state of the app's resource tree and then
// constructs the necessary data structures to print the app as a tree.
func resourceParentChild(ctx context.Context, acdClient argocdclient.Client, appName string, appNs string) (map[string]argoappv1.ResourceNode, map[string][]string, map[string]struct{}, map[string]*resourceState) {
_, appIf := acdClient.NewApplicationClientOrDie()
mapUIDToNode, mapParentToChild, parentNode := parentChildDetails(ctx, appIf, appName, appNs)
app, err := appIf.Get(ctx, &application.ApplicationQuery{Name: ptr.To(appName), AppNamespace: ptr.To(appNs)})
errors.CheckError(err)
mapNodeNameToResourceState := make(map[string]*resourceState)
for _, res := range getResourceStates(app, nil) {
mapNodeNameToResourceState[res.Kind+"/"+res.Name] = res
}
return mapUIDToNode, mapParentToChild, parentNode, mapNodeNameToResourceState
}
const waitFormatString = "%s\t%5s\t%10s\t%10s\t%20s\t%8s\t%7s\t%10s\t%s\n"
// AppWithLock encapsulates the application and its lock
type AppWithLock struct {
mu sync.Mutex
app *argoappv1.Application
}
// NewAppWithLock creates a new AppWithLock instance
func NewAppWithLock() *AppWithLock {
return &AppWithLock{}
}
// SetApp safely updates the application
func (a *AppWithLock) SetApp(app *argoappv1.Application) {
a.mu.Lock()
defer a.mu.Unlock()
a.app = app
}
// GetApp safely retrieves the application
func (a *AppWithLock) GetApp() *argoappv1.Application {
a.mu.Lock()
defer a.mu.Unlock()
return a.app
}
// waitOnApplicationStatus watches an application and blocks until either the desired watch conditions
// are fulfilled or we reach the timeout. Returns the app once desired conditions have been filled.
// Additionally return the operationState at time of fulfilment (which may be different than returned app).
func waitOnApplicationStatus(ctx context.Context, acdClient argocdclient.Client, appName string, timeout uint, watch watchOpts, selectedResources []*argoappv1.SyncOperationResource, output string) (*argoappv1.Application, *argoappv1.OperationState, error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
appWithLock := NewAppWithLock()
// refresh controls whether or not we refresh the app before printing the final status.
// We only want to do this when an operation is in progress, since operations are the only
// time when the sync status lags behind when an operation completes
refresh := false
// appURL is declared here so that it can be used in the printFinalStatus function when the context is cancelled
appURL := getAppURL(ctx, acdClient, appName)
// printSummary controls whether we print the app summary table, OperationState, and ResourceState
// We don't want to print these when output type is json or yaml, as the output would become unparsable.
printSummary := output != "json" && output != "yaml"
appRealName, appNs := argo.ParseFromQualifiedName(appName, "")
printFinalStatus := func(app *argoappv1.Application) *argoappv1.Application {
var err error
if refresh {
conn, appClient := acdClient.NewApplicationClientOrDie()
refreshType := string(argoappv1.RefreshTypeNormal)
app, err = appClient.Get(ctx, &application.ApplicationQuery{
Name: &appRealName,
Refresh: &refreshType,
AppNamespace: &appNs,
})
errors.CheckError(err)
_ = conn.Close()
}
if printSummary {
fmt.Println()
printAppSummaryTable(app, appURL, nil)
fmt.Println()
if watch.operation {
printOperationResult(app.Status.OperationState)
}
}
switch output {
case "yaml", "json":
err := PrintResource(app, output)
errors.CheckError(err)
case "wide", "":
if len(app.Status.Resources) > 0 {
fmt.Println()
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
printAppResources(w, app)
_ = w.Flush()
}
case "tree":
mapUIDToNode, mapParentToChild, parentNode, mapNodeNameToResourceState := resourceParentChild(ctx, acdClient, appRealName, appNs)
if len(mapUIDToNode) > 0 {
fmt.Println()
printTreeView(mapUIDToNode, mapParentToChild, parentNode, mapNodeNameToResourceState)
}
case "tree=detailed":
mapUIDToNode, mapParentToChild, parentNode, mapNodeNameToResourceState := resourceParentChild(ctx, acdClient, appRealName, appNs)
if len(mapUIDToNode) > 0 {
fmt.Println()
printTreeViewDetailed(mapUIDToNode, mapParentToChild, parentNode, mapNodeNameToResourceState)
}
default:
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
}
return app
}
if timeout != 0 {
time.AfterFunc(time.Duration(timeout)*time.Second, func() {
conn, appClient := acdClient.NewApplicationClientOrDie()
defer conn.Close()
// We want to print the final status of the app even if the conditions are not met
if printSummary {
fmt.Println()
fmt.Println("This is the state of the app after wait timed out:")
}
// Setting refresh = false because we don't want printFinalStatus to execute a refresh
refresh = false
// Updating the app object to the latest state
app, err := appClient.Get(ctx, &application.ApplicationQuery{
Name: &appRealName,
AppNamespace: &appNs,
})
errors.CheckError(err)
// Update the app object
appWithLock.SetApp(app)
// Cancel the context to stop the watch
cancel()
if printSummary {
fmt.Println()
fmt.Println("The command timed out waiting for the conditions to be met.")
}
})
}
w := tabwriter.NewWriter(os.Stdout, 5, 0, 2, ' ', 0)
if printSummary {
_, _ = fmt.Fprintf(w, waitFormatString, "TIMESTAMP", "GROUP", "KIND", "NAMESPACE", "NAME", "STATUS", "HEALTH", "HOOK", "MESSAGE")
}
prevStates := make(map[string]*resourceState)
conn, appClient := acdClient.NewApplicationClientOrDie()
defer utilio.Close(conn)
app, err := appClient.Get(ctx, &application.ApplicationQuery{
Name: &appRealName,
AppNamespace: &appNs,
})
errors.CheckError(err)
appWithLock.SetApp(app) // Update the app object
// printFinalStatus() will refresh and update the app object, potentially causing the app's
// status.operationState to be different than the version when we break out of the event loop.
// This means the app.status is unreliable for determining the final state of the operation.
// finalOperationState captures the operationState as it was seen when we met the conditions of
// the wait, so the caller can rely on it to determine the outcome of the operation.
// See: https://github.com/argoproj/argo-cd/issues/5592
finalOperationState := appWithLock.GetApp().Status.OperationState
appEventCh := acdClient.WatchApplicationWithRetry(ctx, appName, appWithLock.GetApp().ResourceVersion)
for appEvent := range appEventCh {
appWithLock.SetApp(&appEvent.Application)
app = appWithLock.GetApp()
finalOperationState = app.Status.OperationState
operationInProgress := false
if watch.delete && appEvent.Type == k8swatch.Deleted {
fmt.Printf("Application '%s' deleted\n", app.QualifiedName())
return nil, nil, nil
}
// consider the operation is in progress
if app.Operation != nil {
// if it just got requested
operationInProgress = true
if !app.Operation.DryRun() {
refresh = true
}
} else if app.Status.OperationState != nil {
if app.Status.OperationState.FinishedAt == nil {
// if it is not finished yet
operationInProgress = true
} else if !app.Status.OperationState.Operation.DryRun() && (app.Status.ReconciledAt == nil || app.Status.ReconciledAt.Before(app.Status.OperationState.FinishedAt)) {
// if it is just finished and we need to wait for controller to reconcile app once after syncing
operationInProgress = true
}
}
hydrationFinished := app.Status.SourceHydrator.CurrentOperation != nil && app.Status.SourceHydrator.CurrentOperation.Phase == argoappv1.HydrateOperationPhaseHydrated && app.Status.SourceHydrator.CurrentOperation.SourceHydrator.DeepEquals(app.Status.SourceHydrator.LastSuccessfulOperation.SourceHydrator) && app.Status.SourceHydrator.CurrentOperation.DrySHA == app.Status.SourceHydrator.LastSuccessfulOperation.DrySHA
var selectedResourcesAreReady bool
// If selected resources are included, wait only on those resources, otherwise wait on the application as a whole.
if len(selectedResources) > 0 {
selectedResourcesAreReady = true
for _, state := range getResourceStates(app, selectedResources) {
resourceIsReady := checkResourceStatus(watch, state.Health, state.Status, appEvent.Application.Operation, hydrationFinished)
if !resourceIsReady {
selectedResourcesAreReady = false
break
}
}
} else {
// Wait on the application as a whole
selectedResourcesAreReady = checkResourceStatus(watch, string(app.Status.Health.Status), string(app.Status.Sync.Status), appEvent.Application.Operation, hydrationFinished)
}
if selectedResourcesAreReady && (!operationInProgress || !watch.operation) {
app = printFinalStatus(app)
return app, finalOperationState, nil
}
newStates := groupResourceStates(app, selectedResources)
for _, newState := range newStates {
var doPrint bool
stateKey := newState.Key()
if prevState, found := prevStates[stateKey]; found {
if watch.health && prevState.Health != string(health.HealthStatusUnknown) && prevState.Health != string(health.HealthStatusDegraded) && newState.Health == string(health.HealthStatusDegraded) {
_ = printFinalStatus(app)
return nil, finalOperationState, fmt.Errorf("application '%s' health state has transitioned from %s to %s", appName, prevState.Health, newState.Health)
}
doPrint = prevState.Merge(newState)
} else {
prevStates[stateKey] = newState
doPrint = true
}
if doPrint && printSummary {
_, _ = fmt.Fprintf(w, waitFormatString, prevStates[stateKey].FormatItems()...)
}
}
_ = w.Flush()
}
_ = printFinalStatus(appWithLock.GetApp())
return nil, finalOperationState, fmt.Errorf("timed out (%ds) waiting for app %q match desired state", timeout, appName)
}
// 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, sourcePosition int) {
if len(parameters) == 0 {
return
}
source := app.Spec.GetSourcePtrByPosition(sourcePosition)
var sourceType argoappv1.ApplicationSourceType
if st, _ := source.ExplicitType(); st != nil {
sourceType = *st
} else if app.Status.SourceType != "" {
sourceType = app.Status.SourceType
} else if len(strings.SplitN(parameters[0], "=", 2)) == 2 {
sourceType = argoappv1.ApplicationSourceTypeHelm
}
switch sourceType {
case argoappv1.ApplicationSourceTypeHelm:
if source.Helm == nil {
source.Helm = &argoappv1.ApplicationSourceHelm{}
}
for _, p := range parameters {
newParam, err := argoappv1.NewHelmParameter(p, false)
if err != nil {
log.Error(err)
continue
}
source.Helm.AddParameter(*newParam)
}
default:
log.Fatalf("Parameters can only be set against Helm applications")
}
}
// Print list of history ID's for an application.
func printApplicationHistoryIDs(revHistory []argoappv1.RevisionHistory) {
for _, depInfo := range revHistory {
fmt.Println(depInfo.ID)
}
}
// Print a history table for an application.
func printApplicationHistoryTable(revHistory []argoappv1.RevisionHistory) {
maxAllowedRevisions := 7
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
type history struct {
id int64
date string
revision string
}
varHistory := map[string][]history{}
varHistoryKeys := []string{}
for _, depInfo := range revHistory {
if depInfo.Sources != nil {
for i, sourceInfo := range depInfo.Sources {
rev := sourceInfo.TargetRevision
if len(depInfo.Revisions) == len(depInfo.Sources) && len(depInfo.Revisions[i]) >= maxAllowedRevisions {
rev = fmt.Sprintf("%s (%s)", rev, depInfo.Revisions[i][0:maxAllowedRevisions])
}
if _, ok := varHistory[sourceInfo.RepoURL]; !ok {
varHistoryKeys = append(varHistoryKeys, sourceInfo.RepoURL)
}
varHistory[sourceInfo.RepoURL] = append(varHistory[sourceInfo.RepoURL], history{
id: depInfo.ID,
date: depInfo.DeployedAt.String(),
revision: rev,
})
}
} else {
rev := depInfo.Source.TargetRevision
if len(depInfo.Revision) >= maxAllowedRevisions {
rev = fmt.Sprintf("%s (%s)", rev, depInfo.Revision[0:maxAllowedRevisions])
}
if _, ok := varHistory[depInfo.Source.RepoURL]; !ok {
varHistoryKeys = append(varHistoryKeys, depInfo.Source.RepoURL)
}
varHistory[depInfo.Source.RepoURL] = append(varHistory[depInfo.Source.RepoURL], history{
id: depInfo.ID,
date: depInfo.DeployedAt.String(),
revision: rev,
})
}
}
for i, key := range varHistoryKeys {
_, _ = fmt.Fprintf(w, "SOURCE\t%s\n", key)
_, _ = fmt.Fprintf(w, "ID\tDATE\tREVISION\n")
for _, history := range varHistory[key] {
_, _ = fmt.Fprintf(w, "%d\t%s\t%s\n", history.id, history.date, history.revision)
}
// Add a newline if it's not the last iteration
if i < len(varHistoryKeys)-1 {
_, _ = fmt.Fprintf(w, "\n")
}
}
_ = w.Flush()
}
// NewApplicationHistoryCommand returns a new instance of an `argocd app history` command
func NewApplicationHistoryCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
output string
appNamespace string
)
command := &cobra.Command{
Use: "history APPNAME",
Short: "Show application deployment history",
Run: func(c *cobra.Command, args []string) {
ctx := c.Context()
if len(args) != 1 {
c.HelpFunc()(c, args)
os.Exit(1)
}
conn, appIf := headless.NewClientOrDie(clientOpts, c).NewApplicationClientOrDie()
defer utilio.Close(conn)
appName, appNs := argo.ParseFromQualifiedName(args[0], appNamespace)
app, err := appIf.Get(ctx, &application.ApplicationQuery{
Name: &appName,
AppNamespace: &appNs,
})
errors.CheckError(err)
if output == "id" {
printApplicationHistoryIDs(app.Status.History)
} else {
printApplicationHistoryTable(app.Status.History)
}
},
}
command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Only show application deployment history in namespace")
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: wide|id")
return command
}
func findRevisionHistory(application *argoappv1.Application, historyId int64) (*argoappv1.RevisionHistory, error) {
// in case if history id not passed and need fetch previous history revision
if historyId == -1 {
l := len(application.Status.History)
if l < 2 {
return nil, fmt.Errorf("application '%s' should have at least two successful deployments", application.Name)
}
return &application.Status.History[l-2], nil
}
for _, di := range application.Status.History {
if di.ID == historyId {
return &di, nil
}
}
return nil, fmt.Errorf("application '%s' does not have deployment id '%d' in history", application.Name, historyId)
}
// NewApplicationRollbackCommand returns a new instance of an `argocd app rollback` command
func NewApplicationRollbackCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
prune bool
timeout uint
output string
appNamespace string
)
command := &cobra.Command{
Use: "rollback APPNAME [ID]",
Short: "Rollback application to a previous deployed version by History ID, omitted will Rollback to the previous version",
Run: func(c *cobra.Command, args []string) {
ctx := c.Context()
if len(args) == 0 {
c.HelpFunc()(c, args)
os.Exit(1)
}
appName, appNs := argo.ParseFromQualifiedName(args[0], appNamespace)
var err error
depID := -1
if len(args) > 1 {
depID, err = strconv.Atoi(args[1])
errors.CheckError(err)
}
acdClient := headless.NewClientOrDie(clientOpts, c)
conn, appIf := acdClient.NewApplicationClientOrDie()
defer utilio.Close(conn)
app, err := appIf.Get(ctx, &application.ApplicationQuery{
Name: &appName,
AppNamespace: &appNs,
})
errors.CheckError(err)
depInfo, err := findRevisionHistory(app, int64(depID))
errors.CheckError(err)
_, err = appIf.Rollback(ctx, &application.ApplicationRollbackRequest{
Name: &appName,
AppNamespace: &appNs,
Id: ptr.To(depInfo.ID),
Prune: ptr.To(prune),
})
errors.CheckError(err)
_, _, err = waitOnApplicationStatus(ctx, acdClient, app.QualifiedName(), timeout, watchOpts{
operation: true,
}, nil, output)
errors.CheckError(err)
},
}
command.Flags().BoolVar(&prune, "prune", false, "Allow deleting unexpected resources")
command.Flags().UintVar(&timeout, "timeout", defaultCheckTimeoutSeconds, "Time out after this many seconds")
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|tree|tree=detailed")
command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Rollback application in namespace")
return command
}
const (
printOpFmtStr = "%-20s%s\n"
defaultCheckTimeoutSeconds = 0
)
func printOperationResult(opState *argoappv1.OperationState) {
if opState == nil {
return
}
if opState.SyncResult != nil {
fmt.Printf(printOpFmtStr, "Operation:", "Sync")
if opState.SyncResult.Sources != nil && opState.SyncResult.Revisions != nil {
fmt.Printf(printOpFmtStr, "Sync Revision:", strings.Join(opState.SyncResult.Revisions, ", "))
} else {
fmt.Printf(printOpFmtStr, "Sync Revision:", opState.SyncResult.Revision)
}
}
fmt.Printf(printOpFmtStr, "Phase:", opState.Phase)
fmt.Printf(printOpFmtStr, "Start:", opState.StartedAt)
fmt.Printf(printOpFmtStr, "Finished:", opState.FinishedAt)
var duration time.Duration
if !opState.FinishedAt.IsZero() {
duration = time.Second * time.Duration(opState.FinishedAt.Unix()-opState.StartedAt.Unix())
} else {
duration = time.Second * time.Duration(time.Now().UTC().Unix()-opState.StartedAt.Unix())
}
fmt.Printf(printOpFmtStr, "Duration:", duration)
if opState.Message != "" {
fmt.Printf(printOpFmtStr, "Message:", opState.Message)
}
}
// NewApplicationManifestsCommand returns a new instance of an `argocd app manifests` command
func NewApplicationManifestsCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
source string
revision string
revisions []string
sourcePositions []int64
sourceNames []string
local string
localRepoRoot string
)
command := &cobra.Command{
Use: "manifests APPNAME",
Short: "Print manifests of an application",
Example: templates.Examples(`
# Get manifests for an application
argocd app manifests my-app
# Get manifests for an application at a specific revision
argocd app manifests my-app --revision 0.0.1
# Get manifests for a multi-source application at specific revisions for specific sources
argocd app manifests my-app --revisions 0.0.1 --source-names src-base --revisions 0.0.2 --source-names src-values
# Get manifests for a multi-source application at specific revisions for specific sources
argocd app manifests my-app --revisions 0.0.1 --source-positions 1 --revisions 0.0.2 --source-positions 2
`),
Run: func(c *cobra.Command, args []string) {
ctx := c.Context()
if len(args) != 1 {
c.HelpFunc()(c, args)
os.Exit(1)
}
if len(sourceNames) > 0 && len(sourcePositions) > 0 {
errors.Fatal(errors.ErrorGeneric, "Only one of source-positions and source-names can be specified.")
}
if len(sourcePositions) > 0 && len(revisions) != len(sourcePositions) {
errors.Fatal(errors.ErrorGeneric, "While using --revisions and --source-positions, length of values for both flags should be same.")
}
if len(sourceNames) > 0 && len(revisions) != len(sourceNames) {
errors.Fatal(errors.ErrorGeneric, "While using --revisions and --source-names, length of values for both flags should be same.")
}
for _, pos := range sourcePositions {
if pos <= 0 {
log.Fatal("source-position cannot be less than or equal to 0, Counting starts at 1")
}
}
appName, appNs := argo.ParseFromQualifiedName(args[0], "")
clientset := headless.NewClientOrDie(clientOpts, c)
conn, appIf := clientset.NewApplicationClientOrDie()
defer utilio.Close(conn)
app, err := appIf.Get(context.Background(), &application.ApplicationQuery{
Name: &appName,
AppNamespace: &appNs,
})
errors.CheckError(err)
if len(sourceNames) > 0 {
sourceNameToPosition := getSourceNameToPositionMap(app)
for _, name := range sourceNames {
pos, ok := sourceNameToPosition[name]
if !ok {
log.Fatalf("Unknown source name '%s'", name)
}
sourcePositions = append(sourcePositions, pos)
}
}
resources, err := appIf.ManagedResources(ctx, &application.ResourcesQuery{
ApplicationName: &appName,
AppNamespace: &appNs,
})
errors.CheckError(err)
var unstructureds []*unstructured.Unstructured
switch source {
case "git":
switch {
case local != "":
settingsConn, settingsIf := clientset.NewSettingsClientOrDie()
defer utilio.Close(settingsConn)
argoSettings, err := settingsIf.Get(context.Background(), &settings.SettingsQuery{})
errors.CheckError(err)
clusterConn, clusterIf := clientset.NewClusterClientOrDie()
defer utilio.Close(clusterConn)
cluster, err := clusterIf.Get(context.Background(), &clusterpkg.ClusterQuery{Name: app.Spec.Destination.Name, Server: app.Spec.Destination.Server})
errors.CheckError(err)
proj := getProject(ctx, c, clientOpts, app.Spec.Project)
unstructureds = getLocalObjects(context.Background(), app, proj.Project, local, localRepoRoot, argoSettings.AppLabelKey, cluster.Info.ServerVersion, cluster.Info.APIVersions, argoSettings.KustomizeOptions, argoSettings.TrackingMethod)
case len(revisions) > 0 && len(sourcePositions) > 0:
q := application.ApplicationManifestQuery{
Name: &appName,
AppNamespace: &appNs,
Revision: ptr.To(revision),
Revisions: revisions,
SourcePositions: sourcePositions,
}
res, err := appIf.GetManifests(ctx, &q)
errors.CheckError(err)
for _, mfst := range res.Manifests {
obj, err := argoappv1.UnmarshalToUnstructured(mfst)
errors.CheckError(err)
unstructureds = append(unstructureds, obj)
}
case revision != "":
q := application.ApplicationManifestQuery{
Name: &appName,
AppNamespace: &appNs,
Revision: ptr.To(revision),
}
res, err := appIf.GetManifests(ctx, &q)
errors.CheckError(err)
for _, mfst := range res.Manifests {
obj, err := argoappv1.UnmarshalToUnstructured(mfst)
errors.CheckError(err)
unstructureds = append(unstructureds, obj)
}
default:
targetObjs, err := targetObjects(resources.Items)
errors.CheckError(err)
unstructureds = targetObjs
}
case "live":
liveObjs, err := cmdutil.LiveObjects(resources.Items)
errors.CheckError(err)
unstructureds = liveObjs
default:
log.Fatalf("Unknown source type '%s'", source)
}
for _, obj := range unstructureds {
fmt.Println("---")
yamlBytes, err := yaml.Marshal(obj)
errors.CheckError(err)
fmt.Printf("%s\n", yamlBytes)
}
},
}
command.Flags().StringVar(&source, "source", "git", "Source of manifests. One of: live|git")
command.Flags().StringVar(&revision, "revision", "", "Show manifests at a specific revision")
command.Flags().StringArrayVar(&revisions, "revisions", []string{}, "Show manifests at specific revisions for the source at position in source-positions")
command.Flags().Int64SliceVar(&sourcePositions, "source-positions", []int64{}, "List of source positions. Default is empty array. Counting start at 1.")
command.Flags().StringArrayVar(&sourceNames, "source-names", []string{}, "List of source names. Default is an empty array.")
command.Flags().StringVar(&local, "local", "", "If set, show locally-generated manifests. Value is the absolute path to app manifests within the manifest repo. Example: '/home/username/apps/env/app-1'.")
command.Flags().StringVar(&localRepoRoot, "local-repo-root", ".", "Path to the local repository root. Used together with --local allows setting the repository root. Example: '/home/username/apps'.")
return command
}
// NewApplicationTerminateOpCommand returns a new instance of an `argocd app terminate-op` command
func NewApplicationTerminateOpCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
command := &cobra.Command{
Use: "terminate-op APPNAME",
Short: "Terminate running operation of an application",
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)
_, err := appIf.TerminateOperation(ctx, &application.OperationTerminateRequest{
Name: &appName,
AppNamespace: &appNs,
})
errors.CheckError(err)
fmt.Printf("Application '%s' operation terminating\n", appName)
},
}
return command
}
func NewApplicationEditCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var appNamespace string
command := &cobra.Command{
Use: "edit APPNAME",
Short: "Edit application",
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], appNamespace)
conn, appIf := headless.NewClientOrDie(clientOpts, c).NewApplicationClientOrDie()
defer utilio.Close(conn)
app, err := appIf.Get(ctx, &application.ApplicationQuery{
Name: &appName,
AppNamespace: &appNs,
})
errors.CheckError(err)
appData, err := json.Marshal(app.Spec)
errors.CheckError(err)
appData, err = yaml.JSONToYAML(appData)
errors.CheckError(err)
cli.InteractiveEdit(appName+"-*-edit.yaml", appData, func(input []byte) error {
input, err = yaml.YAMLToJSON(input)
if err != nil {
return fmt.Errorf("error converting YAML to JSON: %w", err)
}
updatedSpec := argoappv1.ApplicationSpec{}
err = json.Unmarshal(input, &updatedSpec)
if err != nil {
return fmt.Errorf("error unmarshaling input into application spec: %w", err)
}
var appOpts cmdutil.AppOptions
// 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,
Validate: &appOpts.Validate,
AppNamespace: &appNs,
})
if err != nil {
return fmt.Errorf("failed to update application spec: %w", err)
}
return nil
})
},
}
command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Only edit application in namespace")
return command
}
func NewApplicationPatchCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
patch string
patchType string
appNamespace string
)
command := cobra.Command{
Use: "patch APPNAME",
Short: "Patch application",
Example: ` # Update an application's source path using json patch
argocd app patch myapplication --patch='[{"op": "replace", "path": "/spec/source/path", "value": "newPath"}]' --type json
# Update an application's repository target revision using merge patch
argocd app patch myapplication --patch '{"spec": { "source": { "targetRevision": "master" } }}' --type merge`,
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], appNamespace)
conn, appIf := headless.NewClientOrDie(clientOpts, c).NewApplicationClientOrDie()
defer utilio.Close(conn)
patchedApp, err := appIf.Patch(ctx, &application.ApplicationPatchRequest{
Name: &appName,
Patch: &patch,
PatchType: &patchType,
AppNamespace: &appNs,
})
errors.CheckError(err)
yamlBytes, err := yaml.Marshal(patchedApp)
errors.CheckError(err)
fmt.Println(string(yamlBytes))
},
}
command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Only patch application in namespace")
command.Flags().StringVar(&patch, "patch", "", "Patch body")
command.Flags().StringVar(&patchType, "type", "json", "The type of patch being provided; one of [json merge]")
return &command
}
// NewApplicationAddSourceCommand returns a new instance of an `argocd app add-source` command
func NewApplicationAddSourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
appOpts cmdutil.AppOptions
appNamespace string
)
command := &cobra.Command{
Use: "add-source APPNAME",
Short: "Adds a source to the list of sources in the application",
Example: ` # Append a source to the list of sources in the application
argocd app add-source guestbook --repo https://github.com/argoproj/argocd-example-apps.git --path guestbook --source-name guestbook`,
Run: func(c *cobra.Command, args []string) {
ctx := c.Context()
if len(args) != 1 {
c.HelpFunc()(c, args)
os.Exit(1)
}
argocdClient := headless.NewClientOrDie(clientOpts, c)
conn, appIf := argocdClient.NewApplicationClientOrDie()
defer utilio.Close(conn)
appName, appNs := argo.ParseFromQualifiedName(args[0], appNamespace)
app, err := appIf.Get(ctx, &application.ApplicationQuery{
Name: &appName,
Refresh: getRefreshType(false, false),
AppNamespace: &appNs,
})
errors.CheckError(err)
if c.Flags() == nil {
errors.Fatal(errors.ErrorGeneric, "ApplicationSource needs atleast repoUrl, path or chart or ref field. No source to add.")
}
if len(app.Spec.Sources) > 0 {
appSource, _ := cmdutil.ConstructSource(&argoappv1.ApplicationSource{}, appOpts, c.Flags())
// sourcePosition is the index at which new source will be appended to spec.Sources
sourcePosition := len(app.Spec.GetSources())
app.Spec.Sources = append(app.Spec.Sources, *appSource)
setParameterOverrides(app, appOpts.Parameters, sourcePosition)
_, err = appIf.UpdateSpec(ctx, &application.ApplicationUpdateSpecRequest{
Name: &app.Name,
Spec: &app.Spec,
Validate: &appOpts.Validate,
AppNamespace: &appNs,
})
errors.CheckError(err)
fmt.Printf("Application '%s' updated successfully\n", app.Name)
} else {
errors.Fatal(errors.ErrorGeneric, fmt.Sprintf("Cannot add source: application %s does not have spec.sources defined", appName))
}
},
}
cmdutil.AddAppFlags(command, &appOpts)
command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Namespace of the target application where the source will be appended")
return command
}
// NewApplicationRemoveSourceCommand returns a new instance of an `argocd app remove-source` command
func NewApplicationRemoveSourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
sourcePosition int
sourceName string
appNamespace string
)
command := &cobra.Command{
Use: "remove-source APPNAME",
Short: "Remove a source from multiple sources application.",
Example: ` # Remove the source at position 1 from application's sources. Counting starts at 1.
argocd app remove-source myapplication --source-position 1
# Remove the source named "test" from application's sources.
argocd app remove-source myapplication --source-name test`,
Run: func(c *cobra.Command, args []string) {
ctx := c.Context()
if len(args) != 1 {
c.HelpFunc()(c, args)
os.Exit(1)
}
if sourceName == "" && sourcePosition <= 0 {
errors.Fatal(errors.ErrorGeneric, "Value of source-position must be greater than 0")
}
argocdClient := headless.NewClientOrDie(clientOpts, c)
conn, appIf := argocdClient.NewApplicationClientOrDie()
defer utilio.Close(conn)
appName, appNs := argo.ParseFromQualifiedName(args[0], appNamespace)
app, err := appIf.Get(ctx, &application.ApplicationQuery{
Name: &appName,
Refresh: getRefreshType(false, false),
AppNamespace: &appNs,
})
errors.CheckError(err)
if sourceName != "" && sourcePosition != -1 {
errors.Fatal(errors.ErrorGeneric, "Only one of source-position and source-name can be specified.")
}
if sourceName != "" {
sourceNameToPosition := getSourceNameToPositionMap(app)
pos, ok := sourceNameToPosition[sourceName]
if !ok {
log.Fatalf("Unknown source name '%s'", sourceName)
}
sourcePosition = int(pos)
}
if !app.Spec.HasMultipleSources() {
errors.Fatal(errors.ErrorGeneric, "Application does not have multiple sources configured")
}
if len(app.Spec.GetSources()) == 1 {
errors.Fatal(errors.ErrorGeneric, "Cannot remove the only source remaining in the app")
}
if len(app.Spec.GetSources()) < sourcePosition {
errors.Fatal(errors.ErrorGeneric, fmt.Sprintf("Application does not have source at %d\n", sourcePosition))
}
app.Spec.Sources = append(app.Spec.Sources[:sourcePosition-1], app.Spec.Sources[sourcePosition:]...)
promptUtil := utils.NewPrompt(clientOpts.PromptsEnabled)
canDelete := promptUtil.Confirm("Are you sure you want to delete the source? [y/n]")
if canDelete {
_, err = appIf.UpdateSpec(ctx, &application.ApplicationUpdateSpecRequest{
Name: &app.Name,
Spec: &app.Spec,
AppNamespace: &appNs,
})
errors.CheckError(err)
fmt.Printf("Application '%s' updated successfully\n", app.Name)
} else {
fmt.Println("The command to delete the source was cancelled")
}
},
}
command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Namespace of the target application where the source will be appended")
command.Flags().IntVar(&sourcePosition, "source-position", -1, "Position of the source from the list of sources of the app. Counting starts at 1.")
command.Flags().StringVar(&sourceName, "source-name", "", "Name of the source from the list of sources of the app.")
return command
}
func NewApplicationConfirmDeletionCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var appNamespace string
command := &cobra.Command{
Use: "confirm-deletion APPNAME",
Short: "Confirms deletion/pruning of an application resources",
Run: func(c *cobra.Command, args []string) {
ctx := c.Context()
if len(args) != 1 {
c.HelpFunc()(c, args)
os.Exit(1)
}
argocdClient := headless.NewClientOrDie(clientOpts, c)
conn, appIf := argocdClient.NewApplicationClientOrDie()
defer utilio.Close(conn)
appName, appNs := argo.ParseFromQualifiedName(args[0], appNamespace)
app, err := appIf.Get(ctx, &application.ApplicationQuery{
Name: &appName,
Refresh: getRefreshType(false, false),
AppNamespace: &appNs,
})
errors.CheckError(err)
annotations := app.Annotations
if annotations == nil {
annotations = map[string]string{}
app.Annotations = annotations
}
annotations[common.AnnotationDeletionApproved] = metav1.Now().Format(time.RFC3339)
_, err = appIf.Update(ctx, &application.ApplicationUpdateRequest{
Application: app,
Validate: ptr.To(false),
Project: &app.Spec.Project,
})
errors.CheckError(err)
fmt.Printf("Application '%s' updated successfully\n", app.Name)
},
}
command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Namespace of the target application where the source will be appended")
return command
}
// prepareObjectsForDiff prepares objects for diffing using the switch statement
// to handle different diff options and building the objKeyLiveTarget items
func prepareObjectsForDiff(ctx context.Context, app *argoappv1.Application, proj *argoappv1.AppProject, resources *application.ManagedResourcesResponse, argoSettings *settings.Settings, diffOptions *DifferenceOption) ([]objKeyLiveTarget, error) {
liveObjs, err := cmdutil.LiveObjects(resources.Items)
if err != nil {
return nil, err
}
items := make([]objKeyLiveTarget, 0)
switch {
case diffOptions.local != "":
localObjs := groupObjsByKey(getLocalObjects(ctx, app, proj, diffOptions.local, diffOptions.localRepoRoot, argoSettings.AppLabelKey, diffOptions.cluster.Info.ServerVersion, diffOptions.cluster.Info.APIVersions, argoSettings.KustomizeOptions, argoSettings.TrackingMethod), liveObjs, app.Spec.Destination.Namespace)
items = groupObjsForDiff(resources, localObjs, items, argoSettings, app.InstanceName(argoSettings.ControllerNamespace), app.Spec.Destination.Namespace)
case diffOptions.revision != "" || len(diffOptions.revisions) > 0:
var unstructureds []*unstructured.Unstructured
for _, mfst := range diffOptions.res.Manifests {
obj, err := argoappv1.UnmarshalToUnstructured(mfst)
if err != nil {
return nil, err
}
unstructureds = append(unstructureds, obj)
}
groupedObjs := groupObjsByKey(unstructureds, liveObjs, app.Spec.Destination.Namespace)
items = groupObjsForDiff(resources, groupedObjs, items, argoSettings, app.InstanceName(argoSettings.ControllerNamespace), app.Spec.Destination.Namespace)
case diffOptions.serversideRes != nil:
var unstructureds []*unstructured.Unstructured
for _, mfst := range diffOptions.serversideRes.Manifests {
obj, err := argoappv1.UnmarshalToUnstructured(mfst)
if err != nil {
return nil, err
}
unstructureds = append(unstructureds, obj)
}
groupedObjs := groupObjsByKey(unstructureds, liveObjs, app.Spec.Destination.Namespace)
items = groupObjsForDiff(resources, groupedObjs, items, argoSettings, app.InstanceName(argoSettings.ControllerNamespace), app.Spec.Destination.Namespace)
default:
for i := range resources.Items {
res := resources.Items[i]
live := &unstructured.Unstructured{}
err := json.Unmarshal([]byte(res.NormalizedLiveState), &live)
if err != nil {
return nil, err
}
target := &unstructured.Unstructured{}
err = json.Unmarshal([]byte(res.TargetState), &target)
if err != nil {
return nil, err
}
items = append(items, objKeyLiveTarget{kube.NewResourceKey(res.Group, res.Kind, res.Namespace, res.Name), live, target})
}
}
return items, nil
}