github.com/argoproj/argo-cd/v3@v3.2.1/util/cli/cli.go (about) 1 // Package cmd provides functionally common to various argo CLIs 2 3 package cli 4 5 import ( 6 "bufio" 7 "bytes" 8 stderrors "errors" 9 "flag" 10 "fmt" 11 "os" 12 "os/exec" 13 "path" 14 "strconv" 15 "strings" 16 17 "github.com/argoproj/gitops-engine/pkg/utils/text" 18 "github.com/google/shlex" 19 log "github.com/sirupsen/logrus" 20 "github.com/spf13/cobra" 21 "github.com/spf13/pflag" 22 terminal "golang.org/x/term" 23 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 24 "k8s.io/client-go/tools/clientcmd" 25 "k8s.io/klog/v2" 26 "k8s.io/kubectl/pkg/util/term" 27 "sigs.k8s.io/yaml" 28 29 "github.com/argoproj/argo-cd/v3/common" 30 "github.com/argoproj/argo-cd/v3/util/errors" 31 utilio "github.com/argoproj/argo-cd/v3/util/io" 32 utillog "github.com/argoproj/argo-cd/v3/util/log" 33 ) 34 35 // NewVersionCmd returns a new `version` command to be used as a sub-command to root 36 func NewVersionCmd(cliName string) *cobra.Command { 37 var short bool 38 versionCmd := cobra.Command{ 39 Use: "version", 40 Short: "Print version information", 41 Run: func(_ *cobra.Command, _ []string) { 42 version := common.GetVersion() 43 fmt.Printf("%s: %s\n", cliName, version) 44 if short { 45 return 46 } 47 fmt.Printf(" BuildDate: %s\n", version.BuildDate) 48 fmt.Printf(" GitCommit: %s\n", version.GitCommit) 49 fmt.Printf(" GitTreeState: %s\n", version.GitTreeState) 50 if version.GitTag != "" { 51 fmt.Printf(" GitTag: %s\n", version.GitTag) 52 } 53 fmt.Printf(" GoVersion: %s\n", version.GoVersion) 54 fmt.Printf(" Compiler: %s\n", version.Compiler) 55 fmt.Printf(" Platform: %s\n", version.Platform) 56 if version.ExtraBuildInfo != "" { 57 fmt.Printf(" ExtraBuildInfo: %s\n", version.ExtraBuildInfo) 58 } 59 }, 60 } 61 versionCmd.Flags().BoolVar(&short, "short", false, "print just the version number") 62 return &versionCmd 63 } 64 65 // AddKubectlFlagsToCmd adds kubectl like flags to a persistent flags of a command and returns the ClientConfig interface 66 // for retrieving the values. 67 func AddKubectlFlagsToCmd(cmd *cobra.Command) clientcmd.ClientConfig { 68 return AddKubectlFlagsToSet(cmd.PersistentFlags()) 69 } 70 71 // AddKubectlFlagsToSet adds kubectl like flags to a provided flag set and returns the ClientConfig interface 72 // for retrieving the values. 73 func AddKubectlFlagsToSet(flags *pflag.FlagSet) clientcmd.ClientConfig { 74 loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() 75 loadingRules.DefaultClientConfig = &clientcmd.DefaultClientConfig 76 overrides := clientcmd.ConfigOverrides{} 77 kflags := clientcmd.RecommendedConfigOverrideFlags("") 78 flags.StringVar(&loadingRules.ExplicitPath, "kubeconfig", "", "Path to a kube config. Only required if out-of-cluster") 79 clientcmd.BindOverrideFlags(&overrides, flags, kflags) 80 return clientcmd.NewInteractiveDeferredLoadingClientConfig(loadingRules, &overrides, os.Stdin) 81 } 82 83 // PromptCredentials is a helper to prompt the user for a username and password (unless already supplied) 84 func PromptCredentials(username, password string) (string, string) { 85 return PromptUsername(username), PromptPassword(password) 86 } 87 88 // PromptUsername prompts the user for a username value 89 func PromptUsername(username string) string { 90 return PromptMessage("Username", username) 91 } 92 93 // PromptMessage prompts the user for a value (unless already supplied) 94 func PromptMessage(message, value string) string { 95 for value == "" { 96 reader := bufio.NewReader(os.Stdin) 97 fmt.Print(message + ": ") 98 valueRaw, err := reader.ReadString('\n') 99 errors.CheckError(err) 100 value = strings.TrimSpace(valueRaw) 101 } 102 return value 103 } 104 105 // PromptPassword prompts the user for a password, without local echo (unless already supplied). 106 // If terminal.ReadPassword fails — often due to stdin not being a terminal (e.g., when input is piped), 107 // we fall back to reading from standard input using bufio.Reader. 108 func PromptPassword(password string) string { 109 for password == "" { 110 fmt.Print("Password: ") 111 passwordRaw, err := terminal.ReadPassword(int(os.Stdin.Fd())) 112 if err != nil { 113 // Fallback: handle cases where stdin is not a terminal (e.g., piped input) 114 reader := bufio.NewReader(os.Stdin) 115 input, err := reader.ReadString('\n') 116 errors.CheckError(err) 117 password = strings.TrimSpace(input) 118 return password 119 } 120 password = string(passwordRaw) 121 fmt.Print("\n") 122 } 123 return password 124 } 125 126 // AskToProceed prompts the user with a message (typically a yes or no question) and returns whether 127 // they responded in the affirmative or negative. 128 func AskToProceed(message string) bool { 129 for { 130 fmt.Print(message) 131 reader := bufio.NewReader(os.Stdin) 132 proceedRaw, err := reader.ReadString('\n') 133 errors.CheckError(err) 134 switch strings.ToLower(strings.TrimSpace(proceedRaw)) { 135 case "y", "yes": 136 return true 137 case "n", "no": 138 return false 139 } 140 } 141 } 142 143 // AskToProceedS prompts the user with a message (typically a yes, no or all question) and returns string 144 // "a", "y" or "n". 145 func AskToProceedS(message string) string { 146 for { 147 fmt.Print(message) 148 reader := bufio.NewReader(os.Stdin) 149 proceedRaw, err := reader.ReadString('\n') 150 errors.CheckError(err) 151 switch strings.ToLower(strings.TrimSpace(proceedRaw)) { 152 case "y", "yes": 153 return "y" 154 case "n", "no": 155 return "n" 156 case "a", "all": 157 return "a" 158 } 159 } 160 } 161 162 // ReadAndConfirmPassword is a helper to read and confirm a password from stdin 163 func ReadAndConfirmPassword(username string) (string, error) { 164 for { 165 fmt.Printf("*** Enter new password for user %s: ", username) 166 password, err := terminal.ReadPassword(int(os.Stdin.Fd())) 167 if err != nil { 168 return "", err 169 } 170 fmt.Print("\n") 171 fmt.Printf("*** Confirm new password for user %s: ", username) 172 confirmPassword, err := terminal.ReadPassword(int(os.Stdin.Fd())) 173 if err != nil { 174 return "", err 175 } 176 fmt.Print("\n") 177 if bytes.Equal(password, confirmPassword) { 178 return string(password), nil 179 } 180 log.Error("Passwords do not match") 181 } 182 } 183 184 // SetLogFormat sets a logrus log format 185 func SetLogFormat(logFormat string) { 186 switch strings.ToLower(logFormat) { 187 case utillog.JsonFormat: 188 os.Setenv(common.EnvLogFormat, utillog.JsonFormat) 189 case utillog.TextFormat, "": 190 os.Setenv(common.EnvLogFormat, utillog.TextFormat) 191 default: 192 log.Fatalf("Unknown log format '%s'", logFormat) 193 } 194 195 log.SetFormatter(utillog.CreateFormatter(logFormat)) 196 } 197 198 // SetLogLevel parses and sets a logrus log level 199 func SetLogLevel(logLevel string) { 200 level, err := log.ParseLevel(text.FirstNonEmpty(logLevel, log.InfoLevel.String())) 201 errors.CheckError(err) 202 os.Setenv(common.EnvLogLevel, level.String()) 203 log.SetLevel(level) 204 } 205 206 // SetGLogLevel set the glog level for the k8s go-client 207 func SetGLogLevel(glogLevel int) { 208 klog.InitFlags(nil) 209 _ = flag.Set("logtostderr", "true") 210 _ = flag.Set("v", strconv.Itoa(glogLevel)) 211 } 212 213 func writeToTempFile(pattern string, data []byte) string { 214 f, err := os.CreateTemp("", pattern) 215 errors.CheckError(err) 216 defer utilio.Close(f) 217 _, err = f.Write(data) 218 errors.CheckError(err) 219 return f.Name() 220 } 221 222 func stripComments(input []byte) []byte { 223 var stripped []byte 224 lines := bytes.Split(input, []byte("\n")) 225 for i, line := range lines { 226 if bytes.HasPrefix(bytes.TrimSpace(line), []byte("#")) { 227 continue 228 } 229 stripped = append(stripped, line...) 230 if i < len(lines)-1 { 231 stripped = append(stripped, '\n') 232 } 233 } 234 return stripped 235 } 236 237 const ( 238 defaultEditor = "vi" 239 editorEnv = "EDITOR" 240 commentsHeader = `# Please edit the object below. Lines beginning with a '#' will be ignored, 241 # and an empty file will abort the edit. If an error occurs while saving this file will be 242 # reopened with the relevant failures." 243 ` 244 ) 245 246 func setComments(input []byte, comments string) []byte { 247 input = stripComments(input) 248 var commentLines []string 249 for _, line := range strings.Split(comments, "\n") { 250 if line != "" { 251 commentLines = append(commentLines, "# "+line) 252 } 253 } 254 parts := []string{commentsHeader} 255 if len(commentLines) > 0 { 256 parts = append(parts, strings.Join(commentLines, "\n")) 257 } 258 parts = append(parts, string(input)) 259 return []byte(strings.Join(parts, "\n")) 260 } 261 262 // InteractiveEdit launches an interactive editor 263 func InteractiveEdit(filePattern string, data []byte, save func(input []byte) error) { 264 var editor string 265 var editorArgs []string 266 if overrideEditor := os.Getenv(editorEnv); overrideEditor == "" { 267 editor = defaultEditor 268 } else { 269 parts := strings.Fields(overrideEditor) 270 editor = parts[0] 271 editorArgs = parts[1:] 272 } 273 274 errorComment := "" 275 for { 276 data = setComments(data, errorComment) 277 tempFile := writeToTempFile(filePattern, data) 278 cmd := exec.Command(editor, append(editorArgs, tempFile)...) 279 cmd.Stdout = os.Stdout 280 cmd.Stderr = os.Stderr 281 cmd.Stdin = os.Stdin 282 283 err := (term.TTY{In: os.Stdin, TryDev: true}).Safe(cmd.Run) 284 errors.CheckError(err) 285 286 updated, err := os.ReadFile(tempFile) 287 errors.CheckError(err) 288 if len(updated) == 0 || bytes.Equal(updated, data) { 289 errors.CheckError(stderrors.New("edit cancelled, no valid changes were saved")) 290 break 291 } 292 data = stripComments(updated) 293 294 err = save(data) 295 if err == nil { 296 break 297 } 298 errorComment = err.Error() 299 } 300 } 301 302 // PrintDiff prints a diff between two unstructured objects to stdout using an external diff utility 303 // Honors the diff utility set in the KUBECTL_EXTERNAL_DIFF environment variable 304 func PrintDiff(name string, live *unstructured.Unstructured, target *unstructured.Unstructured) error { 305 tempDir, err := os.MkdirTemp("", "argocd-diff") 306 if err != nil { 307 return err 308 } 309 targetFile := path.Join(tempDir, name) 310 targetData := []byte("") 311 if target != nil { 312 targetData, err = yaml.Marshal(target) 313 if err != nil { 314 return err 315 } 316 } 317 err = os.WriteFile(targetFile, targetData, 0o644) 318 if err != nil { 319 return err 320 } 321 liveFile := path.Join(tempDir, name+"-live.yaml") 322 liveData := []byte("") 323 if live != nil { 324 liveData, err = yaml.Marshal(live) 325 if err != nil { 326 return err 327 } 328 } 329 err = os.WriteFile(liveFile, liveData, 0o644) 330 if err != nil { 331 return err 332 } 333 cmdBinary := "diff" 334 var args []string 335 if envDiff := os.Getenv("KUBECTL_EXTERNAL_DIFF"); envDiff != "" { 336 parts, err := shlex.Split(envDiff) 337 if err != nil { 338 return err 339 } 340 cmdBinary = parts[0] 341 args = parts[1:] 342 } 343 cmd := exec.Command(cmdBinary, append(args, liveFile, targetFile)...) 344 cmd.Stderr = os.Stderr 345 cmd.Stdout = os.Stdout 346 return cmd.Run() 347 }