GitOps for k8s
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

helmrelease.go 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. package resource
  2. import (
  3. "fmt"
  4. "github.com/Jeffail/gabs"
  5. "sort"
  6. "strings"
  7. "github.com/fluxcd/flux/image"
  8. "github.com/fluxcd/flux/resource"
  9. )
  10. const (
  11. // ReleaseContainerName is the name used when Flux interprets a
  12. // HelmRelease as having a container with an image, by virtue of
  13. // having a `values` stanza with an image field:
  14. //
  15. // spec:
  16. // ...
  17. // values:
  18. // image: some/image:version
  19. //
  20. // The name refers to the source of the image value.
  21. ReleaseContainerName = "chart-image"
  22. // ImageBasePath is the default base path for image path mappings
  23. // in a HelmRelease resource.
  24. ImageBasePath = "spec.values."
  25. // ImageRegistryPrefix is the annotation key prefix for image
  26. // registry path mappings.
  27. ImageRegistryPrefix = "registry.fluxcd.io/"
  28. // ImageRepositoryPrefix is the annotation key prefix for image
  29. // repository path mappings.
  30. ImageRepositoryPrefix = "repository.fluxcd.io/"
  31. // ImageRepositoryPrefix is the annotation key prefix for image
  32. // tag path mappings.
  33. ImageTagPrefix = "tag.fluxcd.io/"
  34. )
  35. // ContainerImageMap holds the YAML dot notation paths to a
  36. // container image.
  37. type ContainerImageMap struct {
  38. BasePath string
  39. Registry string
  40. Repository string
  41. Tag string
  42. }
  43. // RepositoryOnly returns if only the repository is defined.
  44. func (c ContainerImageMap) RepositoryOnly() bool {
  45. return c.Repository != "" && c.Registry == "" && c.Tag == ""
  46. }
  47. // RegistryRepository returns if the repository and tag are
  48. // defined, but the registry is not.
  49. func (c ContainerImageMap) RepositoryTag() bool {
  50. return c.Repository != "" && c.Tag != "" && c.Registry == ""
  51. }
  52. // RegistryRepository returns if the registry and repository are
  53. // defined, but the tag is not.
  54. func (c ContainerImageMap) RegistryRepository() bool {
  55. return c.Registry != "" && c.Repository != "" && c.Tag == ""
  56. }
  57. // AllDefined returns if all image elements are defined.
  58. func (c ContainerImageMap) AllDefined() bool {
  59. return c.Registry != "" && c.Repository != "" && c.Tag != ""
  60. }
  61. // GetRegistry returns the full registry path (with base path).
  62. func (c ContainerImageMap) GetRegistry() string {
  63. if c.Registry == "" {
  64. return c.Registry
  65. }
  66. return fmt.Sprintf("%s%s", c.BasePath, c.Registry)
  67. }
  68. // GetRepository returns the full repository path (with base path).
  69. func (c ContainerImageMap) GetRepository() string {
  70. if c.Repository == "" {
  71. return c.Repository
  72. }
  73. return fmt.Sprintf("%s%s", c.BasePath, c.Repository)
  74. }
  75. // GetTag returns the full tag path (with base path).
  76. func (c ContainerImageMap) GetTag() string {
  77. if c.Tag == "" {
  78. return c.Tag
  79. }
  80. return fmt.Sprintf("%s%s", c.BasePath, c.Tag)
  81. }
  82. // MapImageRef maps the given imageRef to the dot notation paths
  83. // ContainerImageMap holds. It needs at least an Repository to be able
  84. // to compose the map, and takes the absence of the registry and/or tag
  85. // paths into account to ensure all image elements (registry,
  86. // repository, tag) are present in the returned map.
  87. func (c ContainerImageMap) MapImageRef(image image.Ref) (map[string]string, bool) {
  88. m := make(map[string]string)
  89. switch {
  90. case c.AllDefined():
  91. m[c.GetRegistry()] = image.Domain
  92. m[c.GetRepository()] = image.Image
  93. m[c.GetTag()] = image.Tag
  94. case c.RegistryRepository():
  95. m[c.GetRegistry()] = image.Domain
  96. m[c.GetRepository()] = image.Image + ":" + image.Tag
  97. case c.RepositoryTag():
  98. m[c.GetRepository()] = image.Name.String()
  99. m[c.GetTag()] = image.Tag
  100. case c.RepositoryOnly():
  101. m[c.GetRepository()] = image.String()
  102. default:
  103. return m, false
  104. }
  105. return m, true
  106. }
  107. // HelmRelease echoes the generated type for the custom resource
  108. // definition. It's here so we can 1. get `baseObject` in there, and
  109. // 3. control the YAML serialisation of fields, which we can't do
  110. // (easily?) with the generated type.
  111. type HelmRelease struct {
  112. baseObject
  113. Spec struct {
  114. Values map[string]interface{}
  115. }
  116. }
  117. type ImageSetter func(image.Ref)
  118. type imageAndSetter struct {
  119. image image.Ref
  120. setter ImageSetter
  121. }
  122. // sorted_containers returns an array of container names in ascending
  123. // order, except for `ReleaseContainerName`, which always goes first.
  124. // We want a stable order to the containers we output, since things
  125. // will jump around in API calls, or fail to verify, otherwise.
  126. func sorted_containers(containers map[string]imageAndSetter) []string {
  127. var keys []string
  128. for k := range containers {
  129. keys = append(keys, k)
  130. }
  131. sort.Slice(keys, func(i, j int) bool {
  132. if keys[i] == ReleaseContainerName {
  133. return true
  134. }
  135. if keys[j] == ReleaseContainerName {
  136. return false
  137. }
  138. return keys[i] < keys[j]
  139. })
  140. return keys
  141. }
  142. // FindHelmReleaseContainers examines the Values from a
  143. // HelmRelease (manifest, or cluster resource, or otherwise) and
  144. // calls visit with each container name and image it finds, as well as
  145. // procedure for changing the image value.
  146. func FindHelmReleaseContainers(annotations map[string]string, values map[string]interface{},
  147. visit func(string, image.Ref, ImageSetter) error) {
  148. containers := make(map[string]imageAndSetter)
  149. // an image defined at the top-level is given a standard container name:
  150. if image, setter, ok := interpretAsContainer(stringMap(values)); ok {
  151. containers[ReleaseContainerName] = imageAndSetter{image, setter}
  152. }
  153. // an image as part of a field is treated as a "container" spec
  154. // named for the field:
  155. for k, v := range values {
  156. if image, setter, ok := interpret(v); ok {
  157. containers[k] = imageAndSetter{image, setter}
  158. }
  159. }
  160. // user mapped images, it will overwrite automagically interpreted
  161. // images with user defined ones:
  162. for k, v := range containerImageMappingsFromAnnotations(annotations) {
  163. if image, setter, ok := interpretMappedContainerImage(values, v); ok {
  164. containers[k] = imageAndSetter{image, setter}
  165. }
  166. }
  167. // sort the found containers by name, using the custom logic
  168. // defined in sorted_containers, so the calls to visit are
  169. // predictable:
  170. for _, k := range sorted_containers(containers) {
  171. visit(k, containers[k].image, containers[k].setter)
  172. }
  173. }
  174. // The following is some machinery for interpreting a
  175. // HelmRelease's `values` field as defining images to be
  176. // interpolated into the chart templates.
  177. //
  178. // The top-level value is a map[string]interface{}, but beneath that,
  179. // we get maps in two varieties: from a YAML (i.e., a file), they are
  180. // `map[interface{}]interface{}`, and from JSON (i.e., Kubernetes API)
  181. // they are a `map[string]interface{}`. To conflate them, here's an
  182. // interface for maps:
  183. type mapper interface {
  184. get(string) (interface{}, bool)
  185. set(string, interface{})
  186. }
  187. type stringMap map[string]interface{}
  188. type anyMap map[interface{}]interface{}
  189. func (m stringMap) get(k string) (interface{}, bool) { v, ok := m[k]; return v, ok }
  190. func (m stringMap) set(k string, v interface{}) { m[k] = v }
  191. func (m anyMap) get(k string) (interface{}, bool) { v, ok := m[k]; return v, ok }
  192. func (m anyMap) set(k string, v interface{}) { m[k] = v }
  193. // interpret gets a value which may contain a description of an image.
  194. func interpret(values interface{}) (image.Ref, ImageSetter, bool) {
  195. switch m := values.(type) {
  196. case map[string]interface{}:
  197. return interpretAsContainer(stringMap(m))
  198. case map[interface{}]interface{}:
  199. return interpretAsContainer(anyMap(m))
  200. }
  201. return image.Ref{}, nil, false
  202. }
  203. // interpretAsContainer takes a `mapper` value that may _contain_ an
  204. // image, and attempts to interpret it.
  205. func interpretAsContainer(m mapper) (image.Ref, ImageSetter, bool) {
  206. imageValue, ok := m.get("image")
  207. if !ok {
  208. return image.Ref{}, nil, false
  209. }
  210. switch img := imageValue.(type) {
  211. case string:
  212. // container:
  213. // image: 'repo/image:tag'
  214. imageRef, err := image.ParseRef(img)
  215. if err == nil {
  216. var reggy bool
  217. if registry, ok := m.get("registry"); ok {
  218. // container:
  219. // registry: registry.com
  220. // image: repo/foo
  221. if registryStr, ok := registry.(string); ok {
  222. reggy = true
  223. imageRef.Domain = registryStr
  224. }
  225. }
  226. var taggy bool
  227. if tag, ok := m.get("tag"); ok {
  228. // container:
  229. // image: repo/foo
  230. // tag: v1
  231. if tagStr, ok := tag.(string); ok {
  232. taggy = true
  233. imageRef.Tag = tagStr
  234. }
  235. }
  236. return imageRef, func(ref image.Ref) {
  237. switch {
  238. case (reggy && taggy):
  239. m.set("registry", ref.Domain)
  240. m.set("image", ref.Image)
  241. m.set("tag", ref.Tag)
  242. return
  243. case reggy:
  244. m.set("registry", ref.Domain)
  245. m.set("image", ref.Name.Image+":"+ref.Tag)
  246. case taggy:
  247. m.set("image", ref.Name.String())
  248. m.set("tag", ref.Tag)
  249. default:
  250. m.set("image", ref.String())
  251. }
  252. }, true
  253. }
  254. case map[string]interface{}:
  255. return interpretAsImage(stringMap(img))
  256. case map[interface{}]interface{}:
  257. return interpretAsImage(anyMap(img))
  258. }
  259. return image.Ref{}, nil, false
  260. }
  261. // interpretAsImage takes a `mapper` value that may represent an
  262. // image, and attempts to interpret it.
  263. func interpretAsImage(m mapper) (image.Ref, ImageSetter, bool) {
  264. var imgRepo interface{}
  265. var ok bool
  266. if imgRepo, ok = m.get("repository"); !ok {
  267. return image.Ref{}, nil, false
  268. }
  269. // image:
  270. // repository: repo/foo
  271. if imgStr, ok := imgRepo.(string); ok {
  272. imageRef, err := image.ParseRef(imgStr)
  273. if err == nil {
  274. var reggy bool
  275. // image:
  276. // registry: registry.com
  277. // repository: repo/foo
  278. if registry, ok := m.get("registry"); ok {
  279. if registryStr, ok := registry.(string); ok {
  280. reggy = true
  281. imageRef.Domain = registryStr
  282. }
  283. }
  284. var taggy bool
  285. // image:
  286. // repository: repo/foo
  287. // tag: v1
  288. if tag, ok := m.get("tag"); ok {
  289. if tagStr, ok := tag.(string); ok {
  290. taggy = true
  291. imageRef.Tag = tagStr
  292. }
  293. }
  294. return imageRef, func(ref image.Ref) {
  295. switch {
  296. case (reggy && taggy):
  297. m.set("registry", ref.Domain)
  298. m.set("repository", ref.Image)
  299. m.set("tag", ref.Tag)
  300. return
  301. case reggy:
  302. m.set("registry", ref.Domain)
  303. m.set("repository", ref.Name.Image+":"+ref.Tag)
  304. case taggy:
  305. m.set("repository", ref.Name.String())
  306. m.set("tag", ref.Tag)
  307. default:
  308. m.set("repository", ref.String())
  309. }
  310. }, true
  311. }
  312. }
  313. return image.Ref{}, nil, false
  314. }
  315. // containerImageMappingsFromAnnotations collects yaml dot notation
  316. // mappings of container images from the given annotations.
  317. func containerImageMappingsFromAnnotations(annotations map[string]string) map[string]ContainerImageMap {
  318. cim := make(map[string]ContainerImageMap)
  319. for k, v := range annotations {
  320. switch {
  321. case strings.HasPrefix(k, ImageRegistryPrefix):
  322. container := strings.TrimPrefix(k, ImageRegistryPrefix)
  323. i, _ := cim[container]
  324. i.Registry = v
  325. cim[container] = i
  326. case strings.HasPrefix(k, ImageRepositoryPrefix):
  327. container := strings.TrimPrefix(k, ImageRepositoryPrefix)
  328. i, _ := cim[container]
  329. i.Repository = v
  330. cim[container] = i
  331. case strings.HasPrefix(k, ImageTagPrefix):
  332. container := strings.TrimPrefix(k, ImageTagPrefix)
  333. i, _ := cim[container]
  334. i.Tag = v
  335. cim[container] = i
  336. }
  337. }
  338. for k, _ := range cim {
  339. i, _ := cim[k]
  340. i.BasePath = ImageBasePath
  341. cim[k] = i
  342. }
  343. return cim
  344. }
  345. // interpretMappedContainerImage attempt to resolve the paths in the
  346. // `ContainerImageMap` from the given values and tries to parse the
  347. // resolved values into a valid `image.Ref`. It returns the
  348. // `image.Ref`, an `ImageSetter` that is able to modify the image in
  349. // the supplied values map, and a boolean that reflects if the
  350. // interpretation was successful.
  351. func interpretMappedContainerImage(values map[string]interface{}, cim ContainerImageMap) (image.Ref, ImageSetter, bool) {
  352. v, err := gabs.Consume(values)
  353. if err != nil {
  354. return image.Ref{}, nil, false
  355. }
  356. imageValue := v.Path(cim.Repository).Data()
  357. if img, ok := imageValue.(string); ok {
  358. switch {
  359. case cim.RepositoryOnly():
  360. if imgRef, err := image.ParseRef(img); err == nil {
  361. return imgRef, func(ref image.Ref) {
  362. v.SetP(ref.String(), cim.Repository)
  363. }, true
  364. }
  365. case cim.AllDefined():
  366. registryValue := v.Path(cim.Registry).Data()
  367. if reg, ok := registryValue.(string); ok {
  368. tagValue := v.Path(cim.Tag).Data()
  369. if tag, ok := tagValue.(string); ok {
  370. if imgRef, err := image.ParseRef(reg + "/" + img + ":" + tag); err == nil {
  371. return imgRef, func(ref image.Ref) {
  372. v.SetP(ref.Domain, cim.Registry)
  373. v.SetP(ref.Image, cim.Repository)
  374. v.SetP(ref.Tag, cim.Tag)
  375. }, true
  376. }
  377. }
  378. }
  379. case cim.RegistryRepository():
  380. registryValue := v.Path(cim.Registry).Data()
  381. if reg, ok := registryValue.(string); ok {
  382. if imgRef, err := image.ParseRef(reg + "/" + img); err == nil {
  383. return imgRef, func(ref image.Ref) {
  384. v.SetP(ref.Domain, cim.Registry)
  385. v.SetP(ref.Name.Image+":"+ref.Tag, cim.Repository)
  386. }, true
  387. }
  388. }
  389. case cim.RepositoryTag():
  390. tagValue := v.Path(cim.Tag).Data()
  391. if tag, ok := tagValue.(string); ok {
  392. if imgRef, err := image.ParseRef(img + ":" + tag); err == nil {
  393. return imgRef, func(ref image.Ref) {
  394. v.SetP(ref.Name.String(), cim.Repository)
  395. v.SetP(ref.Tag, cim.Tag)
  396. }, true
  397. }
  398. }
  399. }
  400. }
  401. return image.Ref{}, nil, false
  402. }
  403. // Containers returns the containers that are defined in the
  404. // HelmRelease.
  405. func (hr HelmRelease) Containers() []resource.Container {
  406. var containers []resource.Container
  407. addContainer := func(container string, image image.Ref, _ ImageSetter) error {
  408. containers = append(containers, resource.Container{
  409. Name: container,
  410. Image: image,
  411. })
  412. return nil
  413. }
  414. FindHelmReleaseContainers(hr.Meta.Annotations, hr.Spec.Values, addContainer)
  415. return containers
  416. }
  417. // SetContainerImage mutates this resource by setting the `image`
  418. // field of `values`, or a subvalue therein, per one of the
  419. // interpretations in `FindHelmReleaseContainers` above. NB we can
  420. // get away with a value-typed receiver because we set a map entry.
  421. func (hr HelmRelease) SetContainerImage(container string, ref image.Ref) error {
  422. found := false
  423. imageSetter := func(name string, image image.Ref, setter ImageSetter) error {
  424. if container == name {
  425. setter(ref)
  426. found = true
  427. }
  428. return nil
  429. }
  430. FindHelmReleaseContainers(hr.Meta.Annotations, hr.Spec.Values, imageSetter)
  431. if !found {
  432. return fmt.Errorf("did not find container %s in HelmRelease", container)
  433. }
  434. return nil
  435. }
  436. // GetContainerImageMap returns the ContainerImageMap for a container,
  437. // or an error if we were unable to interpret the mapping, or no mapping
  438. // was found.
  439. func (hr HelmRelease) GetContainerImageMap(container string) (ContainerImageMap, error) {
  440. cim := containerImageMappingsFromAnnotations(hr.Meta.Annotations)
  441. if c, ok := cim[container]; ok {
  442. if _, _, ok = interpretMappedContainerImage(hr.Spec.Values, c); ok {
  443. return c, nil
  444. }
  445. }
  446. return ContainerImageMap{}, fmt.Errorf("did not find image map for container %s in HelmRelease", container)
  447. }