postrenderer.go 11 KB

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