apply.go 21 KB

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