2
0

apply.go 34 KB

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