apply.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. package cmd
  2. import (
  3. "context"
  4. "fmt"
  5. "io/ioutil"
  6. "os"
  7. "path/filepath"
  8. "strconv"
  9. "github.com/cli/cli/git"
  10. "github.com/fatih/color"
  11. "github.com/mitchellh/mapstructure"
  12. api "github.com/porter-dev/porter/api/client"
  13. "github.com/porter-dev/porter/api/types"
  14. "github.com/porter-dev/porter/cli/cmd/deploy"
  15. "github.com/porter-dev/porter/internal/templater/utils"
  16. "github.com/porter-dev/switchboard/pkg/drivers"
  17. "github.com/porter-dev/switchboard/pkg/models"
  18. "github.com/porter-dev/switchboard/pkg/parser"
  19. switchboardTypes "github.com/porter-dev/switchboard/pkg/types"
  20. "github.com/porter-dev/switchboard/pkg/worker"
  21. "github.com/rs/zerolog"
  22. "github.com/spf13/cobra"
  23. )
  24. // applyCmd represents the "porter apply" base command when called
  25. // with a porter.yaml file as an argument
  26. var applyCmd = &cobra.Command{
  27. Use: "apply",
  28. Short: "Applies a configuration to an application",
  29. Long: fmt.Sprintf(`
  30. %s
  31. Applies a configuration to an application by either creating a new one or updating an existing
  32. one. For example:
  33. %s
  34. This command will apply the configuration contained in porter.yaml to the requested project and
  35. cluster either provided inside the porter.yaml file or through environment variables. Note that
  36. environment variables will always take precendence over values specified in the porter.yaml file.
  37. By default, this command expects to be run from a local git repository.
  38. The following are the environment variables that can be used to set certain values while
  39. applying a configuration:
  40. PORTER_CLUSTER Cluster ID that contains the project
  41. PORTER_PROJECT Project ID that contains the application
  42. PORTER_NAMESPACE The Kubernetes namespace that the application belongs to
  43. PORTER_SOURCE_NAME Name of the source Helm chart
  44. PORTER_SOURCE_REPO The URL of the Helm charts registry
  45. PORTER_SOURCE_VERSION The version of the Helm chart to use
  46. PORTER_TAG The Docker image tag to use (like the git commit hash)
  47. `,
  48. color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter apply\":"),
  49. color.New(color.FgGreen, color.Bold).Sprintf("porter apply -f porter.yaml"),
  50. ),
  51. Run: func(cmd *cobra.Command, args []string) {
  52. err := checkLoginAndRun(args, apply)
  53. if err != nil {
  54. os.Exit(1)
  55. }
  56. },
  57. }
  58. var porterYAML string
  59. func init() {
  60. rootCmd.AddCommand(applyCmd)
  61. applyCmd.Flags().StringVarP(&porterYAML, "file", "f", "", "path to porter.yaml")
  62. applyCmd.MarkFlagRequired("file")
  63. }
  64. func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
  65. fileBytes, err := ioutil.ReadFile(porterYAML)
  66. if err != nil {
  67. return err
  68. }
  69. resGroup, err := parser.ParseRawBytes(fileBytes)
  70. if err != nil {
  71. return err
  72. }
  73. basePath, err := os.Getwd()
  74. if err != nil {
  75. return err
  76. }
  77. worker := worker.NewWorker()
  78. worker.RegisterDriver("porter.deploy", NewPorterDriver)
  79. worker.SetDefaultDriver("porter.deploy")
  80. return worker.Apply(resGroup, &switchboardTypes.ApplyOpts{
  81. BasePath: basePath,
  82. })
  83. }
  84. type Source struct {
  85. Name string
  86. Repo string
  87. Version string
  88. }
  89. type Target struct {
  90. Project uint
  91. Cluster uint
  92. Namespace string
  93. }
  94. type Config struct {
  95. Build struct {
  96. Method string
  97. Context string
  98. Dockerfile string
  99. }
  100. Values map[string]interface{}
  101. }
  102. type Driver struct {
  103. source *Source
  104. target *Target
  105. config *Config
  106. sourceDefaultValues map[string]interface{}
  107. output map[string]interface{}
  108. lookupTable *map[string]drivers.Driver
  109. logger *zerolog.Logger
  110. shouldApply bool
  111. }
  112. func NewPorterDriver(resource *models.Resource, opts *drivers.SharedDriverOpts) (drivers.Driver, error) {
  113. driver := &Driver{
  114. lookupTable: opts.DriverLookupTable,
  115. logger: opts.Logger,
  116. output: make(map[string]interface{}),
  117. shouldApply: true,
  118. }
  119. err := driver.getSource(resource.Source)
  120. if err != nil {
  121. return nil, err
  122. }
  123. if driver.source.Repo == "https://chart-addons.getporter.dev" {
  124. driver.shouldApply = false
  125. }
  126. err = driver.getTarget(resource.Target)
  127. if err != nil {
  128. return nil, err
  129. }
  130. resourceConfig, err := driver.getConfig(resource.Config)
  131. if err != nil {
  132. return nil, err
  133. }
  134. driver.config = resourceConfig
  135. return driver, nil
  136. }
  137. func (d *Driver) ShouldApply(resource *models.Resource) bool {
  138. return d.shouldApply
  139. }
  140. func (d *Driver) Apply(resource *models.Resource) (*models.Resource, error) {
  141. client := GetAPIClient(config)
  142. if resource.Name == "" {
  143. return nil, fmt.Errorf("empty app name")
  144. }
  145. resource.Name = fmt.Sprintf("preview-%s", resource.Name)
  146. namespace := d.target.Namespace
  147. existingNamespaces, err := client.GetK8sNamespaces(context.Background(), d.target.Project, d.target.Cluster)
  148. if err != nil {
  149. return nil, err
  150. }
  151. namespaceFound := false
  152. for _, ns := range existingNamespaces.Items {
  153. if namespace == ns.Name {
  154. namespaceFound = true
  155. break
  156. }
  157. }
  158. if !namespaceFound {
  159. _, err := client.CreateNewK8sNamespace(
  160. context.Background(), d.target.Project, d.target.Cluster, namespace)
  161. if err != nil {
  162. return nil, err
  163. }
  164. }
  165. method := d.config.Build.Method
  166. if method != "pack" && method != "docker" {
  167. return nil, fmt.Errorf("method should either be \"docker\" or \"pack\"")
  168. }
  169. fullPath, err := filepath.Abs(d.config.Build.Context)
  170. if err != nil {
  171. return nil, err
  172. }
  173. tag := os.Getenv("PORTER_TAG")
  174. if tag == "" {
  175. commit, err := git.LastCommit()
  176. if err != nil {
  177. return nil, err
  178. }
  179. tag = commit.Sha[:7]
  180. }
  181. if tag == "" {
  182. return nil, fmt.Errorf("could not find commit SHA to tag the image")
  183. }
  184. _, err = client.GetRelease(context.Background(), d.target.Project,
  185. d.target.Cluster, d.target.Namespace, resource.Name)
  186. if err != nil {
  187. // create new release
  188. color.New(color.FgGreen).Printf("Creating %s release: %s\n", d.source.Name, resource.Name)
  189. regList, err := client.ListRegistries(context.Background(), d.target.Project)
  190. if err != nil {
  191. return nil, err
  192. }
  193. var registryURL string
  194. if len(*regList) == 0 {
  195. return nil, fmt.Errorf("no registry found")
  196. } else {
  197. registryURL = (*regList)[0].URL
  198. }
  199. createAgent := &deploy.CreateAgent{
  200. Client: client,
  201. CreateOpts: &deploy.CreateOpts{
  202. SharedOpts: &deploy.SharedOpts{
  203. ProjectID: d.target.Project,
  204. ClusterID: d.target.Cluster,
  205. Namespace: namespace,
  206. LocalPath: fullPath,
  207. LocalDockerfile: d.config.Build.Dockerfile,
  208. Method: deploy.DeployBuildType(method),
  209. },
  210. Kind: d.source.Name,
  211. ReleaseName: resource.Name,
  212. RegistryURL: registryURL,
  213. },
  214. }
  215. subdomain, err := createAgent.CreateFromDocker(d.config.Values, tag)
  216. return resource, handleSubdomainCreate(subdomain, err)
  217. }
  218. // update an existing release
  219. color.New(color.FgGreen).Println("Deploying app:", resource.Name)
  220. updateAgent, err := deploy.NewDeployAgent(client, resource.Name, &deploy.DeployOpts{
  221. SharedOpts: &deploy.SharedOpts{
  222. ProjectID: d.target.Project,
  223. ClusterID: d.target.Cluster,
  224. Namespace: namespace,
  225. LocalPath: fullPath,
  226. LocalDockerfile: d.config.Build.Dockerfile,
  227. OverrideTag: tag,
  228. Method: deploy.DeployBuildType(method),
  229. },
  230. Local: true,
  231. })
  232. if err != nil {
  233. return nil, err
  234. }
  235. buildEnv, err := updateAgent.GetBuildEnv()
  236. if err != nil {
  237. return nil, err
  238. }
  239. err = updateAgent.SetBuildEnv(buildEnv)
  240. if err != nil {
  241. return nil, err
  242. }
  243. err = updateAgent.Build()
  244. if err != nil {
  245. return nil, err
  246. }
  247. err = updateAgent.Push()
  248. if err != nil {
  249. return nil, err
  250. }
  251. err = updateAgent.UpdateImageAndValues(d.config.Values)
  252. if err != nil {
  253. return nil, err
  254. }
  255. d.output[resource.Name] = utils.CoalesceValues(d.sourceDefaultValues, d.config.Values)
  256. return resource, nil
  257. }
  258. func (d *Driver) Output() (map[string]interface{}, error) {
  259. return d.output, nil
  260. }
  261. func (d *Driver) getSource(genericSource map[string]interface{}) error {
  262. d.source = &Source{}
  263. // first read from env vars
  264. d.source.Name = os.Getenv("PORTER_SOURCE_NAME")
  265. d.source.Repo = os.Getenv("PORTER_SOURCE_REPO")
  266. d.source.Version = os.Getenv("PORTER_SOURCE_VERSION")
  267. // next, check for values in the YAML file
  268. if d.source.Name == "" {
  269. if name, ok := genericSource["name"]; ok {
  270. nameVal, ok := name.(string)
  271. if !ok {
  272. return fmt.Errorf("invalid name provided")
  273. }
  274. d.source.Name = nameVal
  275. }
  276. }
  277. if d.source.Name == "" {
  278. return fmt.Errorf("source name required")
  279. }
  280. if d.source.Repo == "" {
  281. if repo, ok := genericSource["repo"]; ok {
  282. repoVal, ok := repo.(string)
  283. if !ok {
  284. return fmt.Errorf("invalid repo provided")
  285. }
  286. d.source.Repo = repoVal
  287. }
  288. }
  289. if d.source.Version == "" {
  290. if version, ok := genericSource["version"]; ok {
  291. versionVal, ok := version.(string)
  292. if !ok {
  293. return fmt.Errorf("invalid version provided")
  294. }
  295. d.source.Version = versionVal
  296. }
  297. }
  298. // lastly, just put in the defaults
  299. if d.source.Version == "" {
  300. d.source.Version = "latest"
  301. }
  302. if d.source.Repo == "" {
  303. d.source.Repo = "https://charts.getporter.dev"
  304. values, err := existsInRepo(d.source.Name, d.source.Version, d.source.Repo)
  305. if err == nil {
  306. // found in "https://charts.getporter.dev"
  307. d.sourceDefaultValues = values
  308. return nil
  309. }
  310. d.source.Repo = "https://chart-addons.getporter.dev"
  311. values, err = existsInRepo(d.source.Name, d.source.Version, d.source.Repo)
  312. if err == nil {
  313. // found in https://chart-addons.getporter.dev
  314. d.sourceDefaultValues = values
  315. return nil
  316. }
  317. return fmt.Errorf("source does not exist in any repo")
  318. }
  319. values, err := existsInRepo(d.source.Name, d.source.Version, d.source.Repo)
  320. if err == nil {
  321. d.sourceDefaultValues = values
  322. return nil
  323. }
  324. return fmt.Errorf("source '%s' does not exist in repo '%s'", d.source.Name, d.source.Repo)
  325. }
  326. func (d *Driver) getTarget(genericTarget map[string]interface{}) error {
  327. d.target = &Target{}
  328. // first read from env vars
  329. if projectEnv := os.Getenv("PORTER_PROJECT"); projectEnv != "" {
  330. project, err := strconv.Atoi(projectEnv)
  331. if err != nil {
  332. return err
  333. }
  334. d.target.Project = uint(project)
  335. }
  336. if clusterEnv := os.Getenv("PORTER_CLUSTER"); clusterEnv != "" {
  337. cluster, err := strconv.Atoi(clusterEnv)
  338. if err != nil {
  339. return err
  340. }
  341. d.target.Cluster = uint(cluster)
  342. }
  343. d.target.Namespace = os.Getenv("PORTER_NAMESPACE")
  344. // next, check for values in the YAML file
  345. if d.target.Project == 0 {
  346. if project, ok := genericTarget["project"]; ok {
  347. projectVal, ok := project.(uint)
  348. if !ok {
  349. return fmt.Errorf("project value must be an integer")
  350. }
  351. d.target.Project = projectVal
  352. }
  353. }
  354. if d.target.Cluster == 0 {
  355. if cluster, ok := genericTarget["cluster"]; ok {
  356. clusterVal, ok := cluster.(uint)
  357. if !ok {
  358. return fmt.Errorf("cluster value must be an integer")
  359. }
  360. d.target.Cluster = clusterVal
  361. }
  362. }
  363. if d.target.Namespace == "" {
  364. if namespace, ok := genericTarget["namespace"]; ok {
  365. namespaceVal, ok := namespace.(string)
  366. if !ok {
  367. return fmt.Errorf("invalid namespace provided")
  368. }
  369. d.target.Namespace = namespaceVal
  370. }
  371. }
  372. // lastly, just put in the defaults
  373. if d.target.Project == 0 {
  374. d.target.Project = config.Project
  375. }
  376. if d.target.Cluster == 0 {
  377. d.target.Cluster = config.Cluster
  378. }
  379. if d.target.Namespace == "" {
  380. d.target.Namespace = "default"
  381. }
  382. return nil
  383. }
  384. func (d *Driver) getConfig(genericConfig map[string]interface{}) (*Config, error) {
  385. config := &Config{}
  386. err := mapstructure.Decode(genericConfig, config)
  387. if err != nil {
  388. return nil, err
  389. }
  390. return config, nil
  391. }
  392. func existsInRepo(name, version, url string) (map[string]interface{}, error) {
  393. chart, err := GetAPIClient(config).GetTemplate(
  394. context.Background(),
  395. name, version,
  396. &types.GetTemplateRequest{
  397. TemplateGetBaseRequest: types.TemplateGetBaseRequest{
  398. RepoURL: url,
  399. },
  400. },
  401. )
  402. if err != nil {
  403. return nil, err
  404. }
  405. return chart.Values, nil
  406. }