apply.go 24 KB

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