apply.go 30 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192
  1. package cmd
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "io/ioutil"
  7. "net/url"
  8. "os"
  9. "path/filepath"
  10. "strconv"
  11. "strings"
  12. "github.com/cli/cli/git"
  13. "github.com/fatih/color"
  14. "github.com/mitchellh/mapstructure"
  15. api "github.com/porter-dev/porter/api/client"
  16. "github.com/porter-dev/porter/api/types"
  17. "github.com/porter-dev/porter/cli/cmd/config"
  18. "github.com/porter-dev/porter/cli/cmd/deploy"
  19. "github.com/porter-dev/porter/cli/cmd/deploy/wait"
  20. "github.com/porter-dev/porter/cli/cmd/preview"
  21. previewV2 "github.com/porter-dev/porter/cli/cmd/preview/v2"
  22. previewInt "github.com/porter-dev/porter/internal/integrations/preview"
  23. "github.com/porter-dev/porter/internal/templater/utils"
  24. "github.com/porter-dev/switchboard/pkg/drivers"
  25. switchboardModels "github.com/porter-dev/switchboard/pkg/models"
  26. "github.com/porter-dev/switchboard/pkg/parser"
  27. switchboardTypes "github.com/porter-dev/switchboard/pkg/types"
  28. switchboardWorker "github.com/porter-dev/switchboard/pkg/worker"
  29. "github.com/rs/zerolog"
  30. "github.com/spf13/cobra"
  31. "gopkg.in/yaml.v2"
  32. )
  33. // applyCmd represents the "porter apply" base command when called
  34. // with a porter.yaml file as an argument
  35. var applyCmd = &cobra.Command{
  36. Use: "apply",
  37. Short: "Applies a configuration to an application",
  38. Long: fmt.Sprintf(`
  39. %s
  40. Applies a configuration to an application by either creating a new one or updating an existing
  41. one. For example:
  42. %s
  43. This command will apply the configuration contained in porter.yaml to the requested project and
  44. cluster either provided inside the porter.yaml file or through environment variables. Note that
  45. environment variables will always take precendence over values specified in the porter.yaml file.
  46. By default, this command expects to be run from a local git repository.
  47. The following are the environment variables that can be used to set certain values while
  48. applying a configuration:
  49. PORTER_CLUSTER Cluster ID that contains the project
  50. PORTER_PROJECT Project ID that contains the application
  51. PORTER_NAMESPACE The Kubernetes namespace that the application belongs to
  52. PORTER_SOURCE_NAME Name of the source Helm chart
  53. PORTER_SOURCE_REPO The URL of the Helm charts registry
  54. PORTER_SOURCE_VERSION The version of the Helm chart to use
  55. PORTER_TAG The Docker image tag to use (like the git commit hash)
  56. `,
  57. color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter apply\":"),
  58. color.New(color.FgGreen, color.Bold).Sprintf("porter apply -f porter.yaml"),
  59. ),
  60. Run: func(cmd *cobra.Command, args []string) {
  61. err := checkLoginAndRun(args, apply)
  62. if err != nil {
  63. if strings.Contains(err.Error(), "Forbidden") {
  64. color.New(color.FgRed).Fprintf(os.Stderr, "You may have to update your GitHub secret token")
  65. }
  66. os.Exit(1)
  67. }
  68. },
  69. }
  70. // applyValidateCmd represents the "porter apply validate" command when called
  71. // with a porter.yaml file as an argument
  72. var applyValidateCmd = &cobra.Command{
  73. Use: "validate",
  74. Short: "Validates a porter.yaml",
  75. Run: func(*cobra.Command, []string) {
  76. err := applyValidate()
  77. if err != nil {
  78. color.New(color.FgRed).Fprintf(os.Stderr, "Error: %s\n", err.Error())
  79. os.Exit(1)
  80. } else {
  81. color.New(color.FgGreen).Printf("The porter.yaml file is valid!\n")
  82. }
  83. },
  84. }
  85. var porterYAML string
  86. func init() {
  87. rootCmd.AddCommand(applyCmd)
  88. applyCmd.AddCommand(applyValidateCmd)
  89. applyCmd.PersistentFlags().StringVarP(&porterYAML, "file", "f", "", "path to porter.yaml")
  90. applyCmd.MarkFlagRequired("file")
  91. }
  92. func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string) error {
  93. // read the porter.yaml file
  94. fileBytes, err := ioutil.ReadFile(porterYAML)
  95. if err != nil {
  96. return fmt.Errorf("error reading porter.yaml: %w", err)
  97. }
  98. var porterYAMLVersion struct{ Version string }
  99. err = yaml.Unmarshal(fileBytes, &porterYAMLVersion)
  100. if err != nil {
  101. return fmt.Errorf("error reading porter.yaml version: %w", err)
  102. }
  103. if porterYAMLVersion.Version == "v2" {
  104. applier, err := previewV2.NewApplier(client, fileBytes, namespace)
  105. if err != nil {
  106. return err
  107. }
  108. return applier.Apply()
  109. }
  110. if _, ok := os.LookupEnv("PORTER_VALIDATE_YAML"); ok {
  111. err := applyValidate()
  112. if err != nil {
  113. return err
  114. }
  115. }
  116. resGroup, err := parser.ParseRawBytes(fileBytes)
  117. if err != nil {
  118. return fmt.Errorf("error parsing porter.yaml: %w", err)
  119. }
  120. basePath, err := os.Getwd()
  121. if err != nil {
  122. return fmt.Errorf("error getting working directory: %w", err)
  123. }
  124. worker := switchboardWorker.NewWorker()
  125. worker.RegisterDriver("deploy", NewDeployDriver)
  126. worker.RegisterDriver("build-image", preview.NewBuildDriver)
  127. worker.RegisterDriver("push-image", preview.NewPushDriver)
  128. worker.RegisterDriver("update-config", preview.NewUpdateConfigDriver)
  129. worker.RegisterDriver("random-string", preview.NewRandomStringDriver)
  130. worker.RegisterDriver("env-group", preview.NewEnvGroupDriver)
  131. worker.RegisterDriver("os-env", preview.NewOSEnvDriver)
  132. worker.SetDefaultDriver("deploy")
  133. if hasDeploymentHookEnvVars() {
  134. deplNamespace := os.Getenv("PORTER_NAMESPACE")
  135. if deplNamespace == "" {
  136. return fmt.Errorf("namespace must be set by PORTER_NAMESPACE")
  137. }
  138. deploymentHook, err := NewDeploymentHook(client, resGroup, deplNamespace)
  139. if err != nil {
  140. return fmt.Errorf("error creating deployment hook: %w", err)
  141. }
  142. worker.RegisterHook("deployment", deploymentHook)
  143. }
  144. cloneEnvGroupHook := NewCloneEnvGroupHook(client, resGroup)
  145. worker.RegisterHook("cloneenvgroup", cloneEnvGroupHook)
  146. return worker.Apply(resGroup, &switchboardTypes.ApplyOpts{
  147. BasePath: basePath,
  148. })
  149. }
  150. func applyValidate() error {
  151. fileBytes, err := ioutil.ReadFile(porterYAML)
  152. if err != nil {
  153. return fmt.Errorf("error reading porter.yaml: %w", err)
  154. }
  155. validationErrors := previewInt.Validate(string(fileBytes))
  156. if len(validationErrors) > 0 {
  157. errString := "the following error(s) were found while validating the porter.yaml file:"
  158. for _, err := range validationErrors {
  159. errString += "\n- " + strings.ReplaceAll(err.Error(), "\n\n*", "\n *")
  160. }
  161. return fmt.Errorf(errString)
  162. }
  163. return nil
  164. }
  165. func hasDeploymentHookEnvVars() bool {
  166. if ghIDStr := os.Getenv("PORTER_GIT_INSTALLATION_ID"); ghIDStr == "" {
  167. return false
  168. }
  169. if prIDStr := os.Getenv("PORTER_PULL_REQUEST_ID"); prIDStr == "" {
  170. return false
  171. }
  172. if branchFrom := os.Getenv("PORTER_BRANCH_FROM"); branchFrom == "" {
  173. return false
  174. }
  175. if branchInto := os.Getenv("PORTER_BRANCH_INTO"); branchInto == "" {
  176. return false
  177. }
  178. if actionIDStr := os.Getenv("PORTER_ACTION_ID"); actionIDStr == "" {
  179. return false
  180. }
  181. if repoName := os.Getenv("PORTER_REPO_NAME"); repoName == "" {
  182. return false
  183. }
  184. if repoOwner := os.Getenv("PORTER_REPO_OWNER"); repoOwner == "" {
  185. return false
  186. }
  187. if prName := os.Getenv("PORTER_PR_NAME"); prName == "" {
  188. return false
  189. }
  190. return true
  191. }
  192. type DeployDriver struct {
  193. source *previewInt.Source
  194. target *previewInt.Target
  195. output map[string]interface{}
  196. lookupTable *map[string]drivers.Driver
  197. logger *zerolog.Logger
  198. }
  199. func NewDeployDriver(resource *switchboardModels.Resource, opts *drivers.SharedDriverOpts) (drivers.Driver, error) {
  200. driver := &DeployDriver{
  201. lookupTable: opts.DriverLookupTable,
  202. logger: opts.Logger,
  203. output: make(map[string]interface{}),
  204. }
  205. source, err := preview.GetSource(resource.Name, resource.Source)
  206. if err != nil {
  207. return nil, err
  208. }
  209. driver.source = source
  210. target, err := preview.GetTarget(resource.Name, resource.Target)
  211. if err != nil {
  212. return nil, err
  213. }
  214. driver.target = target
  215. return driver, nil
  216. }
  217. func (d *DeployDriver) ShouldApply(_ *switchboardModels.Resource) bool {
  218. return true
  219. }
  220. func (d *DeployDriver) Apply(resource *switchboardModels.Resource) (*switchboardModels.Resource, error) {
  221. client := config.GetAPIClient()
  222. _, err := client.GetRelease(
  223. context.Background(),
  224. d.target.Project,
  225. d.target.Cluster,
  226. d.target.Namespace,
  227. resource.Name,
  228. )
  229. shouldCreate := err != nil
  230. if err != nil {
  231. color.New(color.FgYellow).Printf("Could not read release %s/%s (%s): attempting creation\n", d.target.Namespace, resource.Name, err.Error())
  232. }
  233. if d.source.IsApplication {
  234. return d.applyApplication(resource, client, shouldCreate)
  235. }
  236. return d.applyAddon(resource, client, shouldCreate)
  237. }
  238. // Simple apply for addons
  239. func (d *DeployDriver) applyAddon(resource *switchboardModels.Resource, client *api.Client, shouldCreate bool) (*switchboardModels.Resource, error) {
  240. addonConfig, err := d.getAddonConfig(resource)
  241. if err != nil {
  242. return nil, fmt.Errorf("error getting addon config for resource %s: %w", resource.Name, err)
  243. }
  244. if shouldCreate {
  245. err := client.DeployAddon(
  246. context.Background(),
  247. d.target.Project,
  248. d.target.Cluster,
  249. d.target.Namespace,
  250. &types.CreateAddonRequest{
  251. CreateReleaseBaseRequest: &types.CreateReleaseBaseRequest{
  252. RepoURL: d.source.Repo,
  253. TemplateName: d.source.Name,
  254. TemplateVersion: d.source.Version,
  255. Values: addonConfig,
  256. Name: resource.Name,
  257. },
  258. },
  259. )
  260. if err != nil {
  261. return nil, fmt.Errorf("error creating addon from resource %s: %w", resource.Name, err)
  262. }
  263. } else {
  264. bytes, err := json.Marshal(addonConfig)
  265. if err != nil {
  266. return nil, fmt.Errorf("error marshalling addon config from resource %s: %w", resource.Name, err)
  267. }
  268. err = client.UpgradeRelease(
  269. context.Background(),
  270. d.target.Project,
  271. d.target.Cluster,
  272. d.target.Namespace,
  273. resource.Name,
  274. &types.UpgradeReleaseRequest{
  275. Values: string(bytes),
  276. },
  277. )
  278. if err != nil {
  279. return nil, fmt.Errorf("error updating addon from resource %s: %w", resource.Name, err)
  280. }
  281. }
  282. if err = d.assignOutput(resource, client); err != nil {
  283. return nil, err
  284. }
  285. return resource, nil
  286. }
  287. func (d *DeployDriver) applyApplication(resource *switchboardModels.Resource, client *api.Client, shouldCreate bool) (*switchboardModels.Resource, error) {
  288. if resource == nil {
  289. return nil, fmt.Errorf("nil resource")
  290. }
  291. resourceName := resource.Name
  292. appConfig, err := d.getApplicationConfig(resource)
  293. if err != nil {
  294. return nil, err
  295. }
  296. fullPath, err := filepath.Abs(appConfig.Build.Context)
  297. if err != nil {
  298. return nil, fmt.Errorf("for resource %s, error getting absolute path for config.build.context: %w", resourceName,
  299. err)
  300. }
  301. tag := os.Getenv("PORTER_TAG")
  302. if tag == "" {
  303. color.New(color.FgYellow).Printf("for resource %s, since PORTER_TAG is not set, the Docker image tag will default to"+
  304. " the git repo SHA", resourceName)
  305. commit, err := git.LastCommit()
  306. if err != nil {
  307. return nil, fmt.Errorf("for resource %s, error getting last git commit: %w", resourceName, err)
  308. }
  309. tag = commit.Sha[:7]
  310. color.New(color.FgYellow).Printf("for resource %s, using tag %s\n", resourceName, tag)
  311. }
  312. // if the method is registry and a tag is defined, we use the provided tag
  313. if appConfig.Build.Method == "registry" {
  314. imageSpl := strings.Split(appConfig.Build.Image, ":")
  315. if len(imageSpl) == 2 {
  316. tag = imageSpl[1]
  317. }
  318. if tag == "" {
  319. tag = "latest"
  320. }
  321. }
  322. sharedOpts := &deploy.SharedOpts{
  323. ProjectID: d.target.Project,
  324. ClusterID: d.target.Cluster,
  325. Namespace: d.target.Namespace,
  326. LocalPath: fullPath,
  327. LocalDockerfile: appConfig.Build.Dockerfile,
  328. OverrideTag: tag,
  329. Method: deploy.DeployBuildType(appConfig.Build.Method),
  330. EnvGroups: appConfig.EnvGroups,
  331. UseCache: appConfig.Build.UseCache,
  332. }
  333. if appConfig.Build.UseCache {
  334. // set the docker config so that pack caching can use the repo credentials
  335. err := config.SetDockerConfig(client)
  336. if err != nil {
  337. return nil, err
  338. }
  339. }
  340. if shouldCreate {
  341. resource, err = d.createApplication(resource, client, sharedOpts, appConfig)
  342. if err != nil {
  343. return nil, fmt.Errorf("error creating app from resource %s: %w", resourceName, err)
  344. }
  345. } else if !appConfig.OnlyCreate {
  346. resource, err = d.updateApplication(resource, client, sharedOpts, appConfig)
  347. if err != nil {
  348. return nil, fmt.Errorf("error updating application from resource %s: %w", resourceName, err)
  349. }
  350. } else {
  351. color.New(color.FgYellow).Printf("Skipping creation for resource %s as onlyCreate is set to true\n", resourceName)
  352. }
  353. if err = d.assignOutput(resource, client); err != nil {
  354. return nil, err
  355. }
  356. if d.source.Name == "job" && appConfig.WaitForJob && (shouldCreate || !appConfig.OnlyCreate) {
  357. color.New(color.FgYellow).Printf("Waiting for job '%s' to finish\n", resourceName)
  358. err = wait.WaitForJob(client, &wait.WaitOpts{
  359. ProjectID: d.target.Project,
  360. ClusterID: d.target.Cluster,
  361. Namespace: d.target.Namespace,
  362. Name: resourceName,
  363. })
  364. if err != nil && appConfig.OnlyCreate {
  365. deleteJobErr := client.DeleteRelease(
  366. context.Background(),
  367. d.target.Project,
  368. d.target.Cluster,
  369. d.target.Namespace,
  370. resourceName,
  371. )
  372. if deleteJobErr != nil {
  373. return nil, fmt.Errorf("error deleting job %s with waitForJob and onlyCreate set to true: %w",
  374. resourceName, deleteJobErr)
  375. }
  376. } else if err != nil {
  377. return nil, fmt.Errorf("error waiting for job %s: %w", resourceName, err)
  378. }
  379. }
  380. return resource, err
  381. }
  382. func (d *DeployDriver) createApplication(resource *switchboardModels.Resource, client *api.Client, sharedOpts *deploy.SharedOpts, appConf *previewInt.ApplicationConfig) (*switchboardModels.Resource, error) {
  383. // create new release
  384. color.New(color.FgGreen).Printf("Creating %s release: %s\n", d.source.Name, resource.Name)
  385. regList, err := client.ListRegistries(context.Background(), d.target.Project)
  386. if err != nil {
  387. return nil, fmt.Errorf("for resource %s, error listing registries: %w", resource.Name, err)
  388. }
  389. var registryURL string
  390. if len(*regList) == 0 {
  391. return nil, fmt.Errorf("no registry found")
  392. } else {
  393. registryURL = (*regList)[0].URL
  394. }
  395. color.New(color.FgBlue).Printf("for resource %s, using registry %s\n", resource.Name, registryURL)
  396. // attempt to get repo suffix from environment variables
  397. var repoSuffix string
  398. if repoName := os.Getenv("PORTER_REPO_NAME"); repoName != "" {
  399. if repoOwner := os.Getenv("PORTER_REPO_OWNER"); repoOwner != "" {
  400. repoSuffix = strings.ToLower(strings.ReplaceAll(fmt.Sprintf("%s-%s", repoOwner, repoName), "_", "-"))
  401. }
  402. }
  403. createAgent := &deploy.CreateAgent{
  404. Client: client,
  405. CreateOpts: &deploy.CreateOpts{
  406. SharedOpts: sharedOpts,
  407. Kind: d.source.Name,
  408. ReleaseName: resource.Name,
  409. RegistryURL: registryURL,
  410. RepoSuffix: repoSuffix,
  411. },
  412. }
  413. var buildConfig *types.BuildConfig
  414. if appConf.Build.Builder != "" {
  415. buildConfig = &types.BuildConfig{
  416. Builder: appConf.Build.Builder,
  417. Buildpacks: appConf.Build.Buildpacks,
  418. }
  419. }
  420. var subdomain string
  421. if appConf.Build.Method == "registry" {
  422. subdomain, err = createAgent.CreateFromRegistry(appConf.Build.Image, appConf.Values)
  423. } else {
  424. // if useCache is set, create the image repository first
  425. if appConf.Build.UseCache {
  426. regID, imageURL, err := createAgent.GetImageRepoURL(resource.Name, sharedOpts.Namespace)
  427. if err != nil {
  428. return nil, err
  429. }
  430. err = client.CreateRepository(
  431. context.Background(),
  432. sharedOpts.ProjectID,
  433. regID,
  434. &types.CreateRegistryRepositoryRequest{
  435. ImageRepoURI: imageURL,
  436. },
  437. )
  438. if err != nil {
  439. return nil, err
  440. }
  441. }
  442. subdomain, err = createAgent.CreateFromDocker(appConf.Values, sharedOpts.OverrideTag, buildConfig)
  443. }
  444. if err != nil {
  445. return nil, err
  446. }
  447. return resource, handleSubdomainCreate(subdomain, err)
  448. }
  449. func (d *DeployDriver) updateApplication(resource *switchboardModels.Resource, client *api.Client, sharedOpts *deploy.SharedOpts, appConf *previewInt.ApplicationConfig) (*switchboardModels.Resource, error) {
  450. color.New(color.FgGreen).Println("Updating existing release:", resource.Name)
  451. if len(appConf.Build.Env) > 0 {
  452. sharedOpts.AdditionalEnv = appConf.Build.Env
  453. }
  454. updateAgent, err := deploy.NewDeployAgent(client, resource.Name, &deploy.DeployOpts{
  455. SharedOpts: sharedOpts,
  456. Local: appConf.Build.Method != "registry",
  457. })
  458. if err != nil {
  459. return nil, err
  460. }
  461. // if the build method is registry, we do not trigger a build
  462. if appConf.Build.Method != "registry" {
  463. buildEnv, err := updateAgent.GetBuildEnv(&deploy.GetBuildEnvOpts{
  464. UseNewConfig: true,
  465. NewConfig: appConf.Values,
  466. })
  467. if err != nil {
  468. return nil, err
  469. }
  470. err = updateAgent.SetBuildEnv(buildEnv)
  471. if err != nil {
  472. return nil, err
  473. }
  474. var buildConfig *types.BuildConfig
  475. if appConf.Build.Builder != "" {
  476. buildConfig = &types.BuildConfig{
  477. Builder: appConf.Build.Builder,
  478. Buildpacks: appConf.Build.Buildpacks,
  479. }
  480. }
  481. err = updateAgent.Build(buildConfig)
  482. if err != nil {
  483. return nil, err
  484. }
  485. if !appConf.Build.UseCache {
  486. err = updateAgent.Push()
  487. if err != nil {
  488. return nil, err
  489. }
  490. }
  491. }
  492. err = updateAgent.UpdateImageAndValues(appConf.Values)
  493. if err != nil {
  494. return nil, err
  495. }
  496. return resource, nil
  497. }
  498. func (d *DeployDriver) assignOutput(resource *switchboardModels.Resource, client *api.Client) error {
  499. release, err := client.GetRelease(
  500. context.Background(),
  501. d.target.Project,
  502. d.target.Cluster,
  503. d.target.Namespace,
  504. resource.Name,
  505. )
  506. if err != nil {
  507. return err
  508. }
  509. d.output = utils.CoalesceValues(d.source.SourceValues, release.Config)
  510. return nil
  511. }
  512. func (d *DeployDriver) Output() (map[string]interface{}, error) {
  513. return d.output, nil
  514. }
  515. func (d *DeployDriver) getApplicationConfig(resource *switchboardModels.Resource) (*previewInt.ApplicationConfig, error) {
  516. populatedConf, err := drivers.ConstructConfig(&drivers.ConstructConfigOpts{
  517. RawConf: resource.Config,
  518. LookupTable: *d.lookupTable,
  519. Dependencies: resource.Dependencies,
  520. })
  521. if err != nil {
  522. return nil, err
  523. }
  524. appConf := &previewInt.ApplicationConfig{}
  525. err = mapstructure.Decode(populatedConf, appConf)
  526. if err != nil {
  527. return nil, err
  528. }
  529. if _, ok := resource.Config["waitForJob"]; !ok && d.source.Name == "job" {
  530. // default to true and wait for the job to finish
  531. appConf.WaitForJob = true
  532. }
  533. return appConf, nil
  534. }
  535. func (d *DeployDriver) getAddonConfig(resource *switchboardModels.Resource) (map[string]interface{}, error) {
  536. return drivers.ConstructConfig(&drivers.ConstructConfigOpts{
  537. RawConf: resource.Config,
  538. LookupTable: *d.lookupTable,
  539. Dependencies: resource.Dependencies,
  540. })
  541. }
  542. type DeploymentHook struct {
  543. client *api.Client
  544. resourceGroup *switchboardTypes.ResourceGroup
  545. gitInstallationID, projectID, clusterID, prID, actionID, envID uint
  546. branchFrom, branchInto, namespace, repoName, repoOwner, prName, commitSHA string
  547. }
  548. func NewDeploymentHook(client *api.Client, resourceGroup *switchboardTypes.ResourceGroup, namespace string) (*DeploymentHook, error) {
  549. res := &DeploymentHook{
  550. client: client,
  551. resourceGroup: resourceGroup,
  552. namespace: namespace,
  553. }
  554. ghIDStr := os.Getenv("PORTER_GIT_INSTALLATION_ID")
  555. ghID, err := strconv.Atoi(ghIDStr)
  556. if err != nil {
  557. return nil, err
  558. }
  559. res.gitInstallationID = uint(ghID)
  560. prIDStr := os.Getenv("PORTER_PULL_REQUEST_ID")
  561. prID, err := strconv.Atoi(prIDStr)
  562. if err != nil {
  563. return nil, err
  564. }
  565. res.prID = uint(prID)
  566. res.projectID = cliConf.Project
  567. if res.projectID == 0 {
  568. return nil, fmt.Errorf("project id must be set")
  569. }
  570. res.clusterID = cliConf.Cluster
  571. if res.clusterID == 0 {
  572. return nil, fmt.Errorf("cluster id must be set")
  573. }
  574. branchFrom := os.Getenv("PORTER_BRANCH_FROM")
  575. res.branchFrom = branchFrom
  576. branchInto := os.Getenv("PORTER_BRANCH_INTO")
  577. res.branchInto = branchInto
  578. actionIDStr := os.Getenv("PORTER_ACTION_ID")
  579. actionID, err := strconv.Atoi(actionIDStr)
  580. if err != nil {
  581. return nil, err
  582. }
  583. res.actionID = uint(actionID)
  584. repoName := os.Getenv("PORTER_REPO_NAME")
  585. res.repoName = repoName
  586. repoOwner := os.Getenv("PORTER_REPO_OWNER")
  587. res.repoOwner = repoOwner
  588. prName := os.Getenv("PORTER_PR_NAME")
  589. res.prName = prName
  590. commit, err := git.LastCommit()
  591. if err != nil {
  592. return nil, fmt.Errorf(err.Error())
  593. }
  594. res.commitSHA = commit.Sha[:7]
  595. return res, nil
  596. }
  597. func (t *DeploymentHook) PreApply() error {
  598. if isSystemNamespace(t.namespace) {
  599. color.New(color.FgYellow).Printf("attempting to deploy to system namespace '%s'\n", t.namespace)
  600. }
  601. envList, err := t.client.ListEnvironments(
  602. context.Background(), t.projectID, t.clusterID,
  603. )
  604. if err != nil {
  605. return err
  606. }
  607. envs := *envList
  608. var deplEnv *types.Environment
  609. for _, env := range envs {
  610. if strings.EqualFold(env.GitRepoOwner, t.repoOwner) &&
  611. strings.EqualFold(env.GitRepoName, t.repoName) &&
  612. env.GitInstallationID == t.gitInstallationID {
  613. t.envID = env.ID
  614. deplEnv = env
  615. break
  616. }
  617. }
  618. if t.envID == 0 {
  619. return fmt.Errorf("could not find environment for deployment")
  620. }
  621. nsList, err := t.client.GetK8sNamespaces(
  622. context.Background(), t.projectID, t.clusterID,
  623. )
  624. if err != nil {
  625. return fmt.Errorf("error fetching namespaces: %w", err)
  626. }
  627. found := false
  628. for _, ns := range *nsList {
  629. if ns.Name == t.namespace {
  630. found = true
  631. break
  632. }
  633. }
  634. if !found {
  635. if isSystemNamespace(t.namespace) {
  636. return fmt.Errorf("attempting to deploy to system namespace '%s' which does not exist, please create it "+
  637. "to continue", t.namespace)
  638. }
  639. createNS := &types.CreateNamespaceRequest{
  640. Name: t.namespace,
  641. }
  642. if len(deplEnv.NamespaceAnnotations) > 0 {
  643. createNS.Annotations = deplEnv.NamespaceAnnotations
  644. }
  645. // create the new namespace
  646. _, err := t.client.CreateNewK8sNamespace(context.Background(), t.projectID, t.clusterID, createNS)
  647. if err != nil && !strings.Contains(err.Error(), "namespace already exists") {
  648. // ignore the error if the namespace already exists
  649. //
  650. // this might happen if someone creates the namespace in between this operation
  651. return fmt.Errorf("error creating namespace: %w", err)
  652. }
  653. }
  654. // attempt to read the deployment -- if it doesn't exist, create it
  655. _, err = t.client.GetDeployment(
  656. context.Background(),
  657. t.projectID, t.clusterID, t.envID,
  658. &types.GetDeploymentRequest{
  659. PRNumber: t.prID,
  660. },
  661. )
  662. if err != nil && strings.Contains(err.Error(), "not found") {
  663. // in this case, create the deployment
  664. _, err = t.client.CreateDeployment(
  665. context.Background(),
  666. t.projectID, t.gitInstallationID, t.clusterID,
  667. t.repoOwner, t.repoName,
  668. &types.CreateDeploymentRequest{
  669. Namespace: t.namespace,
  670. PullRequestID: t.prID,
  671. CreateGHDeploymentRequest: &types.CreateGHDeploymentRequest{
  672. ActionID: t.actionID,
  673. },
  674. GitHubMetadata: &types.GitHubMetadata{
  675. PRName: t.prName,
  676. RepoName: t.repoName,
  677. RepoOwner: t.repoOwner,
  678. CommitSHA: t.commitSHA,
  679. PRBranchFrom: t.branchFrom,
  680. PRBranchInto: t.branchInto,
  681. },
  682. },
  683. )
  684. } else if err == nil {
  685. _, err = t.client.UpdateDeployment(
  686. context.Background(),
  687. t.projectID, t.gitInstallationID, t.clusterID,
  688. t.repoOwner, t.repoName,
  689. &types.UpdateDeploymentRequest{
  690. Namespace: t.namespace,
  691. PRNumber: t.prID,
  692. CreateGHDeploymentRequest: &types.CreateGHDeploymentRequest{
  693. ActionID: t.actionID,
  694. },
  695. PRBranchFrom: t.branchFrom,
  696. CommitSHA: t.commitSHA,
  697. },
  698. )
  699. }
  700. return err
  701. }
  702. func (t *DeploymentHook) DataQueries() map[string]interface{} {
  703. res := make(map[string]interface{})
  704. // use the resource group to find all web applications that can have an exposed subdomain
  705. // that we can query for
  706. for _, resource := range t.resourceGroup.Resources {
  707. isWeb := false
  708. if sourceNameInter, exists := resource.Source["name"]; exists {
  709. if sourceName, ok := sourceNameInter.(string); ok {
  710. if sourceName == "web" {
  711. isWeb = true
  712. }
  713. }
  714. }
  715. if isWeb {
  716. // determine if we should query for porter_hosts or just hosts
  717. isCustomDomain := false
  718. ingressMap, err := deploy.GetNestedMap(resource.Config, "values", "ingress")
  719. if err == nil {
  720. enabledVal, enabledExists := ingressMap["enabled"]
  721. customDomVal, customDomExists := ingressMap["custom_domain"]
  722. if enabledExists && customDomExists {
  723. enabled, eOK := enabledVal.(bool)
  724. customDomain, cOK := customDomVal.(bool)
  725. if eOK && cOK && enabled {
  726. if customDomain {
  727. // return the first custom domain when one exists
  728. hostsArr, hostsExists := ingressMap["hosts"]
  729. if hostsExists {
  730. hostsArrVal, hostsArrOk := hostsArr.([]interface{})
  731. if hostsArrOk && len(hostsArrVal) > 0 {
  732. if _, ok := hostsArrVal[0].(string); ok {
  733. res[resource.Name] = fmt.Sprintf("{ .%s.ingress.hosts[0] }", resource.Name)
  734. isCustomDomain = true
  735. }
  736. }
  737. }
  738. }
  739. }
  740. }
  741. }
  742. if !isCustomDomain {
  743. res[resource.Name] = fmt.Sprintf("{ .%s.ingress.porter_hosts[0] }", resource.Name)
  744. }
  745. }
  746. }
  747. return res
  748. }
  749. func (t *DeploymentHook) PostApply(populatedData map[string]interface{}) error {
  750. subdomains := make([]string, 0)
  751. for _, data := range populatedData {
  752. domain, ok := data.(string)
  753. if !ok {
  754. continue
  755. }
  756. if _, err := url.Parse("https://" + domain); err == nil {
  757. subdomains = append(subdomains, "https://"+domain)
  758. }
  759. }
  760. req := &types.FinalizeDeploymentRequest{
  761. PRNumber: t.prID,
  762. Subdomain: strings.Join(subdomains, ", "),
  763. }
  764. for _, res := range t.resourceGroup.Resources {
  765. releaseType := getReleaseType(res)
  766. releaseName := getReleaseName(res)
  767. if releaseType != "" && releaseName != "" {
  768. req.SuccessfulResources = append(req.SuccessfulResources, &types.SuccessfullyDeployedResource{
  769. ReleaseName: releaseName,
  770. ReleaseType: releaseType,
  771. })
  772. }
  773. }
  774. // finalize the deployment
  775. _, err := t.client.FinalizeDeployment(
  776. context.Background(),
  777. t.projectID, t.gitInstallationID, t.clusterID,
  778. t.repoOwner, t.repoName, req,
  779. )
  780. return err
  781. }
  782. func (t *DeploymentHook) OnError(error) {
  783. // if the deployment exists, throw an error for that deployment
  784. _, err := t.client.GetDeployment(
  785. context.Background(),
  786. t.projectID, t.clusterID, t.envID,
  787. &types.GetDeploymentRequest{
  788. PRNumber: t.prID,
  789. },
  790. )
  791. if err == nil {
  792. // FIXME: try to use the error with a custom logger
  793. t.client.UpdateDeploymentStatus(
  794. context.Background(),
  795. t.projectID, t.gitInstallationID, t.clusterID,
  796. t.repoOwner, t.repoName,
  797. &types.UpdateDeploymentStatusRequest{
  798. PRNumber: t.prID,
  799. CreateGHDeploymentRequest: &types.CreateGHDeploymentRequest{
  800. ActionID: t.actionID,
  801. },
  802. PRBranchFrom: t.branchFrom,
  803. Status: string(types.DeploymentStatusFailed),
  804. },
  805. )
  806. }
  807. }
  808. func (t *DeploymentHook) OnConsolidatedErrors(allErrors map[string]error) {
  809. // if the deployment exists, throw an error for that deployment
  810. _, getDeplErr := t.client.GetDeployment(
  811. context.Background(),
  812. t.projectID, t.clusterID, t.envID,
  813. &types.GetDeploymentRequest{
  814. PRNumber: t.prID,
  815. },
  816. )
  817. if getDeplErr == nil {
  818. req := &types.FinalizeDeploymentWithErrorsRequest{
  819. PRNumber: t.prID,
  820. Errors: make(map[string]string),
  821. }
  822. for _, res := range t.resourceGroup.Resources {
  823. if _, ok := allErrors[res.Name]; !ok {
  824. req.SuccessfulResources = append(req.SuccessfulResources, &types.SuccessfullyDeployedResource{
  825. ReleaseName: getReleaseName(res),
  826. ReleaseType: getReleaseType(res),
  827. })
  828. }
  829. }
  830. for res, err := range allErrors {
  831. req.Errors[res] = err.Error()
  832. }
  833. // FIXME: handle the error
  834. t.client.FinalizeDeploymentWithErrors(
  835. context.Background(),
  836. t.projectID, t.gitInstallationID, t.clusterID,
  837. t.repoOwner, t.repoName,
  838. req,
  839. )
  840. }
  841. }
  842. type CloneEnvGroupHook struct {
  843. client *api.Client
  844. resGroup *switchboardTypes.ResourceGroup
  845. }
  846. func NewCloneEnvGroupHook(client *api.Client, resourceGroup *switchboardTypes.ResourceGroup) *CloneEnvGroupHook {
  847. return &CloneEnvGroupHook{
  848. client: client,
  849. resGroup: resourceGroup,
  850. }
  851. }
  852. func (t *CloneEnvGroupHook) PreApply() error {
  853. for _, res := range t.resGroup.Resources {
  854. if res.Driver == "env-group" {
  855. continue
  856. }
  857. appConf := &previewInt.ApplicationConfig{}
  858. err := mapstructure.Decode(res.Config, &appConf)
  859. if err != nil {
  860. continue
  861. }
  862. if appConf != nil && len(appConf.EnvGroups) > 0 {
  863. target, err := preview.GetTarget(res.Name, res.Target)
  864. if err != nil {
  865. return err
  866. }
  867. for _, group := range appConf.EnvGroups {
  868. if group.Name == "" {
  869. return fmt.Errorf("env group name cannot be empty")
  870. }
  871. _, err := t.client.GetEnvGroup(
  872. context.Background(),
  873. target.Project,
  874. target.Cluster,
  875. target.Namespace,
  876. &types.GetEnvGroupRequest{
  877. Name: group.Name,
  878. Version: group.Version,
  879. },
  880. )
  881. if err != nil && err.Error() == "env group not found" {
  882. if group.Namespace == "" {
  883. return fmt.Errorf("env group namespace cannot be empty")
  884. }
  885. color.New(color.FgBlue, color.Bold).
  886. Printf("Env group '%s' does not exist in the target namespace '%s'\n", group.Name, target.Namespace)
  887. color.New(color.FgBlue, color.Bold).
  888. Printf("Cloning env group '%s' from namespace '%s' to target namespace '%s'\n",
  889. group.Name, group.Namespace, target.Namespace)
  890. _, err = t.client.CloneEnvGroup(
  891. context.Background(), target.Project, target.Cluster, group.Namespace,
  892. &types.CloneEnvGroupRequest{
  893. SourceName: group.Name,
  894. TargetNamespace: target.Namespace,
  895. },
  896. )
  897. if err != nil {
  898. return err
  899. }
  900. } else if err != nil {
  901. return err
  902. }
  903. }
  904. }
  905. }
  906. return nil
  907. }
  908. func (t *CloneEnvGroupHook) DataQueries() map[string]interface{} {
  909. return nil
  910. }
  911. func (t *CloneEnvGroupHook) PostApply(map[string]interface{}) error {
  912. return nil
  913. }
  914. func (t *CloneEnvGroupHook) OnError(error) {}
  915. func (t *CloneEnvGroupHook) OnConsolidatedErrors(map[string]error) {}
  916. func getReleaseName(res *switchboardTypes.Resource) string {
  917. // can ignore the error because this method is called once
  918. // GetTarget has alrealy been called and validated previously
  919. target, _ := preview.GetTarget(res.Name, res.Target)
  920. if target.AppName != "" {
  921. return target.AppName
  922. }
  923. return res.Name
  924. }
  925. func getReleaseType(res *switchboardTypes.Resource) string {
  926. // can ignore the error because this method is called once
  927. // GetSource has alrealy been called and validated previously
  928. source, _ := preview.GetSource(res.Name, res.Source)
  929. if source != nil && source.Name != "" {
  930. return source.Name
  931. }
  932. return ""
  933. }
  934. func isSystemNamespace(namespace string) bool {
  935. return namespace == "cert-manager" || namespace == "ingress-nginx" ||
  936. namespace == "kube-node-lease" || namespace == "kube-public" ||
  937. namespace == "kube-system" || namespace == "monitoring" ||
  938. namespace == "porter-agent-system" || namespace == "default" ||
  939. namespace == "ingress-nginx-private"
  940. }