postrenderer.go 11 KB

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