2
0

create.go 11 KB

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