create.go 14 KB

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