apply.go 36 KB

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