apply.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756
  1. package cmd
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "io/ioutil"
  7. "net/url"
  8. "os"
  9. "path/filepath"
  10. "strconv"
  11. "strings"
  12. "github.com/cli/cli/git"
  13. "github.com/fatih/color"
  14. "github.com/mitchellh/mapstructure"
  15. api "github.com/porter-dev/porter/api/client"
  16. "github.com/porter-dev/porter/api/types"
  17. "github.com/porter-dev/porter/cli/cmd/deploy"
  18. "github.com/porter-dev/porter/internal/templater/utils"
  19. "github.com/porter-dev/switchboard/pkg/drivers"
  20. "github.com/porter-dev/switchboard/pkg/models"
  21. "github.com/porter-dev/switchboard/pkg/parser"
  22. switchboardTypes "github.com/porter-dev/switchboard/pkg/types"
  23. "github.com/porter-dev/switchboard/pkg/worker"
  24. "github.com/rs/zerolog"
  25. "github.com/spf13/cobra"
  26. )
  27. // applyCmd represents the "porter apply" base command when called
  28. // with a porter.yaml file as an argument
  29. var applyCmd = &cobra.Command{
  30. Use: "apply",
  31. Short: "Applies a configuration to an application",
  32. Long: fmt.Sprintf(`
  33. %s
  34. Applies a configuration to an application by either creating a new one or updating an existing
  35. one. For example:
  36. %s
  37. This command will apply the configuration contained in porter.yaml to the requested project and
  38. cluster either provided inside the porter.yaml file or through environment variables. Note that
  39. environment variables will always take precendence over values specified in the porter.yaml file.
  40. By default, this command expects to be run from a local git repository.
  41. The following are the environment variables that can be used to set certain values while
  42. applying a configuration:
  43. PORTER_CLUSTER Cluster ID that contains the project
  44. PORTER_PROJECT Project ID that contains the application
  45. PORTER_NAMESPACE The Kubernetes namespace that the application belongs to
  46. PORTER_SOURCE_NAME Name of the source Helm chart
  47. PORTER_SOURCE_REPO The URL of the Helm charts registry
  48. PORTER_SOURCE_VERSION The version of the Helm chart to use
  49. PORTER_TAG The Docker image tag to use (like the git commit hash)
  50. `,
  51. color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter apply\":"),
  52. color.New(color.FgGreen, color.Bold).Sprintf("porter apply -f porter.yaml"),
  53. ),
  54. Run: func(cmd *cobra.Command, args []string) {
  55. err := checkLoginAndRun(args, apply)
  56. if err != nil {
  57. os.Exit(1)
  58. }
  59. },
  60. }
  61. var porterYAML string
  62. func init() {
  63. rootCmd.AddCommand(applyCmd)
  64. applyCmd.Flags().StringVarP(&porterYAML, "file", "f", "", "path to porter.yaml")
  65. applyCmd.MarkFlagRequired("file")
  66. }
  67. func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
  68. fileBytes, err := ioutil.ReadFile(porterYAML)
  69. if err != nil {
  70. return err
  71. }
  72. resGroup, err := parser.ParseRawBytes(fileBytes)
  73. if err != nil {
  74. return err
  75. }
  76. basePath, err := os.Getwd()
  77. if err != nil {
  78. return err
  79. }
  80. worker := worker.NewWorker()
  81. worker.RegisterDriver("porter.deploy", NewPorterDriver)
  82. worker.SetDefaultDriver("porter.deploy")
  83. deplNamespace := os.Getenv("PORTER_NAMESPACE")
  84. if deplNamespace == "" {
  85. return fmt.Errorf("namespace must be set by PORTER_NAMESPACE")
  86. }
  87. deploymentHook, err := NewDeploymentHook(client, resGroup, deplNamespace)
  88. if err != nil {
  89. return err
  90. }
  91. worker.RegisterHook("deployment", deploymentHook)
  92. return worker.Apply(resGroup, &switchboardTypes.ApplyOpts{
  93. BasePath: basePath,
  94. })
  95. }
  96. type Source struct {
  97. Name string
  98. Repo string
  99. Version string
  100. IsApplication bool
  101. SourceValues map[string]interface{}
  102. }
  103. type Target struct {
  104. Project uint
  105. Cluster uint
  106. Namespace string
  107. }
  108. type ApplicationConfig struct {
  109. Build struct {
  110. Method string
  111. Context string
  112. Dockerfile string
  113. }
  114. Values map[string]interface{}
  115. }
  116. type Driver struct {
  117. source *Source
  118. target *Target
  119. output map[string]interface{}
  120. lookupTable *map[string]drivers.Driver
  121. logger *zerolog.Logger
  122. }
  123. func NewPorterDriver(resource *models.Resource, opts *drivers.SharedDriverOpts) (drivers.Driver, error) {
  124. driver := &Driver{
  125. lookupTable: opts.DriverLookupTable,
  126. logger: opts.Logger,
  127. output: make(map[string]interface{}),
  128. }
  129. err := driver.getSource(resource.Source)
  130. if err != nil {
  131. return nil, err
  132. }
  133. err = driver.getTarget(resource.Target)
  134. if err != nil {
  135. return nil, err
  136. }
  137. return driver, nil
  138. }
  139. func (d *Driver) ShouldApply(resource *models.Resource) bool {
  140. return true
  141. }
  142. func (d *Driver) Apply(resource *models.Resource) (*models.Resource, error) {
  143. client := GetAPIClient(config)
  144. name := resource.Name
  145. if name == "" {
  146. return nil, fmt.Errorf("empty app name")
  147. }
  148. _, err := client.GetRelease(
  149. context.Background(),
  150. d.target.Project,
  151. d.target.Cluster,
  152. d.target.Namespace,
  153. resource.Name,
  154. )
  155. shouldCreate := err != nil
  156. if err != nil {
  157. color.New(color.FgYellow).Printf("Could not read release %s/%s (%s): attempting creation\n", d.target.Namespace, resource.Name, err.Error())
  158. }
  159. if d.source.IsApplication {
  160. return d.applyApplication(resource, client, shouldCreate)
  161. }
  162. return d.applyAddon(resource, client, shouldCreate)
  163. }
  164. // Simple apply for addons
  165. func (d *Driver) applyAddon(resource *models.Resource, client *api.Client, shouldCreate bool) (*models.Resource, error) {
  166. var err error
  167. if shouldCreate {
  168. err = client.DeployAddon(
  169. context.Background(),
  170. d.target.Project,
  171. d.target.Cluster,
  172. d.target.Namespace,
  173. &types.CreateAddonRequest{
  174. CreateReleaseBaseRequest: &types.CreateReleaseBaseRequest{
  175. RepoURL: d.source.Repo,
  176. TemplateName: d.source.Name,
  177. TemplateVersion: d.source.Version,
  178. Values: resource.Config,
  179. Name: resource.Name,
  180. },
  181. },
  182. )
  183. } else {
  184. bytes, err := json.Marshal(resource.Config)
  185. if err != nil {
  186. return nil, err
  187. }
  188. err = client.UpgradeRelease(
  189. context.Background(),
  190. d.target.Project,
  191. d.target.Cluster,
  192. d.target.Namespace,
  193. resource.Name,
  194. &types.UpgradeReleaseRequest{
  195. Values: string(bytes),
  196. },
  197. )
  198. }
  199. if err != nil {
  200. return nil, err
  201. }
  202. d.output = utils.CoalesceValues(d.source.SourceValues, resource.Config)
  203. return resource, err
  204. }
  205. func (d *Driver) applyApplication(resource *models.Resource, client *api.Client, shouldCreate bool) (*models.Resource, error) {
  206. appConfig, err := d.getApplicationConfig(resource)
  207. if err != nil {
  208. return nil, err
  209. }
  210. method := appConfig.Build.Method
  211. if method != "pack" && method != "docker" {
  212. return nil, fmt.Errorf("method should either be \"docker\" or \"pack\"")
  213. }
  214. fullPath, err := filepath.Abs(appConfig.Build.Context)
  215. if err != nil {
  216. return nil, err
  217. }
  218. tag := os.Getenv("PORTER_TAG")
  219. if tag == "" {
  220. commit, err := git.LastCommit()
  221. if err != nil {
  222. return nil, err
  223. }
  224. tag = commit.Sha[:7]
  225. }
  226. sharedOpts := &deploy.SharedOpts{
  227. ProjectID: d.target.Project,
  228. ClusterID: d.target.Cluster,
  229. Namespace: d.target.Namespace,
  230. LocalPath: fullPath,
  231. LocalDockerfile: appConfig.Build.Dockerfile,
  232. OverrideTag: tag,
  233. Method: deploy.DeployBuildType(method),
  234. }
  235. if shouldCreate {
  236. return d.createApplication(resource, client, sharedOpts, appConfig)
  237. }
  238. return d.updateApplication(resource, client, sharedOpts, appConfig)
  239. }
  240. func (d *Driver) createApplication(resource *models.Resource, client *api.Client, sharedOpts *deploy.SharedOpts, appConf *ApplicationConfig) (*models.Resource, error) {
  241. // create new release
  242. color.New(color.FgGreen).Printf("Creating %s release: %s\n", d.source.Name, resource.Name)
  243. regList, err := client.ListRegistries(context.Background(), d.target.Project)
  244. if err != nil {
  245. return nil, err
  246. }
  247. var registryURL string
  248. if len(*regList) == 0 {
  249. return nil, fmt.Errorf("no registry found")
  250. } else {
  251. registryURL = (*regList)[0].URL
  252. }
  253. createAgent := &deploy.CreateAgent{
  254. Client: client,
  255. CreateOpts: &deploy.CreateOpts{
  256. SharedOpts: sharedOpts,
  257. Kind: d.source.Name,
  258. ReleaseName: resource.Name,
  259. RegistryURL: registryURL,
  260. },
  261. }
  262. subdomain, err := createAgent.CreateFromDocker(appConf.Values, sharedOpts.OverrideTag)
  263. d.output = utils.CoalesceValues(d.source.SourceValues, appConf.Values)
  264. return resource, handleSubdomainCreate(subdomain, err)
  265. }
  266. func (d *Driver) updateApplication(resource *models.Resource, client *api.Client, sharedOpts *deploy.SharedOpts, appConf *ApplicationConfig) (*models.Resource, error) {
  267. color.New(color.FgGreen).Println("Updating existing release:", resource.Name)
  268. updateAgent, err := deploy.NewDeployAgent(client, resource.Name, &deploy.DeployOpts{
  269. SharedOpts: sharedOpts,
  270. Local: true,
  271. })
  272. if err != nil {
  273. return nil, err
  274. }
  275. buildEnv, err := updateAgent.GetBuildEnv()
  276. if err != nil {
  277. return nil, err
  278. }
  279. err = updateAgent.SetBuildEnv(buildEnv)
  280. if err != nil {
  281. return nil, err
  282. }
  283. err = updateAgent.Build()
  284. if err != nil {
  285. return nil, err
  286. }
  287. err = updateAgent.Push()
  288. if err != nil {
  289. return nil, err
  290. }
  291. err = updateAgent.UpdateImageAndValues(appConf.Values)
  292. if err != nil {
  293. return nil, err
  294. }
  295. d.output = utils.CoalesceValues(d.source.SourceValues, appConf.Values)
  296. return resource, nil
  297. }
  298. func (d *Driver) Output() (map[string]interface{}, error) {
  299. return d.output, nil
  300. }
  301. func (d *Driver) getSource(genericSource map[string]interface{}) error {
  302. d.source = &Source{}
  303. // first read from env vars
  304. d.source.Name = os.Getenv("PORTER_SOURCE_NAME")
  305. d.source.Repo = os.Getenv("PORTER_SOURCE_REPO")
  306. d.source.Version = os.Getenv("PORTER_SOURCE_VERSION")
  307. // next, check for values in the YAML file
  308. if d.source.Name == "" {
  309. if name, ok := genericSource["name"]; ok {
  310. nameVal, ok := name.(string)
  311. if !ok {
  312. return fmt.Errorf("invalid name provided")
  313. }
  314. d.source.Name = nameVal
  315. }
  316. }
  317. if d.source.Name == "" {
  318. return fmt.Errorf("source name required")
  319. }
  320. if d.source.Repo == "" {
  321. if repo, ok := genericSource["repo"]; ok {
  322. repoVal, ok := repo.(string)
  323. if !ok {
  324. return fmt.Errorf("invalid repo provided")
  325. }
  326. d.source.Repo = repoVal
  327. }
  328. }
  329. if d.source.Version == "" {
  330. if version, ok := genericSource["version"]; ok {
  331. versionVal, ok := version.(string)
  332. if !ok {
  333. return fmt.Errorf("invalid version provided")
  334. }
  335. d.source.Version = versionVal
  336. }
  337. }
  338. // lastly, just put in the defaults
  339. if d.source.Version == "" {
  340. d.source.Version = "latest"
  341. }
  342. d.source.IsApplication = d.source.Repo == "https://charts.getporter.dev"
  343. if d.source.Repo == "" {
  344. d.source.Repo = "https://charts.getporter.dev"
  345. values, err := existsInRepo(d.source.Name, d.source.Version, d.source.Repo)
  346. if err == nil {
  347. // found in "https://charts.getporter.dev"
  348. d.source.SourceValues = values
  349. d.source.IsApplication = true
  350. return nil
  351. }
  352. d.source.Repo = "https://chart-addons.getporter.dev"
  353. values, err = existsInRepo(d.source.Name, d.source.Version, d.source.Repo)
  354. if err == nil {
  355. // found in https://chart-addons.getporter.dev
  356. d.source.SourceValues = values
  357. return nil
  358. }
  359. return fmt.Errorf("source does not exist in any repo")
  360. }
  361. return fmt.Errorf("source '%s' does not exist in repo '%s'", d.source.Name, d.source.Repo)
  362. }
  363. func (d *Driver) getTarget(genericTarget map[string]interface{}) error {
  364. d.target = &Target{}
  365. // first read from env vars
  366. if projectEnv := os.Getenv("PORTER_PROJECT"); projectEnv != "" {
  367. project, err := strconv.Atoi(projectEnv)
  368. if err != nil {
  369. return err
  370. }
  371. d.target.Project = uint(project)
  372. }
  373. if clusterEnv := os.Getenv("PORTER_CLUSTER"); clusterEnv != "" {
  374. cluster, err := strconv.Atoi(clusterEnv)
  375. if err != nil {
  376. return err
  377. }
  378. d.target.Cluster = uint(cluster)
  379. }
  380. d.target.Namespace = os.Getenv("PORTER_NAMESPACE")
  381. // next, check for values in the YAML file
  382. if d.target.Project == 0 {
  383. if project, ok := genericTarget["project"]; ok {
  384. projectVal, ok := project.(uint)
  385. if !ok {
  386. return fmt.Errorf("project value must be an integer")
  387. }
  388. d.target.Project = projectVal
  389. }
  390. }
  391. if d.target.Cluster == 0 {
  392. if cluster, ok := genericTarget["cluster"]; ok {
  393. clusterVal, ok := cluster.(uint)
  394. if !ok {
  395. return fmt.Errorf("cluster value must be an integer")
  396. }
  397. d.target.Cluster = clusterVal
  398. }
  399. }
  400. if d.target.Namespace == "" {
  401. if namespace, ok := genericTarget["namespace"]; ok {
  402. namespaceVal, ok := namespace.(string)
  403. if !ok {
  404. return fmt.Errorf("invalid namespace provided")
  405. }
  406. d.target.Namespace = namespaceVal
  407. }
  408. }
  409. // lastly, just put in the defaults
  410. if d.target.Project == 0 {
  411. d.target.Project = config.Project
  412. }
  413. if d.target.Cluster == 0 {
  414. d.target.Cluster = config.Cluster
  415. }
  416. if d.target.Namespace == "" {
  417. d.target.Namespace = "default"
  418. }
  419. return nil
  420. }
  421. func (d *Driver) getApplicationConfig(resource *models.Resource) (*ApplicationConfig, error) {
  422. populatedConf, err := drivers.ConstructConfig(&drivers.ConstructConfigOpts{
  423. RawConf: resource.Config,
  424. LookupTable: *d.lookupTable,
  425. Dependencies: resource.Dependencies,
  426. })
  427. if err != nil {
  428. return nil, err
  429. }
  430. config := &ApplicationConfig{}
  431. err = mapstructure.Decode(populatedConf, config)
  432. if err != nil {
  433. return nil, err
  434. }
  435. return config, nil
  436. }
  437. func existsInRepo(name, version, url string) (map[string]interface{}, error) {
  438. chart, err := GetAPIClient(config).GetTemplate(
  439. context.Background(),
  440. name, version,
  441. &types.GetTemplateRequest{
  442. TemplateGetBaseRequest: types.TemplateGetBaseRequest{
  443. RepoURL: url,
  444. },
  445. },
  446. )
  447. if err != nil {
  448. return nil, err
  449. }
  450. return chart.Values, nil
  451. }
  452. type DeploymentHook struct {
  453. client *api.Client
  454. resourceGroup *switchboardTypes.ResourceGroup
  455. gitInstallationID, projectID, clusterID, prID, actionID uint
  456. branch, namespace string
  457. }
  458. func NewDeploymentHook(client *api.Client, resourceGroup *switchboardTypes.ResourceGroup, namespace string) (*DeploymentHook, error) {
  459. res := &DeploymentHook{
  460. client: client,
  461. resourceGroup: resourceGroup,
  462. namespace: namespace,
  463. }
  464. if ghIDStr := os.Getenv("PORTER_GIT_INSTALLATION_ID"); ghIDStr != "" {
  465. ghID, err := strconv.Atoi(ghIDStr)
  466. if err != nil {
  467. return nil, err
  468. }
  469. res.gitInstallationID = uint(ghID)
  470. } else if ghIDStr == "" {
  471. return nil, fmt.Errorf("Git installation ID must be defined, set by PORTER_GIT_INSTALLATION_ID")
  472. }
  473. if prIDStr := os.Getenv("PORTER_PULL_REQUEST_ID"); prIDStr != "" {
  474. prID, err := strconv.Atoi(prIDStr)
  475. if err != nil {
  476. return nil, err
  477. }
  478. res.prID = uint(prID)
  479. } else if prIDStr == "" {
  480. return nil, fmt.Errorf("Pull request ID must be defined, set by PORTER_PULL_REQUEST_ID")
  481. }
  482. res.projectID = config.Project
  483. if res.projectID == 0 {
  484. return nil, fmt.Errorf("project id must be set")
  485. }
  486. res.clusterID = config.Cluster
  487. if res.clusterID == 0 {
  488. return nil, fmt.Errorf("cluster id must be set")
  489. }
  490. if branchName := os.Getenv("PORTER_BRANCH_NAME"); branchName != "" {
  491. res.branch = branchName
  492. } else if branchName == "" {
  493. return nil, fmt.Errorf("Branch name must be defined, set by PORTER_BRANCH_NAME")
  494. }
  495. if actionIDStr := os.Getenv("PORTER_ACTION_ID"); actionIDStr != "" {
  496. actionID, err := strconv.Atoi(actionIDStr)
  497. if err != nil {
  498. return nil, err
  499. }
  500. res.actionID = uint(actionID)
  501. } else if actionIDStr == "" {
  502. return nil, fmt.Errorf("Action Run ID must be defined, set by PORTER_ACTION_ID")
  503. }
  504. return res, nil
  505. }
  506. func (t *DeploymentHook) PreApply() error {
  507. // attempt to read the deployment -- if it doesn't exist, create it
  508. _, err := t.client.GetDeployment(
  509. context.Background(),
  510. t.projectID, t.gitInstallationID, t.clusterID,
  511. &types.GetDeploymentRequest{
  512. Namespace: t.namespace,
  513. },
  514. )
  515. // TODO: case this on the response status code rather than text
  516. if err != nil && strings.Contains(err.Error(), "deployment not found") {
  517. // in this case, create the deployment
  518. _, err = t.client.CreateDeployment(
  519. context.Background(),
  520. t.projectID, t.gitInstallationID, t.clusterID,
  521. &types.CreateDeploymentRequest{
  522. Namespace: t.namespace,
  523. PullRequestID: t.prID,
  524. Branch: t.branch,
  525. ActionID: t.actionID,
  526. },
  527. )
  528. return err
  529. }
  530. return err
  531. }
  532. func (t *DeploymentHook) DataQueries() map[string]interface{} {
  533. res := make(map[string]interface{})
  534. // use the resource group to find all web applications that can have an exposed subdomain
  535. // that we can query for
  536. for _, resource := range t.resourceGroup.Resources {
  537. isWeb := false
  538. if sourceNameInter, exists := resource.Source["name"]; exists {
  539. if sourceName, ok := sourceNameInter.(string); ok {
  540. if sourceName == "web" {
  541. isWeb = true
  542. }
  543. }
  544. }
  545. if isWeb {
  546. valuesInter, exists := resource.Config["values"]
  547. if !exists {
  548. continue
  549. }
  550. values, ok := valuesInter.(map[string]interface{})
  551. if !ok {
  552. continue
  553. }
  554. ingressInter, exists := values["ingress"]
  555. if !exists {
  556. continue
  557. }
  558. ingress, ok := ingressInter.(map[string]interface{})
  559. if !ok {
  560. continue
  561. }
  562. enabledInter, exists := ingress["enabled"]
  563. if !exists {
  564. continue
  565. }
  566. enabled, ok := enabledInter.(bool)
  567. if ok && enabled {
  568. res[resource.Name] = fmt.Sprintf("{ .%s.ingress.porter_hosts[0] }", resource.Name)
  569. }
  570. }
  571. }
  572. return res
  573. }
  574. func (t *DeploymentHook) PostApply(populatedData map[string]interface{}) error {
  575. subdomains := make([]string, 0)
  576. for _, data := range populatedData {
  577. domain, ok := data.(string)
  578. if !ok {
  579. continue
  580. }
  581. if _, err := url.Parse(domain); err == nil {
  582. subdomains = append(subdomains, domain)
  583. }
  584. }
  585. // finalize the deployment
  586. _, err := t.client.FinalizeDeployment(
  587. context.Background(),
  588. t.projectID, t.gitInstallationID, t.clusterID,
  589. &types.FinalizeDeploymentRequest{
  590. Namespace: t.namespace,
  591. Subdomain: strings.Join(subdomains, ","),
  592. },
  593. )
  594. return err
  595. }