create.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  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.ID,
  52. )
  53. if err != nil {
  54. return "", err
  55. }
  56. for _, githubRepo := range githubRepos {
  57. if githubRepo.FullName == ghOpts.Repo {
  58. gitRepoMatch = gitRepo.ID
  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, "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. err = agent.PushImage(fmt.Sprintf("%s:%s", imageURL, "latest"))
  247. if err != nil {
  248. return "", err
  249. }
  250. subdomain, err := c.CreateSubdomainIfRequired(mergedValues)
  251. if err != nil {
  252. return "", err
  253. }
  254. err = c.Client.DeployTemplate(
  255. context.Background(),
  256. opts.ProjectID,
  257. opts.ClusterID,
  258. opts.Kind,
  259. latestVersion,
  260. &api.DeployTemplateRequest{
  261. TemplateName: opts.Kind,
  262. ImageURL: imageURL,
  263. FormValues: mergedValues,
  264. Namespace: opts.Namespace,
  265. Name: opts.ReleaseName,
  266. },
  267. )
  268. if err != nil {
  269. return "", err
  270. }
  271. return subdomain, nil
  272. }
  273. // HasDefaultDockerfile detects if there is a dockerfile at the path `./Dockerfile`
  274. func (c *CreateAgent) HasDefaultDockerfile(buildPath string) bool {
  275. dockerFilePath := filepath.Join(buildPath, "./Dockerfile")
  276. info, err := os.Stat(dockerFilePath)
  277. return err == nil && !os.IsNotExist(err) && !info.IsDir()
  278. }
  279. // GetImageRepoURL creates the image repository url by finding the first valid image
  280. // registry linked to Porter, and then generates a new name of the form:
  281. // `{registry}/{name}-{namespace}`
  282. func (c *CreateAgent) GetImageRepoURL(name, namespace string) (uint, string, error) {
  283. // get all image registries linked to the project
  284. // get the list of namespaces
  285. registries, err := c.Client.ListRegistries(
  286. context.Background(),
  287. c.CreateOpts.ProjectID,
  288. )
  289. if err != nil {
  290. return 0, "", err
  291. } else if len(registries) == 0 {
  292. return 0, "", fmt.Errorf("must have created or linked an image registry")
  293. }
  294. // get the first non-empty registry
  295. var imageURI string
  296. var regID uint
  297. for _, reg := range registries {
  298. if reg.URL != "" {
  299. regID = reg.ID
  300. imageURI = fmt.Sprintf("%s/%s-%s", reg.URL, name, namespace)
  301. break
  302. }
  303. }
  304. return regID, imageURI, nil
  305. }
  306. // GetLatestTemplateVersion retrieves the latest template version for a specific
  307. // Porter template from the chart repository.
  308. func (c *CreateAgent) GetLatestTemplateVersion(templateName string) (string, error) {
  309. templates, err := c.Client.ListTemplates(
  310. context.Background(),
  311. )
  312. if err != nil {
  313. return "", err
  314. }
  315. var version string
  316. // find the matching template name
  317. for _, template := range templates {
  318. if templateName == template.Name {
  319. version = template.Versions[0]
  320. break
  321. }
  322. }
  323. if version == "" {
  324. return "", fmt.Errorf("matching template version not found")
  325. }
  326. return version, nil
  327. }
  328. // GetLatestTemplateDefaultValues gets the default config (`values.yaml`) set for a specific
  329. // template.
  330. func (c *CreateAgent) GetLatestTemplateDefaultValues(templateName, templateVersion string) (map[string]interface{}, error) {
  331. chart, err := c.Client.GetTemplate(
  332. context.Background(),
  333. templateName,
  334. templateVersion,
  335. )
  336. if err != nil {
  337. return nil, err
  338. }
  339. return chart.Values, nil
  340. }
  341. func (c *CreateAgent) getMergedValues(overrideValues map[string]interface{}) (string, map[string]interface{}, error) {
  342. // deploy the template
  343. latestVersion, err := c.GetLatestTemplateVersion(c.CreateOpts.Kind)
  344. if err != nil {
  345. return "", nil, err
  346. }
  347. // get the values of the template
  348. values, err := c.GetLatestTemplateDefaultValues(c.CreateOpts.Kind, latestVersion)
  349. if err != nil {
  350. return "", nil, err
  351. }
  352. // merge existing values with overriding values
  353. mergedValues := utils.CoalesceValues(values, overrideValues)
  354. return latestVersion, mergedValues, err
  355. }
  356. func (c *CreateAgent) CreateSubdomainIfRequired(mergedValues map[string]interface{}) (string, error) {
  357. subdomain := ""
  358. // check for automatic subdomain creation if web kind
  359. if c.CreateOpts.Kind == "web" {
  360. // look for ingress.enabled and no custom domains set
  361. ingressMap, err := getNestedMap(mergedValues, "ingress")
  362. if err == nil {
  363. enabledVal, enabledExists := ingressMap["enabled"]
  364. customDomVal, customDomExists := ingressMap["custom_domain"]
  365. if enabledExists && customDomExists {
  366. enabled, eOK := enabledVal.(bool)
  367. customDomain, cOK := customDomVal.(bool)
  368. // in the case of ingress enabled but no custom domain, create subdomain
  369. if eOK && cOK && enabled && !customDomain {
  370. dnsRecord, err := c.Client.CreateDNSRecord(
  371. context.Background(),
  372. c.CreateOpts.ProjectID,
  373. c.CreateOpts.ClusterID,
  374. &api.CreateDNSRecordRequest{
  375. ReleaseName: c.CreateOpts.ReleaseName,
  376. },
  377. )
  378. if err != nil {
  379. return "", fmt.Errorf("Error creating subdomain: %s", err.Error())
  380. }
  381. subdomain = dnsRecord.ExternalURL
  382. if ingressVal, ok := mergedValues["ingress"]; !ok {
  383. mergedValues["ingress"] = map[string]interface{}{
  384. "porter_hosts": []string{
  385. subdomain,
  386. },
  387. }
  388. } else {
  389. ingressValMap := ingressVal.(map[string]interface{})
  390. ingressValMap["porter_hosts"] = []string{
  391. subdomain,
  392. }
  393. }
  394. }
  395. }
  396. }
  397. }
  398. return subdomain, nil
  399. }