app.go 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683
  1. package commands
  2. import (
  3. "context"
  4. "encoding/base64"
  5. "errors"
  6. "fmt"
  7. "io"
  8. "os"
  9. "strings"
  10. "time"
  11. "github.com/fatih/color"
  12. api "github.com/porter-dev/porter/api/client"
  13. "github.com/porter-dev/porter/api/types"
  14. "github.com/porter-dev/porter/cli/cmd/commands/flags"
  15. "github.com/porter-dev/porter/cli/cmd/config"
  16. "github.com/porter-dev/porter/cli/cmd/utils"
  17. v2 "github.com/porter-dev/porter/cli/cmd/v2"
  18. appV2 "github.com/porter-dev/porter/internal/porter_app/v2"
  19. "github.com/spf13/cobra"
  20. batchv1 "k8s.io/api/batch/v1"
  21. v1 "k8s.io/api/core/v1"
  22. rbacv1 "k8s.io/api/rbac/v1"
  23. "k8s.io/apimachinery/pkg/api/resource"
  24. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  25. "k8s.io/apimachinery/pkg/fields"
  26. "k8s.io/apimachinery/pkg/watch"
  27. "k8s.io/kubectl/pkg/util/term"
  28. "k8s.io/apimachinery/pkg/runtime"
  29. "k8s.io/apimachinery/pkg/runtime/schema"
  30. "k8s.io/client-go/kubernetes"
  31. "k8s.io/client-go/rest"
  32. "k8s.io/client-go/tools/clientcmd"
  33. "k8s.io/client-go/tools/remotecommand"
  34. )
  35. var (
  36. appDeployMethod string
  37. appContainerName string
  38. appCpuMilli int
  39. appExistingPod bool
  40. appInteractive bool
  41. appMemoryMi int
  42. appNamespace string
  43. appTag string
  44. appVerbose bool
  45. appWait bool
  46. deploymentTargetName string
  47. jobName string
  48. )
  49. const (
  50. // CommandPrefix_CNB_LIFECYCLE_LAUNCHER is the prefix for the container start command if the image is built using heroku buildpacks
  51. CommandPrefix_CNB_LIFECYCLE_LAUNCHER = "/cnb/lifecycle/launcher"
  52. // CommandPrefix_LAUNCHER is a shortened form of the above
  53. CommandPrefix_LAUNCHER = "launcher"
  54. )
  55. func registerCommand_App(cliConf config.CLIConfig) *cobra.Command {
  56. appCmd := &cobra.Command{
  57. Use: "app",
  58. Short: "Runs a command for your application.",
  59. }
  60. appCmd.PersistentFlags().StringVarP(
  61. &deploymentTargetName,
  62. "target",
  63. "x",
  64. "",
  65. "the name of the deployment target for the app",
  66. )
  67. appCreateCommand := &cobra.Command{
  68. Use: "create",
  69. Args: cobra.NoArgs,
  70. Short: "Creates and deploys a new app in your project.",
  71. Long: fmt.Sprintf(`
  72. %s
  73. Creates a new app in your project. You can specify the name of the app using the --name flag:
  74. %s
  75. If no flags are specified, you will be directed to a series of required prompts to configure the app.
  76. `,
  77. color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter app create\":"),
  78. color.New(color.FgGreen, color.Bold).Sprintf("porter app create --name example-app"),
  79. ),
  80. RunE: func(cmd *cobra.Command, args []string) error {
  81. return checkLoginAndRunWithConfig(cmd, cliConf, args, appCreate)
  82. },
  83. }
  84. appCreateCommand.PersistentFlags().StringP(
  85. flags.App_Name,
  86. "n",
  87. "",
  88. "the name of the app",
  89. )
  90. appCreateCommand.PersistentFlags().StringVarP(
  91. &appDeployMethod,
  92. "deploy-method",
  93. "m",
  94. "",
  95. "the deployment method for the app (docker, repo)",
  96. )
  97. appCreateCommand.PersistentFlags().StringVarP(&porterYAML, "file", "f", "", "path to porter.yaml")
  98. flags.UseAppConfigFlags(appCreateCommand)
  99. flags.UseAppBuildFlags(appCreateCommand)
  100. flags.UseAppImageFlags(appCreateCommand)
  101. appCmd.AddCommand(appCreateCommand)
  102. appBuildCommand := &cobra.Command{
  103. Use: "build [application]",
  104. Args: cobra.MinimumNArgs(1),
  105. Short: "Builds your application.",
  106. Long: fmt.Sprintf(`
  107. %s
  108. Builds a new version of the specified app. Attempts to use any build settings
  109. previously configured for the app, which can be overridden with flags.
  110. If you would like to change the build context, you can do so by using the --build-context flag:
  111. %s
  112. When using "--method docker", you can specify the path to the Dockerfile using the
  113. --dockerfile flag. This will also override the Dockerfile path that you may have linked
  114. for the application:
  115. %s
  116. To use buildpacks with the "--method pack" flag, you can specify the builder and attach
  117. buildpacks using the --builder and --attach-buildpacks flags:
  118. %s
  119. `,
  120. color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter app build\":"),
  121. color.New(color.FgGreen, color.Bold).Sprintf("porter app build example --build-context ./app"),
  122. color.New(color.FgGreen, color.Bold).Sprintf("porter app build example-app --method docker --dockerfile ./prod.Dockerfile"),
  123. color.New(color.FgGreen, color.Bold).Sprintf("porter app build example-app --method pack --builder heroku/buildpacks:20 --attach-buildpacks heroku/nodejs"),
  124. ),
  125. RunE: func(cmd *cobra.Command, args []string) error {
  126. return checkLoginAndRunWithConfig(cmd, cliConf, args, appBuild)
  127. },
  128. }
  129. flags.UseAppBuildFlags(appBuildCommand)
  130. appBuildCommand.PersistentFlags().String(
  131. flags.App_ImageTag,
  132. "",
  133. "set the image tag to use for the build",
  134. )
  135. appBuildCommand.PersistentFlags().Bool(
  136. flags.App_NoPull,
  137. false,
  138. "do not pull the previous image before building",
  139. )
  140. appCmd.AddCommand(appBuildCommand)
  141. appPushCommand := &cobra.Command{
  142. Use: "push [application]",
  143. Args: cobra.MinimumNArgs(1),
  144. Short: "Pushes your application to a remote registry.",
  145. Long: fmt.Sprintf(`
  146. %s
  147. Pushes the specified app to your default Porter registry. If no tag is specified, the latest
  148. commit SHA from the current branch will be used as the tag.
  149. You can specify a tag using the --tag flag:
  150. %s
  151. `,
  152. color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter app push\":"),
  153. color.New(color.FgGreen, color.Bold).Sprintf("porter app push example-app --tag v1.0.0"),
  154. ),
  155. RunE: func(cmd *cobra.Command, args []string) error {
  156. return checkLoginAndRunWithConfig(cmd, cliConf, args, appPush)
  157. },
  158. }
  159. appPushCommand.PersistentFlags().String(
  160. flags.App_ImageTag,
  161. "",
  162. "set the image tag to use for the push",
  163. )
  164. appCmd.AddCommand(appPushCommand)
  165. appUpdateCommand := &cobra.Command{
  166. Use: "update [application]",
  167. Args: cobra.MinimumNArgs(1),
  168. Short: "Updates an application with the provided configuration.",
  169. Long: fmt.Sprintf(`
  170. %s
  171. Updates the specified app with the provided configuration. This command differs from "porter apply"
  172. in that it only updates the app, but does not attempt to build a new image.`,
  173. color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter app update\":"),
  174. ),
  175. RunE: func(cmd *cobra.Command, args []string) error {
  176. return checkLoginAndRunWithConfig(cmd, cliConf, args, appUpdate)
  177. },
  178. }
  179. appUpdateCommand.PersistentFlags().StringVarP(&porterYAML, "file", "f", "", "path to porter.yaml")
  180. appUpdateCommand.PersistentFlags().BoolVarP(
  181. &appWait,
  182. "wait",
  183. "w",
  184. false,
  185. "set this to wait until an update has rolled out successfully, otherwise time out",
  186. )
  187. flags.UseAppConfigFlags(appUpdateCommand)
  188. flags.UseAppImageFlags(appUpdateCommand)
  189. appCmd.AddCommand(appUpdateCommand)
  190. // appRunCmd represents the "porter app run" subcommand
  191. appRunCmd := &cobra.Command{
  192. Use: "run [application] -- COMMAND [args...]",
  193. Args: cobra.MinimumNArgs(1),
  194. Short: "Runs a command inside a connected cluster container.",
  195. Run: func(cmd *cobra.Command, args []string) {
  196. err := checkLoginAndRunWithConfig(cmd, cliConf, args, appRun)
  197. if err != nil {
  198. os.Exit(1)
  199. }
  200. },
  201. }
  202. appRunFlags(appRunCmd)
  203. appCmd.AddCommand(appRunCmd)
  204. // appRunCleanupCmd represents the "porter app run cleanup" subcommand
  205. appRunCleanupCmd := &cobra.Command{
  206. Use: "cleanup",
  207. Args: cobra.NoArgs,
  208. Short: "Delete any lingering ephemeral pods that were created with \"porter app run\".",
  209. Run: func(cmd *cobra.Command, args []string) {
  210. err := checkLoginAndRunWithConfig(cmd, cliConf, args, appCleanup)
  211. if err != nil {
  212. os.Exit(1)
  213. }
  214. },
  215. }
  216. appRunCmd.AddCommand(appRunCleanupCmd)
  217. // appUpdateTagCmd represents the "porter app update-tag" subcommand
  218. appUpdateTagCmd := &cobra.Command{
  219. Use: "update-tag [application]",
  220. Args: cobra.MinimumNArgs(1),
  221. Short: "Updates the image tag for an application.",
  222. Run: func(cmd *cobra.Command, args []string) {
  223. err := checkLoginAndRunWithConfig(cmd, cliConf, args, appUpdateTag)
  224. if err != nil {
  225. os.Exit(1)
  226. }
  227. },
  228. }
  229. appUpdateTagCmd.PersistentFlags().BoolVarP(
  230. &appWait,
  231. "wait",
  232. "w",
  233. false,
  234. "set this to wait and be notified when an update is successful, otherwise time out",
  235. )
  236. appUpdateTagCmd.PersistentFlags().StringVarP(
  237. &appTag,
  238. "tag",
  239. "t",
  240. "",
  241. "the specified tag to use, default is \"latest\"",
  242. )
  243. appCmd.AddCommand(appUpdateTagCmd)
  244. // appRollback represents the "porter app rollback" subcommand
  245. appRollbackCmd := &cobra.Command{
  246. Use: "rollback [application]",
  247. Args: cobra.MinimumNArgs(1),
  248. Short: "Rolls back an application to the last successful revision.",
  249. RunE: func(cmd *cobra.Command, args []string) error {
  250. return checkLoginAndRunWithConfig(cmd, cliConf, args, appRollback)
  251. },
  252. }
  253. appCmd.AddCommand(appRollbackCmd)
  254. // appManifestsCmd represents the "porter app manifest" subcommand
  255. appManifestsCmd := &cobra.Command{
  256. Use: "manifests [application]",
  257. Args: cobra.MinimumNArgs(1),
  258. Short: "Prints the kubernetes manifests for an application.",
  259. RunE: func(cmd *cobra.Command, args []string) error {
  260. return checkLoginAndRunWithConfig(cmd, cliConf, args, appManifests)
  261. },
  262. }
  263. appCmd.AddCommand(appManifestsCmd)
  264. // appLogsCmd represents the "porter app logs" subcommand
  265. appLogsCmd := &cobra.Command{
  266. Use: "logs [application]",
  267. Args: cobra.MinimumNArgs(1),
  268. Short: "Streams the latest logs for an application.",
  269. RunE: func(cmd *cobra.Command, args []string) error {
  270. return checkLoginAndRunWithConfig(cmd, cliConf, args, appLogs)
  271. },
  272. }
  273. appLogsCmd.PersistentFlags().String("service", "", "the name of the service to get logs for")
  274. appCmd.AddCommand(appLogsCmd)
  275. return appCmd
  276. }
  277. func appRunFlags(appRunCmd *cobra.Command) {
  278. appRunCmd.PersistentFlags().BoolVarP(
  279. &appExistingPod,
  280. "existing_pod",
  281. "e",
  282. false,
  283. "whether to connect to an existing pod (default false)",
  284. )
  285. appRunCmd.PersistentFlags().BoolVarP(
  286. &appVerbose,
  287. "verbose",
  288. "v",
  289. false,
  290. "whether to print verbose output",
  291. )
  292. appRunCmd.PersistentFlags().BoolVar(
  293. &appInteractive,
  294. "interactive",
  295. false,
  296. "whether to run in interactive mode (default false)",
  297. )
  298. appRunCmd.PersistentFlags().BoolVar(
  299. &appWait,
  300. "wait",
  301. false,
  302. "whether to wait for the command to complete before exiting for non-interactive mode (default false)",
  303. )
  304. appRunCmd.PersistentFlags().IntVarP(
  305. &appCpuMilli,
  306. "cpu",
  307. "",
  308. 0,
  309. "cpu allocation in millicores (1000 millicores = 1 vCPU)",
  310. )
  311. appRunCmd.PersistentFlags().IntVarP(
  312. &appMemoryMi,
  313. "ram",
  314. "",
  315. 0,
  316. "ram allocation in Mi (1024 Mi = 1 GB)",
  317. )
  318. appRunCmd.PersistentFlags().StringVarP(
  319. &appContainerName,
  320. "container",
  321. "c",
  322. "",
  323. "name of the container inside pod to run the command in",
  324. )
  325. appRunCmd.PersistentFlags().StringVar(
  326. &jobName,
  327. "job",
  328. "",
  329. "name of the job to run (will run the job as defined instead of the provided command, and returns the job run id without waiting for the job to complete or displaying logs)",
  330. )
  331. }
  332. func appCreate(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConfig config.CLIConfig, _ config.FeatureFlags, cmd *cobra.Command, args []string) error {
  333. name, err := cmd.Flags().GetString(flags.App_Name)
  334. if err != nil {
  335. return fmt.Errorf("error getting app name: %w", err)
  336. }
  337. buildValues, err := flags.AppBuildValuesFromCmd(cmd)
  338. if err != nil {
  339. return err
  340. }
  341. imageValues, err := flags.AppImageValuesFromCmd(cmd)
  342. if err != nil {
  343. return err
  344. }
  345. configValues, err := flags.AppConfigValuesFromCmd(cmd)
  346. if err != nil {
  347. return err
  348. }
  349. err = v2.CreateApp(ctx, v2.CreateAppInput{
  350. CLIConfig: cliConfig,
  351. Client: client,
  352. AppName: name,
  353. PorterYamlPath: porterYAML,
  354. DeploymentTargetName: deploymentTargetName,
  355. BuildMethod: buildValues.BuildMethod,
  356. Dockerfile: buildValues.Dockerfile,
  357. Builder: buildValues.Builder,
  358. Buildpacks: buildValues.Buildpacks,
  359. BuildContext: buildValues.BuildContext,
  360. ImageTag: imageValues.Tag,
  361. ImageRepo: imageValues.Repository,
  362. EnvGroups: configValues.AttachEnvGroups,
  363. })
  364. if err != nil {
  365. return fmt.Errorf("failed to create app: %w", err)
  366. }
  367. return nil
  368. }
  369. func appBuild(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConfig config.CLIConfig, _ config.FeatureFlags, cmd *cobra.Command, args []string) error {
  370. appName := args[0]
  371. if appName == "" {
  372. return fmt.Errorf("app name must be specified")
  373. }
  374. buildValues, err := flags.AppBuildValuesFromCmd(cmd)
  375. if err != nil {
  376. return err
  377. }
  378. patchOperations := appV2.PatchOperationsFromFlagValues(appV2.PatchOperationsFromFlagValuesInput{
  379. BuildMethod: buildValues.BuildMethod,
  380. Dockerfile: buildValues.Dockerfile,
  381. Builder: buildValues.Builder,
  382. Buildpacks: buildValues.Buildpacks,
  383. BuildContext: buildValues.BuildContext,
  384. })
  385. tag, err := cmd.Flags().GetString(flags.App_ImageTag)
  386. if err != nil {
  387. return fmt.Errorf("error getting tag: %w", err)
  388. }
  389. noPull, err := cmd.Flags().GetBool(flags.App_NoPull)
  390. if err != nil {
  391. return fmt.Errorf("could not retrieve no-pull flag from command")
  392. }
  393. pullBeforeBuild := !noPull
  394. err = v2.AppBuild(ctx, v2.AppBuildInput{
  395. CLIConfig: cliConfig,
  396. Client: client,
  397. AppName: appName,
  398. DeploymentTargetName: deploymentTargetName,
  399. BuildMethod: buildValues.BuildMethod,
  400. Dockerfile: buildValues.Dockerfile,
  401. Builder: buildValues.Builder,
  402. Buildpacks: buildValues.Buildpacks,
  403. BuildContext: buildValues.BuildContext,
  404. ImageTag: tag,
  405. PatchOperations: patchOperations,
  406. PullImageBeforeBuild: pullBeforeBuild,
  407. })
  408. if err != nil {
  409. return fmt.Errorf("failed to build app: %w", err)
  410. }
  411. return nil
  412. }
  413. func appPush(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConfig config.CLIConfig, _ config.FeatureFlags, cmd *cobra.Command, args []string) error {
  414. appName := args[0]
  415. if appName == "" {
  416. return fmt.Errorf("app name must be specified")
  417. }
  418. tag, err := cmd.Flags().GetString(flags.App_ImageTag)
  419. if err != nil {
  420. return fmt.Errorf("error getting tag: %w", err)
  421. }
  422. err = v2.AppPush(ctx, v2.AppPushInput{
  423. CLIConfig: cliConfig,
  424. Client: client,
  425. AppName: appName,
  426. DeploymentTargetName: deploymentTargetName,
  427. ImageTag: tag,
  428. })
  429. if err != nil {
  430. return fmt.Errorf("failed to push image for app: %w", err)
  431. }
  432. return nil
  433. }
  434. func appUpdate(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConfig config.CLIConfig, _ config.FeatureFlags, cmd *cobra.Command, args []string) error {
  435. appName := args[0]
  436. if appName == "" {
  437. return fmt.Errorf("app name must be specified")
  438. }
  439. extraAppConfig, err := flags.AppConfigValuesFromCmd(cmd)
  440. if err != nil {
  441. return fmt.Errorf("could not retrieve app config values from command")
  442. }
  443. imageValues, err := flags.AppImageValuesFromCmd(cmd)
  444. if err != nil {
  445. return fmt.Errorf("could not retrieve image values from command")
  446. }
  447. patchOperations := appV2.PatchOperationsFromFlagValues(appV2.PatchOperationsFromFlagValuesInput{
  448. EnvGroups: extraAppConfig.AttachEnvGroups,
  449. ImageRepository: imageValues.Repository,
  450. ImageTag: imageValues.Tag,
  451. })
  452. inp := v2.ApplyInput{
  453. CLIConfig: cliConfig,
  454. Client: client,
  455. PorterYamlPath: porterYAML,
  456. AppName: appName,
  457. ImageTagOverride: imageValues.Tag,
  458. PreviewApply: previewApply,
  459. WaitForSuccessfulDeployment: appWait,
  460. Exact: exact,
  461. PatchOperations: patchOperations,
  462. SkipBuild: true, // skip build for update
  463. }
  464. err = v2.Apply(ctx, inp)
  465. if err != nil {
  466. return err
  467. }
  468. return nil
  469. }
  470. func appManifests(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConfig config.CLIConfig, _ config.FeatureFlags, _ *cobra.Command, args []string) error {
  471. appName := args[0]
  472. if appName == "" {
  473. return fmt.Errorf("app name must be specified")
  474. }
  475. manifest, err := client.GetAppManifests(ctx, cliConfig.Project, cliConfig.Cluster, appName)
  476. if err != nil {
  477. return fmt.Errorf("failed to get app manifest: %w", err)
  478. }
  479. decoded, err := base64.StdEncoding.DecodeString(manifest.Base64Manifests)
  480. if err != nil {
  481. return fmt.Errorf("failed to decode app manifest: %w", err)
  482. }
  483. _, err = os.Stdout.WriteString(string(decoded))
  484. if err != nil {
  485. return fmt.Errorf("failed to write app manifest: %w", err)
  486. }
  487. return nil
  488. }
  489. func appRollback(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConfig config.CLIConfig, _ config.FeatureFlags, _ *cobra.Command, args []string) error {
  490. project, err := client.GetProject(ctx, cliConfig.Project)
  491. if err != nil {
  492. return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
  493. }
  494. if !project.ValidateApplyV2 {
  495. return fmt.Errorf("rollback command is not enabled for this project")
  496. }
  497. appName := args[0]
  498. if appName == "" {
  499. return fmt.Errorf("app name must be specified")
  500. }
  501. err = v2.Rollback(ctx, v2.RollbackInput{
  502. CLIConfig: cliConfig,
  503. Client: client,
  504. AppName: appName,
  505. })
  506. if err != nil {
  507. return fmt.Errorf("failed to rollback app: %w", err)
  508. }
  509. return nil
  510. }
  511. func appLogs(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConfig config.CLIConfig, _ config.FeatureFlags, cmd *cobra.Command, args []string) error {
  512. appName := args[0]
  513. if appName == "" {
  514. return fmt.Errorf("app name must be specified")
  515. }
  516. serviceFlag, err := cmd.Flags().GetString("service")
  517. if err != nil {
  518. return fmt.Errorf("error getting service flag: %w", err)
  519. }
  520. serviceName := v2.ServiceName_AllServices
  521. if serviceFlag != "" {
  522. serviceName = serviceFlag
  523. }
  524. err = v2.AppLogs(ctx, v2.AppLogsInput{
  525. CLIConfig: cliConfig,
  526. Client: client,
  527. AppName: appName,
  528. DeploymentTargetName: deploymentTargetName,
  529. ServiceName: serviceName,
  530. })
  531. if err != nil {
  532. return fmt.Errorf("failed to get app logs: %w", err)
  533. }
  534. return nil
  535. }
  536. func appRun(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConfig config.CLIConfig, ff config.FeatureFlags, _ *cobra.Command, args []string) error {
  537. if jobName != "" {
  538. if !ff.ValidateApplyV2Enabled {
  539. return fmt.Errorf("job flag is not supported on this project")
  540. }
  541. return v2.RunAppJob(ctx, v2.RunAppJobInput{
  542. CLIConfig: cliConfig,
  543. Client: client,
  544. DeploymentTargetName: deploymentTargetName,
  545. AppName: args[0],
  546. JobName: jobName,
  547. WaitForExit: appWait,
  548. })
  549. }
  550. if len(args) < 2 {
  551. return fmt.Errorf("porter app run requires at least 2 arguments")
  552. }
  553. execArgs := args[1:]
  554. color.New(color.FgGreen).Println("Attempting to run", strings.Join(execArgs, " "), "for application", args[0])
  555. project, err := client.GetProject(ctx, cliConfig.Project)
  556. if err != nil {
  557. return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
  558. }
  559. var podsSimple []appPodSimple
  560. // updated exec args includes launcher command prepended if needed, otherwise it is the same as execArgs
  561. var updatedExecArgs []string
  562. if project.ValidateApplyV2 {
  563. podsSimple, updatedExecArgs, namespace, err = getPodsFromV2PorterYaml(ctx, execArgs, client, cliConfig, args[0], deploymentTargetName)
  564. if err != nil {
  565. return err
  566. }
  567. appNamespace = namespace
  568. } else {
  569. appNamespace = fmt.Sprintf("porter-stack-%s", args[0])
  570. podsSimple, updatedExecArgs, err = getPodsFromV1PorterYaml(ctx, execArgs, client, cliConfig, args[0], appNamespace)
  571. if err != nil {
  572. return err
  573. }
  574. }
  575. // if length of pods is 0, throw error
  576. var selectedPod appPodSimple
  577. if len(podsSimple) == 0 {
  578. return fmt.Errorf("At least one pod must exist in this deployment.")
  579. } else if !appExistingPod || len(podsSimple) == 1 {
  580. selectedPod = podsSimple[0]
  581. } else {
  582. podNames := make([]string, 0)
  583. for _, podSimple := range podsSimple {
  584. podNames = append(podNames, podSimple.Name)
  585. }
  586. selectedPodName, err := utils.PromptSelect("Select the pod:", podNames)
  587. if err != nil {
  588. return err
  589. }
  590. // find selected pod
  591. for _, podSimple := range podsSimple {
  592. if selectedPodName == podSimple.Name {
  593. selectedPod = podSimple
  594. }
  595. }
  596. }
  597. var selectedContainerName string
  598. // if --container is provided, check whether the provided container exists in the pod.
  599. if appContainerName != "" {
  600. // check if provided container name exists in the pod
  601. for _, name := range selectedPod.ContainerNames {
  602. if name == appContainerName {
  603. selectedContainerName = name
  604. break
  605. }
  606. }
  607. if selectedContainerName == "" {
  608. return fmt.Errorf("provided container %s does not exist in pod %s", appContainerName, selectedPod.Name)
  609. }
  610. }
  611. if len(selectedPod.ContainerNames) == 0 {
  612. return fmt.Errorf("At least one container must exist in the selected pod.")
  613. } else if len(selectedPod.ContainerNames) >= 1 {
  614. selectedContainerName = selectedPod.ContainerNames[0]
  615. }
  616. config := &KubernetesSharedConfig{
  617. Client: client,
  618. CLIConfig: cliConfig,
  619. }
  620. err = config.setSharedConfig(ctx)
  621. if err != nil {
  622. return fmt.Errorf("Could not retrieve kube credentials: %s", err.Error())
  623. }
  624. imageName, err := getImageNameFromPod(ctx, config.Clientset, appNamespace, selectedPod.Name, selectedContainerName)
  625. if err != nil {
  626. return err
  627. }
  628. if appExistingPod {
  629. _, _ = color.New(color.FgGreen).Printf("Connecting to existing pod which is running an image named: %s\n", imageName)
  630. return appExecuteRun(config, appNamespace, selectedPod.Name, selectedContainerName, updatedExecArgs)
  631. }
  632. _, _ = color.New(color.FgGreen).Println("Creating a copy pod using image: ", imageName)
  633. return appExecuteRunEphemeral(ctx, config, appNamespace, selectedPod.Name, selectedContainerName, updatedExecArgs)
  634. }
  635. func getImageNameFromPod(ctx context.Context, clientset *kubernetes.Clientset, namespace, podName, containerName string) (string, error) {
  636. pod, err := clientset.CoreV1().Pods(namespace).Get(ctx, podName, metav1.GetOptions{})
  637. if err != nil {
  638. return "", err
  639. }
  640. for _, container := range pod.Spec.Containers {
  641. if container.Name == containerName {
  642. return container.Image, nil
  643. }
  644. }
  645. return "", fmt.Errorf("could not find container %s in pod %s", containerName, podName)
  646. }
  647. func appCleanup(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConfig config.CLIConfig, _ config.FeatureFlags, _ *cobra.Command, _ []string) error {
  648. config := &KubernetesSharedConfig{
  649. Client: client,
  650. CLIConfig: cliConfig,
  651. }
  652. err := config.setSharedConfig(ctx)
  653. if err != nil {
  654. return fmt.Errorf("Could not retrieve kube credentials: %s", err.Error())
  655. }
  656. proceed, err := utils.PromptSelect(
  657. fmt.Sprintf("You have chosen the '%s' namespace for cleanup. Do you want to proceed?", appNamespace),
  658. []string{"Yes", "No", "All namespaces"},
  659. )
  660. if err != nil {
  661. return err
  662. }
  663. if proceed == "No" {
  664. return nil
  665. }
  666. var podNames []string
  667. color.New(color.FgGreen).Println("Fetching ephemeral pods for cleanup")
  668. if proceed == "All namespaces" {
  669. namespaces, err := config.Clientset.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
  670. if err != nil {
  671. return err
  672. }
  673. for _, namespace := range namespaces.Items {
  674. if pods, err := appGetEphemeralPods(ctx, namespace.Name, config.Clientset); err == nil {
  675. podNames = append(podNames, pods...)
  676. } else {
  677. return err
  678. }
  679. }
  680. } else {
  681. if pods, err := appGetEphemeralPods(ctx, appNamespace, config.Clientset); err == nil {
  682. podNames = append(podNames, pods...)
  683. } else {
  684. return err
  685. }
  686. }
  687. if len(podNames) == 0 {
  688. color.New(color.FgBlue).Println("No ephemeral pods to delete")
  689. return nil
  690. }
  691. selectedPods, err := utils.PromptMultiselect("Select ephemeral pods to delete", podNames)
  692. if err != nil {
  693. return err
  694. }
  695. for _, podName := range selectedPods {
  696. _, _ = color.New(color.FgBlue).Printf("Deleting ephemeral pod: %s\n", podName)
  697. err = config.Clientset.CoreV1().Pods(appNamespace).Delete(
  698. ctx, podName, metav1.DeleteOptions{},
  699. )
  700. if err != nil {
  701. return err
  702. }
  703. }
  704. return nil
  705. }
  706. func appGetEphemeralPods(ctx context.Context, namespace string, clientset *kubernetes.Clientset) ([]string, error) {
  707. var podNames []string
  708. pods, err := clientset.CoreV1().Pods(namespace).List(
  709. ctx, metav1.ListOptions{LabelSelector: "porter/ephemeral-pod"},
  710. )
  711. if err != nil {
  712. return nil, err
  713. }
  714. for _, pod := range pods.Items {
  715. podNames = append(podNames, pod.Name)
  716. }
  717. return podNames, nil
  718. }
  719. // KubernetesSharedConfig allows for interacting with a kubernetes cluster
  720. type KubernetesSharedConfig struct {
  721. Client api.Client
  722. RestConf *rest.Config
  723. Clientset *kubernetes.Clientset
  724. RestClient *rest.RESTClient
  725. CLIConfig config.CLIConfig
  726. }
  727. func (p *KubernetesSharedConfig) setSharedConfig(ctx context.Context) error {
  728. pID := p.CLIConfig.Project
  729. cID := p.CLIConfig.Cluster
  730. kubeResp, err := p.Client.GetKubeconfig(ctx, pID, cID, p.CLIConfig.Kubeconfig)
  731. if err != nil {
  732. return err
  733. }
  734. kubeBytes := kubeResp.Kubeconfig
  735. cmdConf, err := clientcmd.NewClientConfigFromBytes(kubeBytes)
  736. if err != nil {
  737. return err
  738. }
  739. restConf, err := cmdConf.ClientConfig()
  740. if err != nil {
  741. return err
  742. }
  743. restConf.GroupVersion = &schema.GroupVersion{
  744. Group: "api",
  745. Version: "v1",
  746. }
  747. restConf.NegotiatedSerializer = runtime.NewSimpleNegotiatedSerializer(runtime.SerializerInfo{})
  748. p.RestConf = restConf
  749. clientset, err := kubernetes.NewForConfig(restConf)
  750. if err != nil {
  751. return err
  752. }
  753. p.Clientset = clientset
  754. restClient, err := rest.RESTClientFor(restConf)
  755. if err != nil {
  756. return err
  757. }
  758. p.RestClient = restClient
  759. return nil
  760. }
  761. type appPodSimple struct {
  762. Name string
  763. ContainerNames []string
  764. }
  765. func appGetPodsV1PorterYaml(ctx context.Context, cliConfig config.CLIConfig, client api.Client, namespace, releaseName string) ([]appPodSimple, bool, error) {
  766. pID := cliConfig.Project
  767. cID := cliConfig.Cluster
  768. var containerHasLauncherStartCommand bool
  769. resp, err := client.GetK8sAllPods(ctx, pID, cID, namespace, releaseName)
  770. if err != nil {
  771. return nil, containerHasLauncherStartCommand, err
  772. }
  773. if resp == nil {
  774. return nil, containerHasLauncherStartCommand, errors.New("get pods response is nil")
  775. }
  776. pods := *resp
  777. if len(pods) == 0 {
  778. return nil, containerHasLauncherStartCommand, errors.New("no running pods found for this application")
  779. }
  780. for _, container := range pods[0].Spec.Containers {
  781. if len(container.Command) > 0 && (container.Command[0] == CommandPrefix_LAUNCHER || container.Command[0] == CommandPrefix_CNB_LIFECYCLE_LAUNCHER) {
  782. containerHasLauncherStartCommand = true
  783. }
  784. }
  785. res := make([]appPodSimple, 0)
  786. for _, pod := range pods {
  787. if pod.Status.Phase == v1.PodRunning {
  788. containerNames := make([]string, 0)
  789. for _, container := range pod.Spec.Containers {
  790. containerNames = append(containerNames, container.Name)
  791. }
  792. res = append(res, appPodSimple{
  793. Name: pod.ObjectMeta.Name,
  794. ContainerNames: containerNames,
  795. })
  796. }
  797. }
  798. return res, containerHasLauncherStartCommand, nil
  799. }
  800. func appGetPodsV2PorterYaml(ctx context.Context, cliConfig config.CLIConfig, client api.Client, porterAppName string, deploymentTargetName string) ([]appPodSimple, string, bool, error) {
  801. pID := cliConfig.Project
  802. cID := cliConfig.Cluster
  803. var containerHasLauncherStartCommand bool
  804. resp, err := client.PorterYamlV2Pods(ctx, pID, cID, porterAppName, deploymentTargetName)
  805. if err != nil {
  806. return nil, "", containerHasLauncherStartCommand, err
  807. }
  808. if resp == nil {
  809. return nil, "", containerHasLauncherStartCommand, errors.New("get pods response is nil")
  810. }
  811. pods := *resp
  812. if len(pods) == 0 {
  813. return nil, "", containerHasLauncherStartCommand, errors.New("no running pods found for this application")
  814. }
  815. namespace := pods[0].Namespace
  816. for _, container := range pods[0].Spec.Containers {
  817. if len(container.Command) > 0 && (container.Command[0] == CommandPrefix_LAUNCHER || container.Command[0] == CommandPrefix_CNB_LIFECYCLE_LAUNCHER) {
  818. containerHasLauncherStartCommand = true
  819. }
  820. }
  821. res := make([]appPodSimple, 0)
  822. for _, pod := range pods {
  823. if pod.Status.Phase == v1.PodRunning {
  824. containerNames := make([]string, 0)
  825. for _, container := range pod.Spec.Containers {
  826. containerNames = append(containerNames, container.Name)
  827. }
  828. res = append(res, appPodSimple{
  829. Name: pod.ObjectMeta.Name,
  830. ContainerNames: containerNames,
  831. })
  832. }
  833. }
  834. return res, namespace, containerHasLauncherStartCommand, nil
  835. }
  836. func appExecuteRun(config *KubernetesSharedConfig, namespace, name, container string, args []string) error {
  837. req := config.RestClient.Post().
  838. Resource("pods").
  839. Name(name).
  840. Namespace(namespace).
  841. SubResource("exec")
  842. for _, arg := range args {
  843. req.Param("command", arg)
  844. }
  845. req.Param("stdin", "true")
  846. req.Param("stdout", "true")
  847. req.Param("tty", "true")
  848. req.Param("container", container)
  849. t := term.TTY{
  850. In: os.Stdin,
  851. Out: os.Stdout,
  852. Raw: true,
  853. }
  854. size := t.GetSize()
  855. sizeQueue := t.MonitorSize(size)
  856. return t.Safe(func() error {
  857. exec, err := remotecommand.NewSPDYExecutor(config.RestConf, "POST", req.URL())
  858. if err != nil {
  859. return err
  860. }
  861. return exec.Stream(remotecommand.StreamOptions{
  862. Stdin: os.Stdin,
  863. Stdout: os.Stdout,
  864. Stderr: os.Stderr,
  865. Tty: true,
  866. TerminalSizeQueue: sizeQueue,
  867. })
  868. })
  869. }
  870. func appExecuteRunEphemeral(ctx context.Context, config *KubernetesSharedConfig, namespace, name, container string, args []string) error {
  871. existing, err := appGetExistingPod(ctx, config, name, namespace)
  872. if err != nil {
  873. return err
  874. }
  875. newPod, err := appCreateEphemeralPodFromExisting(ctx, config, existing, container, args)
  876. if err != nil {
  877. return err
  878. }
  879. podName := newPod.ObjectMeta.Name
  880. // delete the ephemeral pod no matter what
  881. defer appDeletePod(ctx, config, podName, namespace) //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
  882. _, _ = color.New(color.FgYellow).Printf("Waiting for pod %s to be ready...", podName)
  883. if err = appWaitForPod(ctx, config, newPod); err != nil {
  884. color.New(color.FgRed).Println("failed")
  885. return appHandlePodAttachError(ctx, err, config, namespace, podName, container)
  886. }
  887. err = appCheckForPodDeletionCronJob(ctx, config)
  888. if err != nil {
  889. return err
  890. }
  891. // refresh pod info for latest status
  892. newPod, err = config.Clientset.CoreV1().
  893. Pods(newPod.Namespace).
  894. Get(ctx, newPod.Name, metav1.GetOptions{})
  895. // pod exited while we were waiting. maybe an error maybe not.
  896. // we dont know if the user wanted an interactive shell or not.
  897. // if it was an error the logs hopefully say so.
  898. if appIsPodExited(newPod) {
  899. color.New(color.FgGreen).Println("complete!")
  900. var writtenBytes int64
  901. writtenBytes, _ = appPipePodLogsToStdout(ctx, config, namespace, podName, container, false)
  902. if appVerbose || writtenBytes == 0 {
  903. color.New(color.FgYellow).Println("Could not get logs. Pod events:")
  904. _ = appPipeEventsToStdout(ctx, config, namespace, podName, container, false) //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
  905. }
  906. return nil
  907. }
  908. color.New(color.FgGreen).Println("ready!")
  909. color.New(color.FgYellow).Println("Attempting connection to the container. If you don't see a command prompt, try pressing enter.")
  910. req := config.RestClient.Post().
  911. Resource("pods").
  912. Name(podName).
  913. Namespace(namespace).
  914. SubResource("attach")
  915. req.Param("stdin", "true")
  916. req.Param("stdout", "true")
  917. req.Param("tty", "true")
  918. req.Param("container", container)
  919. t := term.TTY{
  920. In: os.Stdin,
  921. Out: os.Stdout,
  922. Raw: true,
  923. }
  924. size := t.GetSize()
  925. sizeQueue := t.MonitorSize(size)
  926. if err = t.Safe(func() error {
  927. exec, err := remotecommand.NewSPDYExecutor(config.RestConf, "POST", req.URL())
  928. if err != nil {
  929. return err
  930. }
  931. return exec.Stream(remotecommand.StreamOptions{
  932. Stdin: os.Stdin,
  933. Stdout: os.Stdout,
  934. Stderr: os.Stderr,
  935. Tty: true,
  936. TerminalSizeQueue: sizeQueue,
  937. })
  938. }); err != nil {
  939. // ugly way to catch no TTY errors, such as when running command "echo \"hello\""
  940. return appHandlePodAttachError(ctx, err, config, namespace, podName, container)
  941. }
  942. if appVerbose {
  943. color.New(color.FgYellow).Println("Pod events:")
  944. _ = appPipeEventsToStdout(ctx, config, namespace, podName, container, false) //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
  945. }
  946. return err
  947. }
  948. func appCheckForPodDeletionCronJob(ctx context.Context, config *KubernetesSharedConfig) error {
  949. // try and create the cron job and all of the other required resources as necessary,
  950. // starting with the service account, then role and then a role binding
  951. err := appCheckForServiceAccount(ctx, config)
  952. if err != nil {
  953. return err
  954. }
  955. err = appCheckForClusterRole(ctx, config)
  956. if err != nil {
  957. return err
  958. }
  959. err = appCheckForRoleBinding(ctx, config)
  960. if err != nil {
  961. return err
  962. }
  963. namespaces, err := config.Clientset.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
  964. if err != nil {
  965. return err
  966. }
  967. for _, namespace := range namespaces.Items {
  968. cronJobs, err := config.Clientset.BatchV1().CronJobs(namespace.Name).List(
  969. ctx, metav1.ListOptions{},
  970. )
  971. if err != nil {
  972. return err
  973. }
  974. if namespace.Name == "default" {
  975. for _, cronJob := range cronJobs.Items {
  976. if cronJob.Name == "porter-ephemeral-pod-deletion-cronjob" {
  977. return nil
  978. }
  979. }
  980. } else {
  981. for _, cronJob := range cronJobs.Items {
  982. if cronJob.Name == "porter-ephemeral-pod-deletion-cronjob" {
  983. err = config.Clientset.BatchV1().CronJobs(namespace.Name).Delete(
  984. ctx, cronJob.Name, metav1.DeleteOptions{},
  985. )
  986. if err != nil {
  987. return err
  988. }
  989. }
  990. }
  991. }
  992. }
  993. // create the cronjob
  994. cronJob := &batchv1.CronJob{
  995. ObjectMeta: metav1.ObjectMeta{
  996. Name: "porter-ephemeral-pod-deletion-cronjob",
  997. },
  998. Spec: batchv1.CronJobSpec{
  999. Schedule: "0 * * * *",
  1000. JobTemplate: batchv1.JobTemplateSpec{
  1001. Spec: batchv1.JobSpec{
  1002. Template: v1.PodTemplateSpec{
  1003. Spec: v1.PodSpec{
  1004. ServiceAccountName: "porter-ephemeral-pod-deletion-service-account",
  1005. RestartPolicy: v1.RestartPolicyNever,
  1006. Containers: []v1.Container{
  1007. {
  1008. Name: "ephemeral-pods-manager",
  1009. Image: "public.ecr.aws/o1j4x7p4/porter-ephemeral-pods-manager:latest",
  1010. ImagePullPolicy: v1.PullAlways,
  1011. Args: []string{"delete"},
  1012. },
  1013. },
  1014. },
  1015. },
  1016. },
  1017. },
  1018. },
  1019. }
  1020. _, err = config.Clientset.BatchV1().CronJobs("default").Create(
  1021. ctx, cronJob, metav1.CreateOptions{},
  1022. )
  1023. if err != nil {
  1024. return err
  1025. }
  1026. return nil
  1027. }
  1028. func appCheckForServiceAccount(ctx context.Context, config *KubernetesSharedConfig) error {
  1029. namespaces, err := config.Clientset.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
  1030. if err != nil {
  1031. return err
  1032. }
  1033. for _, namespace := range namespaces.Items {
  1034. serviceAccounts, err := config.Clientset.CoreV1().ServiceAccounts(namespace.Name).List(
  1035. ctx, metav1.ListOptions{},
  1036. )
  1037. if err != nil {
  1038. return err
  1039. }
  1040. if namespace.Name == "default" {
  1041. for _, svcAccount := range serviceAccounts.Items {
  1042. if svcAccount.Name == "porter-ephemeral-pod-deletion-service-account" {
  1043. return nil
  1044. }
  1045. }
  1046. } else {
  1047. for _, svcAccount := range serviceAccounts.Items {
  1048. if svcAccount.Name == "porter-ephemeral-pod-deletion-service-account" {
  1049. err = config.Clientset.CoreV1().ServiceAccounts(namespace.Name).Delete(
  1050. ctx, svcAccount.Name, metav1.DeleteOptions{},
  1051. )
  1052. if err != nil {
  1053. return err
  1054. }
  1055. }
  1056. }
  1057. }
  1058. }
  1059. serviceAccount := &v1.ServiceAccount{
  1060. ObjectMeta: metav1.ObjectMeta{
  1061. Name: "porter-ephemeral-pod-deletion-service-account",
  1062. },
  1063. }
  1064. _, err = config.Clientset.CoreV1().ServiceAccounts("default").Create(
  1065. ctx, serviceAccount, metav1.CreateOptions{},
  1066. )
  1067. if err != nil {
  1068. return err
  1069. }
  1070. return nil
  1071. }
  1072. func appCheckForClusterRole(ctx context.Context, config *KubernetesSharedConfig) error {
  1073. roles, err := config.Clientset.RbacV1().ClusterRoles().List(
  1074. ctx, metav1.ListOptions{},
  1075. )
  1076. if err != nil {
  1077. return err
  1078. }
  1079. for _, role := range roles.Items {
  1080. if role.Name == "porter-ephemeral-pod-deletion-cluster-role" {
  1081. return nil
  1082. }
  1083. }
  1084. role := &rbacv1.ClusterRole{
  1085. ObjectMeta: metav1.ObjectMeta{
  1086. Name: "porter-ephemeral-pod-deletion-cluster-role",
  1087. },
  1088. Rules: []rbacv1.PolicyRule{
  1089. {
  1090. APIGroups: []string{""},
  1091. Resources: []string{"pods"},
  1092. Verbs: []string{"list", "delete"},
  1093. },
  1094. {
  1095. APIGroups: []string{""},
  1096. Resources: []string{"namespaces"},
  1097. Verbs: []string{"list"},
  1098. },
  1099. },
  1100. }
  1101. _, err = config.Clientset.RbacV1().ClusterRoles().Create(
  1102. ctx, role, metav1.CreateOptions{},
  1103. )
  1104. if err != nil {
  1105. return err
  1106. }
  1107. return nil
  1108. }
  1109. func appCheckForRoleBinding(ctx context.Context, config *KubernetesSharedConfig) error {
  1110. bindings, err := config.Clientset.RbacV1().ClusterRoleBindings().List(
  1111. ctx, metav1.ListOptions{},
  1112. )
  1113. if err != nil {
  1114. return err
  1115. }
  1116. for _, binding := range bindings.Items {
  1117. if binding.Name == "porter-ephemeral-pod-deletion-cluster-rolebinding" {
  1118. return nil
  1119. }
  1120. }
  1121. binding := &rbacv1.ClusterRoleBinding{
  1122. ObjectMeta: metav1.ObjectMeta{
  1123. Name: "porter-ephemeral-pod-deletion-cluster-rolebinding",
  1124. },
  1125. RoleRef: rbacv1.RoleRef{
  1126. APIGroup: "rbac.authorization.k8s.io",
  1127. Kind: "ClusterRole",
  1128. Name: "porter-ephemeral-pod-deletion-cluster-role",
  1129. },
  1130. Subjects: []rbacv1.Subject{
  1131. {
  1132. APIGroup: "",
  1133. Kind: "ServiceAccount",
  1134. Name: "porter-ephemeral-pod-deletion-service-account",
  1135. Namespace: "default",
  1136. },
  1137. },
  1138. }
  1139. _, err = config.Clientset.RbacV1().ClusterRoleBindings().Create(
  1140. ctx, binding, metav1.CreateOptions{},
  1141. )
  1142. if err != nil {
  1143. return err
  1144. }
  1145. return nil
  1146. }
  1147. func appWaitForPod(ctx context.Context, config *KubernetesSharedConfig, pod *v1.Pod) error {
  1148. var (
  1149. w watch.Interface
  1150. err error
  1151. ok bool
  1152. )
  1153. // immediately after creating a pod, the API may return a 404. heuristically 1
  1154. // second seems to be plenty.
  1155. watchRetries := 3
  1156. for i := 0; i < watchRetries; i++ {
  1157. selector := fields.OneTermEqualSelector("metadata.name", pod.Name).String()
  1158. w, err = config.Clientset.CoreV1().
  1159. Pods(pod.Namespace).
  1160. Watch(ctx, metav1.ListOptions{FieldSelector: selector})
  1161. if err == nil {
  1162. break
  1163. }
  1164. time.Sleep(time.Second)
  1165. }
  1166. if err != nil {
  1167. return err
  1168. }
  1169. defer w.Stop()
  1170. for {
  1171. select {
  1172. case <-time.Tick(time.Second):
  1173. // poll every second in case we already missed the ready event while
  1174. // creating the listener.
  1175. pod, err = config.Clientset.CoreV1().
  1176. Pods(pod.Namespace).
  1177. Get(ctx, pod.Name, metav1.GetOptions{})
  1178. if appIsPodReady(pod) || appIsPodExited(pod) {
  1179. return nil
  1180. }
  1181. case evt := <-w.ResultChan():
  1182. pod, ok = evt.Object.(*v1.Pod)
  1183. if !ok {
  1184. return fmt.Errorf("unexpected object type: %T", evt.Object)
  1185. }
  1186. if appIsPodReady(pod) || appIsPodExited(pod) {
  1187. return nil
  1188. }
  1189. case <-time.After(time.Second * 10):
  1190. return errors.New("timed out waiting for pod")
  1191. }
  1192. }
  1193. }
  1194. func appIsPodReady(pod *v1.Pod) bool {
  1195. ready := false
  1196. conditions := pod.Status.Conditions
  1197. for i := range conditions {
  1198. if conditions[i].Type == v1.PodReady {
  1199. ready = pod.Status.Conditions[i].Status == v1.ConditionTrue
  1200. }
  1201. }
  1202. return ready
  1203. }
  1204. func appIsPodExited(pod *v1.Pod) bool {
  1205. return pod.Status.Phase == v1.PodSucceeded || pod.Status.Phase == v1.PodFailed
  1206. }
  1207. func appHandlePodAttachError(ctx context.Context, err error, config *KubernetesSharedConfig, namespace, podName, container string) error {
  1208. if appVerbose {
  1209. color.New(color.FgYellow).Fprintf(os.Stderr, "Error: %s\n", err)
  1210. }
  1211. color.New(color.FgYellow).Fprintln(os.Stderr, "Could not open a shell to this container. Container logs:")
  1212. var writtenBytes int64
  1213. writtenBytes, _ = appPipePodLogsToStdout(ctx, config, namespace, podName, container, false)
  1214. if appVerbose || writtenBytes == 0 {
  1215. color.New(color.FgYellow).Fprintln(os.Stderr, "Could not get logs. Pod events:")
  1216. _ = appPipeEventsToStdout(ctx, config, namespace, podName, container, false) //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
  1217. }
  1218. return err
  1219. }
  1220. func appPipePodLogsToStdout(ctx context.Context, config *KubernetesSharedConfig, namespace, name, container string, follow bool) (int64, error) {
  1221. podLogOpts := v1.PodLogOptions{
  1222. Container: container,
  1223. Follow: follow,
  1224. }
  1225. req := config.Clientset.CoreV1().Pods(namespace).GetLogs(name, &podLogOpts)
  1226. podLogs, err := req.Stream(
  1227. ctx,
  1228. )
  1229. if err != nil {
  1230. return 0, err
  1231. }
  1232. defer podLogs.Close()
  1233. return io.Copy(os.Stdout, podLogs)
  1234. }
  1235. func appPipeEventsToStdout(ctx context.Context, config *KubernetesSharedConfig, namespace, name, _ string, _ bool) error {
  1236. // update the config in case the operation has taken longer than token expiry time
  1237. config.setSharedConfig(ctx) //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
  1238. // creates the clientset
  1239. resp, err := config.Clientset.CoreV1().Events(namespace).List(
  1240. ctx,
  1241. metav1.ListOptions{
  1242. FieldSelector: fmt.Sprintf("involvedObject.name=%s,involvedObject.namespace=%s", name, namespace),
  1243. },
  1244. )
  1245. if err != nil {
  1246. return err
  1247. }
  1248. for _, event := range resp.Items {
  1249. color.New(color.FgRed).Println(event.Message)
  1250. }
  1251. return nil
  1252. }
  1253. func appGetExistingPod(ctx context.Context, config *KubernetesSharedConfig, name, namespace string) (*v1.Pod, error) {
  1254. return config.Clientset.CoreV1().Pods(namespace).Get(
  1255. ctx,
  1256. name,
  1257. metav1.GetOptions{},
  1258. )
  1259. }
  1260. func appDeletePod(ctx context.Context, config *KubernetesSharedConfig, name, namespace string) error {
  1261. // update the config in case the operation has taken longer than token expiry time
  1262. config.setSharedConfig(ctx) //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
  1263. err := config.Clientset.CoreV1().Pods(namespace).Delete(
  1264. ctx,
  1265. name,
  1266. metav1.DeleteOptions{},
  1267. )
  1268. if err != nil {
  1269. color.New(color.FgRed).Fprintf(os.Stderr, "Could not delete ephemeral pod: %s\n", err.Error())
  1270. return err
  1271. }
  1272. color.New(color.FgGreen).Println("Sucessfully deleted ephemeral pod")
  1273. return nil
  1274. }
  1275. func appCreateEphemeralPodFromExisting(
  1276. ctx context.Context,
  1277. config *KubernetesSharedConfig,
  1278. existing *v1.Pod,
  1279. container string,
  1280. args []string,
  1281. ) (*v1.Pod, error) {
  1282. newPod := existing.DeepCopy()
  1283. // only copy the pod spec, overwrite metadata
  1284. newPod.ObjectMeta = metav1.ObjectMeta{
  1285. Name: strings.ToLower(fmt.Sprintf("%s-copy-%s", existing.ObjectMeta.Name, utils.String(4))),
  1286. Namespace: existing.ObjectMeta.Namespace,
  1287. }
  1288. newPod.Status = v1.PodStatus{}
  1289. // set restart policy to never
  1290. newPod.Spec.RestartPolicy = v1.RestartPolicyNever
  1291. // change the command in the pod to the passed in pod command
  1292. cmdRoot := args[0]
  1293. cmdArgs := make([]string, 0)
  1294. // annotate with the ephemeral pod tag
  1295. newPod.Labels = make(map[string]string)
  1296. newPod.Labels["porter/ephemeral-pod"] = "true"
  1297. if len(args) > 1 {
  1298. cmdArgs = args[1:]
  1299. }
  1300. for i := 0; i < len(newPod.Spec.Containers); i++ {
  1301. if newPod.Spec.Containers[i].Name == container {
  1302. newPod.Spec.Containers[i].Command = []string{cmdRoot}
  1303. newPod.Spec.Containers[i].Args = cmdArgs
  1304. newPod.Spec.Containers[i].TTY = true
  1305. newPod.Spec.Containers[i].Stdin = true
  1306. newPod.Spec.Containers[i].StdinOnce = true
  1307. var newCpu int
  1308. if appCpuMilli != 0 {
  1309. newCpu = appCpuMilli
  1310. } else if newPod.Spec.Containers[i].Resources.Requests.Cpu() != nil && newPod.Spec.Containers[i].Resources.Requests.Cpu().MilliValue() > 500 {
  1311. newCpu = 500
  1312. }
  1313. if newCpu != 0 {
  1314. newPod.Spec.Containers[i].Resources.Limits[v1.ResourceCPU] = resource.MustParse(fmt.Sprintf("%dm", newCpu))
  1315. newPod.Spec.Containers[i].Resources.Requests[v1.ResourceCPU] = resource.MustParse(fmt.Sprintf("%dm", newCpu))
  1316. for j := 0; j < len(newPod.Spec.Containers[i].Env); j++ {
  1317. if newPod.Spec.Containers[i].Env[j].Name == "PORTER_RESOURCES_CPU" {
  1318. newPod.Spec.Containers[i].Env[j].Value = fmt.Sprintf("%dm", newCpu)
  1319. break
  1320. }
  1321. }
  1322. }
  1323. var newMemory int
  1324. if appMemoryMi != 0 {
  1325. newMemory = appMemoryMi
  1326. } else if newPod.Spec.Containers[i].Resources.Requests.Memory() != nil && newPod.Spec.Containers[i].Resources.Requests.Memory().Value() > 1000*1024*1024 {
  1327. newMemory = 1000
  1328. }
  1329. if newMemory != 0 {
  1330. newPod.Spec.Containers[i].Resources.Limits[v1.ResourceMemory] = resource.MustParse(fmt.Sprintf("%dMi", newMemory))
  1331. newPod.Spec.Containers[i].Resources.Requests[v1.ResourceMemory] = resource.MustParse(fmt.Sprintf("%dMi", newMemory))
  1332. for j := 0; j < len(newPod.Spec.Containers[i].Env); j++ {
  1333. if newPod.Spec.Containers[i].Env[j].Name == "PORTER_RESOURCES_RAM" {
  1334. newPod.Spec.Containers[i].Env[j].Value = fmt.Sprintf("%dMi", newMemory)
  1335. break
  1336. }
  1337. }
  1338. }
  1339. }
  1340. // remove health checks and probes
  1341. newPod.Spec.Containers[i].LivenessProbe = nil
  1342. newPod.Spec.Containers[i].ReadinessProbe = nil
  1343. newPod.Spec.Containers[i].StartupProbe = nil
  1344. }
  1345. newPod.Spec.NodeName = ""
  1346. // create the pod and return it
  1347. return config.Clientset.CoreV1().Pods(existing.ObjectMeta.Namespace).Create(
  1348. ctx,
  1349. newPod,
  1350. metav1.CreateOptions{},
  1351. )
  1352. }
  1353. func appUpdateTag(ctx context.Context, user *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, cmd *cobra.Command, args []string) error {
  1354. project, err := client.GetProject(ctx, cliConf.Project)
  1355. if err != nil {
  1356. return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
  1357. }
  1358. if project.ValidateApplyV2 {
  1359. err := v2.UpdateImage(ctx, v2.UpdateImageInput{
  1360. ProjectID: cliConf.Project,
  1361. ClusterID: cliConf.Cluster,
  1362. AppName: args[0],
  1363. DeploymentTargetName: deploymentTargetName,
  1364. Tag: appTag,
  1365. Client: client,
  1366. WaitForSuccessfulDeployment: appWait,
  1367. })
  1368. if err != nil {
  1369. return fmt.Errorf("error updating tag: %w", err)
  1370. }
  1371. return nil
  1372. } else {
  1373. namespace := fmt.Sprintf("porter-stack-%s", args[0])
  1374. if appTag == "" {
  1375. appTag = "latest"
  1376. }
  1377. release, err := client.GetRelease(ctx, cliConf.Project, cliConf.Cluster, namespace, args[0])
  1378. if err != nil {
  1379. return fmt.Errorf("Unable to find application %s", args[0])
  1380. }
  1381. repository, ok := release.Config["global"].(map[string]interface{})["image"].(map[string]interface{})["repository"].(string)
  1382. if !ok || repository == "" {
  1383. return fmt.Errorf("Application %s does not have an associated image repository. Unable to update tag", args[0])
  1384. }
  1385. imageInfo := types.ImageInfo{
  1386. Repository: repository,
  1387. Tag: appTag,
  1388. }
  1389. createUpdatePorterAppRequest := &types.CreatePorterAppRequest{
  1390. ClusterID: cliConf.Cluster,
  1391. ProjectID: cliConf.Project,
  1392. ImageInfo: imageInfo,
  1393. OverrideRelease: false,
  1394. }
  1395. _, _ = color.New(color.FgGreen).Printf("Updating application %s to build using tag \"%s\"\n", args[0], appTag)
  1396. _, err = client.CreatePorterApp(
  1397. ctx,
  1398. cliConf.Project,
  1399. cliConf.Cluster,
  1400. args[0],
  1401. createUpdatePorterAppRequest,
  1402. )
  1403. if err != nil {
  1404. return fmt.Errorf("Unable to update application %s: %w", args[0], err)
  1405. }
  1406. _, _ = color.New(color.FgGreen).Printf("Successfully updated application %s to use tag \"%s\"\n", args[0], appTag)
  1407. return nil
  1408. }
  1409. }
  1410. func getPodsFromV1PorterYaml(ctx context.Context, execArgs []string, client api.Client, cliConfig config.CLIConfig, porterAppName string, namespace string) ([]appPodSimple, []string, error) {
  1411. podsSimple, containerHasLauncherStartCommand, err := appGetPodsV1PorterYaml(ctx, cliConfig, client, namespace, porterAppName)
  1412. if err != nil {
  1413. return nil, nil, fmt.Errorf("could not retrieve list of pods: %s", err.Error())
  1414. }
  1415. if len(execArgs) > 0 && execArgs[0] != CommandPrefix_CNB_LIFECYCLE_LAUNCHER && execArgs[0] != CommandPrefix_LAUNCHER && containerHasLauncherStartCommand {
  1416. execArgs = append([]string{CommandPrefix_CNB_LIFECYCLE_LAUNCHER}, execArgs...)
  1417. }
  1418. return podsSimple, execArgs, nil
  1419. }
  1420. func getPodsFromV2PorterYaml(ctx context.Context, execArgs []string, client api.Client, cliConfig config.CLIConfig, porterAppName string, deploymentTargetName string) ([]appPodSimple, []string, string, error) {
  1421. podsSimple, namespace, containerHasLauncherStartCommand, err := appGetPodsV2PorterYaml(ctx, cliConfig, client, porterAppName, deploymentTargetName)
  1422. if err != nil {
  1423. return nil, nil, "", fmt.Errorf("could not retrieve list of pods: %w", err)
  1424. }
  1425. if len(execArgs) > 0 && execArgs[0] != CommandPrefix_CNB_LIFECYCLE_LAUNCHER && execArgs[0] != CommandPrefix_LAUNCHER && containerHasLauncherStartCommand {
  1426. execArgs = append([]string{CommandPrefix_CNB_LIFECYCLE_LAUNCHER}, execArgs...)
  1427. }
  1428. return podsSimple, execArgs, namespace, nil
  1429. }