apply.go 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873
  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. Image string
  114. Builder string
  115. Buildpacks []string
  116. }
  117. Values map[string]interface{}
  118. }
  119. type Driver struct {
  120. source *Source
  121. target *Target
  122. output map[string]interface{}
  123. lookupTable *map[string]drivers.Driver
  124. logger *zerolog.Logger
  125. }
  126. func NewPorterDriver(resource *models.Resource, opts *drivers.SharedDriverOpts) (drivers.Driver, error) {
  127. driver := &Driver{
  128. lookupTable: opts.DriverLookupTable,
  129. logger: opts.Logger,
  130. output: make(map[string]interface{}),
  131. }
  132. err := driver.getSource(resource.Source)
  133. if err != nil {
  134. return nil, err
  135. }
  136. err = driver.getTarget(resource.Target)
  137. if err != nil {
  138. return nil, err
  139. }
  140. return driver, nil
  141. }
  142. func (d *Driver) ShouldApply(resource *models.Resource) bool {
  143. return true
  144. }
  145. func (d *Driver) Apply(resource *models.Resource) (*models.Resource, error) {
  146. client := GetAPIClient(config)
  147. name := resource.Name
  148. if name == "" {
  149. return nil, fmt.Errorf("empty app name")
  150. }
  151. _, err := client.GetRelease(
  152. context.Background(),
  153. d.target.Project,
  154. d.target.Cluster,
  155. d.target.Namespace,
  156. resource.Name,
  157. )
  158. shouldCreate := err != nil
  159. if err != nil {
  160. color.New(color.FgYellow).Printf("Could not read release %s/%s (%s): attempting creation\n", d.target.Namespace, resource.Name, err.Error())
  161. }
  162. if d.source.IsApplication {
  163. return d.applyApplication(resource, client, shouldCreate)
  164. }
  165. return d.applyAddon(resource, client, shouldCreate)
  166. }
  167. // Simple apply for addons
  168. func (d *Driver) applyAddon(resource *models.Resource, client *api.Client, shouldCreate bool) (*models.Resource, error) {
  169. var err error
  170. if shouldCreate {
  171. err = client.DeployAddon(
  172. context.Background(),
  173. d.target.Project,
  174. d.target.Cluster,
  175. d.target.Namespace,
  176. &types.CreateAddonRequest{
  177. CreateReleaseBaseRequest: &types.CreateReleaseBaseRequest{
  178. RepoURL: d.source.Repo,
  179. TemplateName: d.source.Name,
  180. TemplateVersion: d.source.Version,
  181. Values: resource.Config,
  182. Name: resource.Name,
  183. },
  184. },
  185. )
  186. } else {
  187. bytes, err := json.Marshal(resource.Config)
  188. if err != nil {
  189. return nil, err
  190. }
  191. err = client.UpgradeRelease(
  192. context.Background(),
  193. d.target.Project,
  194. d.target.Cluster,
  195. d.target.Namespace,
  196. resource.Name,
  197. &types.UpgradeReleaseRequest{
  198. Values: string(bytes),
  199. },
  200. )
  201. }
  202. if err != nil {
  203. return nil, err
  204. }
  205. if err = d.assignOutput(resource, client); err != nil {
  206. return nil, err
  207. }
  208. return resource, err
  209. }
  210. func (d *Driver) applyApplication(resource *models.Resource, client *api.Client, shouldCreate bool) (*models.Resource, error) {
  211. appConfig, err := d.getApplicationConfig(resource)
  212. if err != nil {
  213. return nil, err
  214. }
  215. method := appConfig.Build.Method
  216. if method != "pack" && method != "docker" && method != "registry" {
  217. return nil, fmt.Errorf("method should either be \"docker\", \"pack\" or \"registry\"")
  218. }
  219. fullPath, err := filepath.Abs(appConfig.Build.Context)
  220. if err != nil {
  221. return nil, err
  222. }
  223. tag := os.Getenv("PORTER_TAG")
  224. if tag == "" {
  225. commit, err := git.LastCommit()
  226. if err != nil {
  227. return nil, err
  228. }
  229. tag = commit.Sha[:7]
  230. }
  231. sharedOpts := &deploy.SharedOpts{
  232. ProjectID: d.target.Project,
  233. ClusterID: d.target.Cluster,
  234. Namespace: d.target.Namespace,
  235. LocalPath: fullPath,
  236. LocalDockerfile: appConfig.Build.Dockerfile,
  237. OverrideTag: tag,
  238. Method: deploy.DeployBuildType(method),
  239. }
  240. if shouldCreate {
  241. resource, err = d.createApplication(resource, client, sharedOpts, appConfig)
  242. if err != nil {
  243. return nil, err
  244. }
  245. } else {
  246. resource, err = d.updateApplication(resource, client, sharedOpts, appConfig)
  247. if err != nil {
  248. return nil, err
  249. }
  250. }
  251. if err = d.assignOutput(resource, client); err != nil {
  252. return nil, err
  253. }
  254. return resource, err
  255. }
  256. func (d *Driver) createApplication(resource *models.Resource, client *api.Client, sharedOpts *deploy.SharedOpts, appConf *ApplicationConfig) (*models.Resource, error) {
  257. // create new release
  258. color.New(color.FgGreen).Printf("Creating %s release: %s\n", d.source.Name, resource.Name)
  259. regList, err := client.ListRegistries(context.Background(), d.target.Project)
  260. if err != nil {
  261. return nil, err
  262. }
  263. var registryURL string
  264. if len(*regList) == 0 {
  265. return nil, fmt.Errorf("no registry found")
  266. } else {
  267. registryURL = (*regList)[0].URL
  268. }
  269. // attempt to get repo suffix from environment variables
  270. var repoSuffix string
  271. if repoName := os.Getenv("PORTER_REPO_NAME"); repoName != "" {
  272. if repoOwner := os.Getenv("PORTER_REPO_OWNER"); repoOwner != "" {
  273. repoSuffix = fmt.Sprintf("%s-%s", repoOwner, repoName)
  274. }
  275. }
  276. createAgent := &deploy.CreateAgent{
  277. Client: client,
  278. CreateOpts: &deploy.CreateOpts{
  279. SharedOpts: sharedOpts,
  280. Kind: d.source.Name,
  281. ReleaseName: resource.Name,
  282. RegistryURL: registryURL,
  283. RepoSuffix: repoSuffix,
  284. },
  285. }
  286. var buildConfig *types.BuildConfig
  287. if appConf.Build.Builder != "" {
  288. buildConfig = &types.BuildConfig{
  289. Builder: appConf.Build.Builder,
  290. Buildpacks: appConf.Build.Buildpacks,
  291. }
  292. }
  293. var subdomain string
  294. if appConf.Build.Method == "registry" {
  295. subdomain, err = createAgent.CreateFromRegistry(appConf.Build.Image, appConf.Values)
  296. } else {
  297. subdomain, err = createAgent.CreateFromDocker(appConf.Values, sharedOpts.OverrideTag, buildConfig)
  298. }
  299. if err != nil {
  300. return nil, err
  301. }
  302. return resource, handleSubdomainCreate(subdomain, err)
  303. }
  304. func (d *Driver) updateApplication(resource *models.Resource, client *api.Client, sharedOpts *deploy.SharedOpts, appConf *ApplicationConfig) (*models.Resource, error) {
  305. color.New(color.FgGreen).Println("Updating existing release:", resource.Name)
  306. updateAgent, err := deploy.NewDeployAgent(client, resource.Name, &deploy.DeployOpts{
  307. SharedOpts: sharedOpts,
  308. Local: appConf.Build.Method != "registry",
  309. })
  310. if err != nil {
  311. return nil, err
  312. }
  313. buildEnv, err := updateAgent.GetBuildEnv(&deploy.GetBuildEnvOpts{
  314. UseNewConfig: true,
  315. NewConfig: appConf.Values,
  316. })
  317. if err != nil {
  318. return nil, err
  319. }
  320. err = updateAgent.SetBuildEnv(buildEnv)
  321. if err != nil {
  322. return nil, err
  323. }
  324. var buildConfig *types.BuildConfig
  325. if appConf.Build.Builder != "" {
  326. buildConfig = &types.BuildConfig{
  327. Builder: appConf.Build.Builder,
  328. Buildpacks: appConf.Build.Buildpacks,
  329. }
  330. }
  331. err = updateAgent.Build(buildConfig)
  332. if err != nil {
  333. return nil, err
  334. }
  335. err = updateAgent.Push()
  336. if err != nil {
  337. return nil, err
  338. }
  339. err = updateAgent.UpdateImageAndValues(appConf.Values)
  340. if err != nil {
  341. return nil, err
  342. }
  343. return resource, nil
  344. }
  345. func (d *Driver) assignOutput(resource *models.Resource, client *api.Client) error {
  346. release, err := client.GetRelease(
  347. context.Background(),
  348. d.target.Project,
  349. d.target.Cluster,
  350. d.target.Namespace,
  351. resource.Name,
  352. )
  353. if err != nil {
  354. return err
  355. }
  356. d.output = utils.CoalesceValues(d.source.SourceValues, release.Config)
  357. return nil
  358. }
  359. func (d *Driver) Output() (map[string]interface{}, error) {
  360. return d.output, nil
  361. }
  362. func (d *Driver) getSource(genericSource map[string]interface{}) error {
  363. d.source = &Source{}
  364. // first read from env vars
  365. d.source.Name = os.Getenv("PORTER_SOURCE_NAME")
  366. d.source.Repo = os.Getenv("PORTER_SOURCE_REPO")
  367. d.source.Version = os.Getenv("PORTER_SOURCE_VERSION")
  368. // next, check for values in the YAML file
  369. if d.source.Name == "" {
  370. if name, ok := genericSource["name"]; ok {
  371. nameVal, ok := name.(string)
  372. if !ok {
  373. return fmt.Errorf("invalid name provided")
  374. }
  375. d.source.Name = nameVal
  376. }
  377. }
  378. if d.source.Name == "" {
  379. return fmt.Errorf("source name required")
  380. }
  381. if d.source.Repo == "" {
  382. if repo, ok := genericSource["repo"]; ok {
  383. repoVal, ok := repo.(string)
  384. if !ok {
  385. return fmt.Errorf("invalid repo provided")
  386. }
  387. d.source.Repo = repoVal
  388. }
  389. }
  390. if d.source.Version == "" {
  391. if version, ok := genericSource["version"]; ok {
  392. versionVal, ok := version.(string)
  393. if !ok {
  394. return fmt.Errorf("invalid version provided")
  395. }
  396. d.source.Version = versionVal
  397. }
  398. }
  399. // lastly, just put in the defaults
  400. if d.source.Version == "" {
  401. d.source.Version = "latest"
  402. }
  403. d.source.IsApplication = d.source.Repo == "https://charts.getporter.dev"
  404. if d.source.Repo == "" {
  405. d.source.Repo = "https://charts.getporter.dev"
  406. values, err := existsInRepo(d.source.Name, d.source.Version, d.source.Repo)
  407. if err == nil {
  408. // found in "https://charts.getporter.dev"
  409. d.source.SourceValues = values
  410. d.source.IsApplication = true
  411. return nil
  412. }
  413. d.source.Repo = "https://chart-addons.getporter.dev"
  414. values, err = existsInRepo(d.source.Name, d.source.Version, d.source.Repo)
  415. if err == nil {
  416. // found in https://chart-addons.getporter.dev
  417. d.source.SourceValues = values
  418. return nil
  419. }
  420. return fmt.Errorf("source does not exist in any repo")
  421. }
  422. return fmt.Errorf("source '%s' does not exist in repo '%s'", d.source.Name, d.source.Repo)
  423. }
  424. func (d *Driver) getTarget(genericTarget map[string]interface{}) error {
  425. d.target = &Target{}
  426. // first read from env vars
  427. if projectEnv := os.Getenv("PORTER_PROJECT"); projectEnv != "" {
  428. project, err := strconv.Atoi(projectEnv)
  429. if err != nil {
  430. return err
  431. }
  432. d.target.Project = uint(project)
  433. }
  434. if clusterEnv := os.Getenv("PORTER_CLUSTER"); clusterEnv != "" {
  435. cluster, err := strconv.Atoi(clusterEnv)
  436. if err != nil {
  437. return err
  438. }
  439. d.target.Cluster = uint(cluster)
  440. }
  441. d.target.Namespace = os.Getenv("PORTER_NAMESPACE")
  442. // next, check for values in the YAML file
  443. if d.target.Project == 0 {
  444. if project, ok := genericTarget["project"]; ok {
  445. projectVal, ok := project.(uint)
  446. if !ok {
  447. return fmt.Errorf("project value must be an integer")
  448. }
  449. d.target.Project = projectVal
  450. }
  451. }
  452. if d.target.Cluster == 0 {
  453. if cluster, ok := genericTarget["cluster"]; ok {
  454. clusterVal, ok := cluster.(uint)
  455. if !ok {
  456. return fmt.Errorf("cluster value must be an integer")
  457. }
  458. d.target.Cluster = clusterVal
  459. }
  460. }
  461. if d.target.Namespace == "" {
  462. if namespace, ok := genericTarget["namespace"]; ok {
  463. namespaceVal, ok := namespace.(string)
  464. if !ok {
  465. return fmt.Errorf("invalid namespace provided")
  466. }
  467. d.target.Namespace = namespaceVal
  468. }
  469. }
  470. // lastly, just put in the defaults
  471. if d.target.Project == 0 {
  472. d.target.Project = config.Project
  473. }
  474. if d.target.Cluster == 0 {
  475. d.target.Cluster = config.Cluster
  476. }
  477. if d.target.Namespace == "" {
  478. d.target.Namespace = "default"
  479. }
  480. return nil
  481. }
  482. func (d *Driver) getApplicationConfig(resource *models.Resource) (*ApplicationConfig, error) {
  483. populatedConf, err := drivers.ConstructConfig(&drivers.ConstructConfigOpts{
  484. RawConf: resource.Config,
  485. LookupTable: *d.lookupTable,
  486. Dependencies: resource.Dependencies,
  487. })
  488. if err != nil {
  489. return nil, err
  490. }
  491. config := &ApplicationConfig{}
  492. err = mapstructure.Decode(populatedConf, config)
  493. if err != nil {
  494. return nil, err
  495. }
  496. return config, nil
  497. }
  498. func existsInRepo(name, version, url string) (map[string]interface{}, error) {
  499. chart, err := GetAPIClient(config).GetTemplate(
  500. context.Background(),
  501. name, version,
  502. &types.GetTemplateRequest{
  503. TemplateGetBaseRequest: types.TemplateGetBaseRequest{
  504. RepoURL: url,
  505. },
  506. },
  507. )
  508. if err != nil {
  509. return nil, err
  510. }
  511. return chart.Values, nil
  512. }
  513. type DeploymentHook struct {
  514. client *api.Client
  515. resourceGroup *switchboardTypes.ResourceGroup
  516. gitInstallationID, projectID, clusterID, prID, actionID uint
  517. branch, namespace, repoName, repoOwner, prName, commitSHA string
  518. }
  519. func NewDeploymentHook(client *api.Client, resourceGroup *switchboardTypes.ResourceGroup, namespace string) (*DeploymentHook, error) {
  520. res := &DeploymentHook{
  521. client: client,
  522. resourceGroup: resourceGroup,
  523. namespace: namespace,
  524. }
  525. if ghIDStr := os.Getenv("PORTER_GIT_INSTALLATION_ID"); ghIDStr != "" {
  526. ghID, err := strconv.Atoi(ghIDStr)
  527. if err != nil {
  528. return nil, err
  529. }
  530. res.gitInstallationID = uint(ghID)
  531. } else if ghIDStr == "" {
  532. return nil, fmt.Errorf("Git installation ID must be defined, set by PORTER_GIT_INSTALLATION_ID")
  533. }
  534. if prIDStr := os.Getenv("PORTER_PULL_REQUEST_ID"); prIDStr != "" {
  535. prID, err := strconv.Atoi(prIDStr)
  536. if err != nil {
  537. return nil, err
  538. }
  539. res.prID = uint(prID)
  540. } else if prIDStr == "" {
  541. return nil, fmt.Errorf("Pull request ID must be defined, set by PORTER_PULL_REQUEST_ID")
  542. }
  543. res.projectID = config.Project
  544. if res.projectID == 0 {
  545. return nil, fmt.Errorf("project id must be set")
  546. }
  547. res.clusterID = config.Cluster
  548. if res.clusterID == 0 {
  549. return nil, fmt.Errorf("cluster id must be set")
  550. }
  551. if branchName := os.Getenv("PORTER_BRANCH_NAME"); branchName != "" {
  552. res.branch = branchName
  553. } else if branchName == "" {
  554. return nil, fmt.Errorf("Branch name must be defined, set by PORTER_BRANCH_NAME")
  555. }
  556. if actionIDStr := os.Getenv("PORTER_ACTION_ID"); actionIDStr != "" {
  557. actionID, err := strconv.Atoi(actionIDStr)
  558. if err != nil {
  559. return nil, err
  560. }
  561. res.actionID = uint(actionID)
  562. } else if actionIDStr == "" {
  563. return nil, fmt.Errorf("Action Run ID must be defined, set by PORTER_ACTION_ID")
  564. }
  565. if repoName := os.Getenv("PORTER_REPO_NAME"); repoName != "" {
  566. res.repoName = repoName
  567. } else if repoName == "" {
  568. return nil, fmt.Errorf("Repo name must be defined, set by PORTER_REPO_NAME")
  569. }
  570. if repoOwner := os.Getenv("PORTER_REPO_OWNER"); repoOwner != "" {
  571. res.repoOwner = repoOwner
  572. } else if repoOwner == "" {
  573. return nil, fmt.Errorf("Repo owner must be defined, set by PORTER_REPO_OWNER")
  574. }
  575. if prName := os.Getenv("PORTER_PR_NAME"); prName != "" {
  576. res.prName = prName
  577. } else if prName == "" {
  578. return nil, fmt.Errorf("PR Name must be supplied, set by PORTER_PR_NAME")
  579. }
  580. commit, err := git.LastCommit()
  581. if err != nil {
  582. return nil, fmt.Errorf(err.Error())
  583. }
  584. res.commitSHA = commit.Sha[:7]
  585. return res, nil
  586. }
  587. func (t *DeploymentHook) PreApply() error {
  588. // attempt to read the deployment -- if it doesn't exist, create it
  589. _, err := t.client.GetDeployment(
  590. context.Background(),
  591. t.projectID, t.gitInstallationID, t.clusterID,
  592. t.repoOwner, t.repoName,
  593. &types.GetDeploymentRequest{
  594. Namespace: t.namespace,
  595. },
  596. )
  597. // TODO: case this on the response status code rather than text
  598. if err != nil && strings.Contains(err.Error(), "deployment not found") {
  599. // in this case, create the deployment
  600. _, err = t.client.CreateDeployment(
  601. context.Background(),
  602. t.projectID, t.gitInstallationID, t.clusterID,
  603. t.repoOwner, t.repoName,
  604. &types.CreateDeploymentRequest{
  605. Namespace: t.namespace,
  606. PullRequestID: t.prID,
  607. CreateGHDeploymentRequest: &types.CreateGHDeploymentRequest{
  608. Branch: t.branch,
  609. ActionID: t.actionID,
  610. },
  611. GitHubMetadata: &types.GitHubMetadata{
  612. PRName: t.prName,
  613. RepoName: t.repoName,
  614. RepoOwner: t.repoOwner,
  615. CommitSHA: t.commitSHA,
  616. },
  617. },
  618. )
  619. } else if err == nil {
  620. _, err = t.client.UpdateDeployment(
  621. context.Background(),
  622. t.projectID, t.gitInstallationID, t.clusterID,
  623. t.repoOwner, t.repoName,
  624. &types.UpdateDeploymentRequest{
  625. Namespace: t.namespace,
  626. CreateGHDeploymentRequest: &types.CreateGHDeploymentRequest{
  627. Branch: t.branch,
  628. ActionID: t.actionID,
  629. },
  630. CommitSHA: t.commitSHA,
  631. },
  632. )
  633. }
  634. return err
  635. }
  636. func (t *DeploymentHook) DataQueries() map[string]interface{} {
  637. res := make(map[string]interface{})
  638. // use the resource group to find all web applications that can have an exposed subdomain
  639. // that we can query for
  640. for _, resource := range t.resourceGroup.Resources {
  641. isWeb := false
  642. if sourceNameInter, exists := resource.Source["name"]; exists {
  643. if sourceName, ok := sourceNameInter.(string); ok {
  644. if sourceName == "web" {
  645. isWeb = true
  646. }
  647. }
  648. }
  649. if isWeb {
  650. res[resource.Name] = fmt.Sprintf("{ .%s.ingress.porter_hosts[0] }", resource.Name)
  651. }
  652. }
  653. return res
  654. }
  655. func (t *DeploymentHook) PostApply(populatedData map[string]interface{}) error {
  656. subdomains := make([]string, 0)
  657. for _, data := range populatedData {
  658. domain, ok := data.(string)
  659. if !ok {
  660. continue
  661. }
  662. if _, err := url.Parse("https://" + domain); err == nil {
  663. subdomains = append(subdomains, "https://"+domain)
  664. }
  665. }
  666. // finalize the deployment
  667. _, err := t.client.FinalizeDeployment(
  668. context.Background(),
  669. t.projectID, t.gitInstallationID, t.clusterID,
  670. t.repoOwner, t.repoName,
  671. &types.FinalizeDeploymentRequest{
  672. Namespace: t.namespace,
  673. Subdomain: strings.Join(subdomains, ","),
  674. },
  675. )
  676. return err
  677. }
  678. func (t *DeploymentHook) OnError(err error) {
  679. // if the deployment exists, throw an error for that deployment
  680. _, getDeplErr := t.client.GetDeployment(
  681. context.Background(),
  682. t.projectID, t.gitInstallationID, t.clusterID,
  683. t.repoOwner, t.repoName,
  684. &types.GetDeploymentRequest{
  685. Namespace: t.namespace,
  686. },
  687. )
  688. if getDeplErr == nil {
  689. _, err = t.client.UpdateDeploymentStatus(
  690. context.Background(),
  691. t.projectID, t.gitInstallationID, t.clusterID,
  692. t.repoOwner, t.repoName,
  693. &types.UpdateDeploymentStatusRequest{
  694. Namespace: t.namespace,
  695. CreateGHDeploymentRequest: &types.CreateGHDeploymentRequest{
  696. Branch: t.branch,
  697. ActionID: t.actionID,
  698. },
  699. Status: string(types.DeploymentStatusFailed),
  700. },
  701. )
  702. }
  703. }