profiles.go 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. package config
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "os"
  7. "path/filepath"
  8. "strconv"
  9. "github.com/fatih/color"
  10. "github.com/sethvargo/go-envconfig"
  11. "gopkg.in/yaml.v2"
  12. )
  13. // ProfilesConfig is the top level config from the porter config file, containing all possible profiles.
  14. // This is only used when parsing and writing, and should now be passed around as config.
  15. // Instead, pass the specific profile to the relevant functions
  16. type ProfilesConfig struct {
  17. CurrentProfile string `yaml:"current_profile" env:"PORTER_PROFILE,default=default"`
  18. Profiles map[string]CLIConfig `yaml:"profiles"`
  19. }
  20. var defaultProfileName string = "default"
  21. // migrateExistingConfigYaml moves the existing config layout to the new config layout with profiles support.
  22. // This can be deprecated after 2025-01-01 as all tokens which were part of the old config, will have expired, meaning all users
  23. // will have had to log out
  24. func migrateExistingConfigYaml(configPath string) error {
  25. fi, err := os.ReadFile(filepath.Clean(configPath))
  26. if err != nil {
  27. return fmt.Errorf("error reading config file: %w", err)
  28. }
  29. // handle edge case where numbers are stored as strings for some values
  30. var existingConfig map[string]any
  31. err = yaml.Unmarshal(fi, &existingConfig)
  32. if err != nil {
  33. return fmt.Errorf("config file is invalid yaml: %w", err)
  34. }
  35. for k, v := range existingConfig {
  36. stringValue, ok := v.(string)
  37. if ok {
  38. valueAsInt, err := strconv.Atoi(stringValue)
  39. if err == nil {
  40. existingConfig[k] = valueAsInt
  41. }
  42. }
  43. }
  44. by, err := yaml.Marshal(existingConfig)
  45. if err != nil {
  46. return fmt.Errorf("unable to marshal existing config to bytes: %w", err)
  47. }
  48. var c CLIConfig
  49. err = yaml.Unmarshal(by, &c)
  50. if err != nil {
  51. return fmt.Errorf("config file is invalid yaml: %w", err)
  52. }
  53. err = updateValuesForSelectedProfile(defaultProfileName, configPath, withCLIConfig(c))
  54. if err != nil {
  55. return fmt.Errorf("unable to write migrated default config to file: %w", err)
  56. }
  57. err = updateCurrentProfileInFile(defaultProfileName, configPath)
  58. if err != nil {
  59. return fmt.Errorf("unable to update current_profile in config file: %w", err)
  60. }
  61. return nil
  62. }
  63. // writeProfileToProfilesConfigFile will write the given profile config to file, overwriting the entire existing file.
  64. // Ensure to call readProfilesConfigFromFile before running this function if you with to preserve current settings,
  65. // or call updateValuesForSelectedProfile to set specific values for a given profile
  66. func writeProfileToProfilesConfigFile(profilesConfig ProfilesConfig, configPath string) error {
  67. by, err := yaml.Marshal(profilesConfig)
  68. if err != nil {
  69. return fmt.Errorf("error marshalling profiles config: %w", err)
  70. }
  71. err = os.WriteFile(configPath, by, os.ModePerm)
  72. if err != nil {
  73. return fmt.Errorf("error writing profiles config to file")
  74. }
  75. return nil
  76. }
  77. // updateCurrentProfileInFile changes the current_profile that is selected in the file for the next run.
  78. func updateCurrentProfileInFile(newProfile string, configPath string) error {
  79. if newProfile == "" {
  80. return errors.New("cannot update profile to an empty profile")
  81. }
  82. profilesConfig, err := readProfilesConfigFromFile(configPath)
  83. if err != nil {
  84. return fmt.Errorf("error reading profiles config file for updating current profile: %w", err)
  85. }
  86. profilesConfig.CurrentProfile = newProfile
  87. err = writeProfileToProfilesConfigFile(profilesConfig, configPath)
  88. if err != nil {
  89. return fmt.Errorf("unable to update current profile in config file: %w", err)
  90. }
  91. return nil
  92. }
  93. // updateValuesForSelectedProfile updates config for a given profile. This can be used with the --profile flag or the PORTER_PROFILE env var,
  94. // and will not necessarily update the current_profile in the config file.
  95. func updateValuesForSelectedProfile(selectedProfile string, configPath string, updatedCLIValues ...cliConfigValue) error {
  96. if selectedProfile == "" {
  97. return errors.New("must specify a profile to update")
  98. }
  99. profilesConfig, err := readProfilesConfigFromFile(configPath)
  100. if err != nil {
  101. return fmt.Errorf("error reading profiles config file for updating selected profile: %w", err)
  102. }
  103. if profilesConfig.Profiles == nil {
  104. profilesConfig.Profiles = map[string]CLIConfig{
  105. selectedProfile: defaultCLIConfig(),
  106. }
  107. }
  108. if _, ok := profilesConfig.Profiles[selectedProfile]; !ok {
  109. profilesConfig.Profiles[selectedProfile] = defaultCLIConfig()
  110. }
  111. baseProfile := profilesConfig.Profiles[selectedProfile]
  112. for _, v := range updatedCLIValues {
  113. v(&baseProfile)
  114. }
  115. profilesConfig.Profiles[selectedProfile] = baseProfile
  116. err = writeProfileToProfilesConfigFile(profilesConfig, configPath)
  117. if err != nil {
  118. return fmt.Errorf("unable to update current profile in config file: %w", err)
  119. }
  120. return nil
  121. }
  122. // readProfilesConfigFromFile reads the config file which supports profiles
  123. func readProfilesConfigFromFile(configPath string) (ProfilesConfig, error) {
  124. var profiles ProfilesConfig
  125. fi, err := os.ReadFile(filepath.Clean(configPath))
  126. if err != nil {
  127. return profiles, fmt.Errorf("error reading config file: %w", err)
  128. }
  129. err = yaml.Unmarshal(fi, &profiles)
  130. if err != nil {
  131. return profiles, fmt.Errorf("config file is invalid yaml: %w", err)
  132. }
  133. return profiles, nil
  134. }
  135. // defaultCLIConfig sets the default values for give profile
  136. func defaultCLIConfig() CLIConfig {
  137. return CLIConfig{
  138. Driver: "local",
  139. Host: "https://dashboard.getporter.dev",
  140. Project: 0,
  141. Cluster: 0,
  142. Token: "",
  143. Registry: 0,
  144. HelmRepo: 0,
  145. }
  146. }
  147. // ensurePorterConfigDirectoryExists checks that the .porter folder exists, and creates it if it doesn't exist
  148. func ensurePorterConfigDirectoryExists() error {
  149. _, err := os.Stat(defaultPorterConfigDir)
  150. if err != nil {
  151. if !os.IsNotExist(err) {
  152. return fmt.Errorf("error reading porter directory: %w", err)
  153. }
  154. err = os.Mkdir(defaultPorterConfigDir, 0o700)
  155. if err != nil {
  156. return fmt.Errorf("error creating porter directory: %w", err)
  157. }
  158. }
  159. return nil
  160. }
  161. // ensurePorterConfigFileExists checks that the porter.yaml config file exists, and creates it if it doesn't exist
  162. func ensurePorterConfigFileExists() error {
  163. _, err := os.Stat(porterConfigFilePath)
  164. if err != nil {
  165. if !os.IsNotExist(err) {
  166. return fmt.Errorf("error reading porter config file: %w", err)
  167. }
  168. by, _ := yaml.Marshal(defaultCLIConfig())
  169. err = os.WriteFile(porterConfigFilePath, by, 0o664)
  170. if err != nil {
  171. if !errors.Is(err, os.ErrExist) {
  172. return fmt.Errorf("error creating porter config file: %w", err)
  173. }
  174. }
  175. }
  176. return nil
  177. }
  178. // overlayProfiles will add all values from the profileToOverlay to the baseProfile,
  179. // returning the new profile with both values
  180. //
  181. //nolint:unparam
  182. func overlayProfiles(baseProfile CLIConfig, profileToOverlay CLIConfig) (CLIConfig, error) {
  183. if profileToOverlay.Cluster != 0 {
  184. baseProfile.Cluster = profileToOverlay.Cluster
  185. }
  186. if profileToOverlay.Driver != "" {
  187. baseProfile.Driver = profileToOverlay.Driver
  188. }
  189. if profileToOverlay.HelmRepo != 0 {
  190. baseProfile.HelmRepo = profileToOverlay.HelmRepo
  191. }
  192. if profileToOverlay.Host != "" {
  193. baseProfile.Host = profileToOverlay.Host
  194. }
  195. if profileToOverlay.Kubeconfig != "" {
  196. baseProfile.Kubeconfig = profileToOverlay.Kubeconfig
  197. }
  198. if profileToOverlay.Project != 0 {
  199. baseProfile.Project = profileToOverlay.Project
  200. }
  201. if profileToOverlay.Registry != 0 {
  202. baseProfile.Registry = profileToOverlay.Registry
  203. }
  204. if profileToOverlay.Token != "" {
  205. baseProfile.Token = profileToOverlay.Token
  206. }
  207. return baseProfile, nil
  208. }
  209. // configForProfileFromConfigFile gets the profile for the current_profile specified in the porter config file
  210. func configForProfileFromConfigFile(selectedProfile string, configPath string) (CLIConfig, string, error) {
  211. if selectedProfile == "" {
  212. selectedProfile = defaultProfileName
  213. }
  214. profilesConfig, err := readProfilesConfigFromFile(configPath)
  215. if err != nil {
  216. return CLIConfig{}, selectedProfile, fmt.Errorf("error reading profiles config file: %w", err)
  217. }
  218. if selectedProfile == defaultProfileName {
  219. if profilesConfig.CurrentProfile != "" {
  220. selectedProfile = profilesConfig.CurrentProfile
  221. }
  222. }
  223. if profilesConfig.CurrentProfile == "" && len(profilesConfig.Profiles) == 0 {
  224. err := migrateExistingConfigYaml(configPath)
  225. if err != nil {
  226. return CLIConfig{}, selectedProfile, fmt.Errorf("error migrating porter.yaml config file. Please delete file at %s. %w", configPath, err)
  227. }
  228. migrated, selectedProfile, err := configForProfileFromConfigFile(selectedProfile, configPath)
  229. if err != nil {
  230. return CLIConfig{}, selectedProfile, fmt.Errorf("error migrating existing porter.yaml to support profiles: %w", err)
  231. }
  232. return migrated, selectedProfile, nil
  233. }
  234. configFile, ok := profilesConfig.Profiles[selectedProfile]
  235. if !ok {
  236. _, _ = color.New(color.FgGreen).Printf("Porter profile '%s' does not exist. Creating one now...\n", currentProfile)
  237. err = updateValuesForSelectedProfile(selectedProfile, configPath, withCLIConfig(defaultCLIConfig()))
  238. if err != nil {
  239. return CLIConfig{}, selectedProfile, fmt.Errorf("error creating new profile: %w", err)
  240. }
  241. }
  242. return configFile, selectedProfile, nil
  243. }
  244. // profileConfigFromEnvVars parses any environment variables that may be setting
  245. // config values, such as PORTER_HOST and PORTER_PROJECT
  246. func profileConfigFromEnvVars(ctx context.Context) (CLIConfig, error) {
  247. var c CLIConfig
  248. if err := envconfig.Process(ctx, &c); err != nil {
  249. return c, fmt.Errorf("error processing porter env vars: %w", err)
  250. }
  251. return c, nil
  252. }