2
0

create.go 12 KB

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