apply.go 22 KB

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