apply.go 32 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268
  1. package cmd
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "io/ioutil"
  7. "net/url"
  8. "os"
  9. "path/filepath"
  10. "strconv"
  11. "strings"
  12. "github.com/cli/cli/git"
  13. "github.com/fatih/color"
  14. "github.com/mitchellh/mapstructure"
  15. api "github.com/porter-dev/porter/api/client"
  16. "github.com/porter-dev/porter/api/types"
  17. "github.com/porter-dev/porter/cli/cmd/config"
  18. "github.com/porter-dev/porter/cli/cmd/deploy"
  19. "github.com/porter-dev/porter/cli/cmd/deploy/wait"
  20. "github.com/porter-dev/porter/cli/cmd/preview"
  21. previewV2Beta1 "github.com/porter-dev/porter/cli/cmd/preview/v2beta1"
  22. previewInt "github.com/porter-dev/porter/internal/integrations/preview"
  23. "github.com/porter-dev/porter/internal/templater/utils"
  24. "github.com/porter-dev/switchboard/pkg/drivers"
  25. switchboardModels "github.com/porter-dev/switchboard/pkg/models"
  26. "github.com/porter-dev/switchboard/pkg/parser"
  27. switchboardTypes "github.com/porter-dev/switchboard/pkg/types"
  28. switchboardWorker "github.com/porter-dev/switchboard/pkg/worker"
  29. "github.com/rs/zerolog"
  30. "github.com/spf13/cobra"
  31. "gopkg.in/yaml.v2"
  32. )
  33. // applyCmd represents the "porter apply" base command when called
  34. // with a porter.yaml file as an argument
  35. var applyCmd = &cobra.Command{
  36. Use: "apply",
  37. Short: "Applies a configuration to an application",
  38. Long: fmt.Sprintf(`
  39. %s
  40. Applies a configuration to an application by either creating a new one or updating an existing
  41. one. For example:
  42. %s
  43. This command will apply the configuration contained in porter.yaml to the requested project and
  44. cluster either provided inside the porter.yaml file or through environment variables. Note that
  45. environment variables will always take precendence over values specified in the porter.yaml file.
  46. By default, this command expects to be run from a local git repository.
  47. The following are the environment variables that can be used to set certain values while
  48. applying a configuration:
  49. PORTER_CLUSTER Cluster ID that contains the project
  50. PORTER_PROJECT Project ID that contains the application
  51. PORTER_NAMESPACE The Kubernetes namespace that the application belongs to
  52. PORTER_SOURCE_NAME Name of the source Helm chart
  53. PORTER_SOURCE_REPO The URL of the Helm charts registry
  54. PORTER_SOURCE_VERSION The version of the Helm chart to use
  55. PORTER_TAG The Docker image tag to use (like the git commit hash)
  56. `,
  57. color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter apply\":"),
  58. color.New(color.FgGreen, color.Bold).Sprintf("porter apply -f porter.yaml"),
  59. ),
  60. Run: func(cmd *cobra.Command, args []string) {
  61. err := checkLoginAndRun(args, apply)
  62. if err != nil {
  63. if strings.Contains(err.Error(), "Forbidden") {
  64. color.New(color.FgRed).Fprintf(os.Stderr, "You may have to update your GitHub secret token")
  65. }
  66. os.Exit(1)
  67. }
  68. },
  69. }
  70. // applyValidateCmd represents the "porter apply validate" command when called
  71. // with a porter.yaml file as an argument
  72. var applyValidateCmd = &cobra.Command{
  73. Use: "validate",
  74. Short: "Validates a porter.yaml",
  75. Run: func(*cobra.Command, []string) {
  76. err := applyValidate()
  77. if err != nil {
  78. color.New(color.FgRed).Fprintf(os.Stderr, "Error: %s\n", err.Error())
  79. os.Exit(1)
  80. } else {
  81. color.New(color.FgGreen).Printf("The porter.yaml file is valid!\n")
  82. }
  83. },
  84. }
  85. var porterYAML string
  86. func init() {
  87. rootCmd.AddCommand(applyCmd)
  88. applyCmd.AddCommand(applyValidateCmd)
  89. applyCmd.PersistentFlags().StringVarP(&porterYAML, "file", "f", "", "path to porter.yaml")
  90. applyCmd.MarkFlagRequired("file")
  91. }
  92. func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string) error {
  93. fileBytes, err := ioutil.ReadFile(porterYAML)
  94. if err != nil {
  95. return fmt.Errorf("error reading porter.yaml: %w", err)
  96. }
  97. var previewVersion struct {
  98. Version string `json:"version"`
  99. }
  100. err = yaml.Unmarshal(fileBytes, &previewVersion)
  101. if err != nil {
  102. return fmt.Errorf("error unmarshaling porter.yaml: %w", err)
  103. }
  104. var resGroup *switchboardTypes.ResourceGroup
  105. if previewVersion.Version == "v2beta1" {
  106. ns := os.Getenv("PORTER_NAMESPACE")
  107. applier, err := previewV2Beta1.NewApplier(client, fileBytes, ns)
  108. if err != nil {
  109. return err
  110. }
  111. resGroup, err = applier.DowngradeToV1()
  112. if err != nil {
  113. return err
  114. }
  115. } else if previewVersion.Version == "v1" {
  116. if _, ok := os.LookupEnv("PORTER_VALIDATE_YAML"); ok {
  117. err := applyValidate()
  118. if err != nil {
  119. return err
  120. }
  121. }
  122. resGroup, err = parser.ParseRawBytes(fileBytes)
  123. if err != nil {
  124. return fmt.Errorf("error parsing porter.yaml: %w", err)
  125. }
  126. } else {
  127. return fmt.Errorf("unknown porter.yaml version: %s", previewVersion.Version)
  128. }
  129. basePath, err := os.Getwd()
  130. if err != nil {
  131. return fmt.Errorf("error getting working directory: %w", err)
  132. }
  133. worker := switchboardWorker.NewWorker()
  134. worker.RegisterDriver("deploy", NewDeployDriver)
  135. worker.RegisterDriver("build-image", preview.NewBuildDriver)
  136. worker.RegisterDriver("push-image", preview.NewPushDriver)
  137. worker.RegisterDriver("update-config", preview.NewUpdateConfigDriver)
  138. worker.RegisterDriver("random-string", preview.NewRandomStringDriver)
  139. worker.RegisterDriver("env-group", preview.NewEnvGroupDriver)
  140. worker.RegisterDriver("os-env", preview.NewOSEnvDriver)
  141. worker.SetDefaultDriver("deploy")
  142. if hasDeploymentHookEnvVars() {
  143. deplNamespace := os.Getenv("PORTER_NAMESPACE")
  144. if deplNamespace == "" {
  145. return fmt.Errorf("namespace must be set by PORTER_NAMESPACE")
  146. }
  147. deploymentHook, err := NewDeploymentHook(client, resGroup, deplNamespace)
  148. if err != nil {
  149. return fmt.Errorf("error creating deployment hook: %w", err)
  150. }
  151. worker.RegisterHook("deployment", deploymentHook)
  152. }
  153. cloneEnvGroupHook := NewCloneEnvGroupHook(client, resGroup)
  154. worker.RegisterHook("cloneenvgroup", cloneEnvGroupHook)
  155. return worker.Apply(resGroup, &switchboardTypes.ApplyOpts{
  156. BasePath: basePath,
  157. })
  158. }
  159. func applyValidate() error {
  160. fileBytes, err := ioutil.ReadFile(porterYAML)
  161. if err != nil {
  162. return fmt.Errorf("error reading porter.yaml: %w", err)
  163. }
  164. validationErrors := previewInt.Validate(string(fileBytes))
  165. if len(validationErrors) > 0 {
  166. errString := "the following error(s) were found while validating the porter.yaml file:"
  167. for _, err := range validationErrors {
  168. errString += "\n- " + strings.ReplaceAll(err.Error(), "\n\n*", "\n *")
  169. }
  170. return fmt.Errorf(errString)
  171. }
  172. return nil
  173. }
  174. func hasDeploymentHookEnvVars() bool {
  175. if ghIDStr := os.Getenv("PORTER_GIT_INSTALLATION_ID"); ghIDStr == "" {
  176. return false
  177. }
  178. if prIDStr := os.Getenv("PORTER_PULL_REQUEST_ID"); prIDStr == "" {
  179. return false
  180. }
  181. if branchFrom := os.Getenv("PORTER_BRANCH_FROM"); branchFrom == "" {
  182. return false
  183. }
  184. if branchInto := os.Getenv("PORTER_BRANCH_INTO"); branchInto == "" {
  185. return false
  186. }
  187. if actionIDStr := os.Getenv("PORTER_ACTION_ID"); actionIDStr == "" {
  188. return false
  189. }
  190. if repoName := os.Getenv("PORTER_REPO_NAME"); repoName == "" {
  191. return false
  192. }
  193. if repoOwner := os.Getenv("PORTER_REPO_OWNER"); repoOwner == "" {
  194. return false
  195. }
  196. if prName := os.Getenv("PORTER_PR_NAME"); prName == "" {
  197. return false
  198. }
  199. return true
  200. }
  201. type DeployDriver struct {
  202. source *previewInt.Source
  203. target *previewInt.Target
  204. output map[string]interface{}
  205. lookupTable *map[string]drivers.Driver
  206. logger *zerolog.Logger
  207. }
  208. func NewDeployDriver(resource *switchboardModels.Resource, opts *drivers.SharedDriverOpts) (drivers.Driver, error) {
  209. driver := &DeployDriver{
  210. lookupTable: opts.DriverLookupTable,
  211. logger: opts.Logger,
  212. output: make(map[string]interface{}),
  213. }
  214. target, err := preview.GetTarget(resource.Name, resource.Target)
  215. if err != nil {
  216. return nil, err
  217. }
  218. driver.target = target
  219. source, err := preview.GetSource(target.Project, resource.Name, resource.Source)
  220. if err != nil {
  221. return nil, err
  222. }
  223. driver.source = source
  224. return driver, nil
  225. }
  226. func (d *DeployDriver) ShouldApply(_ *switchboardModels.Resource) bool {
  227. return true
  228. }
  229. func (d *DeployDriver) Apply(resource *switchboardModels.Resource) (*switchboardModels.Resource, error) {
  230. client := config.GetAPIClient()
  231. _, err := client.GetRelease(
  232. context.Background(),
  233. d.target.Project,
  234. d.target.Cluster,
  235. d.target.Namespace,
  236. resource.Name,
  237. )
  238. shouldCreate := err != nil
  239. if err != nil {
  240. color.New(color.FgYellow).Printf("Could not read release %s/%s (%s): attempting creation\n", d.target.Namespace, resource.Name, err.Error())
  241. }
  242. if d.source.IsApplication {
  243. return d.applyApplication(resource, client, shouldCreate)
  244. }
  245. return d.applyAddon(resource, client, shouldCreate)
  246. }
  247. // Simple apply for addons
  248. func (d *DeployDriver) applyAddon(resource *switchboardModels.Resource, client *api.Client, shouldCreate bool) (*switchboardModels.Resource, error) {
  249. addonConfig, err := d.getAddonConfig(resource)
  250. if err != nil {
  251. return nil, fmt.Errorf("error getting addon config for resource %s: %w", resource.Name, err)
  252. }
  253. if shouldCreate {
  254. err := client.DeployAddon(
  255. context.Background(),
  256. d.target.Project,
  257. d.target.Cluster,
  258. d.target.Namespace,
  259. &types.CreateAddonRequest{
  260. CreateReleaseBaseRequest: &types.CreateReleaseBaseRequest{
  261. RepoURL: d.source.Repo,
  262. TemplateName: d.source.Name,
  263. TemplateVersion: d.source.Version,
  264. Values: addonConfig,
  265. Name: resource.Name,
  266. },
  267. },
  268. )
  269. if err != nil {
  270. return nil, fmt.Errorf("error creating addon from resource %s: %w", resource.Name, err)
  271. }
  272. } else {
  273. bytes, err := json.Marshal(addonConfig)
  274. if err != nil {
  275. return nil, fmt.Errorf("error marshalling addon config from resource %s: %w", resource.Name, err)
  276. }
  277. err = client.UpgradeRelease(
  278. context.Background(),
  279. d.target.Project,
  280. d.target.Cluster,
  281. d.target.Namespace,
  282. resource.Name,
  283. &types.UpgradeReleaseRequest{
  284. Values: string(bytes),
  285. },
  286. )
  287. if err != nil {
  288. return nil, fmt.Errorf("error updating addon from resource %s: %w", resource.Name, err)
  289. }
  290. }
  291. if err = d.assignOutput(resource, client); err != nil {
  292. return nil, err
  293. }
  294. return resource, nil
  295. }
  296. func (d *DeployDriver) applyApplication(resource *switchboardModels.Resource, client *api.Client, shouldCreate bool) (*switchboardModels.Resource, error) {
  297. if resource == nil {
  298. return nil, fmt.Errorf("nil resource")
  299. }
  300. resourceName := resource.Name
  301. appConfig, err := d.getApplicationConfig(resource)
  302. if err != nil {
  303. return nil, err
  304. }
  305. fullPath, err := filepath.Abs(appConfig.Build.Context)
  306. if err != nil {
  307. return nil, fmt.Errorf("for resource %s, error getting absolute path for config.build.context: %w", resourceName,
  308. err)
  309. }
  310. tag := os.Getenv("PORTER_TAG")
  311. if tag == "" {
  312. color.New(color.FgYellow).Printf("for resource %s, since PORTER_TAG is not set, the Docker image tag will default to"+
  313. " the git repo SHA\n", resourceName)
  314. commit, err := git.LastCommit()
  315. if err != nil {
  316. return nil, fmt.Errorf("for resource %s, error getting last git commit: %w", resourceName, err)
  317. }
  318. tag = commit.Sha[:7]
  319. color.New(color.FgYellow).Printf("for resource %s, using tag %s\n", resourceName, tag)
  320. }
  321. // if the method is registry and a tag is defined, we use the provided tag
  322. if appConfig.Build.Method == "registry" {
  323. imageSpl := strings.Split(appConfig.Build.Image, ":")
  324. if len(imageSpl) == 2 {
  325. tag = imageSpl[1]
  326. }
  327. if tag == "" {
  328. tag = "latest"
  329. }
  330. }
  331. sharedOpts := &deploy.SharedOpts{
  332. ProjectID: d.target.Project,
  333. ClusterID: d.target.Cluster,
  334. Namespace: d.target.Namespace,
  335. LocalPath: fullPath,
  336. LocalDockerfile: appConfig.Build.Dockerfile,
  337. OverrideTag: tag,
  338. Method: deploy.DeployBuildType(appConfig.Build.Method),
  339. EnvGroups: appConfig.EnvGroups,
  340. UseCache: appConfig.Build.UseCache,
  341. }
  342. if appConfig.Build.UseCache {
  343. // set the docker config so that pack caching can use the repo credentials
  344. err := config.SetDockerConfig(client)
  345. if err != nil {
  346. return nil, err
  347. }
  348. }
  349. if shouldCreate {
  350. resource, err = d.createApplication(resource, client, sharedOpts, appConfig)
  351. if err != nil {
  352. return nil, fmt.Errorf("error creating app from resource %s: %w", resourceName, err)
  353. }
  354. } else if !appConfig.OnlyCreate {
  355. resource, err = d.updateApplication(resource, client, sharedOpts, appConfig)
  356. if err != nil {
  357. return nil, fmt.Errorf("error updating application from resource %s: %w", resourceName, err)
  358. }
  359. } else {
  360. color.New(color.FgYellow).Printf("Skipping creation for resource %s as onlyCreate is set to true\n", resourceName)
  361. }
  362. if err = d.assignOutput(resource, client); err != nil {
  363. return nil, err
  364. }
  365. if d.source.Name == "job" && appConfig.WaitForJob && (shouldCreate || !appConfig.OnlyCreate) {
  366. color.New(color.FgYellow).Printf("Waiting for job '%s' to finish\n", resourceName)
  367. err = wait.WaitForJob(client, &wait.WaitOpts{
  368. ProjectID: d.target.Project,
  369. ClusterID: d.target.Cluster,
  370. Namespace: d.target.Namespace,
  371. Name: resourceName,
  372. })
  373. if err != nil && appConfig.OnlyCreate {
  374. deleteJobErr := client.DeleteRelease(
  375. context.Background(),
  376. d.target.Project,
  377. d.target.Cluster,
  378. d.target.Namespace,
  379. resourceName,
  380. )
  381. if deleteJobErr != nil {
  382. return nil, fmt.Errorf("error deleting job %s with waitForJob and onlyCreate set to true: %w",
  383. resourceName, deleteJobErr)
  384. }
  385. } else if err != nil {
  386. return nil, fmt.Errorf("error waiting for job %s: %w", resourceName, err)
  387. }
  388. }
  389. return resource, err
  390. }
  391. func (d *DeployDriver) createApplication(resource *switchboardModels.Resource, client *api.Client, sharedOpts *deploy.SharedOpts, appConf *previewInt.ApplicationConfig) (*switchboardModels.Resource, error) {
  392. // create new release
  393. color.New(color.FgGreen).Printf("Creating %s release: %s\n", d.source.Name, resource.Name)
  394. regList, err := client.ListRegistries(context.Background(), d.target.Project)
  395. if err != nil {
  396. return nil, fmt.Errorf("for resource %s, error listing registries: %w", resource.Name, err)
  397. }
  398. var registryURL string
  399. if len(*regList) == 0 {
  400. return nil, fmt.Errorf("no registry found")
  401. } else {
  402. registryURL = (*regList)[0].URL
  403. }
  404. color.New(color.FgBlue).Printf("for resource %s, using registry %s\n", resource.Name, registryURL)
  405. // attempt to get repo suffix from environment variables
  406. var repoSuffix string
  407. if repoName := os.Getenv("PORTER_REPO_NAME"); repoName != "" {
  408. if repoOwner := os.Getenv("PORTER_REPO_OWNER"); repoOwner != "" {
  409. repoSuffix = strings.ToLower(strings.ReplaceAll(fmt.Sprintf("%s-%s", repoOwner, repoName), "_", "-"))
  410. }
  411. }
  412. createAgent := &deploy.CreateAgent{
  413. Client: client,
  414. CreateOpts: &deploy.CreateOpts{
  415. SharedOpts: sharedOpts,
  416. Kind: d.source.Name,
  417. ReleaseName: resource.Name,
  418. RegistryURL: registryURL,
  419. RepoSuffix: repoSuffix,
  420. },
  421. }
  422. var buildConfig *types.BuildConfig
  423. if appConf.Build.Builder != "" {
  424. buildConfig = &types.BuildConfig{
  425. Builder: appConf.Build.Builder,
  426. Buildpacks: appConf.Build.Buildpacks,
  427. }
  428. }
  429. var subdomain string
  430. if appConf.Build.Method == "registry" {
  431. subdomain, err = createAgent.CreateFromRegistry(appConf.Build.Image, appConf.Values)
  432. } else {
  433. // if useCache is set, create the image repository first
  434. if appConf.Build.UseCache {
  435. regID, imageURL, err := createAgent.GetImageRepoURL(resource.Name, sharedOpts.Namespace)
  436. if err != nil {
  437. return nil, err
  438. }
  439. err = client.CreateRepository(
  440. context.Background(),
  441. sharedOpts.ProjectID,
  442. regID,
  443. &types.CreateRegistryRepositoryRequest{
  444. ImageRepoURI: imageURL,
  445. },
  446. )
  447. if err != nil {
  448. return nil, err
  449. }
  450. }
  451. subdomain, err = createAgent.CreateFromDocker(appConf.Values, sharedOpts.OverrideTag, buildConfig)
  452. }
  453. if err != nil {
  454. return nil, err
  455. }
  456. return resource, handleSubdomainCreate(subdomain, err)
  457. }
  458. func (d *DeployDriver) updateApplication(resource *switchboardModels.Resource, client *api.Client, sharedOpts *deploy.SharedOpts, appConf *previewInt.ApplicationConfig) (*switchboardModels.Resource, error) {
  459. color.New(color.FgGreen).Println("Updating existing release:", resource.Name)
  460. if len(appConf.Build.Env) > 0 {
  461. sharedOpts.AdditionalEnv = appConf.Build.Env
  462. }
  463. updateAgent, err := deploy.NewDeployAgent(client, resource.Name, &deploy.DeployOpts{
  464. SharedOpts: sharedOpts,
  465. Local: appConf.Build.Method != "registry",
  466. })
  467. if err != nil {
  468. return nil, err
  469. }
  470. // if the build method is registry, we do not trigger a build
  471. if appConf.Build.Method != "registry" {
  472. buildEnv, err := updateAgent.GetBuildEnv(&deploy.GetBuildEnvOpts{
  473. UseNewConfig: true,
  474. NewConfig: appConf.Values,
  475. })
  476. if err != nil {
  477. return nil, err
  478. }
  479. err = updateAgent.SetBuildEnv(buildEnv)
  480. if err != nil {
  481. return nil, err
  482. }
  483. var buildConfig *types.BuildConfig
  484. if appConf.Build.Builder != "" {
  485. buildConfig = &types.BuildConfig{
  486. Builder: appConf.Build.Builder,
  487. Buildpacks: appConf.Build.Buildpacks,
  488. }
  489. }
  490. err = updateAgent.Build(buildConfig)
  491. if err != nil {
  492. return nil, err
  493. }
  494. if !appConf.Build.UseCache {
  495. err = updateAgent.Push()
  496. if err != nil {
  497. return nil, err
  498. }
  499. }
  500. }
  501. err = updateAgent.UpdateImageAndValues(appConf.Values)
  502. if err != nil {
  503. return nil, err
  504. }
  505. return resource, nil
  506. }
  507. func (d *DeployDriver) assignOutput(resource *switchboardModels.Resource, client *api.Client) error {
  508. release, err := client.GetRelease(
  509. context.Background(),
  510. d.target.Project,
  511. d.target.Cluster,
  512. d.target.Namespace,
  513. resource.Name,
  514. )
  515. if err != nil {
  516. return err
  517. }
  518. d.output = utils.CoalesceValues(d.source.SourceValues, release.Config)
  519. return nil
  520. }
  521. func (d *DeployDriver) Output() (map[string]interface{}, error) {
  522. return d.output, nil
  523. }
  524. func (d *DeployDriver) getApplicationConfig(resource *switchboardModels.Resource) (*previewInt.ApplicationConfig, error) {
  525. populatedConf, err := drivers.ConstructConfig(&drivers.ConstructConfigOpts{
  526. RawConf: resource.Config,
  527. LookupTable: *d.lookupTable,
  528. Dependencies: resource.Dependencies,
  529. })
  530. if err != nil {
  531. return nil, err
  532. }
  533. appConf := &previewInt.ApplicationConfig{}
  534. err = mapstructure.Decode(populatedConf, appConf)
  535. if err != nil {
  536. return nil, err
  537. }
  538. if _, ok := resource.Config["waitForJob"]; !ok && d.source.Name == "job" {
  539. // default to true and wait for the job to finish
  540. appConf.WaitForJob = true
  541. }
  542. return appConf, nil
  543. }
  544. func (d *DeployDriver) getAddonConfig(resource *switchboardModels.Resource) (map[string]interface{}, error) {
  545. return drivers.ConstructConfig(&drivers.ConstructConfigOpts{
  546. RawConf: resource.Config,
  547. LookupTable: *d.lookupTable,
  548. Dependencies: resource.Dependencies,
  549. })
  550. }
  551. type DeploymentHook struct {
  552. client *api.Client
  553. resourceGroup *switchboardTypes.ResourceGroup
  554. gitInstallationID, projectID, clusterID, prID, actionID, envID uint
  555. branchFrom, branchInto, namespace, repoName, repoOwner, prName, commitSHA string
  556. }
  557. func NewDeploymentHook(client *api.Client, resourceGroup *switchboardTypes.ResourceGroup, namespace string) (*DeploymentHook, error) {
  558. res := &DeploymentHook{
  559. client: client,
  560. resourceGroup: resourceGroup,
  561. namespace: namespace,
  562. }
  563. ghIDStr := os.Getenv("PORTER_GIT_INSTALLATION_ID")
  564. ghID, err := strconv.Atoi(ghIDStr)
  565. if err != nil {
  566. return nil, err
  567. }
  568. res.gitInstallationID = uint(ghID)
  569. prIDStr := os.Getenv("PORTER_PULL_REQUEST_ID")
  570. prID, err := strconv.Atoi(prIDStr)
  571. if err != nil {
  572. return nil, err
  573. }
  574. res.prID = uint(prID)
  575. res.projectID = cliConf.Project
  576. if res.projectID == 0 {
  577. return nil, fmt.Errorf("project id must be set")
  578. }
  579. res.clusterID = cliConf.Cluster
  580. if res.clusterID == 0 {
  581. return nil, fmt.Errorf("cluster id must be set")
  582. }
  583. branchFrom := os.Getenv("PORTER_BRANCH_FROM")
  584. res.branchFrom = branchFrom
  585. branchInto := os.Getenv("PORTER_BRANCH_INTO")
  586. res.branchInto = branchInto
  587. actionIDStr := os.Getenv("PORTER_ACTION_ID")
  588. actionID, err := strconv.Atoi(actionIDStr)
  589. if err != nil {
  590. return nil, err
  591. }
  592. res.actionID = uint(actionID)
  593. repoName := os.Getenv("PORTER_REPO_NAME")
  594. res.repoName = repoName
  595. repoOwner := os.Getenv("PORTER_REPO_OWNER")
  596. res.repoOwner = repoOwner
  597. prName := os.Getenv("PORTER_PR_NAME")
  598. res.prName = prName
  599. commit, err := git.LastCommit()
  600. if err != nil {
  601. return nil, fmt.Errorf(err.Error())
  602. }
  603. res.commitSHA = commit.Sha[:7]
  604. return res, nil
  605. }
  606. func (t *DeploymentHook) isBranchDeploy() bool {
  607. return t.branchFrom != "" && t.branchInto != "" && t.branchFrom == t.branchInto
  608. }
  609. func (t *DeploymentHook) PreApply() error {
  610. if isSystemNamespace(t.namespace) {
  611. color.New(color.FgYellow).Printf("attempting to deploy to system namespace '%s'\n", t.namespace)
  612. }
  613. envList, err := t.client.ListEnvironments(
  614. context.Background(), t.projectID, t.clusterID,
  615. )
  616. if err != nil {
  617. return err
  618. }
  619. envs := *envList
  620. var deplEnv *types.Environment
  621. for _, env := range envs {
  622. if strings.EqualFold(env.GitRepoOwner, t.repoOwner) &&
  623. strings.EqualFold(env.GitRepoName, t.repoName) &&
  624. env.GitInstallationID == t.gitInstallationID {
  625. t.envID = env.ID
  626. deplEnv = env
  627. break
  628. }
  629. }
  630. if t.envID == 0 {
  631. return fmt.Errorf("could not find environment for deployment")
  632. }
  633. nsList, err := t.client.GetK8sNamespaces(
  634. context.Background(), t.projectID, t.clusterID,
  635. )
  636. if err != nil {
  637. return fmt.Errorf("error fetching namespaces: %w", err)
  638. }
  639. found := false
  640. for _, ns := range *nsList {
  641. if ns.Name == t.namespace {
  642. found = true
  643. break
  644. }
  645. }
  646. if !found {
  647. if isSystemNamespace(t.namespace) {
  648. return fmt.Errorf("attempting to deploy to system namespace '%s' which does not exist, please create it "+
  649. "to continue", t.namespace)
  650. }
  651. createNS := &types.CreateNamespaceRequest{
  652. Name: t.namespace,
  653. }
  654. if len(deplEnv.NamespaceLabels) > 0 {
  655. createNS.Labels = deplEnv.NamespaceLabels
  656. }
  657. // create the new namespace
  658. _, err := t.client.CreateNewK8sNamespace(context.Background(), t.projectID, t.clusterID, createNS)
  659. if err != nil && !strings.Contains(err.Error(), "namespace already exists") {
  660. // ignore the error if the namespace already exists
  661. //
  662. // this might happen if someone creates the namespace in between this operation
  663. return fmt.Errorf("error creating namespace: %w", err)
  664. }
  665. }
  666. var deplErr error
  667. if t.isBranchDeploy() {
  668. _, deplErr = t.client.GetDeployment(
  669. context.Background(),
  670. t.projectID, t.clusterID, t.envID,
  671. &types.GetDeploymentRequest{
  672. Branch: t.branchFrom,
  673. },
  674. )
  675. } else {
  676. _, deplErr = t.client.GetDeployment(
  677. context.Background(),
  678. t.projectID, t.clusterID, t.envID,
  679. &types.GetDeploymentRequest{
  680. PRNumber: t.prID,
  681. },
  682. )
  683. }
  684. if deplErr != nil && strings.Contains(deplErr.Error(), "not found") {
  685. // in this case, create the deployment
  686. createReq := &types.CreateDeploymentRequest{
  687. Namespace: t.namespace,
  688. PullRequestID: t.prID,
  689. CreateGHDeploymentRequest: &types.CreateGHDeploymentRequest{
  690. ActionID: t.actionID,
  691. },
  692. GitHubMetadata: &types.GitHubMetadata{
  693. PRName: t.prName,
  694. RepoName: t.repoName,
  695. RepoOwner: t.repoOwner,
  696. CommitSHA: t.commitSHA,
  697. PRBranchFrom: t.branchFrom,
  698. PRBranchInto: t.branchInto,
  699. },
  700. }
  701. if t.isBranchDeploy() {
  702. createReq.PullRequestID = 0
  703. }
  704. _, err = t.client.CreateDeployment(
  705. context.Background(),
  706. t.projectID, t.gitInstallationID, t.clusterID,
  707. t.repoOwner, t.repoName, createReq,
  708. )
  709. } else if err == nil {
  710. updateReq := &types.UpdateDeploymentRequest{
  711. Namespace: t.namespace,
  712. PRNumber: t.prID,
  713. CreateGHDeploymentRequest: &types.CreateGHDeploymentRequest{
  714. ActionID: t.actionID,
  715. },
  716. PRBranchFrom: t.branchFrom,
  717. CommitSHA: t.commitSHA,
  718. }
  719. if t.isBranchDeploy() {
  720. updateReq.PRNumber = 0
  721. }
  722. _, err = t.client.UpdateDeployment(
  723. context.Background(),
  724. t.projectID, t.gitInstallationID, t.clusterID,
  725. t.repoOwner, t.repoName, updateReq,
  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. // determine if we should query for porter_hosts or just hosts
  745. isCustomDomain := false
  746. ingressMap, err := deploy.GetNestedMap(resource.Config, "values", "ingress")
  747. if err == nil {
  748. enabledVal, enabledExists := ingressMap["enabled"]
  749. customDomVal, customDomExists := ingressMap["custom_domain"]
  750. if enabledExists && customDomExists {
  751. enabled, eOK := enabledVal.(bool)
  752. customDomain, cOK := customDomVal.(bool)
  753. if eOK && cOK && enabled {
  754. if customDomain {
  755. // return the first custom domain when one exists
  756. hostsArr, hostsExists := ingressMap["hosts"]
  757. if hostsExists {
  758. hostsArrVal, hostsArrOk := hostsArr.([]interface{})
  759. if hostsArrOk && len(hostsArrVal) > 0 {
  760. if _, ok := hostsArrVal[0].(string); ok {
  761. res[resource.Name] = fmt.Sprintf("{ .%s.ingress.hosts[0] }", resource.Name)
  762. isCustomDomain = true
  763. }
  764. }
  765. }
  766. }
  767. }
  768. }
  769. }
  770. if !isCustomDomain {
  771. res[resource.Name] = fmt.Sprintf("{ .%s.ingress.porter_hosts[0] }", resource.Name)
  772. }
  773. }
  774. }
  775. return res
  776. }
  777. func (t *DeploymentHook) PostApply(populatedData map[string]interface{}) error {
  778. subdomains := make([]string, 0)
  779. for _, data := range populatedData {
  780. domain, ok := data.(string)
  781. if !ok {
  782. continue
  783. }
  784. if _, err := url.Parse("https://" + domain); err == nil {
  785. subdomains = append(subdomains, "https://"+domain)
  786. }
  787. }
  788. req := &types.FinalizeDeploymentRequest{
  789. Subdomain: strings.Join(subdomains, ", "),
  790. }
  791. if t.isBranchDeploy() {
  792. req.Namespace = t.namespace
  793. } else {
  794. req.PRNumber = t.prID
  795. }
  796. for _, res := range t.resourceGroup.Resources {
  797. releaseType := getReleaseType(t.projectID, res)
  798. releaseName := getReleaseName(res)
  799. if releaseType != "" && releaseName != "" {
  800. req.SuccessfulResources = append(req.SuccessfulResources, &types.SuccessfullyDeployedResource{
  801. ReleaseName: releaseName,
  802. ReleaseType: releaseType,
  803. })
  804. }
  805. }
  806. // finalize the deployment
  807. _, err := t.client.FinalizeDeployment(
  808. context.Background(),
  809. t.projectID, t.gitInstallationID, t.clusterID,
  810. t.repoOwner, t.repoName, req,
  811. )
  812. return err
  813. }
  814. func (t *DeploymentHook) OnError(error) {
  815. var deplErr error
  816. if t.isBranchDeploy() {
  817. _, deplErr = t.client.GetDeployment(
  818. context.Background(),
  819. t.projectID, t.clusterID, t.envID,
  820. &types.GetDeploymentRequest{
  821. Branch: t.branchFrom,
  822. },
  823. )
  824. } else {
  825. _, deplErr = t.client.GetDeployment(
  826. context.Background(),
  827. t.projectID, t.clusterID, t.envID,
  828. &types.GetDeploymentRequest{
  829. PRNumber: t.prID,
  830. },
  831. )
  832. }
  833. // if the deployment exists, throw an error for that deployment
  834. if deplErr == nil {
  835. req := &types.UpdateDeploymentStatusRequest{
  836. CreateGHDeploymentRequest: &types.CreateGHDeploymentRequest{
  837. ActionID: t.actionID,
  838. },
  839. PRBranchFrom: t.branchFrom,
  840. Status: string(types.DeploymentStatusFailed),
  841. }
  842. if t.isBranchDeploy() {
  843. req.Namespace = t.namespace
  844. } else {
  845. req.PRNumber = t.prID
  846. }
  847. // FIXME: try to use the error with a custom logger
  848. t.client.UpdateDeploymentStatus(
  849. context.Background(),
  850. t.projectID, t.gitInstallationID, t.clusterID,
  851. t.repoOwner, t.repoName, req,
  852. )
  853. }
  854. }
  855. func (t *DeploymentHook) OnConsolidatedErrors(allErrors map[string]error) {
  856. var deplErr error
  857. if t.isBranchDeploy() {
  858. _, deplErr = t.client.GetDeployment(
  859. context.Background(),
  860. t.projectID, t.clusterID, t.envID,
  861. &types.GetDeploymentRequest{
  862. Branch: t.branchFrom,
  863. },
  864. )
  865. } else {
  866. _, deplErr = t.client.GetDeployment(
  867. context.Background(),
  868. t.projectID, t.clusterID, t.envID,
  869. &types.GetDeploymentRequest{
  870. PRNumber: t.prID,
  871. },
  872. )
  873. }
  874. // if the deployment exists, throw an error for that deployment
  875. if deplErr == nil {
  876. req := &types.FinalizeDeploymentWithErrorsRequest{
  877. Errors: make(map[string]string),
  878. }
  879. if t.isBranchDeploy() {
  880. req.Namespace = t.namespace
  881. } else {
  882. req.PRNumber = t.prID
  883. }
  884. for _, res := range t.resourceGroup.Resources {
  885. if _, ok := allErrors[res.Name]; !ok {
  886. req.SuccessfulResources = append(req.SuccessfulResources, &types.SuccessfullyDeployedResource{
  887. ReleaseName: getReleaseName(res),
  888. ReleaseType: getReleaseType(t.projectID, res),
  889. })
  890. }
  891. }
  892. for res, err := range allErrors {
  893. req.Errors[res] = err.Error()
  894. }
  895. // FIXME: handle the error
  896. t.client.FinalizeDeploymentWithErrors(
  897. context.Background(),
  898. t.projectID, t.gitInstallationID, t.clusterID,
  899. t.repoOwner, t.repoName,
  900. req,
  901. )
  902. }
  903. }
  904. type CloneEnvGroupHook struct {
  905. client *api.Client
  906. resGroup *switchboardTypes.ResourceGroup
  907. }
  908. func NewCloneEnvGroupHook(client *api.Client, resourceGroup *switchboardTypes.ResourceGroup) *CloneEnvGroupHook {
  909. return &CloneEnvGroupHook{
  910. client: client,
  911. resGroup: resourceGroup,
  912. }
  913. }
  914. func (t *CloneEnvGroupHook) PreApply() error {
  915. for _, res := range t.resGroup.Resources {
  916. if res.Driver == "env-group" {
  917. continue
  918. }
  919. appConf := &previewInt.ApplicationConfig{}
  920. err := mapstructure.Decode(res.Config, &appConf)
  921. if err != nil {
  922. continue
  923. }
  924. if appConf != nil && len(appConf.EnvGroups) > 0 {
  925. target, err := preview.GetTarget(res.Name, res.Target)
  926. if err != nil {
  927. return err
  928. }
  929. for _, group := range appConf.EnvGroups {
  930. if group.Name == "" {
  931. return fmt.Errorf("env group name cannot be empty")
  932. }
  933. _, err := t.client.GetEnvGroup(
  934. context.Background(),
  935. target.Project,
  936. target.Cluster,
  937. target.Namespace,
  938. &types.GetEnvGroupRequest{
  939. Name: group.Name,
  940. Version: group.Version,
  941. },
  942. )
  943. if err != nil && err.Error() == "env group not found" {
  944. if group.Namespace == "" {
  945. return fmt.Errorf("env group namespace cannot be empty")
  946. }
  947. color.New(color.FgBlue, color.Bold).
  948. Printf("Env group '%s' does not exist in the target namespace '%s'\n", group.Name, target.Namespace)
  949. color.New(color.FgBlue, color.Bold).
  950. Printf("Cloning env group '%s' from namespace '%s' to target namespace '%s'\n",
  951. group.Name, group.Namespace, target.Namespace)
  952. _, err = t.client.CloneEnvGroup(
  953. context.Background(), target.Project, target.Cluster, group.Namespace,
  954. &types.CloneEnvGroupRequest{
  955. SourceName: group.Name,
  956. TargetNamespace: target.Namespace,
  957. },
  958. )
  959. if err != nil {
  960. return err
  961. }
  962. } else if err != nil {
  963. return err
  964. }
  965. }
  966. }
  967. }
  968. return nil
  969. }
  970. func (t *CloneEnvGroupHook) DataQueries() map[string]interface{} {
  971. return nil
  972. }
  973. func (t *CloneEnvGroupHook) PostApply(map[string]interface{}) error {
  974. return nil
  975. }
  976. func (t *CloneEnvGroupHook) OnError(error) {}
  977. func (t *CloneEnvGroupHook) OnConsolidatedErrors(map[string]error) {}
  978. func getReleaseName(res *switchboardTypes.Resource) string {
  979. // can ignore the error because this method is called once
  980. // GetTarget has alrealy been called and validated previously
  981. target, _ := preview.GetTarget(res.Name, res.Target)
  982. if target.AppName != "" {
  983. return target.AppName
  984. }
  985. return res.Name
  986. }
  987. func getReleaseType(projectID uint, res *switchboardTypes.Resource) string {
  988. // can ignore the error because this method is called once
  989. // GetSource has alrealy been called and validated previously
  990. source, _ := preview.GetSource(projectID, res.Name, res.Source)
  991. if source != nil && source.Name != "" {
  992. return source.Name
  993. }
  994. return ""
  995. }
  996. func isSystemNamespace(namespace string) bool {
  997. return namespace == "cert-manager" || namespace == "ingress-nginx" ||
  998. namespace == "kube-node-lease" || namespace == "kube-public" ||
  999. namespace == "kube-system" || namespace == "monitoring" ||
  1000. namespace == "porter-agent-system" || namespace == "default" ||
  1001. namespace == "ingress-nginx-private"
  1002. }