create.go 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. package commands
  2. import (
  3. "context"
  4. "fmt"
  5. "io/ioutil"
  6. "os"
  7. "path/filepath"
  8. "strings"
  9. v2 "github.com/porter-dev/porter/cli/cmd/v2"
  10. "github.com/fatih/color"
  11. api "github.com/porter-dev/porter/api/client"
  12. "github.com/porter-dev/porter/api/types"
  13. "github.com/porter-dev/porter/cli/cmd/config"
  14. "github.com/porter-dev/porter/cli/cmd/deploy"
  15. "github.com/porter-dev/porter/cli/cmd/gitutils"
  16. "github.com/porter-dev/porter/cli/cmd/utils"
  17. "github.com/spf13/cobra"
  18. "k8s.io/client-go/util/homedir"
  19. "sigs.k8s.io/yaml"
  20. )
  21. var (
  22. name string
  23. values string
  24. source string
  25. image string
  26. registryURL string
  27. forceBuild bool
  28. )
  29. func registerCommand_Create(cliConf config.CLIConfig) *cobra.Command {
  30. createCmd := &cobra.Command{
  31. Use: "create [kind]",
  32. Args: cobra.ExactArgs(1),
  33. Short: "Creates a new application with name given by the --app flag.",
  34. Long: fmt.Sprintf(`
  35. %s
  36. Creates a new application with name given by the --app flag and a "kind", which can be one of
  37. web, worker, or job. For example:
  38. %s
  39. To modify the default configuration of the application, you can pass a values.yaml file in via the
  40. --values flag.
  41. %s
  42. To read more about the configuration options, go here:
  43. https://docs.porter.run/docs/deploying-from-the-cli#common-configuration-options
  44. This command will automatically build from a local path, and will create a new Docker image in your
  45. default Docker registry. The path can be configured via the --path flag. For example:
  46. %s
  47. To connect the application to Github, so that the application rebuilds and redeploys on each push
  48. to a Github branch, you can specify "--source github". If your local branch is set to track changes
  49. from an upstream remote branch, Porter will try to use the connected remote and remote branch as the
  50. Github repository to link to. Otherwise, Porter will use the remote given by origin. For example:
  51. %s
  52. To deploy an application from a Docker registry, use "--source registry" and pass the image in via the
  53. --image flag. The image flag must be of the form repository:tag. For example:
  54. %s
  55. `,
  56. color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter create\":"),
  57. color.New(color.FgGreen, color.Bold).Sprintf("porter create web --app example-app"),
  58. color.New(color.FgGreen, color.Bold).Sprintf("porter create web --app example-app --values values.yaml"),
  59. color.New(color.FgGreen, color.Bold).Sprintf("porter create web --app example-app --path ./path/to/app"),
  60. color.New(color.FgGreen, color.Bold).Sprintf("porter create web --app example-app --source github"),
  61. color.New(color.FgGreen, color.Bold).Sprintf("porter create web --app example-app --source registry --image gcr.io/snowflake-12345/example-app:latest"),
  62. ),
  63. Run: func(cmd *cobra.Command, args []string) {
  64. err := checkLoginAndRunWithConfig(cmd, cliConf, args, createFull)
  65. if err != nil {
  66. os.Exit(1)
  67. }
  68. },
  69. }
  70. createCmd.PersistentFlags().StringVar(
  71. &name,
  72. "app",
  73. "",
  74. "name of the new application/job/worker.",
  75. )
  76. createCmd.MarkPersistentFlagRequired("app")
  77. createCmd.PersistentFlags().StringVarP(
  78. &localPath,
  79. "path",
  80. "p",
  81. "",
  82. "if local build, the path to the build directory",
  83. )
  84. createCmd.PersistentFlags().StringVar(
  85. &namespace,
  86. "namespace",
  87. "default",
  88. "namespace of the application",
  89. )
  90. createCmd.PersistentFlags().StringVarP(
  91. &values,
  92. "values",
  93. "v",
  94. "",
  95. "filepath to a values.yaml file",
  96. )
  97. createCmd.PersistentFlags().StringVar(
  98. &dockerfile,
  99. "dockerfile",
  100. "",
  101. "the path to the dockerfile",
  102. )
  103. createCmd.PersistentFlags().StringArrayVarP(
  104. &buildFlagsEnv,
  105. "env",
  106. "e",
  107. []string{},
  108. "Build-time environment variable, in the form 'VAR=VALUE'. These are not available at image runtime.",
  109. )
  110. createCmd.PersistentFlags().StringVar(
  111. &method,
  112. "method",
  113. "",
  114. "the build method to use (\"docker\" or \"pack\")",
  115. )
  116. createCmd.PersistentFlags().StringVar(
  117. &source,
  118. "source",
  119. "local",
  120. "the type of source (\"local\", \"github\", or \"registry\")",
  121. )
  122. createCmd.PersistentFlags().StringVar(
  123. &image,
  124. "image",
  125. "",
  126. "if the source is \"registry\", the image to use, in repository:tag format",
  127. )
  128. createCmd.PersistentFlags().StringVar(
  129. &registryURL,
  130. "registry-url",
  131. "",
  132. "the registry URL to use (must exist in \"porter registries list\")",
  133. )
  134. createCmd.PersistentFlags().BoolVar(
  135. &forceBuild,
  136. "force-build",
  137. false,
  138. "set this to force build an image",
  139. )
  140. createCmd.PersistentFlags().BoolVar(
  141. &useCache,
  142. "use-cache",
  143. false,
  144. "Whether to use cache (currently in beta)",
  145. )
  146. createCmd.PersistentFlags().MarkDeprecated("force-build", "--force-build is deprecated")
  147. return createCmd
  148. }
  149. var supportedKinds = map[string]string{"web": "", "job": "", "worker": ""}
  150. func createFull(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, cmd *cobra.Command, args []string) error {
  151. if featureFlags.ValidateApplyV2Enabled {
  152. err := v2.CreateFull(ctx)
  153. if err != nil {
  154. return err
  155. }
  156. return nil
  157. }
  158. // check the kind
  159. if _, exists := supportedKinds[args[0]]; !exists {
  160. return fmt.Errorf("%s is not a supported type: specify web, job, or worker", args[0])
  161. }
  162. fullPath, err := filepath.Abs(localPath)
  163. if err != nil {
  164. return err
  165. }
  166. if os.Getenv("GITHUB_ACTIONS") == "" && source == "local" && fullPath == homedir.HomeDir() {
  167. proceed, err := utils.PromptConfirm("You are deploying your home directory. Do you want to continue?", false)
  168. if err != nil {
  169. return err
  170. }
  171. if !proceed {
  172. return nil
  173. }
  174. }
  175. // read the values if necessary
  176. valuesObj, err := readValuesFile()
  177. if err != nil {
  178. return err
  179. }
  180. color.New(color.FgGreen).Printf("Creating %s release: %s\n", args[0], name)
  181. var buildMethod deploy.DeployBuildType
  182. if method != "" {
  183. buildMethod = deploy.DeployBuildType(method)
  184. } else if dockerfile != "" {
  185. buildMethod = deploy.DeployBuildTypeDocker
  186. }
  187. // add additional env, if they exist
  188. additionalEnv := make(map[string]string)
  189. for _, buildEnv := range buildFlagsEnv {
  190. if strSplArr := strings.SplitN(buildEnv, "=", 2); len(strSplArr) >= 2 {
  191. additionalEnv[strSplArr[0]] = strSplArr[1]
  192. }
  193. }
  194. createAgent := &deploy.CreateAgent{
  195. Client: client,
  196. CreateOpts: &deploy.CreateOpts{
  197. SharedOpts: &deploy.SharedOpts{
  198. ProjectID: cliConf.Project,
  199. ClusterID: cliConf.Cluster,
  200. Namespace: namespace,
  201. LocalPath: fullPath,
  202. LocalDockerfile: dockerfile,
  203. Method: buildMethod,
  204. AdditionalEnv: additionalEnv,
  205. UseCache: useCache,
  206. },
  207. Kind: args[0],
  208. ReleaseName: name,
  209. RegistryURL: registryURL,
  210. },
  211. }
  212. if source == "local" {
  213. if useCache {
  214. regID, imageURL, err := createAgent.GetImageRepoURL(ctx, name, namespace)
  215. if err != nil {
  216. return err
  217. }
  218. err = client.CreateRepository(
  219. ctx,
  220. cliConf.Project,
  221. regID,
  222. &types.CreateRegistryRepositoryRequest{
  223. ImageRepoURI: imageURL,
  224. },
  225. )
  226. if err != nil {
  227. return err
  228. }
  229. err = config.SetDockerConfig(ctx, createAgent.Client, cliConf.Project)
  230. if err != nil {
  231. return err
  232. }
  233. }
  234. subdomain, err := createAgent.CreateFromDocker(ctx, valuesObj, "default", nil)
  235. return handleSubdomainCreate(subdomain, err)
  236. } else if source == "github" {
  237. return createFromGithub(ctx, createAgent, valuesObj)
  238. }
  239. subdomain, err := createAgent.CreateFromRegistry(ctx, image, valuesObj)
  240. return handleSubdomainCreate(subdomain, err)
  241. }
  242. func handleSubdomainCreate(subdomain string, err error) error {
  243. if err != nil {
  244. return err
  245. }
  246. if subdomain != "" {
  247. color.New(color.FgGreen).Printf("Your web application is ready at: %s\n", subdomain)
  248. } else {
  249. color.New(color.FgGreen).Printf("Application created successfully\n")
  250. }
  251. return nil
  252. }
  253. func createFromGithub(ctx context.Context, createAgent *deploy.CreateAgent, overrideValues map[string]interface{}) error {
  254. fullPath, err := filepath.Abs(localPath)
  255. if err != nil {
  256. return err
  257. }
  258. _, err = gitutils.GitDirectory(fullPath)
  259. if err != nil {
  260. return err
  261. }
  262. remote, gitBranch, err := gitutils.GetRemoteBranch(fullPath)
  263. if err != nil {
  264. return err
  265. } else if gitBranch == "" {
  266. return fmt.Errorf("git branch not automatically detectable")
  267. }
  268. ok, remoteRepo := gitutils.ParseGithubRemote(remote)
  269. if !ok {
  270. return fmt.Errorf("remote is not a Github repository")
  271. }
  272. subdomain, err := createAgent.CreateFromGithub(
  273. ctx,
  274. &deploy.GithubOpts{
  275. Branch: gitBranch,
  276. Repo: remoteRepo,
  277. }, overrideValues)
  278. return handleSubdomainCreate(subdomain, err)
  279. }
  280. func readValuesFile() (map[string]interface{}, error) {
  281. res := make(map[string]interface{})
  282. if values == "" {
  283. return res, nil
  284. }
  285. valuesFilePath, err := filepath.Abs(values)
  286. if err != nil {
  287. return nil, err
  288. }
  289. if info, err := os.Stat(valuesFilePath); os.IsNotExist(err) || info.IsDir() {
  290. return nil, fmt.Errorf("values file does not exist or is a directory")
  291. }
  292. reader, err := os.Open(valuesFilePath)
  293. if err != nil {
  294. return nil, err
  295. }
  296. bytes, err := ioutil.ReadAll(reader)
  297. if err != nil {
  298. return nil, err
  299. }
  300. err = yaml.Unmarshal(bytes, &res)
  301. if err != nil {
  302. return nil, err
  303. }
  304. return res, nil
  305. }