update.go 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212
  1. package commands
  2. import (
  3. "context"
  4. "fmt"
  5. "os"
  6. "path/filepath"
  7. "sort"
  8. "strings"
  9. "time"
  10. v2 "github.com/porter-dev/porter/cli/cmd/v2"
  11. "github.com/briandowns/spinner"
  12. "github.com/fatih/color"
  13. api "github.com/porter-dev/porter/api/client"
  14. "github.com/porter-dev/porter/api/types"
  15. "github.com/porter-dev/porter/cli/cmd/config"
  16. "github.com/porter-dev/porter/cli/cmd/deploy"
  17. "github.com/porter-dev/porter/cli/cmd/docker"
  18. "github.com/porter-dev/porter/cli/cmd/utils"
  19. templaterUtils "github.com/porter-dev/porter/internal/templater/utils"
  20. "github.com/spf13/cobra"
  21. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  22. "k8s.io/client-go/util/homedir"
  23. )
  24. var (
  25. app string
  26. getEnvFileDest string
  27. localPath string
  28. tag string
  29. dockerfile string
  30. method string
  31. stream bool
  32. buildFlagsEnv []string
  33. forcePush bool
  34. useCache bool
  35. version uint
  36. varType string
  37. normalEnvGroupVars []string
  38. secretEnvGroupVars []string
  39. waitForSuccessfulDeploy bool
  40. )
  41. func registerCommand_Update(cliConf config.CLIConfig) *cobra.Command {
  42. buildFlagsEnv = []string{}
  43. // updateCmd represents the "porter update" base command when called
  44. // without any subcommands
  45. updateCmd := &cobra.Command{
  46. Use: "update",
  47. Short: "Builds and updates a specified application given by the --app flag.",
  48. Long: fmt.Sprintf(`
  49. %s
  50. Builds and updates a specified application given by the --app flag. For example:
  51. %s
  52. This command will automatically build from a local path. The path can be configured via the
  53. --path flag. You can also overwrite the tag using the --tag flag. For example, to build from the
  54. local directory ~/path-to-dir with the tag "testing":
  55. %s
  56. If the application has a remote Git repository source configured, you can specify that the remote
  57. Git repository should be used to build the new image by specifying "--source github". Porter will use
  58. the latest commit from the remote repo and branch to update an application, and will use the latest
  59. commit as the image tag.
  60. %s
  61. To add new configuration or update existing configuration, you can pass a values.yaml file in via the
  62. --values flag. For example;
  63. %s
  64. If your application is set up to use a Dockerfile by default, you can use a buildpack via the flag
  65. "--method pack". Conversely, if your application is set up to use a buildpack by default, you can
  66. use a Dockerfile by passing the flag "--method docker". You can specify the relative path to a Dockerfile
  67. in your remote Git repository. For example, if a Dockerfile is found at ./docker/prod.Dockerfile, you can
  68. specify it as follows:
  69. %s
  70. `,
  71. color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter update\":"),
  72. color.New(color.FgGreen, color.Bold).Sprintf("porter update --app example-app"),
  73. color.New(color.FgGreen, color.Bold).Sprintf("porter update --app example-app --path ~/path-to-dir --tag testing"),
  74. color.New(color.FgGreen, color.Bold).Sprintf("porter update --app remote-git-app --source github"),
  75. color.New(color.FgGreen, color.Bold).Sprintf("porter update --app example-app --values my-values.yaml"),
  76. color.New(color.FgGreen, color.Bold).Sprintf("porter update --app example-app --method docker --dockerfile ./docker/prod.Dockerfile"),
  77. ),
  78. Run: func(cmd *cobra.Command, args []string) {
  79. err := checkLoginAndRunWithConfig(cmd, cliConf, args, updateFull)
  80. if err != nil {
  81. os.Exit(1)
  82. }
  83. },
  84. }
  85. updateGetEnvCmd := &cobra.Command{
  86. Use: "get-env",
  87. Short: "Gets environment variables for a deployment for a specified application given by the --app flag.",
  88. Long: fmt.Sprintf(`
  89. %s
  90. Gets environment variables for a deployment for a specified application given by the --app
  91. flag. By default, env variables are printed via stdout for use in downstream commands:
  92. %s
  93. Output can also be written to a file via the --file flag, which should specify the
  94. destination path for a .env file. For example:
  95. %s
  96. `,
  97. color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter update get-env\":"),
  98. color.New(color.FgGreen, color.Bold).Sprintf("porter update get-env --app example-app | xargs"),
  99. color.New(color.FgGreen, color.Bold).Sprintf("porter update get-env --app example-app --file .env"),
  100. ),
  101. Run: func(cmd *cobra.Command, args []string) {
  102. err := checkLoginAndRunWithConfig(cmd, cliConf, args, updateGetEnv)
  103. if err != nil {
  104. os.Exit(1)
  105. }
  106. },
  107. }
  108. updateBuildCmd := &cobra.Command{
  109. Use: "build",
  110. Short: "Builds a new version of the application specified by the --app flag.",
  111. Long: fmt.Sprintf(`
  112. %s
  113. Builds a new version of the application specified by the --app flag. Depending on the
  114. configured settings, this command may work automatically or will require a specified
  115. --method flag.
  116. If you have configured the Dockerfile path and/or a build context for this application,
  117. this command will by default use those settings, so you just need to specify the --app
  118. flag:
  119. %s
  120. If you have not linked the build-time requirements for this application, the command will
  121. use a local build. By default, the cloud-native buildpacks builder will automatically be run
  122. from the current directory. If you would like to change the build method, you can do so by
  123. using the --method flag, for example:
  124. %s
  125. When using "--method docker", you can specify the path to the Dockerfile using the
  126. --dockerfile flag. This will also override the Dockerfile path that you may have linked
  127. for the application:
  128. %s
  129. `,
  130. color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter update build\":"),
  131. color.New(color.FgGreen, color.Bold).Sprintf("porter update build --app example-app"),
  132. color.New(color.FgGreen, color.Bold).Sprintf("porter update build --app example-app --method docker"),
  133. color.New(color.FgGreen, color.Bold).Sprintf("porter update build --app example-app --method docker --dockerfile ./prod.Dockerfile"),
  134. ),
  135. Run: func(cmd *cobra.Command, args []string) {
  136. err := checkLoginAndRunWithConfig(cmd, cliConf, args, updateBuild)
  137. if err != nil {
  138. os.Exit(1)
  139. }
  140. },
  141. }
  142. updatePushCmd := &cobra.Command{
  143. Use: "push",
  144. Short: "Pushes an image to a Docker registry linked to your Porter project.",
  145. Args: cobra.MaximumNArgs(1),
  146. Long: fmt.Sprintf(`
  147. %s
  148. Pushes a local Docker image to a registry linked to your Porter project. This command
  149. requires the project ID to be set either by using the %s command
  150. or the --project flag. For example, to push a local nginx image:
  151. %s
  152. %s
  153. Pushes a new image for an application specified by the --app flag. This command uses
  154. the image repository saved in the application config by default. For example, if an
  155. application "nginx" was created from the image repo "gcr.io/snowflake-123456/nginx",
  156. the following command would push the image "gcr.io/snowflake-123456/nginx:new-tag":
  157. %s
  158. This command will not use your pre-saved authentication set up via "docker login," so if you
  159. are using an image registry that was created outside of Porter, make sure that you have
  160. linked it via "porter connect".
  161. `,
  162. color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter update push\":"),
  163. color.New(color.FgBlue).Sprintf("porter config set-project"),
  164. color.New(color.FgGreen, color.Bold).Sprintf("porter update push gcr.io/snowflake-123456/nginx:1234567"),
  165. color.New(color.Bold).Sprintf("LEGACY USAGE:"),
  166. color.New(color.FgGreen, color.Bold).Sprintf("porter update push --app nginx --tag new-tag"),
  167. ),
  168. Run: func(cmd *cobra.Command, args []string) {
  169. err := checkLoginAndRunWithConfig(cmd, cliConf, args, updatePush)
  170. if err != nil {
  171. os.Exit(1)
  172. }
  173. },
  174. }
  175. updateConfigCmd := &cobra.Command{
  176. Use: "config",
  177. Short: "Updates the configuration for an application specified by the --app flag.",
  178. Long: fmt.Sprintf(`
  179. %s
  180. Updates the configuration for an application specified by the --app flag, using the configuration
  181. given by the --values flag. This will trigger a new deployment for the application with
  182. new configuration set. Note that this will merge your existing configuration with configuration
  183. specified in the --values file. For example:
  184. %s
  185. You can update the configuration with only a new tag with the --tag flag, which will only update
  186. the image that the application uses if no --values file is specified:
  187. %s
  188. `,
  189. color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter update config\":"),
  190. color.New(color.FgGreen, color.Bold).Sprintf("porter update config --app example-app --values my-values.yaml"),
  191. color.New(color.FgGreen, color.Bold).Sprintf("porter update config --app example-app --tag custom-tag"),
  192. ),
  193. Run: func(cmd *cobra.Command, args []string) {
  194. err := checkLoginAndRunWithConfig(cmd, cliConf, args, updateUpgrade)
  195. if err != nil {
  196. os.Exit(1)
  197. }
  198. },
  199. }
  200. updateEnvGroupCmd := &cobra.Command{
  201. Use: "env-group",
  202. Aliases: []string{"eg", "envgroup", "env-groups", "envgroups"},
  203. Short: "Updates an environment group's variables, specified by the --name flag.",
  204. Run: func(cmd *cobra.Command, args []string) {
  205. _, _ = color.New(color.FgRed).Fprintln(os.Stderr, "need to specify an operation to continue")
  206. },
  207. }
  208. updateSetEnvGroupCmd := &cobra.Command{
  209. Use: "set",
  210. Short: "Sets the desired value of an environment variable in an env group in the form VAR=VALUE.",
  211. Args: cobra.MaximumNArgs(1),
  212. Run: func(cmd *cobra.Command, args []string) {
  213. err := checkLoginAndRunWithConfig(cmd, cliConf, args, updateSetEnvGroup)
  214. if err != nil {
  215. os.Exit(1)
  216. }
  217. },
  218. }
  219. updateUnsetEnvGroupCmd := &cobra.Command{
  220. Use: "unset",
  221. Short: "Removes an environment variable from an env group.",
  222. Args: cobra.MinimumNArgs(1),
  223. Run: func(cmd *cobra.Command, args []string) {
  224. err := checkLoginAndRunWithConfig(cmd, cliConf, args, updateUnsetEnvGroup)
  225. if err != nil {
  226. os.Exit(1)
  227. }
  228. },
  229. }
  230. updateCmd.PersistentFlags().StringVar(
  231. &app,
  232. "app",
  233. "",
  234. "Application in the Porter dashboard",
  235. )
  236. updateCmd.PersistentFlags().BoolVar(
  237. &useCache,
  238. "use-cache",
  239. false,
  240. "Whether to use cache (currently in beta)",
  241. )
  242. updateCmd.PersistentFlags().StringVar(
  243. &namespace,
  244. "namespace",
  245. "default",
  246. "Namespace of the application",
  247. )
  248. updateCmd.PersistentFlags().StringVar(
  249. &source,
  250. "source",
  251. "local",
  252. "the type of source (\"local\" or \"github\")",
  253. )
  254. updateCmd.PersistentFlags().StringVarP(
  255. &localPath,
  256. "path",
  257. "p",
  258. "",
  259. "If local build, the path to the build directory. If remote build, the relative path from the repository root to the build directory.",
  260. )
  261. updateCmd.PersistentFlags().StringVarP(
  262. &tag,
  263. "tag",
  264. "t",
  265. "",
  266. "the specified tag to use, if not \"latest\"",
  267. )
  268. updateCmd.PersistentFlags().StringVarP(
  269. &values,
  270. "values",
  271. "v",
  272. "",
  273. "Filepath to a values.yaml file",
  274. )
  275. updateCmd.PersistentFlags().StringVar(
  276. &dockerfile,
  277. "dockerfile",
  278. "",
  279. "the path to the dockerfile",
  280. )
  281. updateCmd.PersistentFlags().StringArrayVarP(
  282. &buildFlagsEnv,
  283. "env",
  284. "e",
  285. []string{},
  286. "Build-time environment variable, in the form 'VAR=VALUE'. These are not available at image runtime.",
  287. )
  288. updateCmd.PersistentFlags().StringVar(
  289. &method,
  290. "method",
  291. "",
  292. "the build method to use (\"docker\" or \"pack\")",
  293. )
  294. updateCmd.PersistentFlags().BoolVar(
  295. &stream,
  296. "stream",
  297. false,
  298. "stream update logs to porter dashboard",
  299. )
  300. updateCmd.PersistentFlags().BoolVar(
  301. &forceBuild,
  302. "force-build",
  303. false,
  304. "set this to force build an image (images tagged with \"latest\" have this set by default)",
  305. )
  306. updateCmd.PersistentFlags().BoolVar(
  307. &forcePush,
  308. "force-push",
  309. false,
  310. "set this to force push an image (images tagged with \"latest\" have this set by default)",
  311. )
  312. updateCmd.PersistentFlags().MarkDeprecated("force-build", "--force-build is now deprecated")
  313. updateCmd.PersistentFlags().MarkDeprecated("force-push", "--force-push is now deprecated")
  314. updateCmd.PersistentFlags().BoolVar(
  315. &waitForSuccessfulDeploy,
  316. "wait",
  317. false,
  318. "set this to wait and be notified when a deployment is successful, otherwise time out",
  319. )
  320. updateCmd.AddCommand(updateGetEnvCmd)
  321. updateGetEnvCmd.PersistentFlags().StringVar(
  322. &getEnvFileDest,
  323. "file",
  324. "",
  325. "file destination for .env files",
  326. )
  327. updateGetEnvCmd.MarkPersistentFlagRequired("app")
  328. updateBuildCmd.MarkPersistentFlagRequired("app")
  329. updateConfigCmd.MarkPersistentFlagRequired("app")
  330. updateEnvGroupCmd.PersistentFlags().StringVar(
  331. &name,
  332. "name",
  333. "",
  334. "the name of the environment group",
  335. )
  336. updateEnvGroupCmd.PersistentFlags().UintVar(
  337. &version,
  338. "version",
  339. 0,
  340. "the version of the environment group",
  341. )
  342. updateEnvGroupCmd.MarkPersistentFlagRequired("name")
  343. updateSetEnvGroupCmd.PersistentFlags().StringVar(
  344. &varType,
  345. "type",
  346. "normal",
  347. "the type of environment variable (either \"normal\" or \"secret\")",
  348. )
  349. updateSetEnvGroupCmd.PersistentFlags().StringArrayVarP(
  350. &normalEnvGroupVars,
  351. "normal",
  352. "n",
  353. []string{},
  354. "list of variables to set, in the form VAR=VALUE",
  355. )
  356. updateSetEnvGroupCmd.PersistentFlags().StringArrayVarP(
  357. &secretEnvGroupVars,
  358. "secret",
  359. "s",
  360. []string{},
  361. "list of secret variables to set, in the form VAR=VALUE",
  362. )
  363. updateEnvGroupCmd.AddCommand(updateSetEnvGroupCmd)
  364. updateEnvGroupCmd.AddCommand(updateUnsetEnvGroupCmd)
  365. updateCmd.AddCommand(updateBuildCmd)
  366. updateCmd.AddCommand(updatePushCmd)
  367. updateCmd.AddCommand(updateConfigCmd)
  368. updateCmd.AddCommand(updateEnvGroupCmd)
  369. return updateCmd
  370. }
  371. func updateFull(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, cmd *cobra.Command, args []string) error {
  372. if featureFlags.ValidateApplyV2Enabled {
  373. err := v2.UpdateFull(ctx, cliConf, client, app)
  374. if err != nil {
  375. return err
  376. }
  377. return nil
  378. }
  379. fullPath, err := filepath.Abs(localPath)
  380. if err != nil {
  381. return err
  382. }
  383. if os.Getenv("GITHUB_ACTIONS") == "" && source == "local" && fullPath == homedir.HomeDir() {
  384. proceed, err := utils.PromptConfirm("You are deploying your home directory. Do you want to continue?", false)
  385. if err != nil {
  386. return err
  387. }
  388. if !proceed {
  389. return nil
  390. }
  391. }
  392. color.New(color.FgGreen).Println("Deploying app:", app)
  393. updateAgent, err := updateGetAgent(ctx, client, cliConf)
  394. if err != nil {
  395. return err
  396. }
  397. err = updateBuildWithAgent(ctx, updateAgent)
  398. if err != nil {
  399. return err
  400. }
  401. err = updatePushWithAgent(ctx, updateAgent)
  402. if err != nil {
  403. return err
  404. }
  405. err = updateUpgradeWithAgent(ctx, updateAgent)
  406. if err != nil {
  407. return err
  408. }
  409. if waitForSuccessfulDeploy {
  410. // solves timing issue where replicasets were not on the cluster, before our initial check
  411. time.Sleep(10 * time.Second)
  412. err := checkDeploymentStatus(ctx, client, cliConf)
  413. if err != nil {
  414. return err
  415. }
  416. }
  417. return nil
  418. }
  419. func updateGetEnv(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, cmd *cobra.Command, args []string) error {
  420. updateAgent, err := updateGetAgent(ctx, client, cliConf)
  421. if err != nil {
  422. return err
  423. }
  424. buildEnv, err := updateAgent.GetBuildEnv(ctx, &deploy.GetBuildEnvOpts{
  425. UseNewConfig: false,
  426. })
  427. if err != nil {
  428. return err
  429. }
  430. // set the environment variables in the process
  431. err = updateAgent.SetBuildEnv(buildEnv)
  432. if err != nil {
  433. return err
  434. }
  435. // write the environment variables to either a file or stdout (stdout by default)
  436. return updateAgent.WriteBuildEnv(getEnvFileDest)
  437. }
  438. func updateBuild(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, cmd *cobra.Command, args []string) error {
  439. if featureFlags.ValidateApplyV2Enabled {
  440. err := v2.UpdateBuild(ctx)
  441. if err != nil {
  442. return err
  443. }
  444. return nil
  445. }
  446. updateAgent, err := updateGetAgent(ctx, client, cliConf)
  447. if err != nil {
  448. return err
  449. }
  450. return updateBuildWithAgent(ctx, updateAgent)
  451. }
  452. func updatePush(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, cmd *cobra.Command, args []string) error {
  453. if app == "" {
  454. if len(args) == 0 {
  455. return fmt.Errorf("please provide the docker image name")
  456. }
  457. image := args[0]
  458. registries, err := client.ListRegistries(ctx, cliConf.Project)
  459. if err != nil {
  460. return err
  461. }
  462. regs := *registries
  463. regID := uint(0)
  464. for _, reg := range regs {
  465. if strings.Contains(image, reg.URL) {
  466. regID = reg.ID
  467. break
  468. }
  469. }
  470. if regID == 0 {
  471. return fmt.Errorf("could not find registry for image: %s", image)
  472. }
  473. err = client.CreateRepository(ctx, cliConf.Project, regID,
  474. &types.CreateRegistryRepositoryRequest{
  475. ImageRepoURI: strings.Split(image, ":")[0],
  476. },
  477. )
  478. if err != nil {
  479. return err
  480. }
  481. agent, err := docker.NewAgentWithAuthGetter(ctx, client, cliConf.Project)
  482. if err != nil {
  483. return err
  484. }
  485. err = agent.PushImage(ctx, image)
  486. if err != nil {
  487. return err
  488. }
  489. return nil
  490. }
  491. updateAgent, err := updateGetAgent(ctx, client, cliConf)
  492. if err != nil {
  493. return err
  494. }
  495. return updatePushWithAgent(ctx, updateAgent)
  496. }
  497. func updateUpgrade(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, cmd *cobra.Command, args []string) error {
  498. if featureFlags.ValidateApplyV2Enabled {
  499. err := v2.UpdateUpgrade(ctx)
  500. if err != nil {
  501. return err
  502. }
  503. return nil
  504. }
  505. updateAgent, err := updateGetAgent(ctx, client, cliConf)
  506. if err != nil {
  507. return err
  508. }
  509. err = updateUpgradeWithAgent(ctx, updateAgent)
  510. if err != nil {
  511. return err
  512. }
  513. if waitForSuccessfulDeploy {
  514. // solves timing issue where replicasets were not on the cluster, before our initial check
  515. time.Sleep(10 * time.Second)
  516. err := checkDeploymentStatus(ctx, client, cliConf)
  517. if err != nil {
  518. return err
  519. }
  520. }
  521. return nil
  522. }
  523. func updateSetEnvGroup(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, cmd *cobra.Command, args []string) error {
  524. if len(normalEnvGroupVars) == 0 && len(secretEnvGroupVars) == 0 && len(args) == 0 {
  525. return fmt.Errorf("please provide one or more variables to update")
  526. }
  527. s := spinner.New(spinner.CharSets[9], 100*time.Millisecond)
  528. s.Color("cyan")
  529. s.Suffix = fmt.Sprintf(" Fetching env group '%s' in namespace '%s'", name, namespace)
  530. s.Start()
  531. envGroupResp, err := client.GetEnvGroup(ctx, cliConf.Project, cliConf.Cluster, namespace,
  532. &types.GetEnvGroupRequest{
  533. Name: name, Version: version,
  534. },
  535. )
  536. s.Stop()
  537. if err != nil {
  538. return err
  539. }
  540. newEnvGroup := &types.CreateEnvGroupRequest{
  541. Name: envGroupResp.Name,
  542. Variables: make(map[string]string),
  543. }
  544. for k, v := range envGroupResp.Variables {
  545. newEnvGroup.Variables[k] = v
  546. }
  547. // first check for multiple variables being set using the -e or -s flags
  548. if len(normalEnvGroupVars) > 0 || len(secretEnvGroupVars) > 0 {
  549. for _, v := range normalEnvGroupVars {
  550. delete(newEnvGroup.Variables, v)
  551. key, value, err := validateVarValue(v)
  552. if err != nil {
  553. return err
  554. }
  555. newEnvGroup.Variables[key] = value
  556. }
  557. if len(secretEnvGroupVars) > 0 {
  558. newEnvGroup.SecretVariables = make(map[string]string)
  559. }
  560. for _, v := range secretEnvGroupVars {
  561. delete(newEnvGroup.Variables, v)
  562. key, value, err := validateVarValue(v)
  563. if err != nil {
  564. return err
  565. }
  566. newEnvGroup.SecretVariables[key] = value
  567. }
  568. s.Suffix = fmt.Sprintf(" Updating env group '%s' in namespace '%s'", name, namespace)
  569. } else { // legacy usage
  570. key, value, err := validateVarValue(args[0])
  571. if err != nil {
  572. return err
  573. }
  574. delete(newEnvGroup.Variables, key)
  575. if varType == "secret" {
  576. newEnvGroup.SecretVariables = make(map[string]string)
  577. newEnvGroup.SecretVariables[key] = value
  578. s.Suffix = fmt.Sprintf(" Adding new secret variable '%s' to env group '%s' in namespace '%s'", key, name, namespace)
  579. } else {
  580. newEnvGroup.Variables[key] = value
  581. s.Suffix = fmt.Sprintf(" Adding new variable '%s' to env group '%s' in namespace '%s'", key, name, namespace)
  582. }
  583. }
  584. s.Start()
  585. _, err = client.CreateEnvGroup(
  586. ctx, cliConf.Project, cliConf.Cluster, namespace, newEnvGroup,
  587. )
  588. s.Stop()
  589. if err != nil {
  590. return err
  591. }
  592. color.New(color.FgGreen).Println("env group successfully updated")
  593. return nil
  594. }
  595. func validateVarValue(in string) (string, string, error) {
  596. key, value, found := strings.Cut(in, "=")
  597. if !found {
  598. return "", "", fmt.Errorf("%s is not in the form of VAR=VALUE", in)
  599. }
  600. return key, value, nil
  601. }
  602. func updateUnsetEnvGroup(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, cmd *cobra.Command, args []string) error {
  603. if len(args) == 0 {
  604. return fmt.Errorf("required variable name")
  605. }
  606. s := spinner.New(spinner.CharSets[9], 100*time.Millisecond)
  607. s.Color("cyan")
  608. s.Suffix = fmt.Sprintf(" Fetching env group '%s' in namespace '%s'", name, namespace)
  609. s.Start()
  610. envGroupResp, err := client.GetEnvGroup(ctx, cliConf.Project, cliConf.Cluster, namespace,
  611. &types.GetEnvGroupRequest{
  612. Name: name, Version: version,
  613. },
  614. )
  615. s.Stop()
  616. if err != nil {
  617. return err
  618. }
  619. newEnvGroup := &types.CreateEnvGroupRequest{
  620. Name: envGroupResp.Name,
  621. Variables: envGroupResp.Variables,
  622. }
  623. for _, v := range args {
  624. delete(newEnvGroup.Variables, v)
  625. }
  626. s.Suffix = fmt.Sprintf(" Removing variables from env group '%s' in namespace '%s'", name, namespace)
  627. s.Start()
  628. _, err = client.CreateEnvGroup(
  629. ctx, cliConf.Project, cliConf.Cluster, namespace, newEnvGroup,
  630. )
  631. s.Stop()
  632. if err != nil {
  633. return err
  634. }
  635. color.New(color.FgGreen).Println("env group successfully updated")
  636. return nil
  637. }
  638. // HELPER METHODS
  639. func updateGetAgent(ctx context.Context, client api.Client, cliConf config.CLIConfig) (*deploy.DeployAgent, error) {
  640. var buildMethod deploy.DeployBuildType
  641. if method != "" {
  642. buildMethod = deploy.DeployBuildType(method)
  643. }
  644. // add additional env, if they exist
  645. additionalEnv := make(map[string]string)
  646. for _, buildEnv := range buildFlagsEnv {
  647. if strSplArr := strings.SplitN(buildEnv, "=", 2); len(strSplArr) >= 2 {
  648. additionalEnv[strSplArr[0]] = strSplArr[1]
  649. }
  650. }
  651. // initialize the update agent
  652. return deploy.NewDeployAgent(ctx, client, app, &deploy.DeployOpts{
  653. SharedOpts: &deploy.SharedOpts{
  654. ProjectID: cliConf.Project,
  655. ClusterID: cliConf.Cluster,
  656. Namespace: namespace,
  657. LocalPath: localPath,
  658. LocalDockerfile: dockerfile,
  659. OverrideTag: tag,
  660. Method: buildMethod,
  661. AdditionalEnv: additionalEnv,
  662. UseCache: useCache,
  663. },
  664. Local: source != "github",
  665. })
  666. }
  667. func updateBuildWithAgent(ctx context.Context, updateAgent *deploy.DeployAgent) error {
  668. // build the deployment
  669. color.New(color.FgGreen).Println("Building docker image for", app)
  670. if stream {
  671. _ = updateAgent.StreamEvent(ctx, types.SubEvent{
  672. EventID: "build",
  673. Name: "Build",
  674. Index: 100,
  675. Status: types.EventStatusInProgress,
  676. Info: "",
  677. }) //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
  678. }
  679. if useCache {
  680. err := config.SetDockerConfig(ctx, updateAgent.Client, updateAgent.Opts.ProjectID)
  681. if err != nil {
  682. return err
  683. }
  684. }
  685. // read the values if necessary
  686. valuesObj, err := readValuesFile()
  687. if err != nil {
  688. return err
  689. }
  690. buildEnv, err := updateAgent.GetBuildEnv(ctx, &deploy.GetBuildEnvOpts{
  691. UseNewConfig: true,
  692. NewConfig: valuesObj,
  693. })
  694. if err != nil {
  695. if stream {
  696. // another concern: is it safe to ignore the error here?
  697. updateAgent.StreamEvent(ctx, types.SubEvent{ //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
  698. EventID: "build",
  699. Name: "Build",
  700. Index: 110,
  701. Status: types.EventStatusFailed,
  702. Info: err.Error(),
  703. })
  704. }
  705. return err
  706. }
  707. // set the environment variables in the process
  708. err = updateAgent.SetBuildEnv(buildEnv)
  709. if err != nil {
  710. if stream {
  711. updateAgent.StreamEvent(ctx, types.SubEvent{ //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
  712. EventID: "build",
  713. Name: "Build",
  714. Index: 120,
  715. Status: types.EventStatusFailed,
  716. Info: err.Error(),
  717. })
  718. }
  719. return err
  720. }
  721. if err := updateAgent.Build(ctx, nil); err != nil {
  722. if stream {
  723. updateAgent.StreamEvent(ctx, types.SubEvent{ //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
  724. EventID: "build",
  725. Name: "Build",
  726. Index: 130,
  727. Status: types.EventStatusFailed,
  728. Info: err.Error(),
  729. })
  730. }
  731. return err
  732. }
  733. if stream {
  734. updateAgent.StreamEvent(ctx, types.SubEvent{ //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
  735. EventID: "build",
  736. Name: "Build",
  737. Index: 140,
  738. Status: types.EventStatusSuccess,
  739. Info: "",
  740. })
  741. }
  742. return nil
  743. }
  744. func updatePushWithAgent(ctx context.Context, updateAgent *deploy.DeployAgent) error {
  745. if useCache {
  746. color.New(color.FgGreen).Println("Skipping image push for", app, "as use-cache is set")
  747. return nil
  748. }
  749. // push the deployment
  750. color.New(color.FgGreen).Println("Pushing new image for", app)
  751. if stream {
  752. updateAgent.StreamEvent( //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
  753. ctx, types.SubEvent{
  754. EventID: "push",
  755. Name: "Push",
  756. Index: 200,
  757. Status: types.EventStatusInProgress,
  758. Info: "",
  759. })
  760. }
  761. if err := updateAgent.Push(ctx); err != nil {
  762. if stream {
  763. updateAgent.StreamEvent(ctx, types.SubEvent{ //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
  764. EventID: "push",
  765. Name: "Push",
  766. Index: 210,
  767. Status: types.EventStatusFailed,
  768. Info: err.Error(),
  769. })
  770. }
  771. return err
  772. }
  773. if stream {
  774. updateAgent.StreamEvent(ctx, types.SubEvent{ //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
  775. EventID: "push",
  776. Name: "Push",
  777. Index: 220,
  778. Status: types.EventStatusSuccess,
  779. Info: "",
  780. })
  781. }
  782. return nil
  783. }
  784. func updateUpgradeWithAgent(ctx context.Context, updateAgent *deploy.DeployAgent) error {
  785. // push the deployment
  786. color.New(color.FgGreen).Println("Upgrading configuration for", app)
  787. if stream {
  788. updateAgent.StreamEvent(ctx, types.SubEvent{ //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
  789. EventID: "upgrade",
  790. Name: "Upgrade",
  791. Index: 300,
  792. Status: types.EventStatusInProgress,
  793. Info: "",
  794. })
  795. }
  796. var err error
  797. // read the values if necessary
  798. valuesObj, err := readValuesFile()
  799. if err != nil {
  800. return err
  801. }
  802. if err != nil {
  803. if stream {
  804. updateAgent.StreamEvent(ctx, types.SubEvent{ //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
  805. EventID: "upgrade",
  806. Name: "Upgrade",
  807. Index: 310,
  808. Status: types.EventStatusFailed,
  809. Info: err.Error(),
  810. })
  811. }
  812. return err
  813. }
  814. if len(updateAgent.Opts.AdditionalEnv) > 0 {
  815. syncedEnv, err := deploy.GetSyncedEnv(
  816. ctx,
  817. updateAgent.Client,
  818. updateAgent.Release.Config,
  819. updateAgent.Opts.ProjectID,
  820. updateAgent.Opts.ClusterID,
  821. updateAgent.Opts.Namespace,
  822. false,
  823. )
  824. if err != nil {
  825. return err
  826. }
  827. for k := range updateAgent.Opts.AdditionalEnv {
  828. if _, ok := syncedEnv[k]; ok {
  829. return fmt.Errorf("environment variable %s already exists as part of a synced environment group", k)
  830. }
  831. }
  832. normalEnv, err := deploy.GetNormalEnv(
  833. updateAgent.Client,
  834. updateAgent.Release.Config,
  835. updateAgent.Opts.ProjectID,
  836. updateAgent.Opts.ClusterID,
  837. updateAgent.Opts.Namespace,
  838. false,
  839. )
  840. if err != nil {
  841. return err
  842. }
  843. // add the additional environment variables to container.env.normal
  844. for k, v := range updateAgent.Opts.AdditionalEnv {
  845. normalEnv[k] = v
  846. }
  847. valuesObj = templaterUtils.CoalesceValues(valuesObj, map[string]interface{}{
  848. "container": map[string]interface{}{
  849. "env": map[string]interface{}{
  850. "normal": normalEnv,
  851. },
  852. },
  853. })
  854. }
  855. err = updateAgent.UpdateImageAndValues(ctx, valuesObj)
  856. if err != nil {
  857. if stream {
  858. updateAgent.StreamEvent(ctx, types.SubEvent{ //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
  859. EventID: "upgrade",
  860. Name: "Upgrade",
  861. Index: 320,
  862. Status: types.EventStatusFailed,
  863. Info: err.Error(),
  864. })
  865. }
  866. return err
  867. }
  868. if stream {
  869. updateAgent.StreamEvent(ctx, types.SubEvent{ //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
  870. EventID: "upgrade",
  871. Name: "Upgrade",
  872. Index: 330,
  873. Status: types.EventStatusSuccess,
  874. Info: "",
  875. })
  876. }
  877. color.New(color.FgGreen).Println("Successfully updated", app)
  878. return nil
  879. }
  880. func checkDeploymentStatus(ctx context.Context, client api.Client, cliConfig config.CLIConfig) error {
  881. color.New(color.FgBlue).Println("waiting for deployment to be ready, this may take a few minutes and will time out if it takes longer than 30 minutes")
  882. sharedConf := &PorterRunSharedConfig{
  883. Client: client,
  884. CLIConfig: cliConfig,
  885. }
  886. err := sharedConf.setSharedConfig(ctx)
  887. if err != nil {
  888. return fmt.Errorf("could not retrieve kubernetes credentials: %w", err)
  889. }
  890. prevRefresh := time.Now()
  891. timeWait := prevRefresh.Add(30 * time.Minute)
  892. success := false
  893. depls, err := sharedConf.Clientset.AppsV1().Deployments(namespace).List(
  894. ctx,
  895. metav1.ListOptions{
  896. LabelSelector: fmt.Sprintf("app.kubernetes.io/instance=%s", app),
  897. },
  898. )
  899. if err != nil {
  900. return fmt.Errorf("could not get deployments for app %s: %w", app, err)
  901. }
  902. if len(depls.Items) == 0 {
  903. return fmt.Errorf("could not find any deployments for app %s", app)
  904. }
  905. sort.Slice(depls.Items, func(i, j int) bool {
  906. return depls.Items[i].CreationTimestamp.After(depls.Items[j].CreationTimestamp.Time)
  907. })
  908. depl := depls.Items[0]
  909. // determine if the deployment has an appropriate number of ready replicas
  910. minAvailable := *(depl.Spec.Replicas) - getMaxUnavailable(depl)
  911. var revision string
  912. for k, v := range depl.Spec.Template.ObjectMeta.Annotations {
  913. if k == "helm.sh/revision" {
  914. revision = v
  915. break
  916. }
  917. }
  918. if revision == "" {
  919. return fmt.Errorf("could not find revision for deployment")
  920. }
  921. pods, err := sharedConf.Clientset.CoreV1().Pods(namespace).List(
  922. ctx, metav1.ListOptions{
  923. LabelSelector: fmt.Sprintf("app.kubernetes.io/instance=%s", app),
  924. },
  925. )
  926. if err != nil {
  927. return fmt.Errorf("error fetching pods for app %s: %w", app, err)
  928. }
  929. if len(pods.Items) == 0 {
  930. return fmt.Errorf("could not find any pods for app %s", app)
  931. }
  932. var rsName string
  933. for _, pod := range pods.Items {
  934. if pod.ObjectMeta.Annotations["helm.sh/revision"] == revision {
  935. for _, ref := range pod.OwnerReferences {
  936. if ref.Kind == "ReplicaSet" {
  937. rs, err := sharedConf.Clientset.AppsV1().ReplicaSets(namespace).Get(
  938. ctx,
  939. ref.Name,
  940. metav1.GetOptions{},
  941. )
  942. if err != nil {
  943. return fmt.Errorf("error fetching new replicaset: %w", err)
  944. }
  945. rsName = rs.Name
  946. break
  947. }
  948. }
  949. if rsName != "" {
  950. break
  951. }
  952. }
  953. }
  954. if rsName == "" {
  955. return fmt.Errorf("could not find replicaset for app %s", app)
  956. }
  957. for time.Now().Before(timeWait) {
  958. // refresh the client every 2 minutes, as the kubeconfig has a minimum TTL of 5 minutes
  959. if time.Now().After(prevRefresh.Add(2 * time.Minute)) {
  960. err = sharedConf.setSharedConfig(ctx)
  961. if err != nil {
  962. return fmt.Errorf("could not retrieve kube credentials: %s", err.Error())
  963. }
  964. prevRefresh = time.Now()
  965. }
  966. rs, err := sharedConf.Clientset.AppsV1().ReplicaSets(namespace).Get(
  967. ctx,
  968. rsName,
  969. metav1.GetOptions{},
  970. )
  971. if err != nil {
  972. return fmt.Errorf("error fetching new replicaset: %w", err)
  973. }
  974. if minAvailable <= rs.Status.ReadyReplicas {
  975. success = true
  976. }
  977. if success {
  978. break
  979. }
  980. time.Sleep(2 * time.Second)
  981. }
  982. if success {
  983. color.New(color.FgGreen).Printf("%s has been successfully deployed on the cluster\n", app)
  984. } else {
  985. return fmt.Errorf("timed out waiting for deployment to be ready, please check the Porter dashboard for more information")
  986. }
  987. return nil
  988. }