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