create.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  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. imageTag string,
  177. ) (string, error) {
  178. opts := c.CreateOpts
  179. // detect the build config
  180. if opts.Method != "" {
  181. if opts.Method == DeployBuildTypeDocker {
  182. if opts.LocalDockerfile == "" {
  183. hasDockerfile := c.HasDefaultDockerfile(opts.LocalPath)
  184. if !hasDockerfile {
  185. return "", fmt.Errorf("Dockerfile not found")
  186. }
  187. opts.LocalDockerfile = "Dockerfile"
  188. }
  189. }
  190. } else {
  191. // try to detect dockerfile, otherwise fall back to `pack`
  192. hasDockerfile := c.HasDefaultDockerfile(opts.LocalPath)
  193. if !hasDockerfile {
  194. opts.Method = DeployBuildTypePack
  195. } else {
  196. opts.Method = DeployBuildTypeDocker
  197. opts.LocalDockerfile = "Dockerfile"
  198. }
  199. }
  200. // overwrite with docker image repository and tag
  201. regID, imageURL, err := c.GetImageRepoURL(opts.ReleaseName, opts.Namespace)
  202. if err != nil {
  203. return "", err
  204. }
  205. latestVersion, mergedValues, err := c.getMergedValues(overrideValues)
  206. if err != nil {
  207. return "", err
  208. }
  209. mergedValues["image"] = map[string]interface{}{
  210. "repository": imageURL,
  211. "tag": imageTag,
  212. }
  213. // create docker agen
  214. agent, err := docker.NewAgentWithAuthGetter(c.Client, opts.ProjectID)
  215. if err != nil {
  216. return "", err
  217. }
  218. env, err := GetEnvFromConfig(mergedValues)
  219. if err != nil {
  220. env = map[string]string{}
  221. }
  222. buildAgent := &BuildAgent{
  223. SharedOpts: opts.SharedOpts,
  224. client: c.Client,
  225. imageRepo: imageURL,
  226. env: env,
  227. imageExists: false,
  228. }
  229. if opts.Method == DeployBuildTypeDocker {
  230. err = buildAgent.BuildDocker(agent, opts.LocalPath, opts.LocalPath, opts.LocalDockerfile, imageTag, "")
  231. } else {
  232. err = buildAgent.BuildPack(agent, opts.LocalPath, imageTag, nil)
  233. }
  234. if err != nil {
  235. return "", err
  236. }
  237. // create repository
  238. err = c.Client.CreateRepository(
  239. context.Background(),
  240. opts.ProjectID,
  241. regID,
  242. &types.CreateRegistryRepositoryRequest{
  243. ImageRepoURI: imageURL,
  244. },
  245. )
  246. if err != nil {
  247. return "", err
  248. }
  249. err = agent.PushImage(fmt.Sprintf("%s:%s", imageURL, imageTag))
  250. if err != nil {
  251. return "", err
  252. }
  253. subdomain, err := c.CreateSubdomainIfRequired(mergedValues)
  254. if err != nil {
  255. return "", err
  256. }
  257. err = c.Client.DeployTemplate(
  258. context.Background(),
  259. opts.ProjectID,
  260. opts.ClusterID,
  261. opts.Namespace,
  262. &types.CreateReleaseRequest{
  263. CreateReleaseBaseRequest: &types.CreateReleaseBaseRequest{
  264. TemplateName: opts.Kind,
  265. TemplateVersion: latestVersion,
  266. Values: mergedValues,
  267. Name: opts.ReleaseName,
  268. },
  269. ImageURL: imageURL,
  270. },
  271. )
  272. if err != nil {
  273. return "", err
  274. }
  275. return subdomain, nil
  276. }
  277. // HasDefaultDockerfile detects if there is a dockerfile at the path `./Dockerfile`
  278. func (c *CreateAgent) HasDefaultDockerfile(buildPath string) bool {
  279. dockerFilePath := filepath.Join(buildPath, "./Dockerfile")
  280. info, err := os.Stat(dockerFilePath)
  281. return err == nil && !os.IsNotExist(err) && !info.IsDir()
  282. }
  283. // GetImageRepoURL creates the image repository url by finding the first valid image
  284. // registry linked to Porter, and then generates a new name of the form:
  285. // `{registry}/{name}-{namespace}`
  286. func (c *CreateAgent) GetImageRepoURL(name, namespace string) (uint, string, error) {
  287. // get all image registries linked to the project
  288. // get the list of namespaces
  289. resp, err := c.Client.ListRegistries(
  290. context.Background(),
  291. c.CreateOpts.ProjectID,
  292. )
  293. registries := *resp
  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. resp, err := c.Client.ListTemplates(
  321. context.Background(),
  322. &types.ListTemplatesRequest{},
  323. )
  324. if err != nil {
  325. return "", err
  326. }
  327. templates := *resp
  328. var version string
  329. // find the matching template name
  330. for _, template := range templates {
  331. if templateName == template.Name {
  332. version = template.Versions[0]
  333. break
  334. }
  335. }
  336. if version == "" {
  337. return "", fmt.Errorf("matching template version not found")
  338. }
  339. return version, nil
  340. }
  341. // GetLatestTemplateDefaultValues gets the default config (`values.yaml`) set for a specific
  342. // template.
  343. func (c *CreateAgent) GetLatestTemplateDefaultValues(templateName, templateVersion string) (map[string]interface{}, error) {
  344. chart, err := c.Client.GetTemplate(
  345. context.Background(),
  346. templateName,
  347. templateVersion,
  348. &types.GetTemplateRequest{},
  349. )
  350. if err != nil {
  351. return nil, err
  352. }
  353. return chart.Values, nil
  354. }
  355. func (c *CreateAgent) getMergedValues(overrideValues map[string]interface{}) (string, map[string]interface{}, error) {
  356. // deploy the template
  357. latestVersion, err := c.GetLatestTemplateVersion(c.CreateOpts.Kind)
  358. if err != nil {
  359. return "", nil, err
  360. }
  361. // get the values of the template
  362. values, err := c.GetLatestTemplateDefaultValues(c.CreateOpts.Kind, latestVersion)
  363. if err != nil {
  364. return "", nil, err
  365. }
  366. // merge existing values with overriding values
  367. mergedValues := utils.CoalesceValues(values, overrideValues)
  368. return latestVersion, mergedValues, err
  369. }
  370. func (c *CreateAgent) CreateSubdomainIfRequired(mergedValues map[string]interface{}) (string, error) {
  371. subdomain := ""
  372. // check for automatic subdomain creation if web kind
  373. if c.CreateOpts.Kind == "web" {
  374. // look for ingress.enabled and no custom domains set
  375. ingressMap, err := getNestedMap(mergedValues, "ingress")
  376. if err == nil {
  377. enabledVal, enabledExists := ingressMap["enabled"]
  378. customDomVal, customDomExists := ingressMap["custom_domain"]
  379. if enabledExists && customDomExists {
  380. enabled, eOK := enabledVal.(bool)
  381. customDomain, cOK := customDomVal.(bool)
  382. // in the case of ingress enabled but no custom domain, create subdomain
  383. if eOK && cOK && enabled && !customDomain {
  384. dnsRecord, err := c.Client.CreateDNSRecord(
  385. context.Background(),
  386. c.CreateOpts.ProjectID,
  387. c.CreateOpts.ClusterID,
  388. c.CreateOpts.Namespace,
  389. c.CreateOpts.ReleaseName,
  390. )
  391. if err != nil {
  392. return "", fmt.Errorf("Error creating subdomain: %s", err.Error())
  393. }
  394. subdomain = dnsRecord.ExternalURL
  395. if ingressVal, ok := mergedValues["ingress"]; !ok {
  396. mergedValues["ingress"] = map[string]interface{}{
  397. "porter_hosts": []string{
  398. subdomain,
  399. },
  400. }
  401. } else {
  402. ingressValMap := ingressVal.(map[string]interface{})
  403. ingressValMap["porter_hosts"] = []string{
  404. subdomain,
  405. }
  406. }
  407. }
  408. }
  409. }
  410. }
  411. return subdomain, nil
  412. }