apply.go 24 KB

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