apply.go 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035
  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 branchFrom := os.Getenv("PORTER_BRANCH_FROM"); branchFrom == "" {
  108. return false
  109. }
  110. if branchInto := os.Getenv("PORTER_BRANCH_INTO"); branchInto == "" {
  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 Source struct {
  128. Name string
  129. Repo string
  130. Version string
  131. IsApplication bool
  132. SourceValues map[string]interface{}
  133. }
  134. type Target struct {
  135. Project uint
  136. Cluster uint
  137. Namespace string
  138. }
  139. type ApplicationConfig struct {
  140. WaitForJob bool
  141. Build struct {
  142. ForceBuild bool
  143. ForcePush bool
  144. Method string
  145. Context string
  146. Dockerfile string
  147. Image string
  148. Builder string
  149. Buildpacks []string
  150. }
  151. EnvGroups []types.EnvGroupMeta
  152. Values map[string]interface{}
  153. }
  154. type Driver struct {
  155. source *Source
  156. target *Target
  157. output map[string]interface{}
  158. lookupTable *map[string]drivers.Driver
  159. logger *zerolog.Logger
  160. }
  161. func NewPorterDriver(resource *models.Resource, opts *drivers.SharedDriverOpts) (drivers.Driver, error) {
  162. driver := &Driver{
  163. lookupTable: opts.DriverLookupTable,
  164. logger: opts.Logger,
  165. output: make(map[string]interface{}),
  166. }
  167. source := &Source{}
  168. err := getSource(resource.Source, source)
  169. if err != nil {
  170. return nil, err
  171. }
  172. driver.source = source
  173. target := &Target{}
  174. err = getTarget(resource.Target, target)
  175. if err != nil {
  176. return nil, err
  177. }
  178. driver.target = target
  179. return driver, nil
  180. }
  181. func (d *Driver) ShouldApply(resource *models.Resource) bool {
  182. return true
  183. }
  184. func (d *Driver) Apply(resource *models.Resource) (*models.Resource, error) {
  185. client := GetAPIClient(config)
  186. name := resource.Name
  187. if name == "" {
  188. return nil, fmt.Errorf("empty app name")
  189. }
  190. _, err := client.GetRelease(
  191. context.Background(),
  192. d.target.Project,
  193. d.target.Cluster,
  194. d.target.Namespace,
  195. resource.Name,
  196. )
  197. shouldCreate := err != nil
  198. if err != nil {
  199. color.New(color.FgYellow).Printf("Could not read release %s/%s (%s): attempting creation\n", d.target.Namespace, resource.Name, err.Error())
  200. }
  201. if d.source.IsApplication {
  202. return d.applyApplication(resource, client, shouldCreate)
  203. }
  204. return d.applyAddon(resource, client, shouldCreate)
  205. }
  206. // Simple apply for addons
  207. func (d *Driver) applyAddon(resource *models.Resource, client *api.Client, shouldCreate bool) (*models.Resource, error) {
  208. var err error
  209. if shouldCreate {
  210. err = client.DeployAddon(
  211. context.Background(),
  212. d.target.Project,
  213. d.target.Cluster,
  214. d.target.Namespace,
  215. &types.CreateAddonRequest{
  216. CreateReleaseBaseRequest: &types.CreateReleaseBaseRequest{
  217. RepoURL: d.source.Repo,
  218. TemplateName: d.source.Name,
  219. TemplateVersion: d.source.Version,
  220. Values: resource.Config,
  221. Name: resource.Name,
  222. },
  223. },
  224. )
  225. } else {
  226. bytes, err := json.Marshal(resource.Config)
  227. if err != nil {
  228. return nil, err
  229. }
  230. err = client.UpgradeRelease(
  231. context.Background(),
  232. d.target.Project,
  233. d.target.Cluster,
  234. d.target.Namespace,
  235. resource.Name,
  236. &types.UpgradeReleaseRequest{
  237. Values: string(bytes),
  238. },
  239. )
  240. }
  241. if err != nil {
  242. return nil, err
  243. }
  244. if err = d.assignOutput(resource, client); err != nil {
  245. return nil, err
  246. }
  247. return resource, err
  248. }
  249. func (d *Driver) applyApplication(resource *models.Resource, client *api.Client, shouldCreate bool) (*models.Resource, error) {
  250. appConfig, err := d.getApplicationConfig(resource)
  251. if err != nil {
  252. return nil, err
  253. }
  254. method := appConfig.Build.Method
  255. if method != "pack" && method != "docker" && method != "registry" {
  256. return nil, fmt.Errorf("method should either be \"docker\", \"pack\" or \"registry\"")
  257. }
  258. fullPath, err := filepath.Abs(appConfig.Build.Context)
  259. if err != nil {
  260. return nil, err
  261. }
  262. tag := os.Getenv("PORTER_TAG")
  263. if tag == "" {
  264. commit, err := git.LastCommit()
  265. if err != nil {
  266. return nil, err
  267. }
  268. tag = commit.Sha[:7]
  269. }
  270. // if the method is registry and a tag is defined, we use the provided tag
  271. if appConfig.Build.Method == "registry" {
  272. imageSpl := strings.Split(appConfig.Build.Image, ":")
  273. if len(imageSpl) == 2 {
  274. tag = imageSpl[1]
  275. }
  276. if tag == "" {
  277. tag = "latest"
  278. }
  279. }
  280. sharedOpts := &deploy.SharedOpts{
  281. ProjectID: d.target.Project,
  282. ClusterID: d.target.Cluster,
  283. Namespace: d.target.Namespace,
  284. LocalPath: fullPath,
  285. LocalDockerfile: appConfig.Build.Dockerfile,
  286. OverrideTag: tag,
  287. Method: deploy.DeployBuildType(method),
  288. EnvGroups: appConfig.EnvGroups,
  289. }
  290. if shouldCreate {
  291. resource, err = d.createApplication(resource, client, sharedOpts, appConfig)
  292. if err != nil {
  293. return nil, err
  294. }
  295. } else {
  296. resource, err = d.updateApplication(resource, client, sharedOpts, appConfig)
  297. if err != nil {
  298. return nil, err
  299. }
  300. }
  301. if err = d.assignOutput(resource, client); err != nil {
  302. return nil, err
  303. }
  304. if d.source.Name == "job" && appConfig.WaitForJob {
  305. color.New(color.FgYellow).Printf("Waiting for job '%s' to finish\n", resource.Name)
  306. prevProject := config.Project
  307. prevCluster := config.Cluster
  308. name = resource.Name
  309. namespace = d.target.Namespace
  310. config.Project = d.target.Project
  311. config.Cluster = d.target.Cluster
  312. err = waitForJob(nil, client, []string{})
  313. if err != nil {
  314. return nil, err
  315. }
  316. config.Project = prevProject
  317. config.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. subdomain, err = createAgent.CreateFromDocker(appConf.Values, sharedOpts.OverrideTag, buildConfig, appConf.Build.ForceBuild)
  363. }
  364. if err != nil {
  365. return nil, err
  366. }
  367. return resource, handleSubdomainCreate(subdomain, err)
  368. }
  369. func (d *Driver) updateApplication(resource *models.Resource, client *api.Client, sharedOpts *deploy.SharedOpts, appConf *ApplicationConfig) (*models.Resource, error) {
  370. color.New(color.FgGreen).Println("Updating existing release:", resource.Name)
  371. updateAgent, err := deploy.NewDeployAgent(client, resource.Name, &deploy.DeployOpts{
  372. SharedOpts: sharedOpts,
  373. Local: appConf.Build.Method != "registry",
  374. })
  375. if err != nil {
  376. return nil, err
  377. }
  378. // if the build method is registry, we do not trigger a build
  379. if appConf.Build.Method != "registry" {
  380. buildEnv, err := updateAgent.GetBuildEnv(&deploy.GetBuildEnvOpts{
  381. UseNewConfig: true,
  382. NewConfig: appConf.Values,
  383. })
  384. if err != nil {
  385. return nil, err
  386. }
  387. err = updateAgent.SetBuildEnv(buildEnv)
  388. if err != nil {
  389. return nil, err
  390. }
  391. var buildConfig *types.BuildConfig
  392. if appConf.Build.Builder != "" {
  393. buildConfig = &types.BuildConfig{
  394. Builder: appConf.Build.Builder,
  395. Buildpacks: appConf.Build.Buildpacks,
  396. }
  397. }
  398. err = updateAgent.Build(buildConfig, appConf.Build.ForceBuild)
  399. if err != nil {
  400. return nil, err
  401. }
  402. err = updateAgent.Push(appConf.Build.ForcePush)
  403. if err != nil {
  404. return nil, err
  405. }
  406. }
  407. err = updateAgent.UpdateImageAndValues(appConf.Values)
  408. if err != nil {
  409. return nil, err
  410. }
  411. return resource, nil
  412. }
  413. func (d *Driver) assignOutput(resource *models.Resource, client *api.Client) error {
  414. release, err := client.GetRelease(
  415. context.Background(),
  416. d.target.Project,
  417. d.target.Cluster,
  418. d.target.Namespace,
  419. resource.Name,
  420. )
  421. if err != nil {
  422. return err
  423. }
  424. d.output = utils.CoalesceValues(d.source.SourceValues, release.Config)
  425. return nil
  426. }
  427. func (d *Driver) Output() (map[string]interface{}, error) {
  428. return d.output, nil
  429. }
  430. func getSource(input map[string]interface{}, output *Source) error {
  431. // first read from env vars
  432. output.Name = os.Getenv("PORTER_SOURCE_NAME")
  433. output.Repo = os.Getenv("PORTER_SOURCE_REPO")
  434. output.Version = os.Getenv("PORTER_SOURCE_VERSION")
  435. // next, check for values in the YAML file
  436. if output.Name == "" {
  437. if name, ok := input["name"]; ok {
  438. nameVal, ok := name.(string)
  439. if !ok {
  440. return fmt.Errorf("invalid name provided")
  441. }
  442. output.Name = nameVal
  443. }
  444. }
  445. if output.Name == "" {
  446. return fmt.Errorf("source name required")
  447. }
  448. if output.Repo == "" {
  449. if repo, ok := input["repo"]; ok {
  450. repoVal, ok := repo.(string)
  451. if !ok {
  452. return fmt.Errorf("invalid repo provided")
  453. }
  454. output.Repo = repoVal
  455. }
  456. }
  457. if output.Version == "" {
  458. if version, ok := input["version"]; ok {
  459. versionVal, ok := version.(string)
  460. if !ok {
  461. return fmt.Errorf("invalid version provided")
  462. }
  463. output.Version = versionVal
  464. }
  465. }
  466. // lastly, just put in the defaults
  467. if output.Version == "" {
  468. output.Version = "latest"
  469. }
  470. output.IsApplication = output.Repo == "https://charts.getporter.dev"
  471. if output.Repo == "" {
  472. output.Repo = "https://charts.getporter.dev"
  473. values, err := existsInRepo(output.Name, output.Version, output.Repo)
  474. if err == nil {
  475. // found in "https://charts.getporter.dev"
  476. output.SourceValues = values
  477. output.IsApplication = true
  478. return nil
  479. }
  480. output.Repo = "https://chart-addons.getporter.dev"
  481. values, err = existsInRepo(output.Name, output.Version, output.Repo)
  482. if err == nil {
  483. // found in https://chart-addons.getporter.dev
  484. output.SourceValues = values
  485. return nil
  486. }
  487. return fmt.Errorf("source does not exist in any repo")
  488. }
  489. return fmt.Errorf("source '%s' does not exist in repo '%s'", output.Name, output.Repo)
  490. }
  491. func getTarget(input map[string]interface{}, output *Target) error {
  492. // first read from env vars
  493. if projectEnv := os.Getenv("PORTER_PROJECT"); projectEnv != "" {
  494. project, err := strconv.Atoi(projectEnv)
  495. if err != nil {
  496. return err
  497. }
  498. output.Project = uint(project)
  499. }
  500. if clusterEnv := os.Getenv("PORTER_CLUSTER"); clusterEnv != "" {
  501. cluster, err := strconv.Atoi(clusterEnv)
  502. if err != nil {
  503. return err
  504. }
  505. output.Cluster = uint(cluster)
  506. }
  507. output.Namespace = os.Getenv("PORTER_NAMESPACE")
  508. // next, check for values in the YAML file
  509. if output.Project == 0 {
  510. if project, ok := input["project"]; ok {
  511. projectVal, ok := project.(uint)
  512. if !ok {
  513. return fmt.Errorf("project value must be an integer")
  514. }
  515. output.Project = projectVal
  516. }
  517. }
  518. if output.Cluster == 0 {
  519. if cluster, ok := input["cluster"]; ok {
  520. clusterVal, ok := cluster.(uint)
  521. if !ok {
  522. return fmt.Errorf("cluster value must be an integer")
  523. }
  524. output.Cluster = clusterVal
  525. }
  526. }
  527. if output.Namespace == "" {
  528. if namespace, ok := input["namespace"]; ok {
  529. namespaceVal, ok := namespace.(string)
  530. if !ok {
  531. return fmt.Errorf("invalid namespace provided")
  532. }
  533. output.Namespace = namespaceVal
  534. }
  535. }
  536. // lastly, just put in the defaults
  537. if output.Project == 0 {
  538. output.Project = config.Project
  539. }
  540. if output.Cluster == 0 {
  541. output.Cluster = config.Cluster
  542. }
  543. if output.Namespace == "" {
  544. output.Namespace = "default"
  545. }
  546. return nil
  547. }
  548. func (d *Driver) getApplicationConfig(resource *models.Resource) (*ApplicationConfig, error) {
  549. populatedConf, err := drivers.ConstructConfig(&drivers.ConstructConfigOpts{
  550. RawConf: resource.Config,
  551. LookupTable: *d.lookupTable,
  552. Dependencies: resource.Dependencies,
  553. })
  554. if err != nil {
  555. return nil, err
  556. }
  557. config := &ApplicationConfig{}
  558. err = mapstructure.Decode(populatedConf, config)
  559. if err != nil {
  560. return nil, err
  561. }
  562. if _, ok := resource.Config["waitForJob"]; !ok && d.source.Name == "job" {
  563. // default to true and wait for the job to finish
  564. config.WaitForJob = true
  565. }
  566. return config, nil
  567. }
  568. func existsInRepo(name, version, url string) (map[string]interface{}, error) {
  569. chart, err := GetAPIClient(config).GetTemplate(
  570. context.Background(),
  571. name, version,
  572. &types.GetTemplateRequest{
  573. TemplateGetBaseRequest: types.TemplateGetBaseRequest{
  574. RepoURL: url,
  575. },
  576. },
  577. )
  578. if err != nil {
  579. return nil, err
  580. }
  581. return chart.Values, nil
  582. }
  583. type DeploymentHook struct {
  584. client *api.Client
  585. resourceGroup *switchboardTypes.ResourceGroup
  586. gitInstallationID, projectID, clusterID, prID, actionID uint
  587. branchFrom, branchInto, namespace, repoName, repoOwner, prName, commitSHA string
  588. }
  589. func NewDeploymentHook(client *api.Client, resourceGroup *switchboardTypes.ResourceGroup, namespace string) (*DeploymentHook, error) {
  590. res := &DeploymentHook{
  591. client: client,
  592. resourceGroup: resourceGroup,
  593. namespace: namespace,
  594. }
  595. ghIDStr := os.Getenv("PORTER_GIT_INSTALLATION_ID")
  596. ghID, err := strconv.Atoi(ghIDStr)
  597. if err != nil {
  598. return nil, err
  599. }
  600. res.gitInstallationID = uint(ghID)
  601. prIDStr := os.Getenv("PORTER_PULL_REQUEST_ID")
  602. prID, err := strconv.Atoi(prIDStr)
  603. if err != nil {
  604. return nil, err
  605. }
  606. res.prID = uint(prID)
  607. res.projectID = config.Project
  608. if res.projectID == 0 {
  609. return nil, fmt.Errorf("project id must be set")
  610. }
  611. res.clusterID = config.Cluster
  612. if res.clusterID == 0 {
  613. return nil, fmt.Errorf("cluster id must be set")
  614. }
  615. branchFrom := os.Getenv("PORTER_BRANCH_FROM")
  616. res.branchFrom = branchFrom
  617. branchInto := os.Getenv("PORTER_BRANCH_INTO")
  618. res.branchInto = branchInto
  619. actionIDStr := os.Getenv("PORTER_ACTION_ID")
  620. actionID, err := strconv.Atoi(actionIDStr)
  621. if err != nil {
  622. return nil, err
  623. }
  624. res.actionID = uint(actionID)
  625. repoName := os.Getenv("PORTER_REPO_NAME")
  626. res.repoName = repoName
  627. repoOwner := os.Getenv("PORTER_REPO_OWNER")
  628. res.repoOwner = repoOwner
  629. prName := os.Getenv("PORTER_PR_NAME")
  630. res.prName = prName
  631. commit, err := git.LastCommit()
  632. if err != nil {
  633. return nil, fmt.Errorf(err.Error())
  634. }
  635. res.commitSHA = commit.Sha[:7]
  636. return res, nil
  637. }
  638. func (t *DeploymentHook) PreApply() error {
  639. // attempt to read the deployment -- if it doesn't exist, create it
  640. _, err := t.client.GetDeployment(
  641. context.Background(),
  642. t.projectID, t.gitInstallationID, t.clusterID,
  643. t.repoOwner, t.repoName,
  644. &types.GetDeploymentRequest{
  645. Namespace: t.namespace,
  646. },
  647. )
  648. // TODO: case this on the response status code rather than text
  649. if err != nil && strings.Contains(err.Error(), "deployment not found") {
  650. // in this case, create the deployment
  651. _, err = t.client.CreateDeployment(
  652. context.Background(),
  653. t.projectID, t.gitInstallationID, t.clusterID,
  654. t.repoOwner, t.repoName,
  655. &types.CreateDeploymentRequest{
  656. Namespace: t.namespace,
  657. PullRequestID: t.prID,
  658. CreateGHDeploymentRequest: &types.CreateGHDeploymentRequest{
  659. ActionID: t.actionID,
  660. },
  661. GitHubMetadata: &types.GitHubMetadata{
  662. PRName: t.prName,
  663. RepoName: t.repoName,
  664. RepoOwner: t.repoOwner,
  665. CommitSHA: t.commitSHA,
  666. PRBranchFrom: t.branchFrom,
  667. PRBranchInto: t.branchInto,
  668. },
  669. },
  670. )
  671. } else if err == nil {
  672. _, err = t.client.UpdateDeployment(
  673. context.Background(),
  674. t.projectID, t.gitInstallationID, t.clusterID,
  675. t.repoOwner, t.repoName,
  676. &types.UpdateDeploymentRequest{
  677. Namespace: t.namespace,
  678. CreateGHDeploymentRequest: &types.CreateGHDeploymentRequest{
  679. ActionID: t.actionID,
  680. },
  681. PRBranchFrom: t.branchFrom,
  682. CommitSHA: t.commitSHA,
  683. },
  684. )
  685. }
  686. return err
  687. }
  688. func (t *DeploymentHook) DataQueries() map[string]interface{} {
  689. res := make(map[string]interface{})
  690. // use the resource group to find all web applications that can have an exposed subdomain
  691. // that we can query for
  692. for _, resource := range t.resourceGroup.Resources {
  693. isWeb := false
  694. if sourceNameInter, exists := resource.Source["name"]; exists {
  695. if sourceName, ok := sourceNameInter.(string); ok {
  696. if sourceName == "web" {
  697. isWeb = true
  698. }
  699. }
  700. }
  701. if isWeb {
  702. res[resource.Name] = fmt.Sprintf("{ .%s.ingress.porter_hosts[0] }", resource.Name)
  703. }
  704. }
  705. return res
  706. }
  707. func (t *DeploymentHook) PostApply(populatedData map[string]interface{}) error {
  708. subdomains := make([]string, 0)
  709. for _, data := range populatedData {
  710. domain, ok := data.(string)
  711. if !ok {
  712. continue
  713. }
  714. if _, err := url.Parse("https://" + domain); err == nil {
  715. subdomains = append(subdomains, "https://"+domain)
  716. }
  717. }
  718. // finalize the deployment
  719. _, err := t.client.FinalizeDeployment(
  720. context.Background(),
  721. t.projectID, t.gitInstallationID, t.clusterID,
  722. t.repoOwner, t.repoName,
  723. &types.FinalizeDeploymentRequest{
  724. Namespace: t.namespace,
  725. Subdomain: strings.Join(subdomains, ","),
  726. },
  727. )
  728. return err
  729. }
  730. func (t *DeploymentHook) OnError(err error) {
  731. // if the deployment exists, throw an error for that deployment
  732. _, getDeplErr := t.client.GetDeployment(
  733. context.Background(),
  734. t.projectID, t.gitInstallationID, t.clusterID,
  735. t.repoOwner, t.repoName,
  736. &types.GetDeploymentRequest{
  737. Namespace: t.namespace,
  738. },
  739. )
  740. if getDeplErr == nil {
  741. _, err = t.client.UpdateDeploymentStatus(
  742. context.Background(),
  743. t.projectID, t.gitInstallationID, t.clusterID,
  744. t.repoOwner, t.repoName,
  745. &types.UpdateDeploymentStatusRequest{
  746. Namespace: t.namespace,
  747. CreateGHDeploymentRequest: &types.CreateGHDeploymentRequest{
  748. ActionID: t.actionID,
  749. },
  750. PRBranchFrom: t.branchFrom,
  751. Status: string(types.DeploymentStatusFailed),
  752. },
  753. )
  754. }
  755. }
  756. type CloneEnvGroupHook struct {
  757. client *api.Client
  758. resGroup *switchboardTypes.ResourceGroup
  759. }
  760. func NewCloneEnvGroupHook(client *api.Client, resourceGroup *switchboardTypes.ResourceGroup) *CloneEnvGroupHook {
  761. return &CloneEnvGroupHook{
  762. client: client,
  763. resGroup: resourceGroup,
  764. }
  765. }
  766. func (t *CloneEnvGroupHook) PreApply() error {
  767. for _, res := range t.resGroup.Resources {
  768. config := &ApplicationConfig{}
  769. err := mapstructure.Decode(res.Config, &config)
  770. if err != nil {
  771. continue
  772. }
  773. if config != nil && len(config.EnvGroups) > 0 {
  774. target := &Target{}
  775. err = getTarget(res.Target, target)
  776. if err != nil {
  777. return err
  778. }
  779. for _, group := range config.EnvGroups {
  780. if group.Name == "" {
  781. return fmt.Errorf("env group name cannot be empty")
  782. }
  783. _, err := t.client.GetEnvGroup(
  784. context.Background(),
  785. target.Project,
  786. target.Cluster,
  787. target.Namespace,
  788. &types.GetEnvGroupRequest{
  789. Name: group.Name,
  790. Version: group.Version,
  791. },
  792. )
  793. if err != nil && err.Error() == "env group not found" {
  794. if group.Namespace == "" {
  795. return fmt.Errorf("env group namespace cannot be empty")
  796. }
  797. color.New(color.FgBlue, color.Bold).
  798. Printf("Env group '%s' does not exist in the target namespace '%s'\n", group.Name, target.Namespace)
  799. color.New(color.FgBlue, color.Bold).
  800. Printf("Cloning env group '%s' from namespace '%s' to target namespace '%s'\n",
  801. group.Name, group.Namespace, target.Namespace)
  802. _, err = t.client.CloneEnvGroup(
  803. context.Background(), target.Project, target.Cluster, group.Namespace,
  804. &types.CloneEnvGroupRequest{
  805. Name: group.Name,
  806. Namespace: target.Namespace,
  807. },
  808. )
  809. if err != nil {
  810. return err
  811. }
  812. } else if err != nil {
  813. return err
  814. }
  815. }
  816. }
  817. }
  818. return nil
  819. }
  820. func (t *CloneEnvGroupHook) DataQueries() map[string]interface{} {
  821. return nil
  822. }
  823. func (t *CloneEnvGroupHook) PostApply(populatedData map[string]interface{}) error {
  824. return nil
  825. }
  826. func (t *CloneEnvGroupHook) OnError(err error) {}