postrenderer.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812
  1. package helm
  2. import (
  3. "bytes"
  4. "io"
  5. "net/url"
  6. "regexp"
  7. "strings"
  8. "github.com/aws/aws-sdk-go/aws/arn"
  9. "github.com/porter-dev/porter/internal/kubernetes"
  10. "github.com/porter-dev/porter/internal/models"
  11. "github.com/porter-dev/porter/internal/repository"
  12. "golang.org/x/oauth2"
  13. "gopkg.in/yaml.v2"
  14. "helm.sh/helm/v3/pkg/postrender"
  15. "github.com/docker/distribution/reference"
  16. )
  17. type PorterPostrenderer struct {
  18. DockerSecretsPostRenderer *DockerSecretsPostRenderer
  19. EnvironmentVariablePostrenderer *EnvironmentVariablePostrenderer
  20. }
  21. func NewPorterPostrenderer(
  22. cluster *models.Cluster,
  23. repo repository.Repository,
  24. agent *kubernetes.Agent,
  25. namespace string,
  26. regs []*models.Registry,
  27. doAuth *oauth2.Config,
  28. ) (postrender.PostRenderer, error) {
  29. var dockerSecretsPostrenderer *DockerSecretsPostRenderer
  30. var err error
  31. if cluster != nil && agent != nil && regs != nil && len(regs) > 0 {
  32. dockerSecretsPostrenderer, err = NewDockerSecretsPostRenderer(cluster, repo, agent, namespace, regs, doAuth)
  33. if err != nil {
  34. return nil, err
  35. }
  36. }
  37. envVarPostrenderer, err := NewEnvironmentVariablePostrenderer()
  38. if err != nil {
  39. return nil, err
  40. }
  41. return &PorterPostrenderer{
  42. DockerSecretsPostRenderer: dockerSecretsPostrenderer,
  43. EnvironmentVariablePostrenderer: envVarPostrenderer,
  44. }, nil
  45. }
  46. func (p *PorterPostrenderer) Run(
  47. renderedManifests *bytes.Buffer,
  48. ) (modifiedManifests *bytes.Buffer, err error) {
  49. if p.DockerSecretsPostRenderer != nil {
  50. renderedManifests, err = p.DockerSecretsPostRenderer.Run(renderedManifests)
  51. if err != nil {
  52. return nil, err
  53. }
  54. }
  55. renderedManifests, err = p.EnvironmentVariablePostrenderer.Run(renderedManifests)
  56. return renderedManifests, err
  57. }
  58. // DockerSecretsPostRenderer is a Helm post-renderer that adds image pull secrets to
  59. // pod specs that would otherwise be unable to pull an image.
  60. //
  61. // The post-renderer currently looks for two types of registries: GCR and ECR (TODO: DOCR
  62. // and Dockerhub). It also detects if the image pull secret is necessary: if GCR image pulls
  63. // occur in a GKE cluster in the same project, or if ECR image pulls exist in an EKS cluster
  64. // in the same organization + region, an image pull is not necessary.
  65. type DockerSecretsPostRenderer struct {
  66. Cluster *models.Cluster
  67. Repo repository.Repository
  68. Agent *kubernetes.Agent
  69. Namespace string
  70. DOAuth *oauth2.Config
  71. registries map[string]*models.Registry
  72. podSpecs []resource
  73. resources []resource
  74. }
  75. // while manifests are map[string]interface{} at the top level,
  76. // nested keys will be of type map[interface{}]interface{}
  77. type resource map[interface{}]interface{}
  78. func NewDockerSecretsPostRenderer(
  79. cluster *models.Cluster,
  80. repo repository.Repository,
  81. agent *kubernetes.Agent,
  82. namespace string,
  83. regs []*models.Registry,
  84. doAuth *oauth2.Config,
  85. ) (*DockerSecretsPostRenderer, error) {
  86. // Registries is a map of registry URLs to registry ids
  87. registries := make(map[string]*models.Registry)
  88. for _, reg := range regs {
  89. regURL := reg.URL
  90. if !strings.Contains(regURL, "http") {
  91. regURL = "https://" + regURL
  92. }
  93. parsedRegURL, err := url.Parse(regURL)
  94. if err != nil {
  95. continue
  96. }
  97. addReg := parsedRegURL.Host
  98. if parsedRegURL.Path != "" {
  99. addReg += "/" + strings.Trim(parsedRegURL.Path, "/")
  100. }
  101. registries[addReg] = reg
  102. }
  103. return &DockerSecretsPostRenderer{
  104. Cluster: cluster,
  105. Repo: repo,
  106. Agent: agent,
  107. Namespace: namespace,
  108. DOAuth: doAuth,
  109. registries: registries,
  110. podSpecs: make([]resource, 0),
  111. resources: make([]resource, 0),
  112. }, nil
  113. }
  114. func (d *DockerSecretsPostRenderer) Run(
  115. renderedManifests *bytes.Buffer,
  116. ) (modifiedManifests *bytes.Buffer, err error) {
  117. bufCopy := bytes.NewBuffer(renderedManifests.Bytes())
  118. linkedRegs, err := d.getRegistriesToLink(bufCopy)
  119. // if we encountered an error here, we'll render the manifests anyway
  120. // without modification
  121. if err != nil {
  122. return renderedManifests, nil
  123. }
  124. // Check to see if the resources loaded into the postrenderer contain a configmap
  125. // with a manifest that needs secrets generation as well. If this is the case, create and
  126. // run another postrenderer for this specific manifest.
  127. for i, res := range d.resources {
  128. kindVal, hasKind := res["kind"]
  129. if !hasKind {
  130. continue
  131. }
  132. kind, ok := kindVal.(string)
  133. if !ok {
  134. continue
  135. }
  136. if kind == "ConfigMap" {
  137. labelVal := getNestedResource(res, "metadata", "labels")
  138. if labelVal == nil {
  139. continue
  140. }
  141. porterLabelVal, exists := labelVal["getporter.dev/manifest"]
  142. if !exists {
  143. continue
  144. }
  145. if labelValStr, ok := porterLabelVal.(string); ok && labelValStr == "true" {
  146. data := getNestedResource(res, "data")
  147. manifestData, exists := data["manifest"]
  148. if !exists {
  149. continue
  150. }
  151. manifestDataStr, ok := manifestData.(string)
  152. if !ok {
  153. continue
  154. }
  155. dCopy := &DockerSecretsPostRenderer{
  156. Cluster: d.Cluster,
  157. Repo: d.Repo,
  158. Agent: d.Agent,
  159. Namespace: d.Namespace,
  160. DOAuth: d.DOAuth,
  161. registries: d.registries,
  162. podSpecs: make([]resource, 0),
  163. resources: make([]resource, 0),
  164. }
  165. newData, err := dCopy.Run(bytes.NewBufferString(manifestDataStr))
  166. if err != nil {
  167. continue
  168. }
  169. data["manifest"] = string(newData.Bytes())
  170. d.resources[i] = res
  171. }
  172. }
  173. }
  174. // create the necessary secrets
  175. secrets, err := d.Agent.CreateImagePullSecrets(
  176. d.Repo,
  177. d.Namespace,
  178. linkedRegs,
  179. d.DOAuth,
  180. )
  181. if err != nil {
  182. return renderedManifests, nil
  183. }
  184. d.updatePodSpecs(secrets)
  185. modifiedManifests = bytes.NewBuffer([]byte{})
  186. encoder := yaml.NewEncoder(modifiedManifests)
  187. defer encoder.Close()
  188. for _, resource := range d.resources {
  189. err = encoder.Encode(resource)
  190. if err != nil {
  191. return nil, err
  192. }
  193. }
  194. return modifiedManifests, nil
  195. }
  196. func (d *DockerSecretsPostRenderer) getRegistriesToLink(renderedManifests *bytes.Buffer) (map[string]*models.Registry, error) {
  197. // create a map of registry names to registries: these are the registries
  198. // that a secret will be generated for, if it does not exist
  199. linkedRegs := make(map[string]*models.Registry)
  200. var err error
  201. d.resources, err = decodeRenderedManifests(renderedManifests)
  202. if err != nil {
  203. return linkedRegs, err
  204. }
  205. // read the pod specs into the post-renderer object
  206. d.getPodSpecs(d.resources)
  207. for _, podSpec := range d.podSpecs {
  208. // get all images
  209. images := d.getImageList(podSpec)
  210. // read the image url
  211. for _, image := range images {
  212. regName, err := getRegNameFromImageRef(image)
  213. if err != nil {
  214. continue
  215. }
  216. // check if the integration is native to the cluster/registry combination
  217. isNative := d.isRegistryNative(regName)
  218. if isNative {
  219. continue
  220. }
  221. reg, exists := d.registries[regName]
  222. if !exists {
  223. continue
  224. }
  225. // if the registry exists, add it to the map
  226. linkedRegs[regName] = reg
  227. }
  228. }
  229. return linkedRegs, nil
  230. }
  231. func decodeRenderedManifests(
  232. renderedManifests *bytes.Buffer,
  233. ) ([]resource, error) {
  234. resArr := make([]resource, 0)
  235. // use the yaml decoder to parse the multi-document yaml.
  236. decoder := yaml.NewDecoder(renderedManifests)
  237. for {
  238. res := make(resource)
  239. err := decoder.Decode(&res)
  240. if err == io.EOF {
  241. break
  242. }
  243. if err != nil {
  244. return resArr, err
  245. }
  246. resArr = append(resArr, res)
  247. }
  248. return resArr, nil
  249. }
  250. func (d *DockerSecretsPostRenderer) getPodSpecs(resources []resource) {
  251. for _, res := range resources {
  252. kindVal, hasKind := res["kind"]
  253. if !hasKind {
  254. continue
  255. }
  256. kind, ok := kindVal.(string)
  257. if !ok {
  258. continue
  259. }
  260. // manifests of list type will have an items field, items should
  261. // be recursively parsed
  262. if itemsVal, isList := res["items"]; isList {
  263. if items, ok := itemsVal.([]interface{}); ok {
  264. // convert items to resource
  265. resArr := make([]resource, 0)
  266. for _, item := range items {
  267. if arrVal, ok := item.(resource); ok {
  268. resArr = append(resArr, arrVal)
  269. }
  270. }
  271. d.getPodSpecs(resArr)
  272. }
  273. continue
  274. }
  275. // otherwise, get the pod spec based on the type of resource
  276. podSpec := getPodSpecFromResource(kind, res)
  277. if podSpec == nil {
  278. continue
  279. }
  280. d.podSpecs = append(d.podSpecs, podSpec)
  281. }
  282. return
  283. }
  284. func (d *DockerSecretsPostRenderer) updatePodSpecs(secrets map[string]string) {
  285. for _, podSpec := range d.podSpecs {
  286. containersVal, hasContainers := podSpec["containers"]
  287. if !hasContainers {
  288. continue
  289. }
  290. containers, ok := containersVal.([]interface{})
  291. if !ok {
  292. continue
  293. }
  294. imagePullSecrets := make([]map[string]interface{}, 0)
  295. if existingPullSecrets, ok := podSpec["imagePullSecrets"]; ok {
  296. if existing, ok := existingPullSecrets.([]map[string]interface{}); ok {
  297. imagePullSecrets = existing
  298. }
  299. }
  300. for _, container := range containers {
  301. _container, ok := container.(resource)
  302. if !ok {
  303. continue
  304. }
  305. image, ok := _container["image"].(string)
  306. if !ok {
  307. continue
  308. }
  309. regName, err := getRegNameFromImageRef(image)
  310. if err != nil {
  311. continue
  312. }
  313. if secretName, ok := secrets[regName]; ok && secretName != "" {
  314. imagePullSecrets = append(imagePullSecrets, map[string]interface{}{
  315. "name": secretName,
  316. })
  317. }
  318. }
  319. if len(imagePullSecrets) > 0 {
  320. podSpec["imagePullSecrets"] = imagePullSecrets
  321. }
  322. }
  323. }
  324. func (d *DockerSecretsPostRenderer) getImageList(podSpec resource) []string {
  325. images := make([]string, 0)
  326. containersVal, hasContainers := podSpec["containers"]
  327. if !hasContainers {
  328. return images
  329. }
  330. containers, ok := containersVal.([]interface{})
  331. if !ok {
  332. return images
  333. }
  334. for _, container := range containers {
  335. _container, ok := container.(resource)
  336. if !ok {
  337. continue
  338. }
  339. image, ok := _container["image"].(string)
  340. if !ok {
  341. continue
  342. }
  343. images = append(images, image)
  344. }
  345. return images
  346. }
  347. var ecrPattern = regexp.MustCompile(`(^[a-zA-Z0-9][a-zA-Z0-9-_]*)\.dkr\.ecr(\-fips)?\.([a-zA-Z0-9][a-zA-Z0-9-_]*)\.amazonaws\.com(\.cn)?`)
  348. func (d *DockerSecretsPostRenderer) isRegistryNative(regName string) bool {
  349. isNative := false
  350. if strings.Contains(regName, "gcr") && d.Cluster.AuthMechanism == models.GCP {
  351. // TODO (POR-33): fix architecture for clusters and re-add the code below
  352. // // get the project id of the cluster
  353. // gcpInt, err := d.Repo.GCPIntegration().ReadGCPIntegration(d.Cluster.ProjectID, d.Cluster.GCPIntegrationID)
  354. // if err != nil {
  355. // return false
  356. // }
  357. // gkeProjectID, err := integrations.GCPProjectIDFromJSON(gcpInt.GCPKeyData)
  358. // if err != nil {
  359. // return false
  360. // }
  361. // // parse the project id of the gcr url
  362. // if regNameArr := strings.Split(regName, "/"); len(regNameArr) >= 2 {
  363. // gcrProjectID := regNameArr[1]
  364. // isNative = gcrProjectID == gkeProjectID
  365. // }
  366. } else if strings.Contains(regName, "ecr") && d.Cluster.AuthMechanism == models.AWS {
  367. matches := ecrPattern.FindStringSubmatch(regName)
  368. if len(matches) < 3 {
  369. return false
  370. }
  371. eksAccountID := matches[1]
  372. eksRegion := matches[3]
  373. awsInt, err := d.Repo.AWSIntegration().ReadAWSIntegration(d.Cluster.ProjectID, d.Cluster.AWSIntegrationID)
  374. if err != nil {
  375. return false
  376. }
  377. err = awsInt.PopulateAWSArn()
  378. if err != nil {
  379. return false
  380. }
  381. parsedARN, err := arn.Parse(awsInt.AWSArn)
  382. if err != nil {
  383. return false
  384. }
  385. isNative = parsedARN.AccountID == eksAccountID && parsedARN.Region == eksRegion
  386. }
  387. return isNative
  388. }
  389. // EnvironmentVariablePostrenderer removes duplicated environment variables, giving preference to synced
  390. // env vars
  391. type EnvironmentVariablePostrenderer struct {
  392. podSpecs []resource
  393. resources []resource
  394. }
  395. func NewEnvironmentVariablePostrenderer() (*EnvironmentVariablePostrenderer, error) {
  396. return &EnvironmentVariablePostrenderer{
  397. podSpecs: make([]resource, 0),
  398. resources: make([]resource, 0),
  399. }, nil
  400. }
  401. func (e *EnvironmentVariablePostrenderer) Run(
  402. renderedManifests *bytes.Buffer,
  403. ) (modifiedManifests *bytes.Buffer, err error) {
  404. e.resources, err = decodeRenderedManifests(renderedManifests)
  405. if err != nil {
  406. return nil, err
  407. }
  408. // Check to see if the resources loaded into the postrenderer contain a configmap
  409. // with a manifest that needs env var cleanup as well. If this is the case, create and
  410. // run another postrenderer for this specific manifest.
  411. for i, res := range e.resources {
  412. kindVal, hasKind := res["kind"]
  413. if !hasKind {
  414. continue
  415. }
  416. kind, ok := kindVal.(string)
  417. if !ok {
  418. continue
  419. }
  420. if kind == "ConfigMap" {
  421. labelVal := getNestedResource(res, "metadata", "labels")
  422. if labelVal == nil {
  423. continue
  424. }
  425. porterLabelVal, exists := labelVal["getporter.dev/manifest"]
  426. if !exists {
  427. continue
  428. }
  429. if labelValStr, ok := porterLabelVal.(string); ok && labelValStr == "true" {
  430. data := getNestedResource(res, "data")
  431. manifestData, exists := data["manifest"]
  432. if !exists {
  433. continue
  434. }
  435. manifestDataStr, ok := manifestData.(string)
  436. if !ok {
  437. continue
  438. }
  439. dCopy := &EnvironmentVariablePostrenderer{
  440. podSpecs: make([]resource, 0),
  441. resources: make([]resource, 0),
  442. }
  443. newData, err := dCopy.Run(bytes.NewBufferString(manifestDataStr))
  444. if err != nil {
  445. continue
  446. }
  447. data["manifest"] = string(newData.Bytes())
  448. e.resources[i] = res
  449. }
  450. }
  451. }
  452. e.getPodSpecs(e.resources)
  453. e.updatePodSpecs()
  454. modifiedManifests = bytes.NewBuffer([]byte{})
  455. encoder := yaml.NewEncoder(modifiedManifests)
  456. defer encoder.Close()
  457. for _, resource := range e.resources {
  458. err = encoder.Encode(resource)
  459. if err != nil {
  460. return nil, err
  461. }
  462. }
  463. return modifiedManifests, nil
  464. }
  465. func (e *EnvironmentVariablePostrenderer) getPodSpecs(resources []resource) {
  466. for _, res := range resources {
  467. kindVal, hasKind := res["kind"]
  468. if !hasKind {
  469. continue
  470. }
  471. kind, ok := kindVal.(string)
  472. if !ok {
  473. continue
  474. }
  475. // manifests of list type will have an items field, items should
  476. // be recursively parsed
  477. if itemsVal, isList := res["items"]; isList {
  478. if items, ok := itemsVal.([]interface{}); ok {
  479. // convert items to resource
  480. resArr := make([]resource, 0)
  481. for _, item := range items {
  482. if arrVal, ok := item.(resource); ok {
  483. resArr = append(resArr, arrVal)
  484. }
  485. }
  486. e.getPodSpecs(resArr)
  487. }
  488. continue
  489. }
  490. // otherwise, get the pod spec based on the type of resource
  491. podSpec := getPodSpecFromResource(kind, res)
  492. if podSpec == nil {
  493. continue
  494. }
  495. e.podSpecs = append(e.podSpecs, podSpec)
  496. }
  497. return
  498. }
  499. func (e *EnvironmentVariablePostrenderer) updatePodSpecs() error {
  500. // for each pod spec, remove duplicate env variables
  501. for _, podSpec := range e.podSpecs {
  502. containersVal, hasContainers := podSpec["containers"]
  503. if !hasContainers {
  504. continue
  505. }
  506. containers, ok := containersVal.([]interface{})
  507. if !ok {
  508. continue
  509. }
  510. newContainers := make([]interface{}, 0)
  511. for _, container := range containers {
  512. envVars := make(map[string]interface{})
  513. _container, ok := container.(resource)
  514. if !ok {
  515. continue
  516. }
  517. // read container env variables
  518. envInter, ok := _container["env"]
  519. if !ok {
  520. newContainers = append(newContainers, _container)
  521. continue
  522. }
  523. env, ok := envInter.([]interface{})
  524. if !ok {
  525. newContainers = append(newContainers, _container)
  526. continue
  527. }
  528. for _, envVar := range env {
  529. envVarMap, ok := envVar.(resource)
  530. if !ok {
  531. continue
  532. }
  533. envVarName, ok := envVarMap["name"]
  534. if !ok {
  535. continue
  536. }
  537. envVarNameStr, ok := envVarName.(string)
  538. if !ok {
  539. continue
  540. }
  541. // check if the env var already exists, if it does perform reconciliation
  542. if currVal, exists := envVars[envVarNameStr]; exists {
  543. currValMap, ok := currVal.(resource)
  544. if !ok {
  545. continue
  546. }
  547. // if the current value has a valueFrom field, this should override the existing env var
  548. if _, currValFromFieldExists := currValMap["valueFrom"]; currValFromFieldExists {
  549. continue
  550. } else {
  551. envVars[envVarNameStr] = envVarMap
  552. }
  553. } else {
  554. envVars[envVarNameStr] = envVarMap
  555. }
  556. }
  557. // flatten env var map to array
  558. envVarArr := make([]interface{}, 0)
  559. for _, envVar := range envVars {
  560. envVarArr = append(envVarArr, envVar)
  561. }
  562. _container["env"] = envVarArr
  563. newContainers = append(newContainers, _container)
  564. }
  565. podSpec["containers"] = newContainers
  566. }
  567. return nil
  568. }
  569. // HELPERS
  570. func getPodSpecFromResource(kind string, res resource) resource {
  571. switch kind {
  572. case "Pod":
  573. return getNestedResource(res, "spec")
  574. case "DaemonSet", "Deployment", "Job", "ReplicaSet", "ReplicationController", "StatefulSet":
  575. return getNestedResource(res, "spec", "template", "spec")
  576. case "PodTemplate":
  577. return getNestedResource(res, "template", "spec")
  578. case "CronJob":
  579. return getNestedResource(res, "spec", "jobTemplate", "spec", "template", "spec")
  580. }
  581. return nil
  582. }
  583. func getNestedResource(res resource, keys ...string) resource {
  584. curr := res
  585. var ok bool
  586. for _, key := range keys {
  587. curr, ok = curr[key].(resource)
  588. if !ok {
  589. return nil
  590. }
  591. }
  592. return curr
  593. }
  594. func getRegNameFromImageRef(image string) (string, error) {
  595. named, err := reference.ParseNormalizedNamed(image)
  596. if err != nil {
  597. return "", err
  598. }
  599. domain := reference.Domain(named)
  600. path := reference.Path(named)
  601. var regName string
  602. // if registry is dockerhub, leave the image name as-is
  603. if strings.Contains(domain, "docker.io") {
  604. regName = "index.docker.io/" + path
  605. } else {
  606. regName = domain
  607. if pathArr := strings.Split(path, "/"); len(pathArr) > 1 {
  608. regName += "/" + strings.Join(pathArr[:len(pathArr)-1], "/")
  609. }
  610. }
  611. return regName, nil
  612. }