app_create.go 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. package v2
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "github.com/charmbracelet/huh"
  7. "github.com/fatih/color"
  8. api "github.com/porter-dev/porter/api/client"
  9. "github.com/porter-dev/porter/cli/cmd/config"
  10. v2 "github.com/porter-dev/porter/internal/porter_app/v2"
  11. )
  12. // AppDeployMethod is the deployment method for the app
  13. type AppDeployMethod string
  14. const (
  15. // AppDeployMethod_Repo is the deployment method when source code is in a git repository and built on apply
  16. AppDeployMethod_Repo AppDeployMethod = "repo"
  17. // AppDeployMethod_Docker is the deployment method when app is sourced from a docker image
  18. AppDeployMethod_Docker AppDeployMethod = "docker"
  19. )
  20. const herokuDefaultBuilder = "heroku/buildpacks:20"
  21. // CreateAppInput is the input for the CreateApp function
  22. type CreateAppInput struct {
  23. CLIConfig config.CLIConfig
  24. // Client is the Porter API client
  25. Client api.Client
  26. // AppName is the name of the app. If not provided, the user will be prompted to provide one
  27. AppName string
  28. // DeploymentMethod is the deployment method for the app, either 'repo' or 'docker'
  29. DeploymentMethod string
  30. // PorterYamlPath is the path to the porter.yaml file.
  31. PorterYamlPath string
  32. // DeploymentTargetName is the name of the deployment target, if provided
  33. DeploymentTargetName string
  34. // BuildMethod is the build method for the app on apply, either 'docker' or 'pack'
  35. BuildMethod string
  36. // Dockerfile is the path to the Dockerfile when build method is 'docker'
  37. Dockerfile string
  38. // Builder is the builder to use when build method is 'pack'
  39. Builder string
  40. // Buildpacks is the buildpacks to use when build method is 'pack'
  41. Buildpacks []string
  42. // BuildContext is the build context for the app, e.g. ./app
  43. BuildContext string
  44. // ImageTag is the image tag to use for the app build
  45. ImageTag string
  46. // ImageRepo is the image repository to use for the app build
  47. ImageRepo string
  48. // EnvGroups is a list of any env groups to attach to the app
  49. EnvGroups []string
  50. }
  51. // CreateApp creates a new app in the Porter project, either from a Porter YAML file or through a form
  52. func CreateApp(ctx context.Context, inp CreateAppInput) error {
  53. if inp.PorterYamlPath == "" {
  54. err := createWithForm(&inp)
  55. if err != nil {
  56. if !errors.Is(err, huh.ErrUserAborted) {
  57. return err
  58. }
  59. return nil
  60. }
  61. }
  62. var builder string
  63. if inp.BuildMethod == "pack" {
  64. builder = herokuDefaultBuilder
  65. }
  66. patchOps := v2.PatchOperationsFromFlagValues(v2.PatchOperationsFromFlagValuesInput{
  67. EnvGroups: inp.EnvGroups,
  68. BuildMethod: inp.BuildMethod,
  69. Builder: builder,
  70. BuildContext: inp.BuildContext,
  71. Buildpacks: inp.Buildpacks,
  72. Dockerfile: inp.Dockerfile,
  73. ImageRepository: inp.ImageRepo,
  74. ImageTag: inp.ImageTag,
  75. })
  76. err := Apply(ctx, ApplyInput{
  77. CLIConfig: inp.CLIConfig,
  78. Client: inp.Client,
  79. PorterYamlPath: inp.PorterYamlPath,
  80. AppName: inp.AppName,
  81. ImageTagOverride: inp.ImageTag,
  82. PatchOperations: patchOps,
  83. })
  84. if err != nil {
  85. return fmt.Errorf("error applying app: %w", err)
  86. }
  87. return nil
  88. }
  89. func createWithForm(inp *CreateAppInput) error {
  90. color.New(color.FgGreen).Printf("Creating a new app\n\n") // nolint:errcheck,gosec
  91. color.New(color.FgBlue).Println("Get started by providing some information about your app.") // nolint:errcheck,gosec
  92. var formGroups []*huh.Group
  93. if inp.AppName == "" {
  94. formGroups = append(formGroups, WithNameOption(inp))
  95. }
  96. var deployMethod AppDeployMethod
  97. if inp.DeploymentMethod != "" {
  98. method, err := validDeployMethod(inp.DeploymentMethod)
  99. if err != nil {
  100. return fmt.Errorf("error getting deployment method: %w", err)
  101. }
  102. deployMethod = method
  103. }
  104. if deployMethod == "" {
  105. formGroups = append(formGroups, WithDeployMethodOption(inp))
  106. }
  107. if inp.BuildContext == "" {
  108. formGroups = append(formGroups, WithBuildContextOption(inp))
  109. }
  110. if inp.BuildMethod == "" {
  111. formGroups = append(formGroups, WithBuildMethodOption(inp))
  112. if inp.Dockerfile == "" {
  113. formGroups = append(formGroups, WithDockerfileOption(inp))
  114. }
  115. if len(inp.Buildpacks) == 0 {
  116. formGroups = append(formGroups, WithBuildpackOptions(inp))
  117. }
  118. }
  119. if inp.ImageRepo == "" || inp.ImageTag == "" {
  120. formGroups = append(formGroups, WithImageOptions(inp))
  121. }
  122. if len(formGroups) > 0 {
  123. form := huh.NewForm(formGroups...)
  124. err := form.Run()
  125. if err != nil {
  126. return err
  127. }
  128. }
  129. return nil
  130. }
  131. // CreateAppFormOption is a functional option for configuring the CreateAppInput through a form
  132. type CreateAppFormOption func(*CreateAppInput) *huh.Group
  133. // WithNameOption returns a form group for the app name
  134. func WithNameOption(inp *CreateAppInput) *huh.Group {
  135. return huh.NewGroup(
  136. huh.NewInput().Title("App Name").CharLimit(31).Value(&inp.AppName),
  137. )
  138. }
  139. // WithDeployMethodOption returns a form group for the deployment method
  140. func WithDeployMethodOption(inp *CreateAppInput) *huh.Group {
  141. return huh.NewGroup(
  142. huh.NewSelect[string]().Title("Deployment Method").Options(
  143. huh.NewOption("Docker", "docker"),
  144. huh.NewOption("From Repository", "repo"),
  145. ).Value(&inp.DeploymentMethod),
  146. )
  147. }
  148. // WithBuildContextOption returns a form group for the build context
  149. func WithBuildContextOption(inp *CreateAppInput) *huh.Group {
  150. return huh.NewGroup(
  151. huh.NewInput().Title("Build Context").Value(&inp.BuildContext),
  152. ).WithHideFunc(func() bool {
  153. return inp.DeploymentMethod != string(AppDeployMethod_Repo)
  154. })
  155. }
  156. // WithBuildMethodOption returns a form group for the build method
  157. func WithBuildMethodOption(inp *CreateAppInput) *huh.Group {
  158. return huh.NewGroup(
  159. huh.NewSelect[string]().Title("Build Method").Options(
  160. huh.NewOption("Dockerfile", "docker"),
  161. huh.NewOption("Buildpacks", "pack"),
  162. ).Value(&inp.BuildMethod),
  163. ).WithHideFunc(func() bool {
  164. return inp.DeploymentMethod != string(AppDeployMethod_Repo)
  165. })
  166. }
  167. // WithBuildpackOptions returns a form group for the buildpack options
  168. func WithBuildpackOptions(inp *CreateAppInput) *huh.Group {
  169. return huh.NewGroup(
  170. huh.NewMultiSelect[string]().Title("Buildpacks").Options(
  171. huh.NewOption("Node.js", "heroku/nodejs"),
  172. huh.NewOption("Ruby", "heroku/ruby"),
  173. huh.NewOption("Python", "heroku/python"),
  174. huh.NewOption("Go", "heroku/go"),
  175. huh.NewOption("Java", "heroku/java"),
  176. ).Value(&inp.Buildpacks),
  177. ).WithHideFunc(func() bool {
  178. return inp.BuildMethod != "pack"
  179. })
  180. }
  181. // WithDockerfileOption returns a form group for the Dockerfile path
  182. func WithDockerfileOption(inp *CreateAppInput) *huh.Group {
  183. return huh.NewGroup(
  184. huh.NewInput().Title("Dockerfile Path").Value(&inp.Dockerfile),
  185. ).WithHideFunc(func() bool {
  186. return inp.BuildMethod != "docker"
  187. })
  188. }
  189. // WithImageOptions returns a form group for the image repository and tag
  190. func WithImageOptions(inp *CreateAppInput) *huh.Group {
  191. return huh.NewGroup(
  192. huh.NewInput().Title("Image Repository").Value(&inp.ImageRepo),
  193. huh.NewInput().Title("Image Tag").Value(&inp.ImageTag),
  194. ).WithHideFunc(func() bool {
  195. return inp.DeploymentMethod != string(AppDeployMethod_Docker)
  196. })
  197. }
  198. func validDeployMethod(m string) (AppDeployMethod, error) {
  199. var method AppDeployMethod
  200. switch m {
  201. case string(AppDeployMethod_Repo):
  202. method = AppDeployMethod_Repo
  203. case string(AppDeployMethod_Docker):
  204. method = AppDeployMethod_Docker
  205. default:
  206. return method, fmt.Errorf("invalid deployment method: %s", method)
  207. }
  208. return method, nil
  209. }