argo-cd/cmd/argocd/commands/app.go

1399 lines
46 KiB
Go

package commands
import (
"context"
"encoding/json"
"fmt"
"io"
"net/url"
"os"
"reflect"
"strconv"
"strings"
"text/tabwriter"
"time"
"github.com/ghodss/yaml"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/yudai/gojsondiff/formatter"
"golang.org/x/crypto/ssh/terminal"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/argoproj/argo-cd/common"
"github.com/argoproj/argo-cd/controller/services"
"github.com/argoproj/argo-cd/errors"
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
argoappv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/server/application"
"github.com/argoproj/argo-cd/util"
"github.com/argoproj/argo-cd/util/argo"
"github.com/argoproj/argo-cd/util/config"
"github.com/argoproj/argo-cd/util/diff"
"github.com/argoproj/argo-cd/util/ksonnet"
kubeutil "github.com/argoproj/argo-cd/util/kube"
)
// NewApplicationCommand returns a new instance of an `argocd app` command
func NewApplicationCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var command = &cobra.Command{
Use: "app",
Short: "Manage applications",
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))
return command
}
// NewApplicationCreateCommand returns a new instance of an `argocd app create` command
func NewApplicationCreateCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
appOpts appOptions
fileURL string
appName string
upsert bool
)
var command = &cobra.Command{
Use: "create APPNAME",
Short: "Create an application from a git location",
Run: func(c *cobra.Command, args []string) {
var app argoappv1.Application
if fileURL != "" {
parsedURL, err := url.ParseRequestURI(fileURL)
if err != nil || !(parsedURL.Scheme == "http" || parsedURL.Scheme == "https") {
err = config.UnmarshalLocalFile(fileURL, &app)
} else {
err = config.UnmarshalRemoteFile(fileURL, &app)
}
errors.CheckError(err)
} else {
if len(args) == 1 {
if appName != "" && appName != args[0] {
log.Fatalf("--name argument '%s' does not match app name %s", appName, args[0])
}
appName = args[0]
}
app = argoappv1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: appName,
},
}
setAppOptions(c.Flags(), &app, &appOpts)
setParameterOverrides(&app, appOpts.parameters)
}
conn, appIf := argocdclient.NewClientOrDie(clientOpts).NewApplicationClientOrDie()
defer util.Close(conn)
appCreateRequest := application.ApplicationCreateRequest{
Application: app,
Upsert: &upsert,
}
created, err := appIf.Create(context.Background(), &appCreateRequest)
errors.CheckError(err)
fmt.Printf("application '%s' created\n", created.ObjectMeta.Name)
},
}
command.Flags().StringVarP(&fileURL, "file", "f", "", "Filename or URL to Kubernetes manifests for the app")
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")
addAppFlags(command, &appOpts)
return command
}
// NewApplicationGetCommand returns a new instance of an `argocd app get` command
func NewApplicationGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
output string
showParams bool
showOperation bool
refresh bool
)
var command = &cobra.Command{
Use: "get APPNAME",
Short: "Get application details",
Run: func(c *cobra.Command, args []string) {
if len(args) == 0 {
c.HelpFunc()(c, args)
os.Exit(1)
}
acdClient := argocdclient.NewClientOrDie(clientOpts)
conn, appIf := acdClient.NewApplicationClientOrDie()
defer util.Close(conn)
appName := args[0]
app, err := appIf.Get(context.Background(), &application.ApplicationQuery{Name: &appName, Refresh: refresh})
errors.CheckError(err)
switch output {
case "yaml":
yamlBytes, err := yaml.Marshal(app)
errors.CheckError(err)
fmt.Println(string(yamlBytes))
case "json":
jsonBytes, err := json.MarshalIndent(app, "", " ")
errors.CheckError(err)
fmt.Println(string(jsonBytes))
case "":
fmt.Printf(printOpFmtStr, "Name:", app.Name)
fmt.Printf(printOpFmtStr, "Server:", app.Spec.Destination.Server)
fmt.Printf(printOpFmtStr, "Namespace:", app.Spec.Destination.Namespace)
fmt.Printf(printOpFmtStr, "URL:", appURL(acdClient, app))
fmt.Printf(printOpFmtStr, "Repo:", app.Spec.Source.RepoURL)
fmt.Printf(printOpFmtStr, "Target:", app.Spec.Source.TargetRevision)
fmt.Printf(printOpFmtStr, "Path:", app.Spec.Source.Path)
printAppSourceDetails(&app.Spec.Source)
var syncPolicy string
if app.Spec.SyncPolicy != nil && app.Spec.SyncPolicy.Automated != nil {
syncPolicy = "Automated"
if app.Spec.SyncPolicy.Automated.Prune {
syncPolicy += " (Prune)"
}
} else {
syncPolicy = "<none>"
}
fmt.Printf(printOpFmtStr, "Sync Policy:", syncPolicy)
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)
}
if len(app.Status.ComparisonResult.Resources) > 0 {
fmt.Println()
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
printAppResources(w, app, showOperation)
_ = w.Flush()
}
default:
log.Fatalf("Unknown output format: %s", output)
}
},
}
command.Flags().StringVarP(&output, "output", "o", "", "Output format. One of: yaml, json")
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")
return command
}
func printAppSourceDetails(appSrc *argoappv1.ApplicationSource) {
if env := argoappv1.KsonnetEnv(appSrc); env != "" {
fmt.Printf(printOpFmtStr, "Environment:", env)
}
valueFiles := argoappv1.HelmValueFiles(appSrc)
if len(valueFiles) > 0 {
fmt.Printf(printOpFmtStr, "Helm Values:", strings.Join(valueFiles, ","))
}
if appSrc.Kustomize != nil {
if appSrc.Kustomize.NamePrefix != "" {
fmt.Printf(printOpFmtStr, "Name Prefix:", appSrc.Kustomize.NamePrefix)
}
}
}
func printAppConditions(w io.Writer, app *argoappv1.Application) {
fmt.Fprintf(w, "CONDITION\tMESSAGE\n")
for _, item := range app.Status.Conditions {
fmt.Fprintf(w, "%s\t%s", item.Type, item.Message)
}
}
// appURL returns the URL of an application
func appURL(acdClient argocdclient.Client, app *argoappv1.Application) 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, app.Name)
}
func truncateString(str string, num int) string {
bnoden := str
if len(str) > num {
if num > 3 {
num -= 3
}
bnoden = str[0:num] + "..."
}
return bnoden
}
// printParams prints parameters and overrides
func printParams(app *argoappv1.Application) {
paramLenLimit := 80
overrides := make(map[string]string)
for _, p := range app.Spec.Source.ComponentParameterOverrides {
overrides[fmt.Sprintf("%s/%s", p.Component, p.Name)] = p.Value
}
fmt.Println()
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
if needsComponentColumn(&app.Spec.Source) {
fmt.Fprintf(w, "COMPONENT\tNAME\tVALUE\tOVERRIDE\n")
for _, p := range app.Status.Parameters {
overrideValue := overrides[fmt.Sprintf("%s/%s", p.Component, p.Name)]
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", p.Component, p.Name, truncateString(p.Value, paramLenLimit), truncateString(overrideValue, paramLenLimit))
}
} else {
fmt.Fprintf(w, "NAME\tVALUE\n")
for _, p := range app.Spec.Source.ComponentParameterOverrides {
fmt.Fprintf(w, "%s\t%s\n", p.Name, truncateString(p.Value, paramLenLimit))
}
}
_ = w.Flush()
}
// needsComponentColumn returns true if the app source is such that it requires parameters in the
// COMPONENT=PARAM=NAME
func needsComponentColumn(source *argoappv1.ApplicationSource) bool {
ksEnv := argoappv1.KsonnetEnv(source)
if ksEnv != "" {
return true
}
return false
}
// NewApplicationSetCommand returns a new instance of an `argocd app set` command
func NewApplicationSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
appOpts appOptions
)
var command = &cobra.Command{
Use: "set APPNAME",
Short: "Set application parameters",
Run: func(c *cobra.Command, args []string) {
if len(args) != 1 {
c.HelpFunc()(c, args)
os.Exit(1)
}
appName := args[0]
conn, appIf := argocdclient.NewClientOrDie(clientOpts).NewApplicationClientOrDie()
defer util.Close(conn)
app, err := appIf.Get(context.Background(), &application.ApplicationQuery{Name: &appName})
errors.CheckError(err)
visited := setAppOptions(c.Flags(), app, &appOpts)
if visited == 0 {
log.Error("Please set at least one option to update")
c.HelpFunc()(c, args)
os.Exit(1)
}
if c.Flags().Changed("auto-prune") {
if app.Spec.SyncPolicy == nil || app.Spec.SyncPolicy.Automated == nil {
log.Fatal("Cannot set --auto-prune: application not configured with automatic sync")
}
app.Spec.SyncPolicy.Automated.Prune = appOpts.autoPrune
}
setParameterOverrides(app, appOpts.parameters)
oldOverrides := app.Spec.Source.ComponentParameterOverrides
updatedSpec, err := appIf.UpdateSpec(context.Background(), &application.ApplicationUpdateSpecRequest{
Name: &app.Name,
Spec: app.Spec,
})
errors.CheckError(err)
newOverrides := updatedSpec.Source.ComponentParameterOverrides
checkDroppedParams(newOverrides, oldOverrides)
},
}
addAppFlags(command, &appOpts)
return command
}
func setAppOptions(flags *pflag.FlagSet, app *argoappv1.Application, appOpts *appOptions) int {
visited := 0
flags.Visit(func(f *pflag.Flag) {
visited++
switch f.Name {
case "repo":
app.Spec.Source.RepoURL = appOpts.repoURL
case "path":
app.Spec.Source.Path = appOpts.appPath
case "env":
setKsonnetOpt(&app.Spec.Source, &appOpts.env)
case "revision":
app.Spec.Source.TargetRevision = appOpts.revision
case "values":
setHelmOpt(&app.Spec.Source, appOpts.valuesFiles, nil)
case "release-name":
setHelmOpt(&app.Spec.Source, nil, &appOpts.releaseName)
case "dest-server":
app.Spec.Destination.Server = appOpts.destServer
case "dest-namespace":
app.Spec.Destination.Namespace = appOpts.destNamespace
case "project":
app.Spec.Project = appOpts.project
case "nameprefix":
setKustomizeOpt(&app.Spec.Source, &appOpts.namePrefix)
case "sync-policy":
switch appOpts.syncPolicy {
case "automated":
app.Spec.SyncPolicy = &argoappv1.SyncPolicy{
Automated: &argoappv1.SyncPolicyAutomated{},
}
case "none":
app.Spec.SyncPolicy = nil
default:
log.Fatalf("Invalid sync-policy: %s", appOpts.syncPolicy)
}
}
})
return visited
}
func setKsonnetOpt(src *argoappv1.ApplicationSource, env *string) {
if src.Ksonnet == nil {
src.Ksonnet = &argoappv1.ApplicationSourceKsonnet{}
}
if env != nil {
src.Ksonnet.Environment = *env
}
}
func setKustomizeOpt(src *argoappv1.ApplicationSource, namePrefix *string) {
if src.Kustomize == nil {
src.Kustomize = &argoappv1.ApplicationSourceKustomize{}
}
if namePrefix != nil {
src.Kustomize.NamePrefix = *namePrefix
}
}
func setHelmOpt(src *argoappv1.ApplicationSource, valueFiles []string, releaseName *string) {
if src.Helm == nil {
src.Helm = &argoappv1.ApplicationSourceHelm{}
}
if valueFiles != nil {
src.Helm.ValueFiles = valueFiles
}
if releaseName != nil {
src.Helm.ReleaseName = *releaseName
}
}
func checkDroppedParams(newOverrides []argoappv1.ComponentParameter, oldOverrides []argoappv1.ComponentParameter) {
newOverrideMap := argo.ParamToMap(newOverrides)
if len(oldOverrides) > len(newOverrides) {
for _, oldOverride := range oldOverrides {
if !argo.CheckValidParam(newOverrideMap, oldOverride) {
log.Warnf("Parameter %s in %s does not exist in ksonnet, parameter override dropped", oldOverride.Name, oldOverride.Component)
}
}
}
}
type appOptions struct {
repoURL string
appPath string
env string
revision string
destServer string
destNamespace string
parameters []string
valuesFiles []string
releaseName string
project string
syncPolicy string
autoPrune bool
namePrefix string
}
func addAppFlags(command *cobra.Command, opts *appOptions) {
command.Flags().StringVar(&opts.repoURL, "repo", "", "Repository URL, ignored if a file is set")
command.Flags().StringVar(&opts.appPath, "path", "", "Path in repository to the ksonnet app directory, ignored if a file is set")
command.Flags().StringVar(&opts.env, "env", "", "Application environment to monitor")
command.Flags().StringVar(&opts.revision, "revision", "HEAD", "The tracking source branch, tag, or commit the application will sync to")
command.Flags().StringVar(&opts.destServer, "dest-server", "", "K8s cluster URL (overrides the server URL specified in the ksonnet app.yaml)")
command.Flags().StringVar(&opts.destNamespace, "dest-namespace", "", "K8s target namespace (overrides the namespace specified in the ksonnet app.yaml)")
command.Flags().StringArrayVarP(&opts.parameters, "parameter", "p", []string{}, "set a parameter override (e.g. -p guestbook=image=example/guestbook:latest)")
command.Flags().StringArrayVar(&opts.valuesFiles, "values", []string{}, "Helm values file(s) to use")
command.Flags().StringVar(&opts.releaseName, "release-name", "", "Helm release-name")
command.Flags().StringVar(&opts.project, "project", "", "Application project name")
command.Flags().StringVar(&opts.syncPolicy, "sync-policy", "", "Set the sync policy (one of: automated, none)")
command.Flags().BoolVar(&opts.autoPrune, "auto-prune", false, "Set automatic pruning when sync is automated")
command.Flags().StringVar(&opts.namePrefix, "nameprefix", "", "Kustomize nameprefix")
}
// NewApplicationUnsetCommand returns a new instance of an `argocd app unset` command
func NewApplicationUnsetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
parameters []string
valuesFiles []string
)
var command = &cobra.Command{
Use: "unset APPNAME -p COMPONENT=PARAM",
Short: "Unset application parameters",
Run: func(c *cobra.Command, args []string) {
if len(args) != 1 || (len(parameters) == 0 && len(valuesFiles) == 0) {
c.HelpFunc()(c, args)
os.Exit(1)
}
appName := args[0]
conn, appIf := argocdclient.NewClientOrDie(clientOpts).NewApplicationClientOrDie()
defer util.Close(conn)
app, err := appIf.Get(context.Background(), &application.ApplicationQuery{Name: &appName})
errors.CheckError(err)
isKsonnetApp := argoappv1.KsonnetEnv(&app.Spec.Source) != ""
updated := false
for _, paramStr := range parameters {
if isKsonnetApp {
parts := strings.SplitN(paramStr, "=", 2)
if len(parts) != 2 {
log.Fatalf("Expected parameter of the form: component=param. Received: %s", paramStr)
}
overrides := app.Spec.Source.ComponentParameterOverrides
for i, override := range overrides {
if override.Component == parts[0] && override.Name == parts[1] {
app.Spec.Source.ComponentParameterOverrides = append(overrides[0:i], overrides[i+1:]...)
updated = true
break
}
}
} else {
overrides := app.Spec.Source.ComponentParameterOverrides
for i, override := range overrides {
if override.Name == paramStr {
app.Spec.Source.ComponentParameterOverrides = append(overrides[0:i], overrides[i+1:]...)
updated = true
break
}
}
}
}
specValueFiles := argoappv1.HelmValueFiles(&app.Spec.Source)
for _, valuesFile := range valuesFiles {
for i, vf := range specValueFiles {
if vf == valuesFile {
specValueFiles = append(specValueFiles[0:i], specValueFiles[i+1:]...)
updated = true
break
}
}
}
setHelmOpt(&app.Spec.Source, specValueFiles, nil)
if !updated {
return
}
_, err = appIf.UpdateSpec(context.Background(), &application.ApplicationUpdateSpecRequest{
Name: &app.Name,
Spec: app.Spec,
})
errors.CheckError(err)
},
}
command.Flags().StringArrayVarP(&parameters, "parameter", "p", []string{}, "unset a parameter override (e.g. -p guestbook=image)")
command.Flags().StringArrayVar(&valuesFiles, "values", []string{}, "unset one or more helm values files")
return command
}
// targetObjects deserializes the list of target states into unstructured objects
func targetObjects(resources []*argoappv1.ResourceState) ([]*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
}
// liveObjects deserializes the list of live states into unstructured objects
func liveObjects(resources []*argoappv1.ResourceState) ([]*unstructured.Unstructured, error) {
objs := make([]*unstructured.Unstructured, len(resources))
for i, resState := range resources {
obj, err := resState.LiveObject()
if err != nil {
return nil, err
}
objs[i] = obj
}
return objs, nil
}
// NewApplicationDiffCommand returns a new instance of an `argocd app diff` command
func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
refresh bool
local string
env string
)
var command = &cobra.Command{
Use: "diff APPNAME",
Short: "Perform a diff against the target and live state",
Run: func(c *cobra.Command, args []string) {
if len(args) == 0 {
c.HelpFunc()(c, args)
os.Exit(1)
}
conn, appIf := argocdclient.NewClientOrDie(clientOpts).NewApplicationClientOrDie()
defer util.Close(conn)
appName := args[0]
app, err := appIf.Get(context.Background(), &application.ApplicationQuery{Name: &appName, Refresh: refresh})
errors.CheckError(err)
resources, err := appIf.Resources(context.Background(), &services.ResourcesQuery{ApplicationName: &appName})
errors.CheckError(err)
liveObjs, err := liveObjects(resources.Items)
errors.CheckError(err)
var compareObjs []*unstructured.Unstructured
if local != "" {
if env == "" {
log.Fatal("--env required when performing local diff")
}
ksApp, err := ksonnet.NewKsonnetApp(local)
errors.CheckError(err)
compareObjs, err = ksApp.Show(env)
errors.CheckError(err)
if len(app.Spec.Source.ComponentParameterOverrides) > 0 {
log.Warnf("Unable to display parameter overrides")
}
compareObjs, liveObjs = diff.MatchObjectLists(compareObjs, liveObjs)
} else {
if env != "" {
log.Fatal("--env option invalid when performing git diff")
}
compareObjs, err = targetObjects(resources.Items)
errors.CheckError(err)
}
// In order for the diff to be clean, need to set our app labels
setAppLabels(appName, compareObjs)
diffResults, err := diff.DiffArray(compareObjs, liveObjs)
errors.CheckError(err)
for i := 0; i < len(compareObjs); i++ {
kind, name := getObjKindName(compareObjs[i], liveObjs[i])
diffRes := diffResults.Diffs[i]
fmt.Printf("===== %s %s ======\n", kind, name)
if diffRes.Modified {
formatOpts := formatter.AsciiFormatterConfig{
Coloring: terminal.IsTerminal(int(os.Stdout.Fd())),
}
out, err := diffResults.Diffs[i].ASCIIFormat(compareObjs[i], formatOpts)
errors.CheckError(err)
fmt.Println(out)
}
}
if local != "" && len(app.Spec.Source.ComponentParameterOverrides) > 0 {
log.Warnf("Unable to display parameter overrides")
}
},
}
command.Flags().BoolVar(&refresh, "refresh", false, "Refresh application data when retrieving")
command.Flags().StringVar(&local, "local", "", "Compare live app to a local ksonnet app")
command.Flags().StringVar(&env, "env", "", "Compare live app to a specific environment")
return command
}
func getObjKindName(compare, live *unstructured.Unstructured) (string, string) {
if compare == nil {
return live.GetKind(), live.GetName()
}
return compare.GetKind(), compare.GetName()
}
func setAppLabels(appName string, compareObjs []*unstructured.Unstructured) {
for _, obj := range compareObjs {
if obj == nil {
continue
}
_ = kubeutil.SetLabel(obj, common.LabelApplicationName, appName)
}
}
// NewApplicationDeleteCommand returns a new instance of an `argocd app delete` command
func NewApplicationDeleteCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
cascade bool
)
var command = &cobra.Command{
Use: "delete APPNAME",
Short: "Delete an application",
Run: func(c *cobra.Command, args []string) {
if len(args) == 0 {
c.HelpFunc()(c, args)
os.Exit(1)
}
conn, appIf := argocdclient.NewClientOrDie(clientOpts).NewApplicationClientOrDie()
defer util.Close(conn)
for _, appName := range args {
appDeleteReq := application.ApplicationDeleteRequest{
Name: &appName,
}
if c.Flag("cascade").Changed {
appDeleteReq.Cascade = &cascade
}
_, err := appIf.Delete(context.Background(), &appDeleteReq)
errors.CheckError(err)
}
},
}
command.Flags().BoolVar(&cascade, "cascade", true, "Perform a cascaded deletion of all application resources")
return command
}
// NewApplicationListCommand returns a new instance of an `argocd app list` command
func NewApplicationListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
output string
)
var command = &cobra.Command{
Use: "list",
Short: "List applications",
Run: func(c *cobra.Command, args []string) {
conn, appIf := argocdclient.NewClientOrDie(clientOpts).NewApplicationClientOrDie()
defer util.Close(conn)
apps, err := appIf.List(context.Background(), &application.ApplicationQuery{})
errors.CheckError(err)
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
var fmtStr string
headers := []interface{}{"NAME", "CLUSTER", "NAMESPACE", "PROJECT", "STATUS", "HEALTH", "CONDITIONS"}
if output == "wide" {
fmtStr = "%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\n"
}
fmt.Fprintf(w, fmtStr, headers...)
for _, app := range apps.Items {
vals := []interface{}{
app.Name,
app.Spec.Destination.Server,
app.Spec.Destination.Namespace,
app.Spec.GetProject(),
app.Status.ComparisonResult.Status,
app.Status.Health.Status,
formatConditionsSummary(app),
}
if output == "wide" {
vals = append(vals, app.Spec.Source.RepoURL, app.Spec.Source.Path, app.Spec.Source.TargetRevision)
}
fmt.Fprintf(w, fmtStr, vals...)
}
_ = w.Flush()
},
}
command.Flags().StringVarP(&output, "output", "o", "", "Output format. One of: wide")
return command
}
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)
}
}
summary := "<none>"
if len(items) > 0 {
summary = strings.Join(items, ",")
}
return summary
}
// NewApplicationWaitCommand returns a new instance of an `argocd app wait` command
func NewApplicationWaitCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
watchSync bool
watchHealth bool
watchOperations bool
timeout uint
)
var command = &cobra.Command{
Use: "wait APPNAME",
Short: "Wait for an application to reach a synced and healthy state",
Run: func(c *cobra.Command, args []string) {
if len(args) != 1 {
c.HelpFunc()(c, args)
os.Exit(1)
}
if !watchSync && !watchHealth && !watchOperations {
watchSync = true
watchHealth = true
watchOperations = true
}
appName := args[0]
conn, appIf := argocdclient.NewClientOrDie(clientOpts).NewApplicationClientOrDie()
defer util.Close(conn)
_, err := waitOnApplicationStatus(appIf, appName, timeout, watchSync, watchHealth, watchOperations, nil)
errors.CheckError(err)
},
}
command.Flags().BoolVar(&watchSync, "sync", false, "Wait for sync")
command.Flags().BoolVar(&watchHealth, "health", false, "Wait for health")
command.Flags().BoolVar(&watchOperations, "operation", false, "Wait for pending operations")
command.Flags().UintVar(&timeout, "timeout", defaultCheckTimeoutSeconds, "Time out after this many seconds")
return command
}
func isCanceledContextErr(err error) bool {
if err == context.Canceled {
return true
}
if stat, ok := status.FromError(err); ok {
if stat.Code() == codes.Canceled {
return true
}
}
return false
}
// watchApp returns a channel of watch events for an app, retrying the watch upon errors. Closes
// the returned channel when the context is discovered to be canceled.
func watchApp(ctx context.Context, appIf application.ApplicationServiceClient, appName string) chan *argoappv1.ApplicationWatchEvent {
appEventsCh := make(chan *argoappv1.ApplicationWatchEvent)
go func() {
defer close(appEventsCh)
for {
wc, err := appIf.Watch(ctx, &application.ApplicationQuery{
Name: &appName,
})
if err != nil {
if isCanceledContextErr(err) {
return
}
if err != io.EOF {
log.Warnf("watch err: %v", err)
}
time.Sleep(1 * time.Second)
continue
}
for {
appEvent, err := wc.Recv()
if err != nil {
if isCanceledContextErr(err) {
return
}
if err != io.EOF {
log.Warnf("recv err: %v", err)
}
time.Sleep(1 * time.Second)
break
} else {
appEventsCh <- appEvent
}
}
}
}()
return appEventsCh
}
// printAppResources prints the resources of an application in a tabwriter table
// Optionally prints the message from the operation state
func printAppResources(w io.Writer, app *argoappv1.Application, showOperation bool) {
messages := make(map[string]string)
opState := app.Status.OperationState
var syncRes *argoappv1.SyncOperationResult
if showOperation {
fmt.Fprintf(w, "KIND\tNAME\tSTATUS\tHEALTH\tHOOK\tOPERATIONMSG\n")
if opState != nil {
if opState.SyncResult != nil {
syncRes = opState.SyncResult
}
}
if syncRes != nil {
for _, resDetails := range syncRes.Resources {
messages[fmt.Sprintf("%s/%s", resDetails.Kind, resDetails.Name)] = resDetails.Message
}
for _, hook := range syncRes.Hooks {
if hook.Type == argoappv1.HookTypePreSync {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", hook.Kind, hook.Name, hook.Status, "", hook.Type, hook.Message)
}
}
}
} else {
fmt.Fprintf(w, "KIND\tNAME\tSTATUS\tHEALTH\n")
}
for _, res := range app.Status.ComparisonResult.Resources {
if showOperation {
message := messages[fmt.Sprintf("%s/%s", res.Kind, res.Name)]
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s", res.Kind, res.Name, res.Status, res.Health.Status, "", message)
} else {
fmt.Fprintf(w, "%s\t%s\t%s\t%s", res.Kind, res.Name, res.Status, res.Health.Status)
}
fmt.Fprint(w, "\n")
}
if showOperation && syncRes != nil {
for _, hook := range syncRes.Hooks {
if hook.Type == argoappv1.HookTypeSync || hook.Type == argoappv1.HookTypePostSync {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", hook.Kind, hook.Name, hook.Status, "", hook.Type, hook.Message)
}
}
}
}
// NewApplicationSyncCommand returns a new instance of an `argocd app sync` command
func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
revision string
resources *[]string
prune bool
dryRun bool
timeout uint
strategy string
force bool
)
const (
resourceFieldDelimiter = ":"
resourceFieldCount = 3
)
var command = &cobra.Command{
Use: "sync APPNAME",
Short: "Sync an application to its target state",
Run: func(c *cobra.Command, args []string) {
if len(args) != 1 {
c.HelpFunc()(c, args)
os.Exit(1)
}
conn, appIf := argocdclient.NewClientOrDie(clientOpts).NewApplicationClientOrDie()
defer util.Close(conn)
appName := args[0]
var syncResources []argoappv1.SyncOperationResource
if resources != nil {
syncResources = []argoappv1.SyncOperationResource{}
for _, r := range *resources {
fields := strings.Split(r, resourceFieldDelimiter)
if len(fields) != resourceFieldCount {
log.Fatalf("Resource should have GROUP%sKIND%sNAME, but instead got: %s", resourceFieldDelimiter, resourceFieldDelimiter, r)
}
rsrc := argoappv1.SyncOperationResource{
Group: fields[0],
Kind: fields[1],
Name: fields[2],
}
syncResources = append(syncResources, rsrc)
}
}
syncReq := application.ApplicationSyncRequest{
Name: &appName,
DryRun: dryRun,
Revision: revision,
Resources: syncResources,
Prune: prune,
}
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)
}
ctx := context.Background()
_, err := appIf.Sync(ctx, &syncReq)
errors.CheckError(err)
app, err := waitOnApplicationStatus(appIf, appName, timeout, false, false, true, syncResources)
errors.CheckError(err)
pruningRequired := 0
for _, resDetails := range app.Status.OperationState.SyncResult.Resources {
if resDetails.Status == argoappv1.ResourceDetailsPruningRequired {
pruningRequired++
}
}
if pruningRequired > 0 {
log.Fatalf("%d resources require pruning", pruningRequired)
}
if !app.Status.OperationState.Phase.Successful() && !dryRun {
os.Exit(1)
}
},
}
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")
resources = command.Flags().StringArray("resource", nil, fmt.Sprintf("Sync only specific resources as GROUP%sKIND%sNAME. Fields may be blank. This option may be specified repeatedly", resourceFieldDelimiter, resourceFieldDelimiter))
command.Flags().UintVar(&timeout, "timeout", defaultCheckTimeoutSeconds, "Time out after this many seconds")
command.Flags().StringVar(&strategy, "strategy", "", "Sync strategy (one of: apply|hook)")
command.Flags().BoolVar(&force, "force", false, "Use a force apply")
return command
}
// ResourceState tracks the state of a resource when waiting on an application status.
type resourceState struct {
Kind string
Name string
Status string
Health string
Hook string
Message string
}
func newResourceState(kind, name, status, health, hook, message string) *resourceState {
return &resourceState{
Kind: kind,
Name: name,
Status: status,
Health: health,
Hook: hook,
Message: message,
}
}
// Key returns a unique-ish key for the resource.
func (rs *resourceState) Key() string {
return fmt.Sprintf("%s/%s", rs.Kind, rs.Name)
}
func (rs *resourceState) String() string {
return fmt.Sprintf("%s\t%s\t%s\t%s\t%s\t%s", rs.Kind, 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 calculateResourceStates(app *argoappv1.Application, syncResources []argoappv1.SyncOperationResource) map[string]*resourceState {
resStates := make(map[string]*resourceState)
for _, res := range app.Status.ComparisonResult.Resources {
if len(syncResources) > 0 && !argo.ContainsSyncResource(res.Name, res.GroupVersionKind(), syncResources) {
continue
}
newState := newResourceState(res.Kind, res.Name, string(res.Status), res.Health.Status, "", "")
key := newState.Key()
if prev, ok := resStates[key]; ok {
prev.Merge(newState)
} else {
resStates[key] = newState
}
}
var opResult *argoappv1.SyncOperationResult
if app.Status.OperationState != nil {
if app.Status.OperationState.SyncResult != nil {
opResult = app.Status.OperationState.SyncResult
}
}
if opResult == nil {
return resStates
}
for _, hook := range opResult.Hooks {
newState := newResourceState(hook.Kind, hook.Name, string(hook.Status), "", string(hook.Type), hook.Message)
key := newState.Key()
if prev, ok := resStates[key]; ok {
prev.Merge(newState)
} else {
resStates[key] = newState
}
}
for _, res := range opResult.Resources {
newState := newResourceState(res.Kind, res.Name, "", "", "", res.Message)
key := newState.Key()
if prev, ok := resStates[key]; ok {
prev.Merge(newState)
} else {
resStates[key] = newState
}
}
return resStates
}
func waitOnApplicationStatus(appClient application.ApplicationServiceClient, appName string, timeout uint, watchSync bool, watchHealth bool, watchOperation bool, syncResources []argoappv1.SyncOperationResource) (*argoappv1.Application, error) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// 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
printFinalStatus := func(app *argoappv1.Application) {
var err error
if refresh {
app, err = appClient.Get(context.Background(), &application.ApplicationQuery{Name: &appName, Refresh: true})
errors.CheckError(err)
}
fmt.Println()
fmt.Printf(printOpFmtStr, "Application:", app.Name)
if watchOperation {
printOperationResult(app.Status.OperationState)
}
if len(app.Status.ComparisonResult.Resources) > 0 {
fmt.Println()
w := tabwriter.NewWriter(os.Stdout, 5, 0, 2, ' ', 0)
printAppResources(w, app, watchOperation)
_ = w.Flush()
}
}
if timeout != 0 {
time.AfterFunc(time.Duration(timeout)*time.Second, func() {
cancel()
})
}
w := tabwriter.NewWriter(os.Stdout, 5, 0, 2, ' ', 0)
fmt.Fprintln(w, "KIND\tNAME\tSTATUS\tHEALTH\tHOOK\tOPERATIONMSG")
prevStates := make(map[string]*resourceState)
appEventCh := watchApp(ctx, appClient, appName)
var app *argoappv1.Application
for appEvent := range appEventCh {
app = &appEvent.Application
if app.Operation != nil {
refresh = true
}
// consider skipped checks successful
synced := !watchSync || app.Status.ComparisonResult.Status == argoappv1.ComparisonStatusSynced
healthy := !watchHealth || app.Status.Health.Status == argoappv1.HealthStatusHealthy
operational := !watchOperation || appEvent.Application.Operation == nil
if len(app.Status.GetErrorConditions()) == 0 && synced && healthy && operational {
printFinalStatus(app)
return app, nil
}
newStates := calculateResourceStates(app, syncResources)
for _, newState := range newStates {
var doPrint bool
stateKey := newState.Key()
if prevState, found := prevStates[stateKey]; found {
doPrint = prevState.Merge(newState)
} else {
prevStates[stateKey] = newState
doPrint = true
}
if doPrint {
fmt.Fprintln(w, prevStates[stateKey])
}
}
_ = w.Flush()
}
printFinalStatus(app)
return nil, 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
// If the app is a ksonnet app, then parameters are expected to be in the form: component=param=value
// Otherwise, 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) {
if len(parameters) == 0 {
return
}
var newParams []argoappv1.ComponentParameter
if len(app.Spec.Source.ComponentParameterOverrides) > 0 {
newParams = app.Spec.Source.ComponentParameterOverrides
} else {
newParams = make([]argoappv1.ComponentParameter, 0)
}
needsComponent := needsComponentColumn(&app.Spec.Source)
for _, paramStr := range parameters {
var newParam argoappv1.ComponentParameter
if needsComponent {
parts := strings.SplitN(paramStr, "=", 3)
if len(parts) != 3 {
log.Fatalf("Expected ksonnet parameter of the form: component=param=value. Received: %s", paramStr)
}
newParam = argoappv1.ComponentParameter{
Component: parts[0],
Name: parts[1],
Value: parts[2],
}
} else {
parts := strings.SplitN(paramStr, "=", 2)
if len(parts) != 2 {
log.Fatalf("Expected helm parameter of the form: param=value. Received: %s", paramStr)
}
newParam = argoappv1.ComponentParameter{
Name: parts[0],
Value: parts[1],
}
}
index := -1
for i, cp := range newParams {
if cp.Component == newParam.Component && cp.Name == newParam.Name {
index = i
break
}
}
if index == -1 {
newParams = append(newParams, newParam)
} else {
newParams[index] = newParam
}
}
app.Spec.Source.ComponentParameterOverrides = newParams
}
// NewApplicationHistoryCommand returns a new instance of an `argocd app history` command
func NewApplicationHistoryCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
output string
)
var command = &cobra.Command{
Use: "history APPNAME",
Short: "Show application deployment history",
Run: func(c *cobra.Command, args []string) {
if len(args) != 1 {
c.HelpFunc()(c, args)
os.Exit(1)
}
conn, appIf := argocdclient.NewClientOrDie(clientOpts).NewApplicationClientOrDie()
defer util.Close(conn)
appName := args[0]
app, err := appIf.Get(context.Background(), &application.ApplicationQuery{Name: &appName})
errors.CheckError(err)
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
switch output {
case "wide":
fmt.Fprintf(w, "ID\tDATE\tCOMMIT\tPARAMETERS\n")
default:
fmt.Fprintf(w, "ID\tDATE\tCOMMIT\n")
}
for _, depInfo := range app.Status.History {
switch output {
case "wide":
manifest, err := appIf.GetManifests(context.Background(), &application.ApplicationManifestQuery{Name: &appName, Revision: depInfo.Revision})
errors.CheckError(err)
paramStr := paramString(manifest.GetParams())
fmt.Fprintf(w, "%d\t%s\t%s\t%s\n", depInfo.ID, depInfo.DeployedAt, depInfo.Revision, paramStr)
default:
fmt.Fprintf(w, "%d\t%s\t%s\n", depInfo.ID, depInfo.DeployedAt, depInfo.Revision)
}
}
_ = w.Flush()
},
}
command.Flags().StringVarP(&output, "output", "o", "", "Output format. One of: wide")
return command
}
func paramString(params []*argoappv1.ComponentParameter) string {
if len(params) == 0 {
return ""
}
paramNames := []string{}
for _, param := range params {
paramNames = append(paramNames, fmt.Sprintf("%s=%s=%s", param.Component, param.Name, param.Value))
}
return strings.Join(paramNames, ",")
}
// NewApplicationRollbackCommand returns a new instance of an `argocd app rollback` command
func NewApplicationRollbackCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
prune bool
timeout uint
)
var command = &cobra.Command{
Use: "rollback APPNAME",
Short: "Rollback application to a previous deployed version",
Run: func(c *cobra.Command, args []string) {
if len(args) != 2 {
c.HelpFunc()(c, args)
os.Exit(1)
}
appName := args[0]
depID, err := strconv.Atoi(args[1])
errors.CheckError(err)
conn, appIf := argocdclient.NewClientOrDie(clientOpts).NewApplicationClientOrDie()
defer util.Close(conn)
ctx := context.Background()
app, err := appIf.Get(ctx, &application.ApplicationQuery{Name: &appName})
errors.CheckError(err)
var depInfo *argoappv1.DeploymentInfo
for _, di := range app.Status.History {
if di.ID == int64(depID) {
depInfo = &di
break
}
}
if depInfo == nil {
log.Fatalf("Application '%s' does not have deployment id '%d' in history\n", app.ObjectMeta.Name, depID)
}
_, err = appIf.Rollback(ctx, &application.ApplicationRollbackRequest{
Name: &appName,
ID: int64(depID),
Prune: prune,
})
errors.CheckError(err)
_, err = waitOnApplicationStatus(appIf, appName, timeout, false, false, true, nil)
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")
return command
}
const printOpFmtStr = "%-20s%s\n"
const defaultCheckTimeoutSeconds = 0
func printOperationResult(opState *argoappv1.OperationState) {
if opState.SyncResult != nil {
fmt.Printf(printOpFmtStr, "Operation:", "Sync")
}
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
)
var command = &cobra.Command{
Use: "manifests APPNAME",
Short: "Print manifests of an application",
Run: func(c *cobra.Command, args []string) {
if len(args) != 1 {
c.HelpFunc()(c, args)
os.Exit(1)
}
appName := args[0]
conn, appIf := argocdclient.NewClientOrDie(clientOpts).NewApplicationClientOrDie()
defer util.Close(conn)
ctx := context.Background()
resources, err := appIf.Resources(ctx, &services.ResourcesQuery{ApplicationName: &appName})
errors.CheckError(err)
var unstructureds []*unstructured.Unstructured
switch source {
case "git":
if revision != "" {
q := application.ApplicationManifestQuery{
Name: &appName,
Revision: 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)
}
} else {
targetObjs, err := targetObjects(resources.Items)
errors.CheckError(err)
unstructureds = targetObjs
}
case "live":
liveObjs, err := 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")
return command
}
// NewApplicationTerminateOpCommand returns a new instance of an `argocd app terminate-op` command
func NewApplicationTerminateOpCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var command = &cobra.Command{
Use: "terminate-op APPNAME",
Short: "Terminate running operation of an application",
Run: func(c *cobra.Command, args []string) {
if len(args) != 1 {
c.HelpFunc()(c, args)
os.Exit(1)
}
appName := args[0]
conn, appIf := argocdclient.NewClientOrDie(clientOpts).NewApplicationClientOrDie()
defer util.Close(conn)
ctx := context.Background()
_, err := appIf.TerminateOperation(ctx, &application.OperationTerminateRequest{Name: &appName})
errors.CheckError(err)
fmt.Printf("Application '%s' operation terminating\n", appName)
},
}
return command
}