apply.go 42 KB

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