postrenderer.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818
  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. // if the resource is empty, we skip encoding it to prevent errors. Helm/k8s expects empty resources to take the form "{}",
  190. // while this library writes an empty string, causing problems during installation.
  191. if len(resource) != 0 {
  192. err = encoder.Encode(resource)
  193. if err != nil {
  194. return nil, err
  195. }
  196. }
  197. }
  198. return modifiedManifests, nil
  199. }
  200. func (d *DockerSecretsPostRenderer) getRegistriesToLink(renderedManifests *bytes.Buffer) (map[string]*models.Registry, error) {
  201. // create a map of registry names to registries: these are the registries
  202. // that a secret will be generated for, if it does not exist
  203. linkedRegs := make(map[string]*models.Registry)
  204. var err error
  205. d.resources, err = decodeRenderedManifests(renderedManifests)
  206. if err != nil {
  207. return linkedRegs, err
  208. }
  209. // read the pod specs into the post-renderer object
  210. d.getPodSpecs(d.resources)
  211. for _, podSpec := range d.podSpecs {
  212. // get all images
  213. images := d.getImageList(podSpec)
  214. // read the image url
  215. for _, image := range images {
  216. regName, err := getRegNameFromImageRef(image)
  217. if err != nil {
  218. continue
  219. }
  220. // check if the integration is native to the cluster/registry combination
  221. isNative := d.isRegistryNative(regName)
  222. if isNative {
  223. continue
  224. }
  225. reg, exists := d.registries[regName]
  226. if !exists {
  227. continue
  228. }
  229. // if the registry exists, add it to the map
  230. linkedRegs[regName] = reg
  231. }
  232. }
  233. return linkedRegs, nil
  234. }
  235. func decodeRenderedManifests(
  236. renderedManifests *bytes.Buffer,
  237. ) ([]resource, error) {
  238. resArr := make([]resource, 0)
  239. // use the yaml decoder to parse the multi-document yaml.
  240. decoder := yaml.NewDecoder(renderedManifests)
  241. for {
  242. res := make(resource)
  243. err := decoder.Decode(&res)
  244. if err == io.EOF {
  245. break
  246. }
  247. if err != nil {
  248. return resArr, err
  249. }
  250. if len(res) != 0 {
  251. resArr = append(resArr, res)
  252. }
  253. }
  254. return resArr, nil
  255. }
  256. func (d *DockerSecretsPostRenderer) getPodSpecs(resources []resource) {
  257. for _, res := range resources {
  258. kindVal, hasKind := res["kind"]
  259. if !hasKind {
  260. continue
  261. }
  262. kind, ok := kindVal.(string)
  263. if !ok {
  264. continue
  265. }
  266. // manifests of list type will have an items field, items should
  267. // be recursively parsed
  268. if itemsVal, isList := res["items"]; isList {
  269. if items, ok := itemsVal.([]interface{}); ok {
  270. // convert items to resource
  271. resArr := make([]resource, 0)
  272. for _, item := range items {
  273. if arrVal, ok := item.(resource); ok {
  274. resArr = append(resArr, arrVal)
  275. }
  276. }
  277. d.getPodSpecs(resArr)
  278. }
  279. continue
  280. }
  281. // otherwise, get the pod spec based on the type of resource
  282. podSpec := getPodSpecFromResource(kind, res)
  283. if podSpec == nil {
  284. continue
  285. }
  286. d.podSpecs = append(d.podSpecs, podSpec)
  287. }
  288. return
  289. }
  290. func (d *DockerSecretsPostRenderer) updatePodSpecs(secrets map[string]string) {
  291. for _, podSpec := range d.podSpecs {
  292. containersVal, hasContainers := podSpec["containers"]
  293. if !hasContainers {
  294. continue
  295. }
  296. containers, ok := containersVal.([]interface{})
  297. if !ok {
  298. continue
  299. }
  300. imagePullSecrets := make([]map[string]interface{}, 0)
  301. if existingPullSecrets, ok := podSpec["imagePullSecrets"]; ok {
  302. if existing, ok := existingPullSecrets.([]map[string]interface{}); ok {
  303. imagePullSecrets = existing
  304. }
  305. }
  306. for _, container := range containers {
  307. _container, ok := container.(resource)
  308. if !ok {
  309. continue
  310. }
  311. image, ok := _container["image"].(string)
  312. if !ok {
  313. continue
  314. }
  315. regName, err := getRegNameFromImageRef(image)
  316. if err != nil {
  317. continue
  318. }
  319. if secretName, ok := secrets[regName]; ok && secretName != "" {
  320. imagePullSecrets = append(imagePullSecrets, map[string]interface{}{
  321. "name": secretName,
  322. })
  323. }
  324. }
  325. if len(imagePullSecrets) > 0 {
  326. podSpec["imagePullSecrets"] = imagePullSecrets
  327. }
  328. }
  329. }
  330. func (d *DockerSecretsPostRenderer) getImageList(podSpec resource) []string {
  331. images := make([]string, 0)
  332. containersVal, hasContainers := podSpec["containers"]
  333. if !hasContainers {
  334. return images
  335. }
  336. containers, ok := containersVal.([]interface{})
  337. if !ok {
  338. return images
  339. }
  340. for _, container := range containers {
  341. _container, ok := container.(resource)
  342. if !ok {
  343. continue
  344. }
  345. image, ok := _container["image"].(string)
  346. if !ok {
  347. continue
  348. }
  349. images = append(images, image)
  350. }
  351. return images
  352. }
  353. 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)?`)
  354. func (d *DockerSecretsPostRenderer) isRegistryNative(regName string) bool {
  355. isNative := false
  356. if strings.Contains(regName, "gcr") && d.Cluster.AuthMechanism == models.GCP {
  357. // TODO (POR-33): fix architecture for clusters and re-add the code below
  358. // // get the project id of the cluster
  359. // gcpInt, err := d.Repo.GCPIntegration().ReadGCPIntegration(d.Cluster.ProjectID, d.Cluster.GCPIntegrationID)
  360. // if err != nil {
  361. // return false
  362. // }
  363. // gkeProjectID, err := integrations.GCPProjectIDFromJSON(gcpInt.GCPKeyData)
  364. // if err != nil {
  365. // return false
  366. // }
  367. // // parse the project id of the gcr url
  368. // if regNameArr := strings.Split(regName, "/"); len(regNameArr) >= 2 {
  369. // gcrProjectID := regNameArr[1]
  370. // isNative = gcrProjectID == gkeProjectID
  371. // }
  372. } else if strings.Contains(regName, "ecr") && d.Cluster.AuthMechanism == models.AWS {
  373. matches := ecrPattern.FindStringSubmatch(regName)
  374. if len(matches) < 3 {
  375. return false
  376. }
  377. eksAccountID := matches[1]
  378. eksRegion := matches[3]
  379. awsInt, err := d.Repo.AWSIntegration().ReadAWSIntegration(d.Cluster.ProjectID, d.Cluster.AWSIntegrationID)
  380. if err != nil {
  381. return false
  382. }
  383. err = awsInt.PopulateAWSArn()
  384. if err != nil {
  385. return false
  386. }
  387. parsedARN, err := arn.Parse(awsInt.AWSArn)
  388. if err != nil {
  389. return false
  390. }
  391. isNative = parsedARN.AccountID == eksAccountID && parsedARN.Region == eksRegion
  392. }
  393. return isNative
  394. }
  395. // EnvironmentVariablePostrenderer removes duplicated environment variables, giving preference to synced
  396. // env vars
  397. type EnvironmentVariablePostrenderer struct {
  398. podSpecs []resource
  399. resources []resource
  400. }
  401. func NewEnvironmentVariablePostrenderer() (*EnvironmentVariablePostrenderer, error) {
  402. return &EnvironmentVariablePostrenderer{
  403. podSpecs: make([]resource, 0),
  404. resources: make([]resource, 0),
  405. }, nil
  406. }
  407. func (e *EnvironmentVariablePostrenderer) Run(
  408. renderedManifests *bytes.Buffer,
  409. ) (modifiedManifests *bytes.Buffer, err error) {
  410. e.resources, err = decodeRenderedManifests(renderedManifests)
  411. if err != nil {
  412. return nil, err
  413. }
  414. // Check to see if the resources loaded into the postrenderer contain a configmap
  415. // with a manifest that needs env var cleanup as well. If this is the case, create and
  416. // run another postrenderer for this specific manifest.
  417. for i, res := range e.resources {
  418. kindVal, hasKind := res["kind"]
  419. if !hasKind {
  420. continue
  421. }
  422. kind, ok := kindVal.(string)
  423. if !ok {
  424. continue
  425. }
  426. if kind == "ConfigMap" {
  427. labelVal := getNestedResource(res, "metadata", "labels")
  428. if labelVal == nil {
  429. continue
  430. }
  431. porterLabelVal, exists := labelVal["getporter.dev/manifest"]
  432. if !exists {
  433. continue
  434. }
  435. if labelValStr, ok := porterLabelVal.(string); ok && labelValStr == "true" {
  436. data := getNestedResource(res, "data")
  437. manifestData, exists := data["manifest"]
  438. if !exists {
  439. continue
  440. }
  441. manifestDataStr, ok := manifestData.(string)
  442. if !ok {
  443. continue
  444. }
  445. dCopy := &EnvironmentVariablePostrenderer{
  446. podSpecs: make([]resource, 0),
  447. resources: make([]resource, 0),
  448. }
  449. newData, err := dCopy.Run(bytes.NewBufferString(manifestDataStr))
  450. if err != nil {
  451. continue
  452. }
  453. data["manifest"] = string(newData.Bytes())
  454. e.resources[i] = res
  455. }
  456. }
  457. }
  458. e.getPodSpecs(e.resources)
  459. e.updatePodSpecs()
  460. modifiedManifests = bytes.NewBuffer([]byte{})
  461. encoder := yaml.NewEncoder(modifiedManifests)
  462. defer encoder.Close()
  463. for _, resource := range e.resources {
  464. err = encoder.Encode(resource)
  465. if err != nil {
  466. return nil, err
  467. }
  468. }
  469. return modifiedManifests, nil
  470. }
  471. func (e *EnvironmentVariablePostrenderer) getPodSpecs(resources []resource) {
  472. for _, res := range resources {
  473. kindVal, hasKind := res["kind"]
  474. if !hasKind {
  475. continue
  476. }
  477. kind, ok := kindVal.(string)
  478. if !ok {
  479. continue
  480. }
  481. // manifests of list type will have an items field, items should
  482. // be recursively parsed
  483. if itemsVal, isList := res["items"]; isList {
  484. if items, ok := itemsVal.([]interface{}); ok {
  485. // convert items to resource
  486. resArr := make([]resource, 0)
  487. for _, item := range items {
  488. if arrVal, ok := item.(resource); ok {
  489. resArr = append(resArr, arrVal)
  490. }
  491. }
  492. e.getPodSpecs(resArr)
  493. }
  494. continue
  495. }
  496. // otherwise, get the pod spec based on the type of resource
  497. podSpec := getPodSpecFromResource(kind, res)
  498. if podSpec == nil {
  499. continue
  500. }
  501. e.podSpecs = append(e.podSpecs, podSpec)
  502. }
  503. return
  504. }
  505. func (e *EnvironmentVariablePostrenderer) updatePodSpecs() error {
  506. // for each pod spec, remove duplicate env variables
  507. for _, podSpec := range e.podSpecs {
  508. containersVal, hasContainers := podSpec["containers"]
  509. if !hasContainers {
  510. continue
  511. }
  512. containers, ok := containersVal.([]interface{})
  513. if !ok {
  514. continue
  515. }
  516. newContainers := make([]interface{}, 0)
  517. for _, container := range containers {
  518. envVars := make(map[string]interface{})
  519. _container, ok := container.(resource)
  520. if !ok {
  521. continue
  522. }
  523. // read container env variables
  524. envInter, ok := _container["env"]
  525. if !ok {
  526. newContainers = append(newContainers, _container)
  527. continue
  528. }
  529. env, ok := envInter.([]interface{})
  530. if !ok {
  531. newContainers = append(newContainers, _container)
  532. continue
  533. }
  534. for _, envVar := range env {
  535. envVarMap, ok := envVar.(resource)
  536. if !ok {
  537. continue
  538. }
  539. envVarName, ok := envVarMap["name"]
  540. if !ok {
  541. continue
  542. }
  543. envVarNameStr, ok := envVarName.(string)
  544. if !ok {
  545. continue
  546. }
  547. // check if the env var already exists, if it does perform reconciliation
  548. if currVal, exists := envVars[envVarNameStr]; exists {
  549. currValMap, ok := currVal.(resource)
  550. if !ok {
  551. continue
  552. }
  553. // if the current value has a valueFrom field, this should override the existing env var
  554. if _, currValFromFieldExists := currValMap["valueFrom"]; currValFromFieldExists {
  555. continue
  556. } else {
  557. envVars[envVarNameStr] = envVarMap
  558. }
  559. } else {
  560. envVars[envVarNameStr] = envVarMap
  561. }
  562. }
  563. // flatten env var map to array
  564. envVarArr := make([]interface{}, 0)
  565. for _, envVar := range envVars {
  566. envVarArr = append(envVarArr, envVar)
  567. }
  568. _container["env"] = envVarArr
  569. newContainers = append(newContainers, _container)
  570. }
  571. podSpec["containers"] = newContainers
  572. }
  573. return nil
  574. }
  575. // HELPERS
  576. func getPodSpecFromResource(kind string, res resource) resource {
  577. switch kind {
  578. case "Pod":
  579. return getNestedResource(res, "spec")
  580. case "DaemonSet", "Deployment", "Job", "ReplicaSet", "ReplicationController", "StatefulSet":
  581. return getNestedResource(res, "spec", "template", "spec")
  582. case "PodTemplate":
  583. return getNestedResource(res, "template", "spec")
  584. case "CronJob":
  585. return getNestedResource(res, "spec", "jobTemplate", "spec", "template", "spec")
  586. }
  587. return nil
  588. }
  589. func getNestedResource(res resource, keys ...string) resource {
  590. curr := res
  591. var ok bool
  592. for _, key := range keys {
  593. curr, ok = curr[key].(resource)
  594. if !ok {
  595. return nil
  596. }
  597. }
  598. return curr
  599. }
  600. func getRegNameFromImageRef(image string) (string, error) {
  601. named, err := reference.ParseNormalizedNamed(image)
  602. if err != nil {
  603. return "", err
  604. }
  605. domain := reference.Domain(named)
  606. path := reference.Path(named)
  607. var regName string
  608. // if registry is dockerhub, leave the image name as-is
  609. if strings.Contains(domain, "docker.io") {
  610. regName = "index.docker.io/" + path
  611. } else {
  612. regName = domain
  613. if pathArr := strings.Split(path, "/"); len(pathArr) > 1 {
  614. regName += "/" + strings.Join(pathArr[:len(pathArr)-1], "/")
  615. }
  616. }
  617. return regName, nil
  618. }