apply.go 24 KB

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