profiles.go 8.6 KB

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