apply.go 25 KB

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