config.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  1. package config
  2. import (
  3. "errors"
  4. "fmt"
  5. "io/ioutil"
  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 (
  16. home = homedir.HomeDir()
  17. porterDir = filepath.Join(home, ".porter")
  18. defaultFile = filepath.Join(porterDir, "default.yaml")
  19. porterFile = filepath.Join(porterDir, "porter.yaml")
  20. currentProfile = "default"
  21. )
  22. // config is a shared object used by all commands
  23. var config = &CLIConfig{}
  24. // CLIConfig is the set of shared configuration options for the CLI commands.
  25. // This config is used by viper: calling Set() function for any parameter will
  26. // update the corresponding field in the viper config file.
  27. type CLIConfig struct {
  28. // Driver can be either "docker" or "local", and represents which driver is
  29. // used to run an instance of the server.
  30. Driver string `yaml:"driver"`
  31. Host string `yaml:"host"`
  32. Project uint `yaml:"project"`
  33. Cluster uint `yaml:"cluster"`
  34. Token string `yaml:"token"`
  35. Registry uint `yaml:"registry"`
  36. HelmRepo uint `yaml:"helm_repo"`
  37. Kubeconfig string `yaml:"kubeconfig"`
  38. }
  39. // InitAndLoadConfig populates the config object with the following precedence rules:
  40. // 1. flag
  41. // 2. env
  42. // 3. config
  43. // 4. default
  44. //
  45. // It populates the shared config object above
  46. func InitAndLoadConfig() {
  47. initAndLoadConfig(config)
  48. }
  49. func InitAndLoadNewConfig() *CLIConfig {
  50. newConfig := &CLIConfig{}
  51. initAndLoadConfig(newConfig)
  52. return newConfig
  53. }
  54. func createDefaultProfileIfNeeded() {
  55. // first check if the porter directory exists at all
  56. if _, err := os.Stat(porterDir); os.IsNotExist(err) {
  57. return
  58. }
  59. // since the porter directory exists, let us check for the existence of the default profile
  60. if _, err := os.Stat(defaultFile); os.IsNotExist(err) {
  61. // since the default profile does not exist, let us check for the existence of an older porter.yaml
  62. if _, err := os.Stat(porterFile); err != nil {
  63. if os.IsNotExist(err) {
  64. return
  65. }
  66. color.New(color.FgRed).Printf("%v\n", err)
  67. os.Exit(1)
  68. } else {
  69. // since the porter.yaml exists, let us copy it to the default profile
  70. bytes, err := ioutil.ReadFile(porterFile)
  71. if err != nil {
  72. color.New(color.FgRed).Printf("%v\n", err)
  73. os.Exit(1)
  74. }
  75. err = ioutil.WriteFile(defaultFile, bytes, 0644)
  76. if err != nil {
  77. color.New(color.FgRed).Printf("%v\n", err)
  78. os.Exit(1)
  79. }
  80. // let us now remove the existing porter.yaml and create a symlink porter.yaml -> default.yaml
  81. err = os.Remove(porterFile)
  82. if err != nil {
  83. color.New(color.FgRed).Printf("%v\n", err)
  84. os.Exit(1)
  85. }
  86. err = os.Symlink(defaultFile, porterFile)
  87. if err != nil {
  88. color.New(color.FgRed).Printf("%v\n", err)
  89. os.Exit(1)
  90. }
  91. }
  92. }
  93. }
  94. func initAndLoadConfig(_config *CLIConfig) {
  95. createDefaultProfileIfNeeded()
  96. initFlagSet()
  97. // check that the .porter folder exists; create if not
  98. if _, err := os.Stat(porterDir); os.IsNotExist(err) {
  99. os.Mkdir(porterDir, 0700)
  100. } else if err != nil {
  101. color.New(color.FgRed).Printf("%v\n", err)
  102. os.Exit(1)
  103. }
  104. viper.SetConfigName("porter")
  105. viper.SetConfigType("yaml")
  106. viper.AddConfigPath(porterDir)
  107. // Bind the flagset initialized above
  108. viper.BindPFlags(utils.DriverFlagSet)
  109. viper.BindPFlags(utils.DefaultFlagSet)
  110. viper.BindPFlags(utils.RegistryFlagSet)
  111. viper.BindPFlags(utils.HelmRepoFlagSet)
  112. // Bind the environment variables with prefix "PORTER_"
  113. viper.SetEnvPrefix("PORTER")
  114. viper.BindEnv("host")
  115. viper.BindEnv("project")
  116. viper.BindEnv("cluster")
  117. viper.BindEnv("token")
  118. currentProfileFile, err := os.Readlink(porterFile)
  119. if err != nil {
  120. color.New(color.FgRed).Printf("%v\n", err)
  121. os.Exit(1)
  122. }
  123. currentProfile = strings.TrimSuffix(filepath.Base(currentProfileFile), filepath.Ext(currentProfileFile))
  124. err = viper.ReadInConfig()
  125. if err != nil {
  126. if _, ok := err.(viper.ConfigFileNotFoundError); ok {
  127. // create blank config file
  128. err := ioutil.WriteFile(defaultFile, []byte{}, 0644)
  129. if err != nil {
  130. color.New(color.FgRed).Printf("%v\n", err)
  131. os.Exit(1)
  132. }
  133. if _, err := os.Stat(porterFile); os.IsExist(err) {
  134. err := os.Remove(porterFile)
  135. if err != nil {
  136. color.New(color.FgRed).Printf("%v\n", err)
  137. os.Exit(1)
  138. }
  139. }
  140. // create symlink to default config file
  141. err = os.Symlink(defaultFile, porterFile)
  142. if err != nil {
  143. color.New(color.FgRed).Printf("%v\n", err)
  144. os.Exit(1)
  145. }
  146. } else {
  147. // Config file was found but another error was produced
  148. color.New(color.FgRed).Printf("%v\n", err)
  149. os.Exit(1)
  150. }
  151. }
  152. // unmarshal the config into the shared config struct
  153. viper.Unmarshal(_config)
  154. }
  155. // initFlagSet initializes the shared flags used by multiple commands
  156. func initFlagSet() {
  157. utils.DriverFlagSet.StringVar(
  158. &config.Driver,
  159. "driver",
  160. "local",
  161. "driver to use (local or docker)",
  162. )
  163. utils.DefaultFlagSet.StringVar(
  164. &config.Host,
  165. "host",
  166. "https://dashboard.getporter.dev",
  167. "host URL of Porter instance",
  168. )
  169. utils.DefaultFlagSet.UintVar(
  170. &config.Project,
  171. "project",
  172. 0,
  173. "project ID of Porter project",
  174. )
  175. utils.DefaultFlagSet.UintVar(
  176. &config.Cluster,
  177. "cluster",
  178. 0,
  179. "cluster ID of Porter cluster",
  180. )
  181. utils.DefaultFlagSet.StringVar(
  182. &config.Token,
  183. "token",
  184. "",
  185. "token for Porter authentication",
  186. )
  187. utils.RegistryFlagSet.UintVar(
  188. &config.Registry,
  189. "registry",
  190. 0,
  191. "registry ID of connected Porter registry",
  192. )
  193. utils.HelmRepoFlagSet.UintVar(
  194. &config.HelmRepo,
  195. "helmrepo",
  196. 0,
  197. "helm repo ID of connected Porter Helm repository",
  198. )
  199. }
  200. func GetCLIConfig() *CLIConfig {
  201. if config == nil {
  202. panic("GetCLIConfig() called before initialisation")
  203. }
  204. return config
  205. }
  206. func GetAPIClient() *api.Client {
  207. config := GetCLIConfig()
  208. if token := config.Token; token != "" {
  209. return api.NewClientWithToken(config.Host+"/api", token)
  210. }
  211. return api.NewClient(config.Host+"/api", "cookie.json")
  212. }
  213. func (c *CLIConfig) SetDriver(driver string) error {
  214. viper.Set("driver", driver)
  215. color.New(color.FgGreen).Printf("Set the current driver as %s\n", driver)
  216. err := viper.WriteConfig()
  217. if err != nil {
  218. return err
  219. }
  220. config.Driver = driver
  221. return nil
  222. }
  223. func (c *CLIConfig) SetHost(host string) error {
  224. // a trailing / can lead to errors with the api server
  225. host = strings.TrimRight(host, "/")
  226. viper.Set("host", host)
  227. color.New(color.FgGreen).Printf("Set the current host as %s\n", host)
  228. err := viper.WriteConfig()
  229. if err != nil {
  230. return err
  231. }
  232. config.Host = host
  233. return nil
  234. }
  235. func (c *CLIConfig) SetProject(projectID uint) error {
  236. if config.Kubeconfig != "" || viper.IsSet("kubeconfig") {
  237. viper.Set("kubeconfig", "")
  238. color.New(color.FgBlue).Println("Removing local kubeconfig")
  239. config.Kubeconfig = ""
  240. }
  241. viper.Set("project", projectID)
  242. color.New(color.FgGreen).Printf("Set the current project as %d\n", projectID)
  243. err := viper.WriteConfig()
  244. if err != nil {
  245. return err
  246. }
  247. config.Project = projectID
  248. return nil
  249. }
  250. func (c *CLIConfig) SetCluster(clusterID uint) error {
  251. if config.Kubeconfig != "" || viper.IsSet("kubeconfig") {
  252. viper.Set("kubeconfig", "")
  253. color.New(color.FgBlue).Println("Removing local kubeconfig")
  254. config.Kubeconfig = ""
  255. }
  256. viper.Set("cluster", clusterID)
  257. color.New(color.FgGreen).Printf("Set the current cluster as %d\n", clusterID)
  258. err := viper.WriteConfig()
  259. if err != nil {
  260. return err
  261. }
  262. config.Cluster = clusterID
  263. return nil
  264. }
  265. func (c *CLIConfig) SetToken(token string) error {
  266. viper.Set("token", token)
  267. err := viper.WriteConfig()
  268. if err != nil {
  269. return err
  270. }
  271. config.Token = token
  272. return nil
  273. }
  274. func (c *CLIConfig) SetRegistry(registryID uint) error {
  275. viper.Set("registry", registryID)
  276. color.New(color.FgGreen).Printf("Set the current registry as %d\n", registryID)
  277. err := viper.WriteConfig()
  278. if err != nil {
  279. return err
  280. }
  281. config.Registry = registryID
  282. return nil
  283. }
  284. func (c *CLIConfig) SetHelmRepo(helmRepoID uint) error {
  285. viper.Set("helm_repo", helmRepoID)
  286. color.New(color.FgGreen).Printf("Set the current Helm repo as %d\n", helmRepoID)
  287. err := viper.WriteConfig()
  288. if err != nil {
  289. return err
  290. }
  291. config.HelmRepo = helmRepoID
  292. return nil
  293. }
  294. func (c *CLIConfig) SetKubeconfig(kubeconfig string) error {
  295. path, err := filepath.Abs(kubeconfig)
  296. if err != nil {
  297. return err
  298. }
  299. if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
  300. return fmt.Errorf("%s does not exist", path)
  301. }
  302. viper.Set("kubeconfig", path)
  303. color.New(color.FgGreen).Printf("Set the path to kubeconfig as %s\n", path)
  304. err = viper.WriteConfig()
  305. if err != nil {
  306. return err
  307. }
  308. config.Kubeconfig = kubeconfig
  309. return nil
  310. }
  311. func (c *CLIConfig) CreateProfile(name string) error {
  312. profiles, err := c.ListProfiles()
  313. if err != nil {
  314. return err
  315. }
  316. exists := false
  317. for _, profile := range profiles {
  318. if profile == name {
  319. exists = true
  320. break
  321. }
  322. }
  323. if exists {
  324. return fmt.Errorf("profile %s already exists", name)
  325. }
  326. err = ioutil.WriteFile(filepath.Join(porterDir, name+".yaml"), []byte{}, 0644)
  327. if err != nil {
  328. return fmt.Errorf("error creating profile: %w", err)
  329. }
  330. return nil
  331. }
  332. func (c *CLIConfig) ListProfiles() ([]string, error) {
  333. files, err := ioutil.ReadDir(porterDir)
  334. if err != nil {
  335. return nil, fmt.Errorf("error listing profiles: %w", err)
  336. }
  337. var profiles []string
  338. for _, file := range files {
  339. profileName := strings.TrimSuffix(filepath.Base(file.Name()), filepath.Ext(file.Name()))
  340. profileFile := filepath.Join(porterDir, file.Name())
  341. fd, err := os.Lstat(profileFile)
  342. if err != nil {
  343. return nil, fmt.Errorf("error listing profiles: %w", err)
  344. }
  345. if !file.IsDir() && filepath.Ext(file.Name()) == ".yaml" && fd.Mode()&os.ModeSymlink != os.ModeSymlink {
  346. f, err := os.Open(profileFile)
  347. if err != nil {
  348. return nil, fmt.Errorf("error reading profile: %s. Error: %w", profileName, err)
  349. }
  350. defer f.Close()
  351. if err := viper.New().ReadConfig(f); err == nil {
  352. profiles = append(profiles, profileName)
  353. } else {
  354. fmt.Println(err)
  355. }
  356. }
  357. }
  358. return profiles, nil
  359. }
  360. func (c *CLIConfig) SetProfile(name string) error {
  361. profiles, err := c.ListProfiles()
  362. if err != nil {
  363. return err
  364. }
  365. exists := false
  366. for _, profile := range profiles {
  367. if profile == name {
  368. exists = true
  369. break
  370. }
  371. }
  372. if !exists {
  373. return fmt.Errorf("profile %s does not exist", name)
  374. }
  375. err = os.Remove(porterFile)
  376. if err != nil {
  377. return fmt.Errorf("error setting profile: %w", err)
  378. }
  379. err = os.Symlink(filepath.Join(porterDir, name+".yaml"), porterFile)
  380. if err != nil {
  381. return fmt.Errorf("error setting profile: %w", err)
  382. }
  383. return nil
  384. }
  385. func (c *CLIConfig) DeleteProfile(name string) error {
  386. if name == "default" {
  387. return fmt.Errorf("cannot delete the default profile")
  388. } else if name == currentProfile {
  389. return fmt.Errorf("cannot delete the current profile")
  390. }
  391. profiles, err := c.ListProfiles()
  392. if err != nil {
  393. return err
  394. }
  395. exists := false
  396. for _, profile := range profiles {
  397. if profile == name {
  398. exists = true
  399. break
  400. }
  401. }
  402. if !exists {
  403. return fmt.Errorf("profile %s does not exist", name)
  404. }
  405. err = os.Remove(filepath.Join(porterDir, name+".yaml"))
  406. if err != nil {
  407. return fmt.Errorf("error deleting profile: %w", err)
  408. }
  409. return nil
  410. }