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.

patch.go 8.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. package kubernetes
  2. import (
  3. "bytes"
  4. "fmt"
  5. "sort"
  6. "github.com/evanphx/json-patch"
  7. jsonyaml "github.com/ghodss/yaml"
  8. "github.com/imdario/mergo"
  9. "gopkg.in/yaml.v2"
  10. "k8s.io/apimachinery/pkg/runtime"
  11. "k8s.io/apimachinery/pkg/runtime/schema"
  12. utilruntime "k8s.io/apimachinery/pkg/util/runtime"
  13. "k8s.io/apimachinery/pkg/util/strategicpatch"
  14. k8sscheme "k8s.io/client-go/kubernetes/scheme"
  15. kresource "github.com/fluxcd/flux/cluster/kubernetes/resource"
  16. "github.com/fluxcd/flux/resource"
  17. )
  18. func createManifestPatch(originalManifests, modifiedManifests []byte, originalSource, modifiedSource string) ([]byte, error) {
  19. originalResources, err := kresource.ParseMultidoc(originalManifests, originalSource)
  20. if err != nil {
  21. fmt.Errorf("cannot parse %s: %s", originalSource, err)
  22. }
  23. modifiedResources, err := kresource.ParseMultidoc(modifiedManifests, modifiedSource)
  24. if err != nil {
  25. fmt.Errorf("cannot parse %s: %s", modifiedSource, err)
  26. }
  27. // Sort output by resource identifiers
  28. var originalIDs []string
  29. for id, _ := range originalResources {
  30. originalIDs = append(originalIDs, id)
  31. }
  32. sort.Strings(originalIDs)
  33. buf := bytes.NewBuffer(nil)
  34. scheme := getFullScheme()
  35. for _, id := range originalIDs {
  36. originalResource := originalResources[id]
  37. modifiedResource, ok := modifiedResources[id]
  38. if !ok {
  39. // Only generate patches for resources present in both files
  40. continue
  41. }
  42. patch, err := getPatch(originalResource, modifiedResource, scheme)
  43. if err != nil {
  44. return nil, fmt.Errorf("cannot obtain patch for resource %s: %s", id, err)
  45. }
  46. if bytes.Equal(patch, []byte("{}\n")) {
  47. // Avoid outputting empty patches
  48. continue
  49. }
  50. if err := appendYAMLToBuffer(patch, buf); err != nil {
  51. return nil, err
  52. }
  53. }
  54. return buf.Bytes(), nil
  55. }
  56. func applyManifestPatch(originalManifests, patchManifests []byte, originalSource, patchSource string) ([]byte, error) {
  57. originalResources, err := kresource.ParseMultidoc(originalManifests, originalSource)
  58. if err != nil {
  59. return nil, fmt.Errorf("cannot parse %s: %s", originalSource, err)
  60. }
  61. patchResources, err := kresource.ParseMultidoc(patchManifests, patchSource)
  62. if err != nil {
  63. return nil, fmt.Errorf("cannot parse %s: %s", patchSource, err)
  64. }
  65. // Make sure all patch resources have a matching resource
  66. for id, patchResource := range patchResources {
  67. if _, ok := originalResources[id]; !ok {
  68. return nil, fmt.Errorf("missing resource (%s) for patch", resourceID(patchResource))
  69. }
  70. }
  71. // Sort output by resource identifiers
  72. var originalIDs []string
  73. for id, _ := range originalResources {
  74. originalIDs = append(originalIDs, id)
  75. }
  76. sort.Strings(originalIDs)
  77. buf := bytes.NewBuffer(nil)
  78. scheme := getFullScheme()
  79. for _, id := range originalIDs {
  80. originalResource := originalResources[id]
  81. resourceBytes := originalResource.Bytes()
  82. if patchedResource, ok := patchResources[id]; ok {
  83. // There was a patch, apply it
  84. patched, err := applyPatch(originalResource, patchedResource, scheme)
  85. if err != nil {
  86. return nil, fmt.Errorf("cannot obtain patch for resource %s: %s", id, err)
  87. }
  88. resourceBytes = patched
  89. }
  90. if err := appendYAMLToBuffer(resourceBytes, buf); err != nil {
  91. return nil, err
  92. }
  93. }
  94. return buf.Bytes(), nil
  95. }
  96. func getFullScheme() *runtime.Scheme {
  97. fullScheme := runtime.NewScheme()
  98. utilruntime.Must(k8sscheme.AddToScheme(fullScheme))
  99. // HelmRelease and FluxHelmRelease are intentionally not added to the scheme.
  100. // This is done for two reasons:
  101. // 1. The kubernetes strategic merge patcher chokes on the freeform
  102. // values under `values:`.
  103. // 2. External tools like kustomize won't be able to apply SMPs
  104. // on Custom Resources, thus we use a normal jsonmerge instead.
  105. //
  106. // utilruntime.Must(fluxscheme.AddToScheme(fullScheme))
  107. return fullScheme
  108. }
  109. func getPatch(originalManifest kresource.KubeManifest, modifiedManifest kresource.KubeManifest, scheme *runtime.Scheme) ([]byte, error) {
  110. groupVersion, err := schema.ParseGroupVersion(originalManifest.GroupVersion())
  111. if err != nil {
  112. return nil, fmt.Errorf("cannot parse groupVersion %q: %s", originalManifest.GroupVersion(), err)
  113. }
  114. manifest1JSON, err := jsonyaml.YAMLToJSON(originalManifest.Bytes())
  115. if err != nil {
  116. return nil, fmt.Errorf("cannot transform original resource (%s) to JSON: %s",
  117. resourceID(originalManifest), err)
  118. }
  119. manifest2JSON, err := jsonyaml.YAMLToJSON(modifiedManifest.Bytes())
  120. if err != nil {
  121. return nil, fmt.Errorf("cannot transform modified resource (%s) to JSON: %s",
  122. resourceID(modifiedManifest), err)
  123. }
  124. gvk := groupVersion.WithKind(originalManifest.GetKind())
  125. obj, err := scheme.New(gvk)
  126. var patchJSON []byte
  127. switch {
  128. case runtime.IsNotRegisteredError(err):
  129. // try a normal JSON merge patch
  130. patchJSON, err = jsonpatch.CreateMergePatch(manifest1JSON, manifest2JSON)
  131. case err != nil:
  132. err = fmt.Errorf("cannot obtain scheme for GroupVersionKind %q: %s", gvk, err)
  133. default:
  134. patchJSON, err = strategicpatch.CreateTwoWayMergePatch(manifest1JSON, manifest2JSON, obj)
  135. }
  136. if err != nil {
  137. return nil, err
  138. }
  139. var jsonObj interface{}
  140. // We are using yaml.Unmarshal here (instead of json.Unmarshal) because the
  141. // Go JSON library doesn't try to pick the right number type (int, float,
  142. // etc.) when unmarshalling to interface{}
  143. err = yaml.Unmarshal(patchJSON, &jsonObj)
  144. if err != nil {
  145. return nil, fmt.Errorf("cannot parse patch (resource %s): %s",
  146. resourceID(originalManifest), err)
  147. }
  148. // Make sure the non-empty patches come with metadata so that they can be matched in multidoc yaml context
  149. if m, ok := jsonObj.(map[interface{}]interface{}); ok && len(m) > 0 {
  150. jsonObj, err = addIdentifyingData(originalManifest.GroupVersion(),
  151. originalManifest.GetKind(), originalManifest.GetName(), originalManifest.GetNamespace(), m)
  152. }
  153. if err != nil {
  154. return nil, fmt.Errorf("cannot add metadata to patch (resource %s): %s", resourceID(originalManifest), err)
  155. }
  156. patch, err := yaml.Marshal(jsonObj)
  157. if err != nil {
  158. return nil, fmt.Errorf("cannot transform updated patch (resource %s) to YAML: %s",
  159. resourceID(originalManifest), err)
  160. }
  161. return patch, nil
  162. }
  163. func addIdentifyingData(apiVersion string, kind string, name string, namespace string,
  164. obj map[interface{}]interface{}) (map[interface{}]interface{}, error) {
  165. toMerge := map[interface{}]interface{}{}
  166. toMerge["apiVersion"] = apiVersion
  167. toMerge["kind"] = kind
  168. metadata := map[string]string{
  169. "name": name,
  170. }
  171. if len(namespace) > 0 {
  172. metadata["namespace"] = namespace
  173. }
  174. toMerge["metadata"] = metadata
  175. err := mergo.Merge(&obj, toMerge)
  176. return obj, err
  177. }
  178. func applyPatch(originalManifest, patchManifest kresource.KubeManifest, scheme *runtime.Scheme) ([]byte, error) {
  179. groupVersion, err := schema.ParseGroupVersion(originalManifest.GroupVersion())
  180. if err != nil {
  181. return nil, fmt.Errorf("cannot parse groupVersion %q: %s", originalManifest.GroupVersion(), err)
  182. }
  183. originalJSON, err := jsonyaml.YAMLToJSON(originalManifest.Bytes())
  184. if err != nil {
  185. return nil, fmt.Errorf("cannot transform original resource (%s) to JSON: %s",
  186. resourceID(originalManifest), err)
  187. }
  188. patchJSON, err := jsonyaml.YAMLToJSON(patchManifest.Bytes())
  189. if err != nil {
  190. return nil, fmt.Errorf("cannot transform patch resource (%s) to JSON: %s",
  191. resourceID(patchManifest), err)
  192. }
  193. obj, err := scheme.New(groupVersion.WithKind(originalManifest.GetKind()))
  194. var patchedJSON []byte
  195. switch {
  196. case runtime.IsNotRegisteredError(err):
  197. // try a normal JSON merging
  198. patchedJSON, err = jsonpatch.MergePatch(originalJSON, patchJSON)
  199. default:
  200. patchedJSON, err = strategicpatch.StrategicMergePatch(originalJSON, patchJSON, obj)
  201. }
  202. if err != nil {
  203. return nil, fmt.Errorf("cannot patch resource %s: %s", resourceID(originalManifest), err)
  204. }
  205. patched, err := jsonyaml.JSONToYAML(patchedJSON)
  206. if err != nil {
  207. return nil, fmt.Errorf("cannot transform patched resource (%s) to YAML: %s",
  208. resourceID(originalManifest), err)
  209. }
  210. return patched, nil
  211. }
  212. // resourceID works like Resource.ID() but avoids <cluster> namespaces,
  213. // since they may be incorrect
  214. func resourceID(manifest kresource.KubeManifest) resource.ID {
  215. return resource.MakeID(manifest.GetNamespace(), manifest.GetKind(), manifest.GetKind())
  216. }