create.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. package deploy
  2. import (
  3. "context"
  4. "fmt"
  5. "os"
  6. "path/filepath"
  7. "strings"
  8. "github.com/porter-dev/porter/cli/cmd/api"
  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. type CreateOpts struct {
  18. *SharedOpts
  19. Kind string
  20. ReleaseName string
  21. }
  22. type GithubOpts struct {
  23. Branch string
  24. Repo string
  25. }
  26. func (c *CreateAgent) CreateFromGithub(
  27. ghOpts *GithubOpts,
  28. overrideValues map[string]interface{},
  29. ) (string, error) {
  30. opts := c.CreateOpts
  31. // get all linked github repos and find matching repo
  32. gitRepos, err := c.Client.ListGitRepos(
  33. context.Background(),
  34. c.CreateOpts.ProjectID,
  35. )
  36. if err != nil {
  37. return "", err
  38. }
  39. var gitRepoMatch uint
  40. for _, gitRepo := range gitRepos {
  41. // for each git repo, search for a matching username/owner
  42. githubRepos, err := c.Client.ListGithubRepos(
  43. context.Background(),
  44. c.CreateOpts.ProjectID,
  45. gitRepo.ID,
  46. )
  47. if err != nil {
  48. return "", err
  49. }
  50. for _, githubRepo := range githubRepos {
  51. if githubRepo.FullName == ghOpts.Repo {
  52. gitRepoMatch = gitRepo.ID
  53. break
  54. }
  55. }
  56. if gitRepoMatch != 0 {
  57. break
  58. }
  59. }
  60. if gitRepoMatch == 0 {
  61. 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)
  62. }
  63. latestVersion, mergedValues, err := c.getMergedValues(overrideValues)
  64. if err != nil {
  65. return "", err
  66. }
  67. if opts.Kind == "web" || opts.Kind == "worker" {
  68. mergedValues["image"] = map[string]interface{}{
  69. "repository": "public.ecr.aws/o1j4x7p4/hello-porter",
  70. "tag": "latest",
  71. }
  72. } else if opts.Kind == "job" {
  73. mergedValues["image"] = map[string]interface{}{
  74. "repository": "public.ecr.aws/o1j4x7p4/hello-porter-job",
  75. "tag": "latest",
  76. }
  77. }
  78. regID, imageURL, err := c.GetImageRepoURL(opts.ReleaseName, opts.Namespace)
  79. if err != nil {
  80. return "", err
  81. }
  82. env, err := GetEnvFromConfig(mergedValues)
  83. if err != nil {
  84. env = map[string]string{}
  85. }
  86. subdomain, err := c.CreateSubdomainIfRequired(mergedValues)
  87. if err != nil {
  88. return "", err
  89. }
  90. err = c.Client.DeployTemplate(
  91. context.Background(),
  92. opts.ProjectID,
  93. opts.ClusterID,
  94. opts.Kind,
  95. latestVersion,
  96. &api.DeployTemplateRequest{
  97. TemplateName: opts.Kind,
  98. ImageURL: imageURL,
  99. FormValues: mergedValues,
  100. Namespace: opts.Namespace,
  101. Name: opts.ReleaseName,
  102. GitAction: &api.DeployTemplateGitAction{
  103. GitRepo: ghOpts.Repo,
  104. GitBranch: ghOpts.Branch,
  105. ImageRepoURI: imageURL,
  106. DockerfilePath: opts.LocalDockerfile,
  107. FolderPath: ".",
  108. GitRepoID: gitRepoMatch,
  109. BuildEnv: env,
  110. RegistryID: regID,
  111. },
  112. },
  113. )
  114. if err != nil {
  115. return "", err
  116. }
  117. return subdomain, nil
  118. }
  119. func (c *CreateAgent) CreateFromRegistry(
  120. image string,
  121. overrideValues map[string]interface{},
  122. ) (string, error) {
  123. if image == "" {
  124. return "", fmt.Errorf("image cannot be empty")
  125. }
  126. // split image into image-path:tag format
  127. imageSpl := strings.Split(image, ":")
  128. if len(imageSpl) != 2 {
  129. return "", fmt.Errorf("invalid image format: must be image-path:tag format")
  130. }
  131. opts := c.CreateOpts
  132. latestVersion, mergedValues, err := c.getMergedValues(overrideValues)
  133. if err != nil {
  134. return "", err
  135. }
  136. mergedValues["image"] = map[string]interface{}{
  137. "repository": imageSpl[0],
  138. "tag": imageSpl[1],
  139. }
  140. subdomain, err := c.CreateSubdomainIfRequired(mergedValues)
  141. if err != nil {
  142. return "", err
  143. }
  144. err = c.Client.DeployTemplate(
  145. context.Background(),
  146. opts.ProjectID,
  147. opts.ClusterID,
  148. opts.Kind,
  149. latestVersion,
  150. &api.DeployTemplateRequest{
  151. TemplateName: opts.Kind,
  152. ImageURL: imageSpl[0],
  153. FormValues: mergedValues,
  154. Namespace: opts.Namespace,
  155. Name: opts.ReleaseName,
  156. },
  157. )
  158. if err != nil {
  159. return "", err
  160. }
  161. return subdomain, nil
  162. }
  163. func (c *CreateAgent) CreateFromDocker(
  164. overrideValues map[string]interface{},
  165. ) (string, error) {
  166. opts := c.CreateOpts
  167. // detect the build config
  168. if opts.Method != "" {
  169. if opts.Method == DeployBuildTypeDocker {
  170. if opts.LocalDockerfile == "" {
  171. hasDockerfile := c.HasDefaultDockerfile(opts.LocalPath)
  172. if !hasDockerfile {
  173. return "", fmt.Errorf("Dockerfile not found")
  174. }
  175. opts.LocalDockerfile = "Dockerfile"
  176. }
  177. }
  178. } else {
  179. // try to detect dockerfile, otherwise fall back to `pack`
  180. hasDockerfile := c.HasDefaultDockerfile(opts.LocalPath)
  181. if !hasDockerfile {
  182. opts.Method = DeployBuildTypePack
  183. } else {
  184. opts.Method = DeployBuildTypeDocker
  185. opts.LocalDockerfile = "Dockerfile"
  186. }
  187. }
  188. // overwrite with docker image repository and tag
  189. regID, imageURL, err := c.GetImageRepoURL(opts.ReleaseName, opts.Namespace)
  190. if err != nil {
  191. return "", err
  192. }
  193. latestVersion, mergedValues, err := c.getMergedValues(overrideValues)
  194. if err != nil {
  195. return "", err
  196. }
  197. mergedValues["image"] = map[string]interface{}{
  198. "repository": imageURL,
  199. "tag": "latest",
  200. }
  201. // create docker agen
  202. agent, err := docker.NewAgentWithAuthGetter(c.Client, opts.ProjectID)
  203. if err != nil {
  204. return "", err
  205. }
  206. env, err := GetEnvFromConfig(mergedValues)
  207. if err != nil {
  208. env = map[string]string{}
  209. }
  210. buildAgent := &BuildAgent{
  211. SharedOpts: opts.SharedOpts,
  212. client: c.Client,
  213. imageRepo: imageURL,
  214. env: env,
  215. imageExists: false,
  216. }
  217. if opts.Method == DeployBuildTypeDocker {
  218. err = buildAgent.BuildDocker(agent, opts.LocalPath, "latest")
  219. } else {
  220. err = buildAgent.BuildPack(agent, opts.LocalPath, "latest")
  221. }
  222. if err != nil {
  223. return "", err
  224. }
  225. // create repository
  226. err = c.Client.CreateRepository(
  227. context.Background(),
  228. opts.ProjectID,
  229. regID,
  230. &api.CreateRepositoryRequest{
  231. ImageRepoURI: imageURL,
  232. },
  233. )
  234. if err != nil {
  235. return "", err
  236. }
  237. err = agent.PushImage(fmt.Sprintf("%s:%s", imageURL, "latest"))
  238. if err != nil {
  239. return "", err
  240. }
  241. subdomain, err := c.CreateSubdomainIfRequired(mergedValues)
  242. if err != nil {
  243. return "", err
  244. }
  245. err = c.Client.DeployTemplate(
  246. context.Background(),
  247. opts.ProjectID,
  248. opts.ClusterID,
  249. opts.Kind,
  250. latestVersion,
  251. &api.DeployTemplateRequest{
  252. TemplateName: opts.Kind,
  253. ImageURL: imageURL,
  254. FormValues: mergedValues,
  255. Namespace: opts.Namespace,
  256. Name: opts.ReleaseName,
  257. },
  258. )
  259. if err != nil {
  260. return "", err
  261. }
  262. return subdomain, nil
  263. }
  264. type CreateConfig struct {
  265. DockerfilePath string
  266. }
  267. // HasDefaultDockerfile detects if there is a dockerfile at the path `./Dockerfile`
  268. func (c *CreateAgent) HasDefaultDockerfile(buildPath string) bool {
  269. dockerFilePath := filepath.Join(buildPath, "./Dockerfile")
  270. info, err := os.Stat(dockerFilePath)
  271. return err == nil && !os.IsNotExist(err) && !info.IsDir()
  272. }
  273. func (c *CreateAgent) GetImageRepoURL(name, namespace string) (uint, string, error) {
  274. // get all image registries linked to the project
  275. // get the list of namespaces
  276. registries, err := c.Client.ListRegistries(
  277. context.Background(),
  278. c.CreateOpts.ProjectID,
  279. )
  280. if err != nil {
  281. return 0, "", err
  282. } else if len(registries) == 0 {
  283. return 0, "", fmt.Errorf("must have created or linked an image registry")
  284. }
  285. // get the first non-empty registry
  286. var imageURI string
  287. var regID uint
  288. for _, reg := range registries {
  289. if reg.URL != "" {
  290. regID = reg.ID
  291. imageURI = fmt.Sprintf("%s/%s-%s", reg.URL, name, namespace)
  292. break
  293. }
  294. }
  295. return regID, imageURI, nil
  296. }
  297. func (c *CreateAgent) GetLatestTemplateVersion(templateName string) (string, error) {
  298. templates, err := c.Client.ListTemplates(
  299. context.Background(),
  300. )
  301. if err != nil {
  302. return "", err
  303. }
  304. var version string
  305. // find the matching template name
  306. for _, template := range templates {
  307. if templateName == template.Name {
  308. version = template.Versions[0]
  309. break
  310. }
  311. }
  312. if version == "" {
  313. return "", fmt.Errorf("matching template version not found")
  314. }
  315. return version, nil
  316. }
  317. func (c *CreateAgent) GetLatestTemplateDefaultValues(templateName, templateVersion string) (map[string]interface{}, error) {
  318. chart, err := c.Client.GetTemplate(
  319. context.Background(),
  320. templateName,
  321. templateVersion,
  322. )
  323. if err != nil {
  324. return nil, err
  325. }
  326. return chart.Values, nil
  327. }
  328. func (c *CreateAgent) getMergedValues(overrideValues map[string]interface{}) (string, map[string]interface{}, error) {
  329. // deploy the template
  330. latestVersion, err := c.GetLatestTemplateVersion(c.CreateOpts.Kind)
  331. if err != nil {
  332. return "", nil, err
  333. }
  334. // get the values of the template
  335. values, err := c.GetLatestTemplateDefaultValues(c.CreateOpts.Kind, latestVersion)
  336. if err != nil {
  337. return "", nil, err
  338. }
  339. // merge existing values with overriding values
  340. mergedValues := utils.CoalesceValues(values, overrideValues)
  341. return latestVersion, mergedValues, err
  342. }
  343. func (c *CreateAgent) CreateSubdomainIfRequired(mergedValues map[string]interface{}) (string, error) {
  344. subdomain := ""
  345. // check for automatic subdomain creation if web kind
  346. if c.CreateOpts.Kind == "web" {
  347. // look for ingress.enabled and no custom domains set
  348. ingressMap, err := getNestedMap(mergedValues, "ingress")
  349. if err == nil {
  350. enabledVal, enabledExists := ingressMap["enabled"]
  351. customDomVal, customDomExists := ingressMap["custom_domain"]
  352. if enabledExists && customDomExists {
  353. enabled, eOK := enabledVal.(bool)
  354. customDomain, cOK := customDomVal.(bool)
  355. // in the case of ingress enabled but no custom domain, create subdomain
  356. if eOK && cOK && enabled && !customDomain {
  357. dnsRecord, err := c.Client.CreateDNSRecord(
  358. context.Background(),
  359. c.CreateOpts.ProjectID,
  360. c.CreateOpts.ClusterID,
  361. &api.CreateDNSRecordRequest{
  362. ReleaseName: c.CreateOpts.ReleaseName,
  363. },
  364. )
  365. if err != nil {
  366. return "", fmt.Errorf("Error creating subdomain: %s", err.Error())
  367. }
  368. subdomain = dnsRecord.ExternalURL
  369. if ingressVal, ok := mergedValues["ingress"]; !ok {
  370. mergedValues["ingress"] = map[string]interface{}{
  371. "porter_hosts": []string{
  372. subdomain,
  373. },
  374. }
  375. } else {
  376. ingressValMap := ingressVal.(map[string]interface{})
  377. ingressValMap["porter_hosts"] = []string{
  378. subdomain,
  379. }
  380. }
  381. }
  382. }
  383. }
  384. }
  385. return subdomain, nil
  386. }