apply.go 24 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027
  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 = 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. branch, 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. branchName := os.Getenv("PORTER_BRANCH_NAME")
  613. res.branch = branchName
  614. actionIDStr := os.Getenv("PORTER_ACTION_ID")
  615. actionID, err := strconv.Atoi(actionIDStr)
  616. if err != nil {
  617. return nil, err
  618. }
  619. res.actionID = uint(actionID)
  620. repoName := os.Getenv("PORTER_REPO_NAME")
  621. res.repoName = repoName
  622. repoOwner := os.Getenv("PORTER_REPO_OWNER")
  623. res.repoOwner = repoOwner
  624. prName := os.Getenv("PORTER_PR_NAME")
  625. res.prName = prName
  626. commit, err := git.LastCommit()
  627. if err != nil {
  628. return nil, fmt.Errorf(err.Error())
  629. }
  630. res.commitSHA = commit.Sha[:7]
  631. return res, nil
  632. }
  633. func (t *DeploymentHook) PreApply() error {
  634. // attempt to read the deployment -- if it doesn't exist, create it
  635. _, err := t.client.GetDeployment(
  636. context.Background(),
  637. t.projectID, t.gitInstallationID, t.clusterID,
  638. t.repoOwner, t.repoName,
  639. &types.GetDeploymentRequest{
  640. Namespace: t.namespace,
  641. },
  642. )
  643. // TODO: case this on the response status code rather than text
  644. if err != nil && strings.Contains(err.Error(), "deployment not found") {
  645. // in this case, create the deployment
  646. _, err = t.client.CreateDeployment(
  647. context.Background(),
  648. t.projectID, t.gitInstallationID, t.clusterID,
  649. t.repoOwner, t.repoName,
  650. &types.CreateDeploymentRequest{
  651. Namespace: t.namespace,
  652. PullRequestID: t.prID,
  653. CreateGHDeploymentRequest: &types.CreateGHDeploymentRequest{
  654. Branch: t.branch,
  655. ActionID: t.actionID,
  656. },
  657. GitHubMetadata: &types.GitHubMetadata{
  658. PRName: t.prName,
  659. RepoName: t.repoName,
  660. RepoOwner: t.repoOwner,
  661. CommitSHA: t.commitSHA,
  662. },
  663. },
  664. )
  665. } else if err == nil {
  666. _, err = t.client.UpdateDeployment(
  667. context.Background(),
  668. t.projectID, t.gitInstallationID, t.clusterID,
  669. t.repoOwner, t.repoName,
  670. &types.UpdateDeploymentRequest{
  671. Namespace: t.namespace,
  672. CreateGHDeploymentRequest: &types.CreateGHDeploymentRequest{
  673. Branch: t.branch,
  674. ActionID: t.actionID,
  675. },
  676. CommitSHA: t.commitSHA,
  677. },
  678. )
  679. }
  680. return err
  681. }
  682. func (t *DeploymentHook) DataQueries() map[string]interface{} {
  683. res := make(map[string]interface{})
  684. // use the resource group to find all web applications that can have an exposed subdomain
  685. // that we can query for
  686. for _, resource := range t.resourceGroup.Resources {
  687. isWeb := false
  688. if sourceNameInter, exists := resource.Source["name"]; exists {
  689. if sourceName, ok := sourceNameInter.(string); ok {
  690. if sourceName == "web" {
  691. isWeb = true
  692. }
  693. }
  694. }
  695. if isWeb {
  696. res[resource.Name] = fmt.Sprintf("{ .%s.ingress.porter_hosts[0] }", resource.Name)
  697. }
  698. }
  699. return res
  700. }
  701. func (t *DeploymentHook) PostApply(populatedData map[string]interface{}) error {
  702. subdomains := make([]string, 0)
  703. for _, data := range populatedData {
  704. domain, ok := data.(string)
  705. if !ok {
  706. continue
  707. }
  708. if _, err := url.Parse("https://" + domain); err == nil {
  709. subdomains = append(subdomains, "https://"+domain)
  710. }
  711. }
  712. // finalize the deployment
  713. _, err := t.client.FinalizeDeployment(
  714. context.Background(),
  715. t.projectID, t.gitInstallationID, t.clusterID,
  716. t.repoOwner, t.repoName,
  717. &types.FinalizeDeploymentRequest{
  718. Namespace: t.namespace,
  719. Subdomain: strings.Join(subdomains, ","),
  720. },
  721. )
  722. return err
  723. }
  724. func (t *DeploymentHook) OnError(err error) {
  725. // if the deployment exists, throw an error for that deployment
  726. _, getDeplErr := t.client.GetDeployment(
  727. context.Background(),
  728. t.projectID, t.gitInstallationID, t.clusterID,
  729. t.repoOwner, t.repoName,
  730. &types.GetDeploymentRequest{
  731. Namespace: t.namespace,
  732. },
  733. )
  734. if getDeplErr == nil {
  735. _, err = t.client.UpdateDeploymentStatus(
  736. context.Background(),
  737. t.projectID, t.gitInstallationID, t.clusterID,
  738. t.repoOwner, t.repoName,
  739. &types.UpdateDeploymentStatusRequest{
  740. Namespace: t.namespace,
  741. CreateGHDeploymentRequest: &types.CreateGHDeploymentRequest{
  742. Branch: t.branch,
  743. ActionID: t.actionID,
  744. },
  745. Status: string(types.DeploymentStatusFailed),
  746. },
  747. )
  748. }
  749. }
  750. type CloneEnvGroupHook struct {
  751. client *api.Client
  752. resGroup *switchboardTypes.ResourceGroup
  753. }
  754. func NewCloneEnvGroupHook(client *api.Client, resourceGroup *switchboardTypes.ResourceGroup) *CloneEnvGroupHook {
  755. return &CloneEnvGroupHook{
  756. client: client,
  757. resGroup: resourceGroup,
  758. }
  759. }
  760. func (t *CloneEnvGroupHook) PreApply() error {
  761. for _, res := range t.resGroup.Resources {
  762. config := &ApplicationConfig{}
  763. err := mapstructure.Decode(res.Config, &config)
  764. if err != nil {
  765. continue
  766. }
  767. if config != nil && len(config.EnvGroups) > 0 {
  768. target := &Target{}
  769. err = getTarget(res.Target, target)
  770. if err != nil {
  771. return err
  772. }
  773. for _, group := range config.EnvGroups {
  774. if group.Name == "" {
  775. return fmt.Errorf("env group name cannot be empty")
  776. }
  777. _, err := t.client.GetEnvGroup(
  778. context.Background(),
  779. target.Project,
  780. target.Cluster,
  781. target.Namespace,
  782. &types.GetEnvGroupRequest{
  783. Name: group.Name,
  784. Version: group.Version,
  785. },
  786. )
  787. if err != nil && err.Error() == "env group not found" {
  788. if group.Namespace == "" {
  789. return fmt.Errorf("env group namespace cannot be empty")
  790. }
  791. color.New(color.FgBlue, color.Bold).
  792. Printf("Env group '%s' does not exist in the target namespace '%s'\n", group.Name, target.Namespace)
  793. color.New(color.FgBlue, color.Bold).
  794. Printf("Cloning env group '%s' from namespace '%s' to target namespace '%s'\n",
  795. group.Name, group.Namespace, target.Namespace)
  796. _, err = t.client.CloneEnvGroup(
  797. context.Background(), target.Project, target.Cluster, group.Namespace,
  798. &types.CloneEnvGroupRequest{
  799. Name: group.Name,
  800. Namespace: target.Namespace,
  801. },
  802. )
  803. if err != nil {
  804. return err
  805. }
  806. } else if err != nil {
  807. return err
  808. }
  809. }
  810. }
  811. }
  812. return nil
  813. }
  814. func (t *CloneEnvGroupHook) DataQueries() map[string]interface{} {
  815. return nil
  816. }
  817. func (t *CloneEnvGroupHook) PostApply(populatedData map[string]interface{}) error {
  818. return nil
  819. }
  820. func (t *CloneEnvGroupHook) OnError(err error) {}