apply.go 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916
  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/config"
  18. "github.com/porter-dev/porter/cli/cmd/deploy"
  19. "github.com/porter-dev/porter/cli/cmd/preview"
  20. "github.com/porter-dev/porter/internal/templater/utils"
  21. "github.com/porter-dev/switchboard/pkg/drivers"
  22. "github.com/porter-dev/switchboard/pkg/models"
  23. "github.com/porter-dev/switchboard/pkg/parser"
  24. switchboardTypes "github.com/porter-dev/switchboard/pkg/types"
  25. "github.com/porter-dev/switchboard/pkg/worker"
  26. "github.com/rs/zerolog"
  27. "github.com/spf13/cobra"
  28. )
  29. // applyCmd represents the "porter apply" base command when called
  30. // with a porter.yaml file as an argument
  31. var applyCmd = &cobra.Command{
  32. Use: "apply",
  33. Short: "Applies a configuration to an application",
  34. Long: fmt.Sprintf(`
  35. %s
  36. Applies a configuration to an application by either creating a new one or updating an existing
  37. one. For example:
  38. %s
  39. This command will apply the configuration contained in porter.yaml to the requested project and
  40. cluster either provided inside the porter.yaml file or through environment variables. Note that
  41. environment variables will always take precendence over values specified in the porter.yaml file.
  42. By default, this command expects to be run from a local git repository.
  43. The following are the environment variables that can be used to set certain values while
  44. applying a configuration:
  45. PORTER_CLUSTER Cluster ID that contains the project
  46. PORTER_PROJECT Project ID that contains the application
  47. PORTER_NAMESPACE The Kubernetes namespace that the application belongs to
  48. PORTER_SOURCE_NAME Name of the source Helm chart
  49. PORTER_SOURCE_REPO The URL of the Helm charts registry
  50. PORTER_SOURCE_VERSION The version of the Helm chart to use
  51. PORTER_TAG The Docker image tag to use (like the git commit hash)
  52. `,
  53. color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter apply\":"),
  54. color.New(color.FgGreen, color.Bold).Sprintf("porter apply -f porter.yaml"),
  55. ),
  56. Run: func(cmd *cobra.Command, args []string) {
  57. err := checkLoginAndRun(args, apply)
  58. if err != nil {
  59. os.Exit(1)
  60. }
  61. },
  62. }
  63. var porterYAML string
  64. func init() {
  65. rootCmd.AddCommand(applyCmd)
  66. applyCmd.Flags().StringVarP(&porterYAML, "file", "f", "", "path to porter.yaml")
  67. applyCmd.MarkFlagRequired("file")
  68. }
  69. func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
  70. fileBytes, err := ioutil.ReadFile(porterYAML)
  71. if err != nil {
  72. return err
  73. }
  74. resGroup, err := parser.ParseRawBytes(fileBytes)
  75. if err != nil {
  76. return err
  77. }
  78. basePath, err := os.Getwd()
  79. if err != nil {
  80. return err
  81. }
  82. worker := worker.NewWorker()
  83. worker.RegisterDriver("deploy", NewPorterDriver)
  84. worker.RegisterDriver("build-image", preview.NewBuildDriver)
  85. worker.RegisterDriver("push-image", preview.NewPushDriver)
  86. worker.RegisterDriver("update-config", preview.NewUpdateConfigDriver)
  87. worker.RegisterDriver("random-string", preview.NewRandomStringDriver)
  88. worker.RegisterDriver("env-group", preview.NewEnvGroupDriver)
  89. worker.SetDefaultDriver("deploy")
  90. if hasDeploymentHookEnvVars() {
  91. deplNamespace := os.Getenv("PORTER_NAMESPACE")
  92. if deplNamespace == "" {
  93. return fmt.Errorf("namespace must be set by PORTER_NAMESPACE")
  94. }
  95. deploymentHook, err := NewDeploymentHook(client, resGroup, deplNamespace)
  96. if err != nil {
  97. return err
  98. }
  99. worker.RegisterHook("deployment", deploymentHook)
  100. }
  101. cloneEnvGroupHook := NewCloneEnvGroupHook(client, resGroup)
  102. worker.RegisterHook("cloneenvgroup", cloneEnvGroupHook)
  103. return worker.Apply(resGroup, &switchboardTypes.ApplyOpts{
  104. BasePath: basePath,
  105. })
  106. }
  107. func hasDeploymentHookEnvVars() bool {
  108. if ghIDStr := os.Getenv("PORTER_GIT_INSTALLATION_ID"); ghIDStr == "" {
  109. return false
  110. }
  111. if prIDStr := os.Getenv("PORTER_PULL_REQUEST_ID"); prIDStr == "" {
  112. return false
  113. }
  114. if branchName := os.Getenv("PORTER_BRANCH_NAME"); branchName == "" {
  115. return false
  116. }
  117. if actionIDStr := os.Getenv("PORTER_ACTION_ID"); actionIDStr == "" {
  118. return false
  119. }
  120. if repoName := os.Getenv("PORTER_REPO_NAME"); repoName == "" {
  121. return false
  122. }
  123. if repoOwner := os.Getenv("PORTER_REPO_OWNER"); repoOwner == "" {
  124. return false
  125. }
  126. if prName := os.Getenv("PORTER_PR_NAME"); prName == "" {
  127. return false
  128. }
  129. return true
  130. }
  131. type ApplicationConfig struct {
  132. WaitForJob bool
  133. // If set to true, this does not run an update, it only creates the initial application and job,
  134. // skipping subsequent updates
  135. OnlyCreate bool
  136. Build struct {
  137. ForceBuild bool `mapstructure:"force_build"`
  138. ForcePush bool `mapstructure:"force_push"`
  139. UseCache bool `mapstructure:"use_cache"`
  140. Method string
  141. Context string
  142. Dockerfile string
  143. Image string
  144. Builder string
  145. Buildpacks []string
  146. }
  147. EnvGroups []types.EnvGroupMeta `mapstructure:"env_groups"`
  148. Values map[string]interface{}
  149. }
  150. type Driver struct {
  151. source *preview.Source
  152. target *preview.Target
  153. output map[string]interface{}
  154. lookupTable *map[string]drivers.Driver
  155. logger *zerolog.Logger
  156. }
  157. func NewPorterDriver(resource *models.Resource, opts *drivers.SharedDriverOpts) (drivers.Driver, error) {
  158. driver := &Driver{
  159. lookupTable: opts.DriverLookupTable,
  160. logger: opts.Logger,
  161. output: make(map[string]interface{}),
  162. }
  163. source, err := preview.GetSource(resource.Source)
  164. if err != nil {
  165. return nil, err
  166. }
  167. driver.source = source
  168. target, err := preview.GetTarget(resource.Target)
  169. if err != nil {
  170. return nil, err
  171. }
  172. driver.target = target
  173. return driver, nil
  174. }
  175. func (d *Driver) ShouldApply(resource *models.Resource) bool {
  176. return true
  177. }
  178. func (d *Driver) Apply(resource *models.Resource) (*models.Resource, error) {
  179. client := config.GetAPIClient()
  180. name := resource.Name
  181. if name == "" {
  182. return nil, fmt.Errorf("empty app name")
  183. }
  184. _, err := client.GetRelease(
  185. context.Background(),
  186. d.target.Project,
  187. d.target.Cluster,
  188. d.target.Namespace,
  189. resource.Name,
  190. )
  191. shouldCreate := err != nil
  192. if err != nil {
  193. color.New(color.FgYellow).Printf("Could not read release %s/%s (%s): attempting creation\n", d.target.Namespace, resource.Name, err.Error())
  194. }
  195. if d.source.IsApplication {
  196. return d.applyApplication(resource, client, shouldCreate)
  197. }
  198. return d.applyAddon(resource, client, shouldCreate)
  199. }
  200. // Simple apply for addons
  201. func (d *Driver) applyAddon(resource *models.Resource, client *api.Client, shouldCreate bool) (*models.Resource, error) {
  202. addonConfig, err := d.getAddonConfig(resource)
  203. if err != nil {
  204. return nil, err
  205. }
  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: addonConfig,
  218. Name: resource.Name,
  219. },
  220. },
  221. )
  222. } else {
  223. bytes, err := json.Marshal(addonConfig)
  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. UseCache: appConfig.Build.UseCache,
  287. }
  288. if appConfig.Build.UseCache {
  289. // set the docker config so that pack caching can use the repo credentials
  290. err := config.SetDockerConfig(client)
  291. if err != nil {
  292. return nil, err
  293. }
  294. }
  295. if shouldCreate {
  296. resource, err = d.createApplication(resource, client, sharedOpts, appConfig)
  297. if err != nil {
  298. return nil, err
  299. }
  300. } else if !appConfig.OnlyCreate {
  301. resource, err = d.updateApplication(resource, client, sharedOpts, appConfig)
  302. if err != nil {
  303. return nil, err
  304. }
  305. } else {
  306. color.New(color.FgYellow).Printf("Skipping creation for %s as onlyCreate is set to true\n", resource.Name)
  307. }
  308. if err = d.assignOutput(resource, client); err != nil {
  309. return nil, err
  310. }
  311. if d.source.Name == "job" && appConfig.WaitForJob && (shouldCreate || !appConfig.OnlyCreate) {
  312. color.New(color.FgYellow).Printf("Waiting for job '%s' to finish\n", resource.Name)
  313. prevProject := cliConf.Project
  314. prevCluster := cliConf.Cluster
  315. name = resource.Name
  316. namespace = d.target.Namespace
  317. cliConf.Project = d.target.Project
  318. cliConf.Cluster = d.target.Cluster
  319. err = waitForJob(nil, client, []string{})
  320. if err != nil {
  321. return nil, err
  322. }
  323. cliConf.Project = prevProject
  324. cliConf.Cluster = prevCluster
  325. }
  326. return resource, err
  327. }
  328. func (d *Driver) createApplication(resource *models.Resource, client *api.Client, sharedOpts *deploy.SharedOpts, appConf *ApplicationConfig) (*models.Resource, error) {
  329. // create new release
  330. color.New(color.FgGreen).Printf("Creating %s release: %s\n", d.source.Name, resource.Name)
  331. regList, err := client.ListRegistries(context.Background(), d.target.Project)
  332. if err != nil {
  333. return nil, err
  334. }
  335. var registryURL string
  336. if len(*regList) == 0 {
  337. return nil, fmt.Errorf("no registry found")
  338. } else {
  339. registryURL = (*regList)[0].URL
  340. }
  341. // attempt to get repo suffix from environment variables
  342. var repoSuffix string
  343. if repoName := os.Getenv("PORTER_REPO_NAME"); repoName != "" {
  344. if repoOwner := os.Getenv("PORTER_REPO_OWNER"); repoOwner != "" {
  345. repoSuffix = strings.ReplaceAll(fmt.Sprintf("%s-%s", repoOwner, repoName), "_", "-")
  346. }
  347. }
  348. createAgent := &deploy.CreateAgent{
  349. Client: client,
  350. CreateOpts: &deploy.CreateOpts{
  351. SharedOpts: sharedOpts,
  352. Kind: d.source.Name,
  353. ReleaseName: resource.Name,
  354. RegistryURL: registryURL,
  355. RepoSuffix: repoSuffix,
  356. },
  357. }
  358. var buildConfig *types.BuildConfig
  359. if appConf.Build.Builder != "" {
  360. buildConfig = &types.BuildConfig{
  361. Builder: appConf.Build.Builder,
  362. Buildpacks: appConf.Build.Buildpacks,
  363. }
  364. }
  365. var subdomain string
  366. if appConf.Build.Method == "registry" {
  367. subdomain, err = createAgent.CreateFromRegistry(appConf.Build.Image, appConf.Values)
  368. } else {
  369. // if useCache is set, create the image repository first
  370. if appConf.Build.UseCache {
  371. regID, imageURL, err := createAgent.GetImageRepoURL(resource.Name, sharedOpts.Namespace)
  372. if err != nil {
  373. return nil, err
  374. }
  375. err = client.CreateRepository(
  376. context.Background(),
  377. sharedOpts.ProjectID,
  378. regID,
  379. &types.CreateRegistryRepositoryRequest{
  380. ImageRepoURI: imageURL,
  381. },
  382. )
  383. if err != nil {
  384. return nil, err
  385. }
  386. }
  387. subdomain, err = createAgent.CreateFromDocker(appConf.Values, sharedOpts.OverrideTag, buildConfig, appConf.Build.ForceBuild)
  388. }
  389. if err != nil {
  390. return nil, err
  391. }
  392. return resource, handleSubdomainCreate(subdomain, err)
  393. }
  394. func (d *Driver) updateApplication(resource *models.Resource, client *api.Client, sharedOpts *deploy.SharedOpts, appConf *ApplicationConfig) (*models.Resource, error) {
  395. color.New(color.FgGreen).Println("Updating existing release:", resource.Name)
  396. updateAgent, err := deploy.NewDeployAgent(client, resource.Name, &deploy.DeployOpts{
  397. SharedOpts: sharedOpts,
  398. Local: appConf.Build.Method != "registry",
  399. })
  400. if err != nil {
  401. return nil, err
  402. }
  403. // if the build method is registry, we do not trigger a build
  404. if appConf.Build.Method != "registry" {
  405. buildEnv, err := updateAgent.GetBuildEnv(&deploy.GetBuildEnvOpts{
  406. UseNewConfig: true,
  407. NewConfig: appConf.Values,
  408. })
  409. if err != nil {
  410. return nil, err
  411. }
  412. err = updateAgent.SetBuildEnv(buildEnv)
  413. if err != nil {
  414. return nil, err
  415. }
  416. var buildConfig *types.BuildConfig
  417. if appConf.Build.Builder != "" {
  418. buildConfig = &types.BuildConfig{
  419. Builder: appConf.Build.Builder,
  420. Buildpacks: appConf.Build.Buildpacks,
  421. }
  422. }
  423. err = updateAgent.Build(buildConfig, appConf.Build.ForceBuild)
  424. if err != nil {
  425. return nil, err
  426. }
  427. if !appConf.Build.UseCache {
  428. err = updateAgent.Push(appConf.Build.ForcePush)
  429. if err != nil {
  430. return nil, err
  431. }
  432. }
  433. }
  434. err = updateAgent.UpdateImageAndValues(appConf.Values)
  435. if err != nil {
  436. return nil, err
  437. }
  438. return resource, nil
  439. }
  440. func (d *Driver) assignOutput(resource *models.Resource, client *api.Client) error {
  441. release, err := client.GetRelease(
  442. context.Background(),
  443. d.target.Project,
  444. d.target.Cluster,
  445. d.target.Namespace,
  446. resource.Name,
  447. )
  448. if err != nil {
  449. return err
  450. }
  451. d.output = utils.CoalesceValues(d.source.SourceValues, release.Config)
  452. return nil
  453. }
  454. func (d *Driver) Output() (map[string]interface{}, error) {
  455. return d.output, nil
  456. }
  457. func (d *Driver) getApplicationConfig(resource *models.Resource) (*ApplicationConfig, error) {
  458. populatedConf, err := drivers.ConstructConfig(&drivers.ConstructConfigOpts{
  459. RawConf: resource.Config,
  460. LookupTable: *d.lookupTable,
  461. Dependencies: resource.Dependencies,
  462. })
  463. if err != nil {
  464. return nil, err
  465. }
  466. config := &ApplicationConfig{}
  467. err = mapstructure.Decode(populatedConf, config)
  468. if err != nil {
  469. return nil, err
  470. }
  471. if _, ok := resource.Config["waitForJob"]; !ok && d.source.Name == "job" {
  472. // default to true and wait for the job to finish
  473. config.WaitForJob = true
  474. }
  475. return config, nil
  476. }
  477. func (d *Driver) getAddonConfig(resource *models.Resource) (map[string]interface{}, error) {
  478. return drivers.ConstructConfig(&drivers.ConstructConfigOpts{
  479. RawConf: resource.Config,
  480. LookupTable: *d.lookupTable,
  481. Dependencies: resource.Dependencies,
  482. })
  483. }
  484. type DeploymentHook struct {
  485. client *api.Client
  486. resourceGroup *switchboardTypes.ResourceGroup
  487. gitInstallationID, projectID, clusterID, prID, actionID uint
  488. branch, namespace, repoName, repoOwner, prName, commitSHA string
  489. }
  490. func NewDeploymentHook(client *api.Client, resourceGroup *switchboardTypes.ResourceGroup, namespace string) (*DeploymentHook, error) {
  491. res := &DeploymentHook{
  492. client: client,
  493. resourceGroup: resourceGroup,
  494. namespace: namespace,
  495. }
  496. ghIDStr := os.Getenv("PORTER_GIT_INSTALLATION_ID")
  497. ghID, err := strconv.Atoi(ghIDStr)
  498. if err != nil {
  499. return nil, err
  500. }
  501. res.gitInstallationID = uint(ghID)
  502. prIDStr := os.Getenv("PORTER_PULL_REQUEST_ID")
  503. prID, err := strconv.Atoi(prIDStr)
  504. if err != nil {
  505. return nil, err
  506. }
  507. res.prID = uint(prID)
  508. res.projectID = cliConf.Project
  509. if res.projectID == 0 {
  510. return nil, fmt.Errorf("project id must be set")
  511. }
  512. res.clusterID = cliConf.Cluster
  513. if res.clusterID == 0 {
  514. return nil, fmt.Errorf("cluster id must be set")
  515. }
  516. branchName := os.Getenv("PORTER_BRANCH_NAME")
  517. res.branch = branchName
  518. actionIDStr := os.Getenv("PORTER_ACTION_ID")
  519. actionID, err := strconv.Atoi(actionIDStr)
  520. if err != nil {
  521. return nil, err
  522. }
  523. res.actionID = uint(actionID)
  524. repoName := os.Getenv("PORTER_REPO_NAME")
  525. res.repoName = repoName
  526. repoOwner := os.Getenv("PORTER_REPO_OWNER")
  527. res.repoOwner = repoOwner
  528. prName := os.Getenv("PORTER_PR_NAME")
  529. res.prName = prName
  530. commit, err := git.LastCommit()
  531. if err != nil {
  532. return nil, fmt.Errorf(err.Error())
  533. }
  534. res.commitSHA = commit.Sha[:7]
  535. return res, nil
  536. }
  537. func (t *DeploymentHook) PreApply() error {
  538. // attempt to read the deployment -- if it doesn't exist, create it
  539. _, err := t.client.GetDeployment(
  540. context.Background(),
  541. t.projectID, t.gitInstallationID, t.clusterID,
  542. t.repoOwner, t.repoName,
  543. &types.GetDeploymentRequest{
  544. Namespace: t.namespace,
  545. },
  546. )
  547. // TODO: case this on the response status code rather than text
  548. if err != nil && strings.Contains(err.Error(), "deployment not found") {
  549. // in this case, create the deployment
  550. _, err = t.client.CreateDeployment(
  551. context.Background(),
  552. t.projectID, t.gitInstallationID, t.clusterID,
  553. t.repoOwner, t.repoName,
  554. &types.CreateDeploymentRequest{
  555. Namespace: t.namespace,
  556. PullRequestID: t.prID,
  557. CreateGHDeploymentRequest: &types.CreateGHDeploymentRequest{
  558. Branch: t.branch,
  559. ActionID: t.actionID,
  560. },
  561. GitHubMetadata: &types.GitHubMetadata{
  562. PRName: t.prName,
  563. RepoName: t.repoName,
  564. RepoOwner: t.repoOwner,
  565. CommitSHA: t.commitSHA,
  566. },
  567. },
  568. )
  569. } else if err == nil {
  570. _, err = t.client.UpdateDeployment(
  571. context.Background(),
  572. t.projectID, t.gitInstallationID, t.clusterID,
  573. t.repoOwner, t.repoName,
  574. &types.UpdateDeploymentRequest{
  575. Namespace: t.namespace,
  576. CreateGHDeploymentRequest: &types.CreateGHDeploymentRequest{
  577. Branch: t.branch,
  578. ActionID: t.actionID,
  579. },
  580. CommitSHA: t.commitSHA,
  581. },
  582. )
  583. }
  584. return err
  585. }
  586. func (t *DeploymentHook) DataQueries() map[string]interface{} {
  587. res := make(map[string]interface{})
  588. // use the resource group to find all web applications that can have an exposed subdomain
  589. // that we can query for
  590. for _, resource := range t.resourceGroup.Resources {
  591. isWeb := false
  592. if sourceNameInter, exists := resource.Source["name"]; exists {
  593. if sourceName, ok := sourceNameInter.(string); ok {
  594. if sourceName == "web" {
  595. isWeb = true
  596. }
  597. }
  598. }
  599. if isWeb {
  600. res[resource.Name] = fmt.Sprintf("{ .%s.ingress.porter_hosts[0] }", resource.Name)
  601. }
  602. }
  603. return res
  604. }
  605. func (t *DeploymentHook) PostApply(populatedData map[string]interface{}) error {
  606. subdomains := make([]string, 0)
  607. for _, data := range populatedData {
  608. domain, ok := data.(string)
  609. if !ok {
  610. continue
  611. }
  612. if _, err := url.Parse("https://" + domain); err == nil {
  613. subdomains = append(subdomains, "https://"+domain)
  614. }
  615. }
  616. // finalize the deployment
  617. _, err := t.client.FinalizeDeployment(
  618. context.Background(),
  619. t.projectID, t.gitInstallationID, t.clusterID,
  620. t.repoOwner, t.repoName,
  621. &types.FinalizeDeploymentRequest{
  622. Namespace: t.namespace,
  623. Subdomain: strings.Join(subdomains, ","),
  624. },
  625. )
  626. return err
  627. }
  628. func (t *DeploymentHook) OnError(err error) {
  629. // if the deployment exists, throw an error for that deployment
  630. _, getDeplErr := 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. if getDeplErr == nil {
  639. _, err = t.client.UpdateDeploymentStatus(
  640. context.Background(),
  641. t.projectID, t.gitInstallationID, t.clusterID,
  642. t.repoOwner, t.repoName,
  643. &types.UpdateDeploymentStatusRequest{
  644. Namespace: t.namespace,
  645. CreateGHDeploymentRequest: &types.CreateGHDeploymentRequest{
  646. Branch: t.branch,
  647. ActionID: t.actionID,
  648. },
  649. Status: string(types.DeploymentStatusFailed),
  650. },
  651. )
  652. }
  653. }
  654. type CloneEnvGroupHook struct {
  655. client *api.Client
  656. resGroup *switchboardTypes.ResourceGroup
  657. }
  658. func NewCloneEnvGroupHook(client *api.Client, resourceGroup *switchboardTypes.ResourceGroup) *CloneEnvGroupHook {
  659. return &CloneEnvGroupHook{
  660. client: client,
  661. resGroup: resourceGroup,
  662. }
  663. }
  664. func (t *CloneEnvGroupHook) PreApply() error {
  665. for _, res := range t.resGroup.Resources {
  666. if res.Driver == "env-group" {
  667. continue
  668. }
  669. config := &ApplicationConfig{}
  670. err := mapstructure.Decode(res.Config, &config)
  671. if err != nil {
  672. continue
  673. }
  674. if config != nil && len(config.EnvGroups) > 0 {
  675. target, err := preview.GetTarget(res.Target)
  676. if err != nil {
  677. return err
  678. }
  679. for _, group := range config.EnvGroups {
  680. if group.Name == "" {
  681. return fmt.Errorf("env group name cannot be empty")
  682. }
  683. _, err := t.client.GetEnvGroup(
  684. context.Background(),
  685. target.Project,
  686. target.Cluster,
  687. target.Namespace,
  688. &types.GetEnvGroupRequest{
  689. Name: group.Name,
  690. Version: group.Version,
  691. },
  692. )
  693. if err != nil && err.Error() == "env group not found" {
  694. if group.Namespace == "" {
  695. return fmt.Errorf("env group namespace cannot be empty")
  696. }
  697. color.New(color.FgBlue, color.Bold).
  698. Printf("Env group '%s' does not exist in the target namespace '%s'\n", group.Name, target.Namespace)
  699. color.New(color.FgBlue, color.Bold).
  700. Printf("Cloning env group '%s' from namespace '%s' to target namespace '%s'\n",
  701. group.Name, group.Namespace, target.Namespace)
  702. _, err = t.client.CloneEnvGroup(
  703. context.Background(), target.Project, target.Cluster, group.Namespace,
  704. &types.CloneEnvGroupRequest{
  705. Name: group.Name,
  706. Namespace: target.Namespace,
  707. },
  708. )
  709. if err != nil {
  710. return err
  711. }
  712. } else if err != nil {
  713. return err
  714. }
  715. }
  716. }
  717. }
  718. return nil
  719. }
  720. func (t *CloneEnvGroupHook) DataQueries() map[string]interface{} {
  721. return nil
  722. }
  723. func (t *CloneEnvGroupHook) PostApply(populatedData map[string]interface{}) error {
  724. return nil
  725. }
  726. func (t *CloneEnvGroupHook) OnError(err error) {}