app.go 42 KB

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