create.go 12 KB

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