apply.go 22 KB

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