Merge pull request #81910 from fabriziopandini/kubeadm-Json6902-Patches

kubeadm: add support for Json6902 Patches
This commit is contained in:
Kubernetes Prow Robot 2019-08-28 03:09:54 -07:00 committed by GitHub
commit b98f622852
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 430 additions and 181 deletions

View File

@ -3,21 +3,25 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library( go_library(
name = "go_default_library", name = "go_default_library",
srcs = [ srcs = [
"json6902.go",
"kustomize.go", "kustomize.go",
"unstructured.go", "strategicmerge.go",
], ],
importpath = "k8s.io/kubernetes/cmd/kubeadm/app/util/kustomize", importpath = "k8s.io/kubernetes/cmd/kubeadm/app/util/kustomize",
visibility = ["//visibility:public"], visibility = ["//visibility:public"],
deps = [ deps = [
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/yaml:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/yaml:go_default_library",
"//staging/src/k8s.io/cli-runtime/pkg/kustomize:go_default_library", "//staging/src/k8s.io/cli-runtime/pkg/kustomize:go_default_library",
"//vendor/github.com/pkg/errors:go_default_library", "//vendor/github.com/pkg/errors:go_default_library",
"//vendor/sigs.k8s.io/kustomize/pkg/constants:go_default_library",
"//vendor/sigs.k8s.io/kustomize/pkg/fs:go_default_library", "//vendor/sigs.k8s.io/kustomize/pkg/fs:go_default_library",
"//vendor/sigs.k8s.io/kustomize/pkg/ifc:go_default_library", "//vendor/sigs.k8s.io/kustomize/pkg/ifc:go_default_library",
"//vendor/sigs.k8s.io/kustomize/pkg/loader:go_default_library", "//vendor/sigs.k8s.io/kustomize/pkg/loader:go_default_library",
"//vendor/sigs.k8s.io/kustomize/pkg/patch:go_default_library",
"//vendor/sigs.k8s.io/kustomize/pkg/types:go_default_library",
"//vendor/sigs.k8s.io/yaml:go_default_library",
], ],
) )
@ -39,10 +43,11 @@ go_test(
name = "go_default_test", name = "go_default_test",
srcs = [ srcs = [
"kustomize_test.go", "kustomize_test.go",
"unstructured_test.go", "strategicmerge_test.go",
], ],
embed = [":go_default_library"], embed = [":go_default_library"],
deps = [ deps = [
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/github.com/lithammer/dedent:go_default_library", "//vendor/github.com/lithammer/dedent:go_default_library",
], ],

View File

@ -0,0 +1,64 @@
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package kustomize contains helpers for working with embedded kustomize commands
package kustomize
import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"sigs.k8s.io/kustomize/pkg/ifc"
"sigs.k8s.io/kustomize/pkg/patch"
)
// json6902 represents a json6902 patch
type json6902 struct {
// Target refers to a Kubernetes object that the json patch will be applied to
*patch.Target
// Patch contain the json patch as a string
Patch string
}
// json6902Slice is a slice of json6902 patches.
type json6902Slice []*json6902
// newJSON6902FromFile returns a json6902 patch from a file
func newJSON6902FromFile(f patch.Json6902, ldr ifc.Loader, file string) (*json6902, error) {
patch, err := ldr.Load(file)
if err != nil {
return nil, err
}
return &json6902{
Target: f.Target,
Patch: string(patch),
}, nil
}
// filterByResource returns all the json6902 patches in the json6902Slice corresponding to a given resource
func (s *json6902Slice) filterByResource(r *unstructured.Unstructured) json6902Slice {
var result json6902Slice
for _, p := range *s {
if p.Group == r.GroupVersionKind().Group &&
p.Version == r.GroupVersionKind().Version &&
p.Kind == r.GroupVersionKind().Kind &&
p.Namespace == r.GetNamespace() &&
p.Name == r.GetName() {
result = append(result, p)
}
}
return result
}

View File

@ -25,15 +25,25 @@ import (
"runtime" "runtime"
"sync" "sync"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
yamlutil "k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/cli-runtime/pkg/kustomize" "k8s.io/cli-runtime/pkg/kustomize"
"sigs.k8s.io/kustomize/pkg/constants"
"sigs.k8s.io/kustomize/pkg/fs" "sigs.k8s.io/kustomize/pkg/fs"
"sigs.k8s.io/kustomize/pkg/ifc"
"sigs.k8s.io/kustomize/pkg/loader" "sigs.k8s.io/kustomize/pkg/loader"
"sigs.k8s.io/kustomize/pkg/patch"
"sigs.k8s.io/kustomize/pkg/types"
"sigs.k8s.io/yaml"
) )
// Manager define a manager that allow access to kustomize capabilities // Manager define a manager that allow access to kustomize capabilities
type Manager struct { type Manager struct {
kustomizeDir string kustomizeDir string
us UnstructuredSlice kustomizationFile *types.Kustomization
strategicMergePatches strategicMergeSlice
json6902Patches json6902Slice
} }
var ( var (
@ -42,6 +52,8 @@ var (
) )
// GetManager return the KustomizeManager singleton instance // GetManager return the KustomizeManager singleton instance
// NB. this is done at singleton instance level because kubeadm has a unique pool
// of patches that are applied to different content, at different time
func GetManager(kustomizeDir string) (*Manager, error) { func GetManager(kustomizeDir string) (*Manager, error) {
lock.Lock() lock.Lock()
defer lock.Unlock() defer lock.Unlock()
@ -52,12 +64,31 @@ func GetManager(kustomizeDir string) (*Manager, error) {
kustomizeDir: kustomizeDir, kustomizeDir: kustomizeDir,
} }
// loads the UnstructuredSlice with all the patches into the Manager // Create a loader that mimics the behavior of kubectl kustomize, including support for reading from
// NB. this is done at singleton instance level because kubeadm has a unique pool // a local folder or git repository like git@github.com:someOrg/someRepo.git or https://github.com/someOrg/someRepo?ref=someHash
// of patches that are applied to different content, at different time // in order to do so you must use ldr.Root() instead of km.kustomizeDir and ldr.Load instead of other ways to read files
if err := km.getUnstructuredSlice(); err != nil { fSys := fs.MakeRealFS()
ldr, err := loader.NewLoader(km.kustomizeDir, fSys)
if err != nil {
return nil, err return nil, err
} }
defer ldr.Cleanup()
// read the Kustomization file and all the patches it is
// referencing (either stategicMerge or json6902 patches)
if err := km.loadFromKustomizationFile(ldr); err != nil {
return nil, err
}
// if a Kustomization file was not found, kubeadm creates
// one using all the patches in the folder; however in this
// case only stategicMerge patches are supported
if km.kustomizationFile == nil {
km.kustomizationFile = &types.Kustomization{}
if err := km.loadFromFolder(ldr); err != nil {
return nil, err
}
}
instances[kustomizeDir] = km instances[kustomizeDir] = km
} }
@ -65,78 +96,103 @@ func GetManager(kustomizeDir string) (*Manager, error) {
return instances[kustomizeDir], nil return instances[kustomizeDir], nil
} }
// getUnstructuredSlice returns a UnstructuredSlice with all the patches. // loadFromKustomizationFile reads a Kustomization file and all the patches it is
func (km *Manager) getUnstructuredSlice() error { // referencing (either stategicMerge or json6902 patches)
// kubeadm does not require a kustomization.yaml file listing all the resources/patches, so it is necessary func (km *Manager) loadFromKustomizationFile(ldr ifc.Loader) error {
// to rebuild the list of patches manually // Kustomize support different KustomizationFileNames, so we try to read all
// TODO: make this git friendly - currently this works only for patches in local folders - var content []byte
files, err := ioutil.ReadDir(km.kustomizeDir) match := 0
for _, kf := range constants.KustomizationFileNames {
c, err := ldr.Load(kf)
if err == nil {
match++
content = c
}
}
// if no kustomization file is found return
if match == 0 {
return nil
}
// if more that one kustomization file is found, return error
if match > 1 {
return errors.Errorf("Found multiple kustomization files under: %s\n", ldr.Root())
}
// Decode the kustomization file
decoder := yamlutil.NewYAMLOrJSONDecoder(bytes.NewReader(content), 1024)
var k = &types.Kustomization{}
if err := decoder.Decode(k); err != nil {
return errors.Wrap(err, "Error decoding kustomization file")
}
km.kustomizationFile = k
// gets all the strategic merge patches
for _, f := range km.kustomizationFile.PatchesStrategicMerge {
smp, err := newStrategicMergeSliceFromFile(ldr, string(f))
if err != nil { if err != nil {
return err return err
} }
km.strategicMergePatches = append(km.strategicMergePatches, smp...)
}
var paths = []string{} // gets all the json6902 patches
for _, file := range files { for _, f := range km.kustomizationFile.PatchesJson6902 {
if file.IsDir() { jp, err := newJSON6902FromFile(f, ldr, f.Path)
if err != nil {
return err
}
km.json6902Patches = append(km.json6902Patches, jp)
}
return nil
}
// loadFromFolder returns all the stategicMerge patches in a folder
func (km *Manager) loadFromFolder(ldr ifc.Loader) error {
files, err := ioutil.ReadDir(ldr.Root())
if err != nil {
return err
}
for _, fileInfo := range files {
if fileInfo.IsDir() {
continue continue
} }
paths = append(paths, file.Name())
}
// Create a loader that mimics the behavior of kubectl kustomize, including support for reading from smp, err := newStrategicMergeSliceFromFile(ldr, fileInfo.Name())
// a local git repository like git@github.com:someOrg/someRepo.git or https://github.com/someOrg/someRepo?ref=someHash
fSys := fs.MakeRealFS()
ldr, err := loader.NewLoader(km.kustomizeDir, fSys)
if err != nil { if err != nil {
return err return err
} }
defer ldr.Cleanup() km.strategicMergePatches = append(km.strategicMergePatches, smp...)
// read all the kustomizations and build the UnstructuredSlice
us, err := NewUnstructuredSliceFromFiles(ldr, paths)
if err != nil {
return err
} }
km.us = us
return nil return nil
} }
// Kustomize apply a set of patches to a resource. // Kustomize apply a set of patches to a resource.
// Portions of the kustomize logic in this function are taken from the kubernetes-sigs/kind project // Portions of the kustomize logic in this function are taken from the kubernetes-sigs/kind project
func (km *Manager) Kustomize(res []byte) ([]byte, error) { func (km *Manager) Kustomize(data []byte) ([]byte, error) {
// create a loader that mimics the behavior of kubectl kustomize // parse the resource to kustomize
// and converts the resource into a UnstructuredSlice decoder := yamlutil.NewYAMLOrJSONDecoder(bytes.NewReader(data), 1024)
// Nb. in kubeadm we are controlling resource generation, and so we var resource *unstructured.Unstructured
// we are expecting 1 object into each resource, eg. the static pod. if err := decoder.Decode(&resource); err != nil {
// Nevertheless, this code is ready for more than one object per resource
resList, err := NewUnstructuredSliceFromBytes(res)
if err != nil {
return nil, err return nil, err
} }
// create a list of resource and corresponding patches // get patches corresponding to this resource
var resources, patches UnstructuredSlice strategicMerge := km.strategicMergePatches.filterByResource(resource)
for _, r := range resList { json6902 := km.json6902Patches.filterByResource(resource)
resources = append(resources, r)
resourcePatches := km.us.FilterResource(r.GroupVersionKind(), r.GetNamespace(), r.GetName())
if len(resourcePatches) > 0 {
fmt.Printf("[kustomize] Applying %d patches to %s Resource=%s/%s\n", len(resourcePatches), r.GroupVersionKind(), r.GetNamespace(), r.GetName())
patches = append(patches, resourcePatches...)
}
}
// if there are no patches, for the target resources, exit // if there are no patches, for the target resources, exit
if len(patches) == 0 { if len(strategicMerge)+len(json6902) == 0 {
return res, nil return data, nil
} }
fmt.Printf("[kustomize] Applying %d patches to %s Resource=%s/%s\n", len(strategicMerge)+len(json6902), resource.GroupVersionKind(), resource.GetNamespace(), resource.GetName())
// create an in memory fs to use for the kustomization // create an in memory fs to use for the kustomization
memFS := fs.MakeFakeFS() memFS := fs.MakeFakeFS()
var kustomization bytes.Buffer
fakeDir := "/" fakeDir := "/"
// for Windows we need this to be a drive because kustomize uses filepath.Abs() // for Windows we need this to be a drive because kustomize uses filepath.Abs()
// which will add a drive letter if there is none. which drive letter is // which will add a drive letter if there is none. which drive letter is
@ -145,33 +201,44 @@ func (km *Manager) Kustomize(res []byte) ([]byte, error) {
fakeDir = `C:\` fakeDir = `C:\`
} }
// write resources and patches to the in memory fs, generate the kustomization.yaml // writes the resource to a file in the temp file system
// that ties everything together b, err := yaml.Marshal(resource)
kustomization.WriteString("resources:\n")
for i, r := range resources {
b, err := r.MarshalJSON()
if err != nil { if err != nil {
return nil, err return nil, err
} }
name := "resource.yaml"
name := fmt.Sprintf("resource-%d.json", i)
_ = memFS.WriteFile(filepath.Join(fakeDir, name), b) _ = memFS.WriteFile(filepath.Join(fakeDir, name), b)
fmt.Fprintf(&kustomization, " - %s\n", name)
}
kustomization.WriteString("patches:\n") km.kustomizationFile.Resources = []string{name}
for i, p := range patches {
b, err := p.MarshalJSON() // writes strategic merge patches to files in the temp file system
km.kustomizationFile.PatchesStrategicMerge = []patch.StrategicMerge{}
for i, p := range strategicMerge {
b, err := yaml.Marshal(p)
if err != nil { if err != nil {
return nil, err return nil, err
} }
name := fmt.Sprintf("patch-%d.yaml", i)
name := fmt.Sprintf("patch-%d.json", i)
_ = memFS.WriteFile(filepath.Join(fakeDir, name), b) _ = memFS.WriteFile(filepath.Join(fakeDir, name), b)
fmt.Fprintf(&kustomization, " - %s\n", name)
km.kustomizationFile.PatchesStrategicMerge = append(km.kustomizationFile.PatchesStrategicMerge, patch.StrategicMerge(name))
} }
memFS.WriteFile(filepath.Join(fakeDir, "kustomization.yaml"), kustomization.Bytes()) // writes json6902 patches to files in the temp file system
km.kustomizationFile.PatchesJson6902 = []patch.Json6902{}
for i, p := range json6902 {
name := fmt.Sprintf("patchjson-%d.yaml", i)
_ = memFS.WriteFile(filepath.Join(fakeDir, name), []byte(p.Patch))
km.kustomizationFile.PatchesJson6902 = append(km.kustomizationFile.PatchesJson6902, patch.Json6902{Target: p.Target, Path: name})
}
// writes the kustomization file to the temp file system
kbytes, err := yaml.Marshal(km.kustomizationFile)
if err != nil {
return nil, err
}
memFS.WriteFile(filepath.Join(fakeDir, "kustomization.yaml"), kbytes)
// Finally customize the target resource // Finally customize the target resource
var out bytes.Buffer var out bytes.Buffer

View File

@ -26,21 +26,14 @@ import (
"github.com/lithammer/dedent" "github.com/lithammer/dedent"
) )
func TestKustomize(t *testing.T) { func TestKustomizeWithoutKustomizationFile(t *testing.T) {
tmpdir, err := ioutil.TempDir("", "") tmpdir, err := ioutil.TempDir("", "")
if err != nil { if err != nil {
t.Fatal("Couldn't create tmpdir") t.Fatal("Couldn't create tmpdir")
} }
defer os.RemoveAll(tmpdir) defer os.RemoveAll(tmpdir)
resourceString := dedent.Dedent(` strategicMergePatch1 := dedent.Dedent(`
apiVersion: v1
kind: Pod
metadata:
name: kube-apiserver
`)
patch1String := dedent.Dedent(`
apiVersion: v1 apiVersion: v1
kind: Pod kind: Pod
metadata: metadata:
@ -49,12 +42,12 @@ func TestKustomize(t *testing.T) {
kustomize: patch for kube-apiserver kustomize: patch for kube-apiserver
`) `)
err = ioutil.WriteFile(filepath.Join(tmpdir, "patch-1.yaml"), []byte(patch1String), 0644) err = ioutil.WriteFile(filepath.Join(tmpdir, "patch-1.yaml"), []byte(strategicMergePatch1), 0644)
if err != nil { if err != nil {
t.Fatalf("WriteFile returned unexpected error: %v", err) t.Fatalf("WriteFile returned unexpected error: %v", err)
} }
patch2String := dedent.Dedent(` strategicMergePatch2 := dedent.Dedent(`
apiVersion: v1 apiVersion: v1
kind: Pod kind: Pod
metadata: metadata:
@ -63,26 +56,148 @@ func TestKustomize(t *testing.T) {
kustomize: patch for kube-scheduler kustomize: patch for kube-scheduler
`) `)
err = ioutil.WriteFile(filepath.Join(tmpdir, "patch-2.yaml"), []byte(patch2String), 0644) err = ioutil.WriteFile(filepath.Join(tmpdir, "patch-2.yaml"), []byte(strategicMergePatch2), 0644)
if err != nil { if err != nil {
t.Fatalf("WriteFile returned unexpected error: %v", err) t.Fatalf("WriteFile returned unexpected error: %v", err)
} }
km, err := GetManager(tmpdir) km, err := GetManager(tmpdir)
if err != nil { if err != nil {
t.Errorf("GetManager returned unexpected error: %v", err) t.Fatalf("GetManager returned unexpected error: %v", err)
} }
kustomized, err := km.Kustomize([]byte(resourceString)) resource := dedent.Dedent(`
apiVersion: v1
kind: Pod
metadata:
name: kube-apiserver
`)
kustomized, err := km.Kustomize([]byte(resource))
if err != nil { if err != nil {
t.Errorf("Kustomize returned unexpected error: %v", err) t.Fatalf("Kustomize returned unexpected error: %v", err)
} }
if !strings.Contains(string(kustomized), "kustomize: patch for kube-apiserver") { if !strings.Contains(string(kustomized), "kustomize: patch for kube-apiserver") {
t.Error("Kustomize did not apply patches corresponding to the resource") t.Error("Kustomize did not apply strategicMergePatch")
} }
if strings.Contains(string(kustomized), "kustomize: patch for kube-scheduler") { if strings.Contains(string(kustomized), "kustomize: patch for kube-scheduler") {
t.Error("Kustomize did apply patches not corresponding to the resource") t.Error("Kustomize did apply patches not corresponding to the resource")
} }
} }
func TestKustomizeWithKustomizationFile(t *testing.T) {
tmpdir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatal("Couldn't create tmpdir")
}
defer os.RemoveAll(tmpdir)
kustomizationFile := dedent.Dedent(`
patchesJson6902:
- target:
version: v1
kind: Pod
name: kube-apiserver
path: patch-1.yaml
- target:
version: v1
kind: Pod
name: kube-scheduler
path: patch-2.yaml
patchesStrategicMerge:
- patch-3.yaml
- patch-4.yaml
`)
err = ioutil.WriteFile(filepath.Join(tmpdir, "kustomization.yaml"), []byte(kustomizationFile), 0644)
if err != nil {
t.Fatalf("WriteFile returned unexpected error: %v", err)
}
jsonPatch1 := dedent.Dedent(`
- op: add
path: "/metadata/labels"
value:
kustomize1: patch for kube-apiserver
`)
err = ioutil.WriteFile(filepath.Join(tmpdir, "patch-1.yaml"), []byte(jsonPatch1), 0644)
if err != nil {
t.Fatalf("WriteFile returned unexpected error: %v", err)
}
jsonPatch2 := dedent.Dedent(`
- op: add
path: "/metadata/labels"
value:
kustomize1: patch for kube-scheduler
`)
err = ioutil.WriteFile(filepath.Join(tmpdir, "patch-2.yaml"), []byte(jsonPatch2), 0644)
if err != nil {
t.Fatalf("WriteFile returned unexpected error: %v", err)
}
strategicMergePatch1 := dedent.Dedent(`
apiVersion: v1
kind: Pod
metadata:
name: kube-apiserver
annotations:
kustomize2: patch for kube-apiserver
`)
err = ioutil.WriteFile(filepath.Join(tmpdir, "patch-3.yaml"), []byte(strategicMergePatch1), 0644)
if err != nil {
t.Fatalf("WriteFile returned unexpected error: %v", err)
}
strategicMergePatch2 := dedent.Dedent(`
apiVersion: v1
kind: Pod
metadata:
name: kube-scheduler
annotations:
kustomize2: patch for kube-scheduler
`)
err = ioutil.WriteFile(filepath.Join(tmpdir, "patch-4.yaml"), []byte(strategicMergePatch2), 0644)
if err != nil {
t.Fatalf("WriteFile returned unexpected error: %v", err)
}
km, err := GetManager(tmpdir)
if err != nil {
t.Fatalf("GetManager returned unexpected error: %v", err)
}
resource := dedent.Dedent(`
apiVersion: v1
kind: Pod
metadata:
name: kube-apiserver
`)
kustomized, err := km.Kustomize([]byte(resource))
if err != nil {
t.Fatalf("Kustomize returned unexpected error: %v", err)
}
if !strings.Contains(string(kustomized), "kustomize1: patch for kube-apiserver") {
t.Error("Kustomize did not apply json patches corresponding to the resource")
}
if strings.Contains(string(kustomized), "kustomize1: patch for kube-scheduler") {
t.Error("Kustomize did apply json patches not corresponding to the resource")
}
if !strings.Contains(string(kustomized), "kustomize2: patch for kube-apiserver") {
t.Error("Kustomize did not apply strategic merge patches corresponding to the resource")
}
if strings.Contains(string(kustomized), "kustomize2: patch for kube-scheduler") {
t.Error("Kustomize did apply strategic merge patches not corresponding to the resource")
}
}

View File

@ -26,41 +26,33 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/yaml" "k8s.io/apimachinery/pkg/util/yaml"
"sigs.k8s.io/kustomize/pkg/ifc" "sigs.k8s.io/kustomize/pkg/ifc"
) )
// UnstructuredSlice is a slice of Unstructured objects. // strategicMergeSlice is a slice of strategic merge patches.
// Unstructured objects are used to represent both resources and patches of any group/version/kind. // Unstructured objects are used to represent strategic merge patches of any group/version/kind.
type UnstructuredSlice []*unstructured.Unstructured type strategicMergeSlice []*unstructured.Unstructured
// NewUnstructuredSliceFromFiles returns a ResMap given a resource path slice. // newStrategicMergeSliceFromFile returns a slice of strategic merge patches from a file
// This func use a Loader to mimic the behavior of kubectl kustomize, and most specifically support for reading from func newStrategicMergeSliceFromFile(loader ifc.Loader, path string) (strategicMergeSlice, error) {
// a local git repository like git@github.com:someOrg/someRepo.git or https://github.com/someOrg/someRepo?ref=someHash
func NewUnstructuredSliceFromFiles(loader ifc.Loader, paths []string) (UnstructuredSlice, error) {
var result UnstructuredSlice
for _, path := range paths {
content, err := loader.Load(path) content, err := loader.Load(path)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "load from path %q failed", path) return nil, errors.Wrapf(err, "load from path %q failed", path)
} }
res, err := NewUnstructuredSliceFromBytes(content) res, err := newStrategicMergeSliceFromBytes(content)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "convert %q to Unstructured failed", path) return nil, errors.Wrapf(err, "convert %q to Unstructured failed", path)
} }
return res, nil
result = append(result, res...)
}
return result, nil
} }
// NewUnstructuredSliceFromBytes returns a slice of Unstructured. // newStrategicMergeSliceFromBytes returns a strategic merge patches contained in a []byte.
// This functions handles all the nuances of Kubernetes yaml (e.g. many yaml // This functions handles all the nuances of Kubernetes yaml (e.g. many yaml
// documents in one file, List of objects) // documents in one file, List of objects)
func NewUnstructuredSliceFromBytes(in []byte) (UnstructuredSlice, error) { func newStrategicMergeSliceFromBytes(in []byte) (strategicMergeSlice, error) {
decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewReader(in), 1024) decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewReader(in), 1024)
var result UnstructuredSlice var result strategicMergeSlice
var err error var err error
// Parse all the yaml documents in the file // Parse all the yaml documents in the file
for err == nil || isEmptyYamlError(err) { for err == nil || isEmptyYamlError(err) {
@ -88,13 +80,13 @@ func NewUnstructuredSliceFromBytes(in []byte) (UnstructuredSlice, error) {
return err return err
} }
// Get the UnstructuredSlice for the item // Get the stategicMergeSlice for the item
itemU, err := NewUnstructuredSliceFromBytes(itemJSON) itemU, err := newStrategicMergeSliceFromBytes(itemJSON)
if err != nil { if err != nil {
return err return err
} }
// append the UnstructuredSlice for the item to the UnstructuredSlice // append the stategicMergeSlice for the item to the stategicMergeSlice
result = append(result, itemU...) result = append(result, itemU...)
return nil return nil
@ -105,7 +97,7 @@ func NewUnstructuredSliceFromBytes(in []byte) (UnstructuredSlice, error) {
continue continue
} }
// append the object to the UnstructuredSlice // append the object to the stategicMergeSlice
result = append(result, &u) result = append(result, &u)
} }
} }
@ -115,14 +107,14 @@ func NewUnstructuredSliceFromBytes(in []byte) (UnstructuredSlice, error) {
return result, nil return result, nil
} }
// FilterResource returns all the Unstructured items in the UnstructuredSlice corresponding to a given resource // filterByResource returns all the strategic merge patches in the strategicMergeSlice corresponding to a given resource
func (rs *UnstructuredSlice) FilterResource(gvk schema.GroupVersionKind, namespace, name string) UnstructuredSlice { func (s *strategicMergeSlice) filterByResource(r *unstructured.Unstructured) strategicMergeSlice {
var result UnstructuredSlice var result strategicMergeSlice
for _, r := range *rs { for _, p := range *s {
if r.GroupVersionKind() == gvk && if p.GroupVersionKind() == r.GroupVersionKind() &&
r.GetNamespace() == namespace && p.GetNamespace() == r.GetNamespace() &&
r.GetName() == name { p.GetName() == r.GetName() {
result = append(result, r) result = append(result, p)
} }
} }
return result return result

View File

@ -20,20 +20,21 @@ import (
"testing" "testing"
"github.com/lithammer/dedent" "github.com/lithammer/dedent"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
) )
func TestNewUnstructuredSliceFromBytes(t *testing.T) { func TestNewStategicMergeSliceFromBytes(t *testing.T) {
var useCases = []struct { var useCases = []struct {
name string name string
in string in string
expectedUnctructured int expectedPatches int
expectedError bool expectedError bool
}{ }{
{ {
name: "empty", name: "empty",
in: "", in: "",
expectedUnctructured: 0, expectedPatches: 0,
}, },
{ {
name: "single patch", name: "single patch",
@ -43,7 +44,7 @@ func TestNewUnstructuredSliceFromBytes(t *testing.T) {
metadata: metadata:
name: kube-apiserver name: kube-apiserver
`), `),
expectedUnctructured: 1, expectedPatches: 1,
}, },
{ {
name: "two patches as separated yaml documents", name: "two patches as separated yaml documents",
@ -58,7 +59,7 @@ func TestNewUnstructuredSliceFromBytes(t *testing.T) {
metadata: metadata:
name: kube-apiserver name: kube-apiserver
`), `),
expectedUnctructured: 2, expectedPatches: 2,
}, },
{ {
name: "two patches as a k8s list", name: "two patches as a k8s list",
@ -75,7 +76,7 @@ func TestNewUnstructuredSliceFromBytes(t *testing.T) {
metadata: metadata:
name: kube-apiserver name: kube-apiserver
`), `),
expectedUnctructured: 2, expectedPatches: 2,
}, },
{ {
name: "nested k8s lists", name: "nested k8s lists",
@ -95,7 +96,7 @@ func TestNewUnstructuredSliceFromBytes(t *testing.T) {
metadata: metadata:
name: kube-apiserver name: kube-apiserver
`), `),
expectedUnctructured: 2, expectedPatches: 2,
}, },
{ {
name: "invalid yaml", name: "invalid yaml",
@ -125,18 +126,18 @@ func TestNewUnstructuredSliceFromBytes(t *testing.T) {
} }
for _, rt := range useCases { for _, rt := range useCases {
t.Run(rt.name, func(t *testing.T) { t.Run(rt.name, func(t *testing.T) {
r, err := NewUnstructuredSliceFromBytes([]byte(rt.in)) r, err := newStrategicMergeSliceFromBytes([]byte(rt.in))
if err != nil { if err != nil {
if !rt.expectedError { if !rt.expectedError {
t.Errorf("NewUnstructuredSliceFromBytes returned unexpected error: %v", err) t.Errorf("newStrategicMergeSliceFromBytes returned unexpected error: %v", err)
} }
return return
} }
if err == nil && rt.expectedError { if err == nil && rt.expectedError {
t.Error("NewUnstructuredSliceFromBytes does not returned expected error") t.Error("newStrategicMergeSliceFromBytes does not returned expected error")
} }
if len(r) != rt.expectedUnctructured { if len(r) != rt.expectedPatches {
t.Errorf("Expected %d Unstructured items in the slice, actual %d", rt.expectedUnctructured, len(r)) t.Errorf("Expected %d strategic merge patches in the slice, actual %d", rt.expectedPatches, len(r))
} }
}) })
} }
@ -162,9 +163,9 @@ func TestFilterResource(t *testing.T) {
name: kube-scheduler name: kube-scheduler
namespace: kube-system namespace: kube-system
`) `)
u, err := NewUnstructuredSliceFromBytes([]byte(in)) u, err := newStrategicMergeSliceFromBytes([]byte(in))
if err != nil { if err != nil {
t.Fatalf("NewUnstructuredSliceFromBytes returned unexpected error: %v", err) t.Fatalf("newStategicMergeSliceFromBytes returned unexpected error: %v", err)
} }
var useCases = []struct { var useCases = []struct {
@ -172,50 +173,55 @@ func TestFilterResource(t *testing.T) {
rgvk schema.GroupVersionKind rgvk schema.GroupVersionKind
rnamespace string rnamespace string
rname string rname string
expectedUnctructured int expectedPatches int
}{ }{
{ {
name: "match 1", name: "match 1",
rgvk: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}, rgvk: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"},
rnamespace: "kube-system", rnamespace: "kube-system",
rname: "kube-apiserver", rname: "kube-apiserver",
expectedUnctructured: 1, expectedPatches: 1,
}, },
{ {
name: "match 2", name: "match 2",
rgvk: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}, rgvk: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"},
rnamespace: "kube-system", rnamespace: "kube-system",
rname: "kube-scheduler", rname: "kube-scheduler",
expectedUnctructured: 2, expectedPatches: 2,
}, },
{ {
name: "match 0 (wrong gvk)", name: "match 0 (wrong gvk)",
rgvk: schema.GroupVersionKind{Group: "something", Version: "v1", Kind: "Pod"}, rgvk: schema.GroupVersionKind{Group: "something", Version: "v1", Kind: "Pod"},
rnamespace: "kube-system", rnamespace: "kube-system",
rname: "kube-scheduler", rname: "kube-scheduler",
expectedUnctructured: 0, expectedPatches: 0,
}, },
{ {
name: "match 0 (wrong namespace)", name: "match 0 (wrong namespace)",
rgvk: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}, rgvk: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"},
rnamespace: "kube-something", rnamespace: "kube-something",
rname: "kube-scheduler", rname: "kube-scheduler",
expectedUnctructured: 0, expectedPatches: 0,
}, },
{ {
name: "match 0 (wrong namr)", name: "match 0 (wrong namr)",
rgvk: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}, rgvk: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"},
rnamespace: "kube-system", rnamespace: "kube-system",
rname: "kube-something", rname: "kube-something",
expectedUnctructured: 0, expectedPatches: 0,
}, },
} }
for _, rt := range useCases { for _, rt := range useCases {
t.Run(rt.name, func(t *testing.T) { t.Run(rt.name, func(t *testing.T) {
r := u.FilterResource(rt.rgvk, rt.rnamespace, rt.rname) resource := &unstructured.Unstructured{}
resource.SetGroupVersionKind(rt.rgvk)
resource.SetNamespace(rt.rnamespace)
resource.SetName(rt.rname)
if len(r) != rt.expectedUnctructured { r := u.filterByResource(resource)
t.Errorf("Expected %d Unstructured items in the slice, actual %d", rt.expectedUnctructured, len(r))
if len(r) != rt.expectedPatches {
t.Errorf("Expected %d strategic merge patches in the slice, actual %d", rt.expectedPatches, len(r))
} }
}) })
} }