create.go 12 KB

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