config.go 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. package config
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "os"
  7. "path/filepath"
  8. "strings"
  9. "github.com/fatih/color"
  10. api "github.com/porter-dev/porter/api/client"
  11. "github.com/porter-dev/porter/cli/cmd/utils"
  12. "github.com/spf13/viper"
  13. "k8s.io/client-go/util/homedir"
  14. )
  15. var home = homedir.HomeDir()
  16. // CLIConfig is the set of shared configuration options for the CLI commands.
  17. // This config is used by viper: calling Set() function for any parameter will
  18. // update the corresponding field in the viper config file.
  19. type CLIConfig struct {
  20. // Driver can be either "docker" or "local", and represents which driver is
  21. // used to run an instance of the server.
  22. Driver string `yaml:"driver"`
  23. Host string `yaml:"host"`
  24. Project uint `yaml:"project"`
  25. Cluster uint `yaml:"cluster"`
  26. Token string `yaml:"token"`
  27. Registry uint `yaml:"registry"`
  28. HelmRepo uint `yaml:"helm_repo"`
  29. Kubeconfig string `yaml:"kubeconfig"`
  30. }
  31. // InitAndLoadConfig populates the config object with the following precedence rules:
  32. // 1. flag
  33. // 2. env
  34. // 3. config
  35. // 4. default
  36. func InitAndLoadConfig() (CLIConfig, error) {
  37. return initAndLoadConfig()
  38. }
  39. func initAndLoadConfig() (CLIConfig, error) {
  40. var config CLIConfig
  41. porterDir, err := getOrCreatePorterDirectoryAndConfig()
  42. if err != nil {
  43. return config, fmt.Errorf("unable to get or create porter directory: %w", err)
  44. }
  45. viper.SetConfigName("porter")
  46. viper.SetConfigType("yaml")
  47. viper.AddConfigPath(porterDir)
  48. utils.DriverFlagSet.StringVar(
  49. &config.Driver,
  50. "driver",
  51. "local",
  52. "driver to use (local or docker)",
  53. )
  54. err = viper.BindPFlags(utils.DriverFlagSet)
  55. if err != nil {
  56. return config, err
  57. }
  58. utils.DefaultFlagSet.StringVar(
  59. &config.Host,
  60. "host",
  61. "https://dashboard.getporter.dev",
  62. "host URL of Porter instance",
  63. )
  64. utils.DefaultFlagSet.UintVar(
  65. &config.Project,
  66. "project",
  67. 0,
  68. "project ID of Porter project",
  69. )
  70. utils.DefaultFlagSet.UintVar(
  71. &config.Cluster,
  72. "cluster",
  73. 0,
  74. "cluster ID of Porter cluster",
  75. )
  76. utils.DefaultFlagSet.StringVar(
  77. &config.Token,
  78. "token",
  79. "",
  80. "token for Porter authentication",
  81. )
  82. err = viper.BindPFlags(utils.DefaultFlagSet)
  83. if err != nil {
  84. return config, err
  85. }
  86. utils.RegistryFlagSet.UintVar(
  87. &config.Registry,
  88. "registry",
  89. 0,
  90. "registry ID of connected Porter registry",
  91. )
  92. err = viper.BindPFlags(utils.RegistryFlagSet)
  93. if err != nil {
  94. return config, err
  95. }
  96. utils.HelmRepoFlagSet.UintVar(
  97. &config.HelmRepo,
  98. "helmrepo",
  99. 0,
  100. "helm repo ID of connected Porter Helm repository",
  101. )
  102. err = viper.BindPFlags(utils.HelmRepoFlagSet)
  103. if err != nil {
  104. return config, err
  105. }
  106. viper.SetEnvPrefix("PORTER")
  107. err = viper.BindEnv("host")
  108. if err != nil {
  109. return config, err
  110. }
  111. err = viper.BindEnv("project")
  112. if err != nil {
  113. return config, err
  114. }
  115. err = viper.BindEnv("cluster")
  116. if err != nil {
  117. return config, err
  118. }
  119. err = viper.BindEnv("token")
  120. if err != nil {
  121. return config, err
  122. }
  123. err = createAndLoadPorterYaml(porterDir)
  124. if err != nil {
  125. return config, fmt.Errorf("unable to load porter config: %w", err)
  126. }
  127. err = viper.Unmarshal(&config)
  128. if err != nil {
  129. return config, fmt.Errorf("unable to unmarshal porter config: %w", err)
  130. }
  131. return config, nil
  132. }
  133. // getOrCreatePorterDirectoryAndConfig checks that the .porter folder exists; create if not
  134. func getOrCreatePorterDirectoryAndConfig() (string, error) {
  135. porterDir := filepath.Join(home, ".porter")
  136. _, err := os.Stat(porterDir)
  137. if err != nil {
  138. if !os.IsNotExist(err) {
  139. return "", fmt.Errorf("error reading porter directory: %w", err)
  140. }
  141. err = os.Mkdir(porterDir, 0o700)
  142. if err != nil {
  143. return "", fmt.Errorf("error creating porter directory: %w", err)
  144. }
  145. }
  146. return porterDir, nil
  147. }
  148. // createAndLoadPorterYaml loads a porter.yaml config into Viper if it exists, or creates the file if it does not
  149. func createAndLoadPorterYaml(porterDir string) error {
  150. err := viper.ReadInConfig()
  151. if err != nil {
  152. _, ok := err.(viper.ConfigFileNotFoundError)
  153. if !ok {
  154. return fmt.Errorf("unknown error reading ~/.porter/porter.yaml config: %w", err)
  155. }
  156. err := os.WriteFile(filepath.Join(porterDir, "porter.yaml"), []byte{}, 0o644) //nolint:gosec // do not want to change program logic. Should be addressed later
  157. if err != nil {
  158. return fmt.Errorf("unable to create ~/.porter/porter.yaml config: %w", err)
  159. }
  160. }
  161. return nil
  162. }
  163. // func GetCLIConfig() *CLIConfig {
  164. // if config == nil {
  165. // panic("GetCLIConfig() called before initialisation")
  166. // }
  167. // return config
  168. // }
  169. // func GetAPIClient() api.Client {
  170. // ctx := ctx
  171. // config := GetCLIConfig()
  172. // client := api.NewClientWithConfig(ctx, api.NewClientInput{
  173. // BaseURL: fmt.Sprintf("%s/api", config.Host),
  174. // BearerToken: config.Token,
  175. // CookieFileName: "cookie.json",
  176. // })
  177. // return client
  178. // }
  179. func (c *CLIConfig) SetDriver(driver string) error {
  180. viper.Set("driver", driver)
  181. color.New(color.FgGreen).Printf("Set the current driver as %s\n", driver)
  182. err := viper.WriteConfig()
  183. if err != nil {
  184. return err
  185. }
  186. c.Driver = driver
  187. return nil
  188. }
  189. func (c *CLIConfig) SetHost(host string) error {
  190. // a trailing / can lead to errors with the api server
  191. host = strings.TrimRight(host, "/")
  192. viper.Set("host", host)
  193. // let us clear the project ID, cluster ID, and token when we reset a host
  194. viper.Set("project", 0)
  195. viper.Set("cluster", 0)
  196. viper.Set("token", "")
  197. err := viper.WriteConfig()
  198. if err != nil {
  199. return err
  200. }
  201. color.New(color.FgGreen).Printf("Set the current host as %s\n", host)
  202. c.Host = host
  203. c.Project = 0
  204. c.Cluster = 0
  205. c.Token = ""
  206. return nil
  207. }
  208. // SetProject sets a project for all API commands
  209. func (c *CLIConfig) SetProject(ctx context.Context, apiClient api.Client, projectID uint) error {
  210. viper.Set("project", projectID)
  211. color.New(color.FgGreen).Printf("Set the current project as %d\n", projectID)
  212. if c.Kubeconfig != "" || viper.IsSet("kubeconfig") {
  213. color.New(color.FgYellow).Println("Please change local kubeconfig if needed")
  214. }
  215. err := viper.WriteConfig()
  216. if err != nil {
  217. return err
  218. }
  219. c.Project = projectID
  220. resp, err := apiClient.ListProjectClusters(ctx, projectID)
  221. if err == nil {
  222. clusters := *resp
  223. if len(clusters) == 1 {
  224. _ = c.SetCluster(clusters[0].ID)
  225. }
  226. }
  227. return nil
  228. }
  229. func (c *CLIConfig) SetCluster(clusterID uint) error {
  230. viper.Set("cluster", clusterID)
  231. color.New(color.FgGreen).Printf("Set the current cluster as %d\n", clusterID)
  232. if c.Kubeconfig != "" || viper.IsSet("kubeconfig") {
  233. color.New(color.FgYellow).Println("Please change local kubeconfig if needed")
  234. }
  235. err := viper.WriteConfig()
  236. if err != nil {
  237. return err
  238. }
  239. c.Cluster = clusterID
  240. return nil
  241. }
  242. func (c *CLIConfig) SetToken(token string) error {
  243. viper.Set("token", token)
  244. err := viper.WriteConfig()
  245. if err != nil {
  246. return err
  247. }
  248. c.Token = token
  249. return nil
  250. }
  251. func (c *CLIConfig) SetRegistry(registryID uint) error {
  252. viper.Set("registry", registryID)
  253. color.New(color.FgGreen).Printf("Set the current registry as %d\n", registryID)
  254. err := viper.WriteConfig()
  255. if err != nil {
  256. return err
  257. }
  258. c.Registry = registryID
  259. return nil
  260. }
  261. func (c *CLIConfig) SetHelmRepo(helmRepoID uint) error {
  262. viper.Set("helm_repo", helmRepoID)
  263. color.New(color.FgGreen).Printf("Set the current Helm repo as %d\n", helmRepoID)
  264. err := viper.WriteConfig()
  265. if err != nil {
  266. return err
  267. }
  268. c.HelmRepo = helmRepoID
  269. return nil
  270. }
  271. func (c *CLIConfig) SetKubeconfig(kubeconfig string) error {
  272. path, err := filepath.Abs(kubeconfig)
  273. if err != nil {
  274. return err
  275. }
  276. if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
  277. return fmt.Errorf("%s does not exist", path)
  278. }
  279. viper.Set("kubeconfig", path)
  280. color.New(color.FgGreen).Printf("Set the path to kubeconfig as %s\n", path)
  281. err = viper.WriteConfig()
  282. if err != nil {
  283. return err
  284. }
  285. c.Kubeconfig = kubeconfig
  286. return nil
  287. }
  288. // ValidateCLIEnvironment checks that all required variables are present for running the CLI
  289. func (c *CLIConfig) ValidateCLIEnvironment() error {
  290. if c.Token == "" {
  291. return fmt.Errorf("no auth token present, please run 'porter auth login' to authenticate")
  292. }
  293. if c.Project == 0 {
  294. return fmt.Errorf("no project selected, please run 'porter config set-project' to select a project")
  295. }
  296. if c.Cluster == 0 {
  297. return fmt.Errorf("no cluster selected, please run 'porter config set-cluster' to select a cluster")
  298. }
  299. return nil
  300. }