apply.go 23 KB

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