apply.go 21 KB

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