create.go 9.1 KB

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