postrenderer.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  1. package helm
  2. import (
  3. "bytes"
  4. "fmt"
  5. "io"
  6. "net/url"
  7. "regexp"
  8. "strings"
  9. "github.com/aws/aws-sdk-go/aws/arn"
  10. "github.com/porter-dev/porter/internal/kubernetes"
  11. "github.com/porter-dev/porter/internal/models"
  12. "github.com/porter-dev/porter/internal/models/integrations"
  13. "github.com/porter-dev/porter/internal/repository"
  14. "golang.org/x/oauth2"
  15. "gopkg.in/yaml.v2"
  16. "helm.sh/helm/v3/pkg/postrender"
  17. "github.com/docker/distribution/reference"
  18. )
  19. // DockerSecretsPostRenderer is a Helm post-renderer that adds image pull secrets to
  20. // pod specs that would otherwise be unable to pull an image.
  21. //
  22. // The post-renderer currently looks for two types of registries: GCR and ECR (TODO: DOCR
  23. // and Dockerhub). It also detects if the image pull secret is necessary: if GCR image pulls
  24. // occur in a GKE cluster in the same project, or if ECR image pulls exist in an EKS cluster
  25. // in the same organization + region, an image pull is not necessary.
  26. type DockerSecretsPostRenderer struct {
  27. Cluster *models.Cluster
  28. Repo repository.Repository
  29. Agent *kubernetes.Agent
  30. Namespace string
  31. DOAuth *oauth2.Config
  32. registries map[string]*models.Registry
  33. podSpecs []resource
  34. resources []resource
  35. }
  36. // while manifests are map[string]interface{} at the top level,
  37. // nested keys will be of type map[interface{}]interface{}
  38. type resource map[interface{}]interface{}
  39. func NewDockerSecretsPostRenderer(
  40. cluster *models.Cluster,
  41. repo repository.Repository,
  42. agent *kubernetes.Agent,
  43. namespace string,
  44. regs []*models.Registry,
  45. doAuth *oauth2.Config,
  46. ) (postrender.PostRenderer, error) {
  47. // Registries is a map of registry URLs to registry ids
  48. registries := make(map[string]*models.Registry)
  49. for _, reg := range regs {
  50. regURL := reg.URL
  51. if !strings.Contains(regURL, "http") {
  52. regURL = "https://" + regURL
  53. }
  54. parsedRegURL, err := url.Parse(regURL)
  55. if err != nil {
  56. continue
  57. }
  58. addReg := parsedRegURL.Host
  59. if parsedRegURL.Path != "" {
  60. addReg += "/" + strings.Trim(parsedRegURL.Path, "/")
  61. }
  62. registries[addReg] = reg
  63. }
  64. return &DockerSecretsPostRenderer{
  65. Cluster: cluster,
  66. Repo: repo,
  67. Agent: agent,
  68. Namespace: namespace,
  69. DOAuth: doAuth,
  70. registries: registries,
  71. podSpecs: make([]resource, 0),
  72. resources: make([]resource, 0),
  73. }, nil
  74. }
  75. func (d *DockerSecretsPostRenderer) Run(
  76. renderedManifests *bytes.Buffer,
  77. ) (modifiedManifests *bytes.Buffer, err error) {
  78. bufCopy := bytes.NewBuffer(renderedManifests.Bytes())
  79. linkedRegs, err := d.getRegistriesToLink(bufCopy)
  80. // if we encountered an error here, we'll render the manifests anyway
  81. // without modification
  82. if err != nil {
  83. return renderedManifests, nil
  84. }
  85. // Check to see if the resources loaded into the postrenderer contain a configmap
  86. // with a manifest that needs secrets generation as well. If this is the case, create and
  87. // run another postrenderer for this specific manifest.
  88. for i, res := range d.resources {
  89. kindVal, hasKind := res["kind"]
  90. if !hasKind {
  91. continue
  92. }
  93. kind, ok := kindVal.(string)
  94. if !ok {
  95. continue
  96. }
  97. if kind == "ConfigMap" {
  98. labelVal := getNestedResource(res, "metadata", "labels")
  99. if labelVal == nil {
  100. continue
  101. }
  102. porterLabelVal, exists := labelVal["getporter.dev/manifest"]
  103. if !exists {
  104. continue
  105. }
  106. if labelValStr, ok := porterLabelVal.(string); ok && labelValStr == "true" {
  107. data := getNestedResource(res, "data")
  108. manifestData, exists := data["manifest"]
  109. if !exists {
  110. continue
  111. }
  112. manifestDataStr, ok := manifestData.(string)
  113. if !ok {
  114. continue
  115. }
  116. dCopy := &DockerSecretsPostRenderer{
  117. Cluster: d.Cluster,
  118. Repo: d.Repo,
  119. Agent: d.Agent,
  120. Namespace: d.Namespace,
  121. DOAuth: d.DOAuth,
  122. registries: d.registries,
  123. podSpecs: make([]resource, 0),
  124. resources: make([]resource, 0),
  125. }
  126. newData, err := dCopy.Run(bytes.NewBufferString(manifestDataStr))
  127. if err != nil {
  128. continue
  129. }
  130. data["manifest"] = string(newData.Bytes())
  131. d.resources[i] = res
  132. fmt.Println("RES IS", res, d.resources[i])
  133. }
  134. }
  135. }
  136. // create the necessary secrets
  137. secrets, err := d.Agent.CreateImagePullSecrets(
  138. d.Repo,
  139. d.Namespace,
  140. linkedRegs,
  141. d.DOAuth,
  142. )
  143. if err != nil {
  144. return renderedManifests, nil
  145. }
  146. d.updatePodSpecs(secrets)
  147. modifiedManifests = bytes.NewBuffer([]byte{})
  148. encoder := yaml.NewEncoder(modifiedManifests)
  149. defer encoder.Close()
  150. for _, resource := range d.resources {
  151. err = encoder.Encode(resource)
  152. if err != nil {
  153. return nil, err
  154. }
  155. }
  156. return modifiedManifests, nil
  157. }
  158. func (d *DockerSecretsPostRenderer) getRegistriesToLink(renderedManifests *bytes.Buffer) (map[string]*models.Registry, error) {
  159. // create a map of registry names to registries: these are the registries
  160. // that a secret will be generated for, if it does not exist
  161. linkedRegs := make(map[string]*models.Registry)
  162. err := d.decodeRenderedManifests(renderedManifests)
  163. if err != nil {
  164. return linkedRegs, err
  165. }
  166. // read the pod specs into the post-renderer object
  167. d.getPodSpecs(d.resources)
  168. for _, podSpec := range d.podSpecs {
  169. // get all images
  170. images := d.getImageList(podSpec)
  171. // read the image url
  172. for _, image := range images {
  173. named, err := reference.ParseNormalizedNamed(image)
  174. if err != nil {
  175. continue
  176. }
  177. domain := reference.Domain(named)
  178. path := reference.Path(named)
  179. regName := domain
  180. if pathArr := strings.Split(path, "/"); len(pathArr) > 1 {
  181. regName += "/" + strings.Join(pathArr[:len(pathArr)-1], "/")
  182. }
  183. // check if the integration is native to the cluster/registry combination
  184. isNative := d.isRegistryNative(regName)
  185. if isNative {
  186. continue
  187. }
  188. reg, exists := d.registries[regName]
  189. if !exists {
  190. continue
  191. }
  192. // if the registry exists, add it to the map
  193. linkedRegs[regName] = reg
  194. }
  195. }
  196. return linkedRegs, nil
  197. }
  198. func (d *DockerSecretsPostRenderer) decodeRenderedManifests(
  199. renderedManifests *bytes.Buffer,
  200. ) error {
  201. // use the yaml decoder to parse the multi-document yaml.
  202. decoder := yaml.NewDecoder(renderedManifests)
  203. for {
  204. res := make(resource)
  205. err := decoder.Decode(&res)
  206. if err == io.EOF {
  207. break
  208. }
  209. if err != nil {
  210. return err
  211. }
  212. d.resources = append(d.resources, res)
  213. }
  214. return nil
  215. }
  216. func (d *DockerSecretsPostRenderer) getPodSpecs(resources []resource) {
  217. for _, res := range resources {
  218. kindVal, hasKind := res["kind"]
  219. if !hasKind {
  220. continue
  221. }
  222. kind, ok := kindVal.(string)
  223. if !ok {
  224. continue
  225. }
  226. // manifests of list type will have an items field, items should
  227. // be recursively parsed
  228. if itemsVal, isList := res["items"]; isList {
  229. if items, ok := itemsVal.([]interface{}); ok {
  230. // convert items to resource
  231. resArr := make([]resource, 0)
  232. for _, item := range items {
  233. if arrVal, ok := item.(resource); ok {
  234. resArr = append(resArr, arrVal)
  235. }
  236. }
  237. d.getPodSpecs(resArr)
  238. }
  239. continue
  240. }
  241. // otherwise, get the pod spec based on the type of resource
  242. podSpec := getPodSpecFromResource(kind, res)
  243. if podSpec == nil {
  244. continue
  245. }
  246. d.podSpecs = append(d.podSpecs, podSpec)
  247. }
  248. return
  249. }
  250. func (d *DockerSecretsPostRenderer) updatePodSpecs(secrets map[string]string) {
  251. for _, podSpec := range d.podSpecs {
  252. containersVal, hasContainers := podSpec["containers"]
  253. if !hasContainers {
  254. continue
  255. }
  256. containers, ok := containersVal.([]interface{})
  257. if !ok {
  258. continue
  259. }
  260. imagePullSecrets := make([]map[string]interface{}, 0)
  261. if existingPullSecrets, ok := podSpec["imagePullSecrets"]; ok {
  262. if existing, ok := existingPullSecrets.([]map[string]interface{}); ok {
  263. imagePullSecrets = existing
  264. }
  265. }
  266. for _, container := range containers {
  267. _container, ok := container.(resource)
  268. if !ok {
  269. continue
  270. }
  271. image, ok := _container["image"].(string)
  272. if !ok {
  273. continue
  274. }
  275. named, err := reference.ParseNormalizedNamed(image)
  276. if err != nil {
  277. continue
  278. }
  279. domain := reference.Domain(named)
  280. path := reference.Path(named)
  281. regName := domain
  282. if pathArr := strings.Split(path, "/"); len(pathArr) > 1 {
  283. regName += "/" + strings.Join(pathArr[:len(pathArr)-1], "/")
  284. }
  285. if secretName, ok := secrets[regName]; ok && secretName != "" {
  286. imagePullSecrets = append(imagePullSecrets, map[string]interface{}{
  287. "name": secretName,
  288. })
  289. }
  290. }
  291. if len(imagePullSecrets) > 0 {
  292. podSpec["imagePullSecrets"] = imagePullSecrets
  293. }
  294. }
  295. }
  296. func (d *DockerSecretsPostRenderer) getImageList(podSpec resource) []string {
  297. images := make([]string, 0)
  298. containersVal, hasContainers := podSpec["containers"]
  299. if !hasContainers {
  300. return images
  301. }
  302. containers, ok := containersVal.([]interface{})
  303. if !ok {
  304. return images
  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. images = append(images, image)
  316. }
  317. return images
  318. }
  319. 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)?`)
  320. func (d *DockerSecretsPostRenderer) isRegistryNative(regName string) bool {
  321. isNative := false
  322. if strings.Contains(regName, "gcr") && d.Cluster.AuthMechanism == models.GCP {
  323. // get the project id of the cluster
  324. gcpInt, err := d.Repo.GCPIntegration.ReadGCPIntegration(d.Cluster.GCPIntegrationID)
  325. if err != nil {
  326. return false
  327. }
  328. gkeProjectID, err := integrations.GCPProjectIDFromJSON(gcpInt.GCPKeyData)
  329. if err != nil {
  330. return false
  331. }
  332. // parse the project id of the gcr url
  333. if regNameArr := strings.Split(regName, "/"); len(regNameArr) >= 2 {
  334. gcrProjectID := regNameArr[1]
  335. isNative = gcrProjectID == gkeProjectID
  336. }
  337. } else if strings.Contains(regName, "ecr") && d.Cluster.AuthMechanism == models.AWS {
  338. matches := ecrPattern.FindStringSubmatch(regName)
  339. if len(matches) < 3 {
  340. return false
  341. }
  342. eksAccountID := matches[1]
  343. eksRegion := matches[3]
  344. awsInt, err := d.Repo.AWSIntegration.ReadAWSIntegration(d.Cluster.AWSIntegrationID)
  345. if err != nil {
  346. return false
  347. }
  348. err = awsInt.PopulateAWSArn()
  349. if err != nil {
  350. return false
  351. }
  352. parsedARN, err := arn.Parse(awsInt.AWSArn)
  353. if err != nil {
  354. return false
  355. }
  356. isNative = parsedARN.AccountID == eksAccountID && parsedARN.Region == eksRegion
  357. }
  358. return isNative
  359. }
  360. func getPodSpecFromResource(kind string, res resource) resource {
  361. switch kind {
  362. case "Pod":
  363. return getNestedResource(res, "spec")
  364. case "DaemonSet", "Deployment", "Job", "ReplicaSet", "ReplicationController", "StatefulSet":
  365. return getNestedResource(res, "spec", "template", "spec")
  366. case "PodTemplate":
  367. return getNestedResource(res, "template", "spec")
  368. case "CronJob":
  369. return getNestedResource(res, "spec", "jobTemplate", "spec", "template", "spec")
  370. }
  371. return nil
  372. }
  373. func getNestedResource(res resource, keys ...string) resource {
  374. curr := res
  375. var ok bool
  376. for _, key := range keys {
  377. curr, ok = curr[key].(resource)
  378. if !ok {
  379. return nil
  380. }
  381. }
  382. return curr
  383. }