apply.go 37 KB

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