github.com/cloudfoundry-attic/cli-with-i18n@v6.32.1-0.20171002233121-7401370d3b85+incompatible/util/configv3/config.go (about) 1 // Package configv3 package contains everything related to the CF CLI Configuration. 2 package configv3 3 4 import ( 5 "encoding/json" 6 "io/ioutil" 7 "math" 8 "os" 9 "os/signal" 10 "path/filepath" 11 "strconv" 12 "strings" 13 "syscall" 14 "time" 15 16 "golang.org/x/crypto/ssh/terminal" 17 18 "code.cloudfoundry.org/cli/command/translatableerror" 19 "code.cloudfoundry.org/cli/version" 20 ) 21 22 const ( 23 // DefaultDialTimeout is the default timeout for the dail. 24 DefaultDialTimeout = 5 * time.Second 25 26 // DefaultOverallPollingTimeout is the default maximum time that the CLI will 27 // poll a job running on the Cloud Controller. By default it's infinit, which 28 // is represented by MaxInt64. 29 DefaultOverallPollingTimeout = time.Duration(1 << 62) 30 // Developer Note: Due to bugs in using MaxInt64 during comparison, the above 31 // was chosen as a replacement. 32 33 // DefaultPollingInterval is the time between consecutive polls of a status. 34 DefaultPollingInterval = 3 * time.Second 35 36 // DefaultStagingTimeout is the default timeout for application staging. 37 DefaultStagingTimeout = 15 * time.Minute 38 39 // DefaultStartupTimeout is the default timeout for application starting. 40 DefaultStartupTimeout = 5 * time.Minute 41 // DefaultPingerThrottle = 5 * time.Second 42 43 // DefaultTarget is the default CFConfig value for Target. 44 DefaultTarget = "" 45 46 // DefaultSSHOAuthClient is the default oauth client ID for SSHing into an 47 // application/process container 48 DefaultSSHOAuthClient = "ssh-proxy" 49 50 // DefaultUAAOAuthClient is the default client ID for the CLI when 51 // communicating with the UAA. 52 DefaultUAAOAuthClient = "cf" 53 54 // DefaultUAAOAuthClientSecret is the default client secret for the CLI when 55 // communicating with the UAA. 56 DefaultUAAOAuthClientSecret = "" 57 ) 58 59 // LoadConfig loads the config from the .cf/config.json and os.ENV. If the 60 // config.json does not exists, it will use a default config in it's place. 61 // Takes in an optional FlagOverride, will only use the first one passed, that 62 // can override the given flag values. 63 // 64 // The '.cf' directory will be read in one of the following locations on UNIX 65 // Systems: 66 // 1. $CF_HOME/.cf if $CF_HOME is set 67 // 2. $HOME/.cf as the default 68 // 69 // The '.cf' directory will be read in one of the following locations on 70 // Windows Systems: 71 // 1. CF_HOME\.cf if CF_HOME is set 72 // 2. HOMEDRIVE\HOMEPATH\.cf if HOMEDRIVE or HOMEPATH is set 73 // 3. USERPROFILE\.cf as the default 74 func LoadConfig(flags ...FlagOverride) (*Config, error) { 75 err := removeOldTempConfigFiles() 76 if err != nil { 77 return nil, err 78 } 79 80 configFilePath := ConfigFilePath() 81 82 config := Config{ 83 ConfigFile: CFConfig{ 84 ConfigVersion: 3, 85 Target: DefaultTarget, 86 ColorEnabled: DefaultColorEnabled, 87 PluginRepositories: []PluginRepository{{ 88 Name: DefaultPluginRepoName, 89 URL: DefaultPluginRepoURL, 90 }}, 91 }, 92 } 93 94 var jsonError error 95 96 if _, err = os.Stat(configFilePath); err == nil || !os.IsNotExist(err) { 97 var file []byte 98 file, err = ioutil.ReadFile(configFilePath) 99 if err != nil { 100 return nil, err 101 } 102 103 if len(file) == 0 { 104 jsonError = translatableerror.EmptyConfigError{FilePath: configFilePath} 105 } else { 106 var configFile CFConfig 107 err = json.Unmarshal(file, &configFile) 108 if err != nil { 109 return nil, err 110 } 111 config.ConfigFile = configFile 112 } 113 } 114 115 if config.ConfigFile.SSHOAuthClient == "" { 116 config.ConfigFile.SSHOAuthClient = DefaultSSHOAuthClient 117 } 118 119 if config.ConfigFile.UAAOAuthClient == "" { 120 config.ConfigFile.UAAOAuthClient = DefaultUAAOAuthClient 121 config.ConfigFile.UAAOAuthClientSecret = DefaultUAAOAuthClientSecret 122 } 123 124 config.ENV = EnvOverride{ 125 BinaryName: filepath.Base(os.Args[0]), 126 CFColor: os.Getenv("CF_COLOR"), 127 CFDialTimeout: os.Getenv("CF_DIAL_TIMEOUT"), 128 CFLogLevel: os.Getenv("CF_LOG_LEVEL"), 129 CFPluginHome: os.Getenv("CF_PLUGIN_HOME"), 130 CFStagingTimeout: os.Getenv("CF_STAGING_TIMEOUT"), 131 CFStartupTimeout: os.Getenv("CF_STARTUP_TIMEOUT"), 132 CFTrace: os.Getenv("CF_TRACE"), 133 DockerPassword: os.Getenv("CF_DOCKER_PASSWORD"), 134 Experimental: os.Getenv("CF_CLI_EXPERIMENTAL"), 135 ForceTTY: os.Getenv("FORCE_TTY"), 136 HTTPSProxy: os.Getenv("https_proxy"), 137 Lang: os.Getenv("LANG"), 138 LCAll: os.Getenv("LC_ALL"), 139 } 140 141 pluginFilePath := filepath.Join(config.PluginHome(), "config.json") 142 if _, err = os.Stat(pluginFilePath); os.IsNotExist(err) { 143 config.pluginsConfig = PluginsConfig{ 144 Plugins: make(map[string]Plugin), 145 } 146 } else { 147 var file []byte 148 file, err = ioutil.ReadFile(pluginFilePath) 149 if err != nil { 150 return nil, err 151 } 152 153 err = json.Unmarshal(file, &config.pluginsConfig) 154 if err != nil { 155 return nil, err 156 } 157 158 for name, plugin := range config.pluginsConfig.Plugins { 159 plugin.Name = name 160 config.pluginsConfig.Plugins[name] = plugin 161 } 162 } 163 164 if len(flags) > 0 { 165 config.Flags = flags[0] 166 } 167 168 pwd, err := os.Getwd() 169 if err != nil { 170 return nil, err 171 } 172 173 // Developer Note: The following is untested! Change at your own risk. 174 isTTY := terminal.IsTerminal(int(os.Stdout.Fd())) 175 terminalWidth := math.MaxInt32 176 177 if isTTY { 178 var err error 179 terminalWidth, _, err = terminal.GetSize(int(os.Stdout.Fd())) 180 if err != nil { 181 return nil, err 182 } 183 } 184 185 config.detectedSettings = detectedSettings{ 186 currentDirectory: pwd, 187 terminalWidth: terminalWidth, 188 tty: isTTY, 189 } 190 191 return &config, jsonError 192 } 193 194 func removeOldTempConfigFiles() error { 195 oldTempFileNames, err := filepath.Glob(filepath.Join(configDirectory(), "temp-config?*")) 196 if err != nil { 197 return err 198 } 199 200 for _, oldTempFileName := range oldTempFileNames { 201 err = os.Remove(oldTempFileName) 202 if err != nil { 203 return err 204 } 205 } 206 207 return nil 208 } 209 210 // WriteConfig creates the .cf directory and then writes the config.json. The 211 // location of .cf directory is written in the same way LoadConfig reads .cf 212 // directory. 213 func WriteConfig(c *Config) error { 214 rawConfig, err := json.MarshalIndent(c.ConfigFile, "", " ") 215 if err != nil { 216 return err 217 } 218 219 dir := configDirectory() 220 err = os.MkdirAll(dir, 0700) 221 if err != nil { 222 return err 223 } 224 225 // Developer Note: The following is untested! Change at your own risk. 226 // Setup notifications of termination signals to channel sig, create a process to 227 // watch for these signals so we can remove transient config temp files. 228 sig := make(chan os.Signal, 10) 229 signal.Notify(sig, syscall.SIGHUP, syscall.SIGINT, syscall.SIGKILL, syscall.SIGQUIT, syscall.SIGTERM, os.Interrupt) 230 defer signal.Stop(sig) 231 232 tempConfigFile, err := ioutil.TempFile(dir, "temp-config") 233 if err != nil { 234 return err 235 } 236 tempConfigFile.Close() 237 tempConfigFileName := tempConfigFile.Name() 238 239 go catchSignal(sig, tempConfigFileName) 240 241 err = ioutil.WriteFile(tempConfigFileName, rawConfig, 0600) 242 if err != nil { 243 return err 244 } 245 246 return os.Rename(tempConfigFileName, ConfigFilePath()) 247 } 248 249 // catchSignal tries to catch SIGHUP, SIGINT, SIGKILL, SIGQUIT and SIGTERM, and 250 // Interrupt for removing temporarily created config files before the program 251 // ends. Note: we cannot intercept a `kill -9`, so a well-timed `kill -9` 252 // will allow a temp config file to linger. 253 func catchSignal(sig chan os.Signal, tempConfigFileName string) { 254 select { 255 case <-sig: 256 _ = os.Remove(tempConfigFileName) 257 os.Exit(2) 258 } 259 } 260 261 // Config combines the settings taken from the .cf/config.json, os.ENV, and the 262 // plugin config. 263 type Config struct { 264 // ConfigFile stores the configuration from the .cf/config 265 ConfigFile CFConfig 266 267 // ENV stores the configuration from os.ENV 268 ENV EnvOverride 269 270 // Flags stores the configuration from gobal flags 271 Flags FlagOverride 272 273 // detectedSettings are settings detected when the config is loaded. 274 detectedSettings detectedSettings 275 276 pluginsConfig PluginsConfig 277 } 278 279 // CFConfig represents .cf/config.json 280 type CFConfig struct { 281 ConfigVersion int `json:"ConfigVersion"` 282 Target string `json:"Target"` 283 APIVersion string `json:"APIVersion"` 284 AuthorizationEndpoint string `json:"AuthorizationEndpoint"` 285 DopplerEndpoint string `json:"DopplerEndPoint"` 286 UAAEndpoint string `json:"UaaEndpoint"` 287 RoutingEndpoint string `json:"RoutingAPIEndpoint"` 288 AccessToken string `json:"AccessToken"` 289 SSHOAuthClient string `json:"SSHOAuthClient"` 290 UAAOAuthClient string `json:"UAAOAuthClient"` 291 UAAOAuthClientSecret string `json:"UAAOAuthClientSecret"` 292 RefreshToken string `json:"RefreshToken"` 293 TargetedOrganization Organization `json:"OrganizationFields"` 294 TargetedSpace Space `json:"SpaceFields"` 295 SkipSSLValidation bool `json:"SSLDisabled"` 296 AsyncTimeout int `json:"AsyncTimeout"` 297 Trace string `json:"Trace"` 298 ColorEnabled string `json:"ColorEnabled"` 299 Locale string `json:"Locale"` 300 PluginRepositories []PluginRepository `json:"PluginRepos"` 301 MinCLIVersion string `json:"MinCLIVersion"` 302 MinRecommendedCLIVersion string `json:"MinRecommendedCLIVersion"` 303 } 304 305 // Organization contains basic information about the targeted organization 306 type Organization struct { 307 GUID string `json:"GUID"` 308 Name string `json:"Name"` 309 QuotaDefinition QuotaDefinition `json:"QuotaDefinition"` 310 } 311 312 // QuotaDefinition contains information about the organization's quota 313 type QuotaDefinition struct { 314 GUID string `json:"guid"` 315 Name string `json:"name"` 316 MemoryLimit int `json:"memory_limit"` 317 InstanceMemoryLimit int `json:"instance_memory_limit"` 318 TotalRoutes int `json:"total_routes"` 319 TotalServices int `json:"total_services"` 320 NonBasicServicesAllowed bool `json:"non_basic_services_allowed"` 321 AppInstanceLimit int `json:"app_instance_limit"` 322 TotalReservedRoutePorts int `json:"total_reserved_route_ports"` 323 } 324 325 // Space contains basic information about the targeted space 326 type Space struct { 327 GUID string `json:"GUID"` 328 Name string `json:"Name"` 329 AllowSSH bool `json:"AllowSSH"` 330 } 331 332 // EnvOverride represents all the environment variables read by the CF CLI 333 type EnvOverride struct { 334 BinaryName string 335 CFColor string 336 CFDialTimeout string 337 CFHome string 338 CFLogLevel string 339 CFPluginHome string 340 CFStagingTimeout string 341 CFStartupTimeout string 342 CFTrace string 343 DockerPassword string 344 Experimental string 345 ForceTTY string 346 HTTPSProxy string 347 Lang string 348 LCAll string 349 } 350 351 // FlagOverride represents all the global flags passed to the CF CLI 352 type FlagOverride struct { 353 Verbose bool 354 } 355 356 // detectedSettings are automatically detected settings determined by the CLI. 357 type detectedSettings struct { 358 currentDirectory string 359 terminalWidth int 360 tty bool 361 } 362 363 // Target returns the CC API URL 364 func (config *Config) Target() string { 365 return config.ConfigFile.Target 366 } 367 368 // PollingInterval returns the time between polls. 369 func (config *Config) PollingInterval() time.Duration { 370 return DefaultPollingInterval 371 } 372 373 // OverallPollingTimeout returns the overall polling timeout for async 374 // operations. The time is based off of: 375 // 1. The config file's AsyncTimeout value (integer) is > 0 376 // 2. Defaults to the DefaultOverallPollingTimeout 377 func (config *Config) OverallPollingTimeout() time.Duration { 378 if config.ConfigFile.AsyncTimeout == 0 { 379 return DefaultOverallPollingTimeout 380 } 381 return time.Duration(config.ConfigFile.AsyncTimeout) * time.Minute 382 } 383 384 // SkipSSLValidation returns whether or not to skip SSL validation when 385 // targeting an API endpoint 386 func (config *Config) SkipSSLValidation() bool { 387 return config.ConfigFile.SkipSSLValidation 388 } 389 390 // AccessToken returns the access token for making authenticated API calls 391 func (config *Config) AccessToken() string { 392 return config.ConfigFile.AccessToken 393 } 394 395 // RefreshToken returns the refresh token for getting a new access token 396 func (config *Config) RefreshToken() string { 397 return config.ConfigFile.RefreshToken 398 } 399 400 // SSHOAuthClient returns the OAuth client id used for SSHing into 401 // application/process containers 402 func (config *Config) SSHOAuthClient() string { 403 return config.ConfigFile.SSHOAuthClient 404 } 405 406 // UAAOAuthClient returns the CLI's UAA client ID 407 func (config *Config) UAAOAuthClient() string { 408 return config.ConfigFile.UAAOAuthClient 409 } 410 411 // UAAOAuthClientSecret returns the CLI's UAA client secret 412 func (config *Config) UAAOAuthClientSecret() string { 413 return config.ConfigFile.UAAOAuthClientSecret 414 } 415 416 // APIVersion returns the CC API Version 417 func (config *Config) APIVersion() string { 418 return config.ConfigFile.APIVersion 419 } 420 421 // MinCLIVersion returns the minimum CLI version requried by the CC 422 func (config *Config) MinCLIVersion() string { 423 return config.ConfigFile.MinCLIVersion 424 } 425 426 // TargetedOrganization returns the currently targeted organization 427 func (config *Config) TargetedOrganization() Organization { 428 return config.ConfigFile.TargetedOrganization 429 } 430 431 // TargetedSpace returns the currently targeted space 432 func (config *Config) TargetedSpace() Space { 433 return config.ConfigFile.TargetedSpace 434 } 435 436 // StagingTimeout returns the max time an application staging should take. The 437 // time is based off of: 438 // 1. The $CF_STAGING_TIMEOUT environment variable if set 439 // 2. Defaults to the DefaultStagingTimeout 440 func (config *Config) StagingTimeout() time.Duration { 441 if config.ENV.CFStagingTimeout != "" { 442 val, err := strconv.ParseInt(config.ENV.CFStagingTimeout, 10, 64) 443 if err == nil { 444 return time.Duration(val) * time.Minute 445 } 446 } 447 448 return DefaultStagingTimeout 449 } 450 451 // StartupTimeout returns the max time an application should take to start. The 452 // time is based off of: 453 // 1. The $CF_STARTUP_TIMEOUT environment variable if set 454 // 2. Defaults to the DefaultStartupTimeout 455 func (config *Config) StartupTimeout() time.Duration { 456 if config.ENV.CFStartupTimeout != "" { 457 val, err := strconv.ParseInt(config.ENV.CFStartupTimeout, 10, 64) 458 if err == nil { 459 return time.Duration(val) * time.Minute 460 } 461 } 462 463 return DefaultStartupTimeout 464 } 465 466 // HTTPSProxy returns the proxy url that the CLI should use. The url is based 467 // off of: 468 // 1. The $https_proxy environment variable if set 469 // 2. Defaults to the empty string 470 func (config *Config) HTTPSProxy() string { 471 if config.ENV.HTTPSProxy != "" { 472 return config.ENV.HTTPSProxy 473 } 474 475 return "" 476 } 477 478 // BinaryName returns the running name of the CF CLI 479 func (config *Config) BinaryName() string { 480 return config.ENV.BinaryName 481 } 482 483 // Experimental returns whether or not to run experimental CLI commands. This 484 // is based off of: 485 // 1. The $CF_CLI_EXPERIMENTAL environment variable if set 486 // 2. Defaults to false 487 func (config *Config) Experimental() bool { 488 if config.ENV.Experimental != "" { 489 envVal, err := strconv.ParseBool(config.ENV.Experimental) 490 if err == nil { 491 return envVal 492 } 493 } 494 495 return false 496 } 497 498 // Verbose returns true if verbose should be displayed to terminal, in addition 499 // a slice of full paths in which verbose text will appear. This is based off 500 // of: 501 // - The config file's trace value (true/false/file path) 502 // - The $CF_TRACE enviroment variable if set (true/false/file path) 503 // - The '-v/--verbose' global flag 504 // - Defaults to false 505 func (config *Config) Verbose() (bool, []string) { 506 var ( 507 verbose bool 508 envOverride bool 509 filePath []string 510 ) 511 if config.ENV.CFTrace != "" { 512 envVal, err := strconv.ParseBool(config.ENV.CFTrace) 513 verbose = envVal 514 if err != nil { 515 filePath = []string{config.ENV.CFTrace} 516 } else { 517 envOverride = true 518 } 519 } 520 if config.ConfigFile.Trace != "" { 521 envVal, err := strconv.ParseBool(config.ConfigFile.Trace) 522 if !envOverride { 523 verbose = envVal || verbose 524 } 525 if err != nil { 526 filePath = append(filePath, config.ConfigFile.Trace) 527 } 528 } 529 verbose = config.Flags.Verbose || verbose 530 531 for i, path := range filePath { 532 if !filepath.IsAbs(path) { 533 filePath[i] = filepath.Join(config.detectedSettings.currentDirectory, path) 534 } 535 } 536 537 return verbose, filePath 538 } 539 540 // IsTTY returns true based off of: 541 // - The $FORCE_TTY is set to true/t/1 542 // - Detected from the STDOUT stream 543 func (config *Config) IsTTY() bool { 544 if config.ENV.ForceTTY != "" { 545 envVal, err := strconv.ParseBool(config.ENV.ForceTTY) 546 if err == nil { 547 return envVal 548 } 549 } 550 551 return config.detectedSettings.tty 552 } 553 554 // LogLevel returns the global log level. The levels follow Logrus's log level 555 // scheme. This value is based off of: 556 // - The $CF_LOG_LEVEL and an int/warn/info/etc... 557 // - Defaults to PANIC/0 (ie no logging) 558 func (config *Config) LogLevel() int { 559 if config.ENV.CFLogLevel != "" { 560 envVal, err := strconv.ParseInt(config.ENV.CFLogLevel, 10, 32) 561 if err == nil { 562 return int(envVal) 563 } 564 565 switch strings.ToLower(config.ENV.CFLogLevel) { 566 case "fatal": 567 return 1 568 case "error": 569 return 2 570 case "warn": 571 return 3 572 case "info": 573 return 4 574 case "debug": 575 return 5 576 } 577 } 578 579 return 0 580 } 581 582 // TerminalWidth returns the width of the terminal from when the config 583 // was loaded. If the terminal width has changed since the config has loaded, 584 // it will **not** return the new width. 585 func (config *Config) TerminalWidth() int { 586 return config.detectedSettings.terminalWidth 587 } 588 589 // DialTimeout returns the timeout to use when dialing. This is based off of: 590 // 1. The $CF_DIAL_TIMEOUT environment variable if set 591 // 2. Defaults to 5 seconds 592 func (config *Config) DialTimeout() time.Duration { 593 if config.ENV.CFDialTimeout != "" { 594 envVal, err := strconv.ParseInt(config.ENV.CFDialTimeout, 10, 64) 595 if err == nil { 596 return time.Duration(envVal) * time.Second 597 } 598 } 599 600 return DefaultDialTimeout 601 } 602 603 func (config *Config) BinaryVersion() string { 604 return version.VersionString() 605 } 606 607 // HasTargetedOrganization returns true if the organization is set 608 func (config *Config) HasTargetedOrganization() bool { 609 return config.ConfigFile.TargetedOrganization.GUID != "" 610 } 611 612 // HasTargetedSpace returns true if the space is set 613 func (config *Config) HasTargetedSpace() bool { 614 return config.ConfigFile.TargetedSpace.GUID != "" 615 } 616 617 // DockerPassword returns the docker password from the environment. 618 func (config *Config) DockerPassword() string { 619 return config.ENV.DockerPassword 620 } 621 622 // SetOrganizationInformation sets the currently targeted organization 623 func (config *Config) SetOrganizationInformation(guid string, name string) { 624 config.ConfigFile.TargetedOrganization.GUID = guid 625 config.ConfigFile.TargetedOrganization.Name = name 626 config.ConfigFile.TargetedOrganization.QuotaDefinition = QuotaDefinition{} 627 } 628 629 // SetSpaceInformation sets the currently targeted space 630 func (config *Config) SetSpaceInformation(guid string, name string, allowSSH bool) { 631 config.ConfigFile.TargetedSpace.GUID = guid 632 config.ConfigFile.TargetedSpace.Name = name 633 config.ConfigFile.TargetedSpace.AllowSSH = allowSSH 634 } 635 636 // SetTargetInformation sets the currently targeted CC API and related other 637 // related API URLs 638 func (config *Config) SetTargetInformation(api string, apiVersion string, auth string, minCLIVersion string, doppler string, routing string, skipSSLValidation bool) { 639 config.ConfigFile.Target = api 640 config.ConfigFile.APIVersion = apiVersion 641 config.ConfigFile.AuthorizationEndpoint = auth 642 config.ConfigFile.MinCLIVersion = minCLIVersion 643 config.ConfigFile.DopplerEndpoint = doppler 644 config.ConfigFile.RoutingEndpoint = routing 645 config.ConfigFile.SkipSSLValidation = skipSSLValidation 646 647 config.UnsetOrganizationInformation() 648 config.UnsetSpaceInformation() 649 } 650 651 // SetTokenInformation sets the current token/user information 652 func (config *Config) SetTokenInformation(accessToken string, refreshToken string, sshOAuthClient string) { 653 config.ConfigFile.AccessToken = accessToken 654 config.ConfigFile.RefreshToken = refreshToken 655 config.ConfigFile.SSHOAuthClient = sshOAuthClient 656 } 657 658 // SetAccessToken sets the current access token 659 func (config *Config) SetAccessToken(accessToken string) { 660 config.ConfigFile.AccessToken = accessToken 661 } 662 663 // SetRefreshToken sets the current refresh token 664 func (config *Config) SetRefreshToken(refreshToken string) { 665 config.ConfigFile.RefreshToken = refreshToken 666 } 667 668 // SetUAAEndpoint sets the UAA endpoint that is obtained from hitting 669 // <AuthorizationEndpoint>/login 670 func (config *Config) SetUAAEndpoint(uaaEndpoint string) { 671 config.ConfigFile.UAAEndpoint = uaaEndpoint 672 } 673 674 // UnsetSpaceInformation resets the space values to default 675 func (config *Config) UnsetSpaceInformation() { 676 config.SetSpaceInformation("", "", false) 677 } 678 679 // UnsetOrganizationInformation resets the organization values to default 680 func (config *Config) UnsetOrganizationInformation() { 681 config.SetOrganizationInformation("", "") 682 }