create.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505
  1. package deploy
  2. import (
  3. "context"
  4. "fmt"
  5. "os"
  6. "path/filepath"
  7. "strings"
  8. "github.com/porter-dev/porter/cli/cmd/api"
  9. "github.com/porter-dev/porter/cli/cmd/docker"
  10. "github.com/porter-dev/porter/internal/templater/utils"
  11. )
  12. // CreateAgent handles the creation of a new application on Porter
  13. type CreateAgent struct {
  14. Client *api.Client
  15. CreateOpts *CreateOpts
  16. }
  17. // CreateOpts are required options for creating a new application on Porter: the
  18. // "kind" (web, worker, job) and the name of the application.
  19. type CreateOpts struct {
  20. *SharedOpts
  21. Kind string
  22. ReleaseName string
  23. }
  24. // GithubOpts are the options for linking a Github source to the app
  25. type GithubOpts struct {
  26. Branch string
  27. Repo string
  28. }
  29. // CreateFromGithub uses the branch/repo to link the Github source for an application.
  30. // This function attempts to find a matching repository in the list of linked repositories
  31. // on Porter. If one is found, it will use that repository as the app source.
  32. func (c *CreateAgent) CreateFromGithub(
  33. ghOpts *GithubOpts,
  34. overrideValues map[string]interface{},
  35. ) (string, error) {
  36. opts := c.CreateOpts
  37. // get all linked github repos and find matching repo
  38. gitRepos, err := c.Client.ListGitRepos(
  39. context.Background(),
  40. c.CreateOpts.ProjectID,
  41. )
  42. if err != nil {
  43. return "", err
  44. }
  45. var gitRepoMatch uint
  46. for _, gitRepo := range gitRepos {
  47. // for each git repo, search for a matching username/owner
  48. githubRepos, err := c.Client.ListGithubRepos(
  49. context.Background(),
  50. c.CreateOpts.ProjectID,
  51. gitRepo,
  52. )
  53. if err != nil {
  54. return "", err
  55. }
  56. for _, githubRepo := range githubRepos {
  57. if githubRepo.FullName == ghOpts.Repo {
  58. gitRepoMatch = gitRepo
  59. break
  60. }
  61. }
  62. if gitRepoMatch != 0 {
  63. break
  64. }
  65. }
  66. if gitRepoMatch == 0 {
  67. return "", fmt.Errorf("could not find a linked github repo for %s. Make sure you have linked your Github account on the Porter dashboard.", ghOpts.Repo)
  68. }
  69. latestVersion, mergedValues, err := c.getMergedValues(overrideValues)
  70. if err != nil {
  71. return "", err
  72. }
  73. if opts.Kind == "web" || opts.Kind == "worker" {
  74. mergedValues["image"] = map[string]interface{}{
  75. "repository": "public.ecr.aws/o1j4x7p4/hello-porter",
  76. "tag": "latest",
  77. }
  78. } else if opts.Kind == "job" {
  79. mergedValues["image"] = map[string]interface{}{
  80. "repository": "public.ecr.aws/o1j4x7p4/hello-porter-job",
  81. "tag": "latest",
  82. }
  83. }
  84. regID, imageURL, err := c.GetImageRepoURL(opts.ReleaseName, opts.Namespace)
  85. if err != nil {
  86. return "", err
  87. }
  88. env, err := GetEnvFromConfig(mergedValues)
  89. if err != nil {
  90. env = map[string]string{}
  91. }
  92. subdomain, err := c.CreateSubdomainIfRequired(mergedValues)
  93. if err != nil {
  94. return "", err
  95. }
  96. err = c.Client.DeployTemplate(
  97. context.Background(),
  98. opts.ProjectID,
  99. opts.ClusterID,
  100. opts.Kind,
  101. latestVersion,
  102. &api.DeployTemplateRequest{
  103. TemplateName: opts.Kind,
  104. ImageURL: imageURL,
  105. FormValues: mergedValues,
  106. Namespace: opts.Namespace,
  107. Name: opts.ReleaseName,
  108. GitAction: &api.DeployTemplateGitAction{
  109. GitRepo: ghOpts.Repo,
  110. GitBranch: ghOpts.Branch,
  111. ImageRepoURI: imageURL,
  112. DockerfilePath: opts.LocalDockerfile,
  113. FolderPath: ".",
  114. GitRepoID: gitRepoMatch,
  115. BuildEnv: env,
  116. RegistryID: regID,
  117. },
  118. },
  119. )
  120. if err != nil {
  121. return "", err
  122. }
  123. return subdomain, nil
  124. }
  125. // CreateFromRegistry deploys a new application from an existing Docker repository + tag.
  126. func (c *CreateAgent) CreateFromRegistry(
  127. image string,
  128. overrideValues map[string]interface{},
  129. ) (string, error) {
  130. if image == "" {
  131. return "", fmt.Errorf("image cannot be empty")
  132. }
  133. // split image into image-path:tag format
  134. imageSpl := strings.Split(image, ":")
  135. if len(imageSpl) != 2 {
  136. return "", fmt.Errorf("invalid image format: must be image-path:tag format")
  137. }
  138. opts := c.CreateOpts
  139. latestVersion, mergedValues, err := c.getMergedValues(overrideValues)
  140. if err != nil {
  141. return "", err
  142. }
  143. mergedValues["image"] = map[string]interface{}{
  144. "repository": imageSpl[0],
  145. "tag": imageSpl[1],
  146. }
  147. subdomain, err := c.CreateSubdomainIfRequired(mergedValues)
  148. if err != nil {
  149. return "", err
  150. }
  151. err = c.Client.DeployTemplate(
  152. context.Background(),
  153. opts.ProjectID,
  154. opts.ClusterID,
  155. opts.Kind,
  156. latestVersion,
  157. &api.DeployTemplateRequest{
  158. TemplateName: opts.Kind,
  159. ImageURL: imageSpl[0],
  160. FormValues: mergedValues,
  161. Namespace: opts.Namespace,
  162. Name: opts.ReleaseName,
  163. },
  164. )
  165. if err != nil {
  166. return "", err
  167. }
  168. return subdomain, nil
  169. }
  170. // CreateFromDocker uses a local build context and a local Docker daemon to build a new
  171. // container image, and then deploys it onto Porter.
  172. func (c *CreateAgent) CreateFromDocker(
  173. overrideValues map[string]interface{},
  174. ) (string, error) {
  175. opts := c.CreateOpts
  176. // detect the build config
  177. if opts.Method != "" {
  178. if opts.Method == DeployBuildTypeDocker {
  179. if opts.LocalDockerfile == "" {
  180. hasDockerfile := c.HasDefaultDockerfile(opts.LocalPath)
  181. if !hasDockerfile {
  182. return "", fmt.Errorf("Dockerfile not found")
  183. }
  184. opts.LocalDockerfile = "Dockerfile"
  185. }
  186. }
  187. } else {
  188. // try to detect dockerfile, otherwise fall back to `pack`
  189. hasDockerfile := c.HasDefaultDockerfile(opts.LocalPath)
  190. if !hasDockerfile {
  191. opts.Method = DeployBuildTypePack
  192. } else {
  193. opts.Method = DeployBuildTypeDocker
  194. opts.LocalDockerfile = "Dockerfile"
  195. }
  196. }
  197. // overwrite with docker image repository and tag
  198. regID, imageURL, err := c.GetImageRepoURL(opts.ReleaseName, opts.Namespace)
  199. if err != nil {
  200. return "", err
  201. }
  202. latestVersion, mergedValues, err := c.getMergedValues(overrideValues)
  203. if err != nil {
  204. return "", err
  205. }
  206. mergedValues["image"] = map[string]interface{}{
  207. "repository": imageURL,
  208. "tag": "latest",
  209. }
  210. // create docker agen
  211. agent, err := docker.NewAgentWithAuthGetter(c.Client, opts.ProjectID)
  212. if err != nil {
  213. return "", err
  214. }
  215. env, err := GetEnvFromConfig(mergedValues)
  216. if err != nil {
  217. env = map[string]string{}
  218. }
  219. buildAgent := &BuildAgent{
  220. SharedOpts: opts.SharedOpts,
  221. client: c.Client,
  222. imageRepo: imageURL,
  223. env: env,
  224. imageExists: false,
  225. }
  226. if opts.Method == DeployBuildTypeDocker {
  227. err = buildAgent.BuildDocker(agent, opts.LocalPath, ".", opts.LocalDockerfile, "latest")
  228. } else {
  229. err = buildAgent.BuildPack(agent, opts.LocalPath, "latest")
  230. }
  231. if err != nil {
  232. return "", err
  233. }
  234. // create repository
  235. err = c.Client.CreateRepository(
  236. context.Background(),
  237. opts.ProjectID,
  238. regID,
  239. &api.CreateRepositoryRequest{
  240. ImageRepoURI: imageURL,
  241. },
  242. )
  243. if err != nil {
  244. return "", err
  245. }
  246. retryCount := 1
  247. // sometimes newly created gcr repositories aren't ready on the initial
  248. // creation, so retry count is set higher
  249. if strings.Contains(imageURL, "gcr.io") {
  250. retryCount = 5
  251. }
  252. err = agent.PushImage(fmt.Sprintf("%s:%s", imageURL, "latest"), retryCount)
  253. if err != nil {
  254. return "", err
  255. }
  256. subdomain, err := c.CreateSubdomainIfRequired(mergedValues)
  257. if err != nil {
  258. return "", err
  259. }
  260. err = c.Client.DeployTemplate(
  261. context.Background(),
  262. opts.ProjectID,
  263. opts.ClusterID,
  264. opts.Kind,
  265. latestVersion,
  266. &api.DeployTemplateRequest{
  267. TemplateName: opts.Kind,
  268. ImageURL: imageURL,
  269. FormValues: mergedValues,
  270. Namespace: opts.Namespace,
  271. Name: opts.ReleaseName,
  272. },
  273. )
  274. if err != nil {
  275. return "", err
  276. }
  277. return subdomain, nil
  278. }
  279. // HasDefaultDockerfile detects if there is a dockerfile at the path `./Dockerfile`
  280. func (c *CreateAgent) HasDefaultDockerfile(buildPath string) bool {
  281. dockerFilePath := filepath.Join(buildPath, "./Dockerfile")
  282. info, err := os.Stat(dockerFilePath)
  283. return err == nil && !os.IsNotExist(err) && !info.IsDir()
  284. }
  285. // GetImageRepoURL creates the image repository url by finding the first valid image
  286. // registry linked to Porter, and then generates a new name of the form:
  287. // `{registry}/{name}-{namespace}`
  288. func (c *CreateAgent) GetImageRepoURL(name, namespace string) (uint, string, error) {
  289. // get all image registries linked to the project
  290. // get the list of namespaces
  291. registries, err := c.Client.ListRegistries(
  292. context.Background(),
  293. c.CreateOpts.ProjectID,
  294. )
  295. if err != nil {
  296. return 0, "", err
  297. } else if len(registries) == 0 {
  298. return 0, "", fmt.Errorf("must have created or linked an image registry")
  299. }
  300. // get the first non-empty registry
  301. var imageURI string
  302. var regID uint
  303. for _, reg := range registries {
  304. if reg.URL != "" {
  305. regID = reg.ID
  306. imageURI = fmt.Sprintf("%s/%s-%s", reg.URL, name, namespace)
  307. break
  308. }
  309. }
  310. return regID, imageURI, nil
  311. }
  312. // GetLatestTemplateVersion retrieves the latest template version for a specific
  313. // Porter template from the chart repository.
  314. func (c *CreateAgent) GetLatestTemplateVersion(templateName string) (string, error) {
  315. templates, err := c.Client.ListTemplates(
  316. context.Background(),
  317. )
  318. if err != nil {
  319. return "", err
  320. }
  321. var version string
  322. // find the matching template name
  323. for _, template := range templates {
  324. if templateName == template.Name {
  325. version = template.Versions[0]
  326. break
  327. }
  328. }
  329. if version == "" {
  330. return "", fmt.Errorf("matching template version not found")
  331. }
  332. return version, nil
  333. }
  334. // GetLatestTemplateDefaultValues gets the default config (`values.yaml`) set for a specific
  335. // template.
  336. func (c *CreateAgent) GetLatestTemplateDefaultValues(templateName, templateVersion string) (map[string]interface{}, error) {
  337. chart, err := c.Client.GetTemplate(
  338. context.Background(),
  339. templateName,
  340. templateVersion,
  341. )
  342. if err != nil {
  343. return nil, err
  344. }
  345. return chart.Values, nil
  346. }
  347. func (c *CreateAgent) getMergedValues(overrideValues map[string]interface{}) (string, map[string]interface{}, error) {
  348. // deploy the template
  349. latestVersion, err := c.GetLatestTemplateVersion(c.CreateOpts.Kind)
  350. if err != nil {
  351. return "", nil, err
  352. }
  353. // get the values of the template
  354. values, err := c.GetLatestTemplateDefaultValues(c.CreateOpts.Kind, latestVersion)
  355. if err != nil {
  356. return "", nil, err
  357. }
  358. // merge existing values with overriding values
  359. mergedValues := utils.CoalesceValues(values, overrideValues)
  360. return latestVersion, mergedValues, err
  361. }
  362. func (c *CreateAgent) CreateSubdomainIfRequired(mergedValues map[string]interface{}) (string, error) {
  363. subdomain := ""
  364. // check for automatic subdomain creation if web kind
  365. if c.CreateOpts.Kind == "web" {
  366. // look for ingress.enabled and no custom domains set
  367. ingressMap, err := getNestedMap(mergedValues, "ingress")
  368. if err == nil {
  369. enabledVal, enabledExists := ingressMap["enabled"]
  370. customDomVal, customDomExists := ingressMap["custom_domain"]
  371. if enabledExists && customDomExists {
  372. enabled, eOK := enabledVal.(bool)
  373. customDomain, cOK := customDomVal.(bool)
  374. // in the case of ingress enabled but no custom domain, create subdomain
  375. if eOK && cOK && enabled && !customDomain {
  376. dnsRecord, err := c.Client.CreateDNSRecord(
  377. context.Background(),
  378. c.CreateOpts.ProjectID,
  379. c.CreateOpts.ClusterID,
  380. &api.CreateDNSRecordRequest{
  381. ReleaseName: c.CreateOpts.ReleaseName,
  382. },
  383. )
  384. if err != nil {
  385. return "", fmt.Errorf("Error creating subdomain: %s", err.Error())
  386. }
  387. subdomain = dnsRecord.ExternalURL
  388. if ingressVal, ok := mergedValues["ingress"]; !ok {
  389. mergedValues["ingress"] = map[string]interface{}{
  390. "porter_hosts": []string{
  391. subdomain,
  392. },
  393. }
  394. } else {
  395. ingressValMap := ingressVal.(map[string]interface{})
  396. ingressValMap["porter_hosts"] = []string{
  397. subdomain,
  398. }
  399. }
  400. }
  401. }
  402. }
  403. }
  404. return subdomain, nil
  405. }