mirror of
https://github.com/kubernetes-sigs/kustomize.git
synced 2026-06-12 01:14:22 +00:00
make k8sdeps.NewFactory() importable
This commit is contained in:
43
k8sdeps/transformer/factory.go
Normal file
43
k8sdeps/transformer/factory.go
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
Copyright 2018 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 transformer provides transformer factory
|
||||
package transformer
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/kustomize/k8sdeps/transformer/hash"
|
||||
"sigs.k8s.io/kustomize/k8sdeps/transformer/patch"
|
||||
"sigs.k8s.io/kustomize/pkg/resource"
|
||||
"sigs.k8s.io/kustomize/pkg/transformers"
|
||||
)
|
||||
|
||||
// FactoryImpl makes patch transformer and name hash transformer
|
||||
type FactoryImpl struct{}
|
||||
|
||||
// NewFactoryImpl makes a new factoryImpl instance
|
||||
func NewFactoryImpl() *FactoryImpl {
|
||||
return &FactoryImpl{}
|
||||
}
|
||||
|
||||
// MakePatchTransformer makes a new patch transformer
|
||||
func (p *FactoryImpl) MakePatchTransformer(slice []*resource.Resource, rf *resource.Factory) (transformers.Transformer, error) {
|
||||
return patch.NewPatchTransformer(slice, rf)
|
||||
}
|
||||
|
||||
// MakeHashTransformer makes a new name hash transformer
|
||||
func (p *FactoryImpl) MakeHashTransformer() transformers.Transformer {
|
||||
return hash.NewNameHashTransformer()
|
||||
}
|
||||
168
k8sdeps/transformer/hash/hash.go
Normal file
168
k8sdeps/transformer/hash/hash.go
Normal file
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
Copyright 2017 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 hash
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
// KustHash compute hash for unstructured objects
|
||||
type KustHash struct{}
|
||||
|
||||
// NewKustHash returns a KustHash object
|
||||
func NewKustHash() *KustHash {
|
||||
return &KustHash{}
|
||||
}
|
||||
|
||||
// Hash returns a hash of either a ConfigMap or a Secret
|
||||
func (h *KustHash) Hash(m map[string]interface{}) (string, error) {
|
||||
u := unstructured.Unstructured{
|
||||
Object: m,
|
||||
}
|
||||
kind := u.GetKind()
|
||||
switch kind {
|
||||
case "ConfigMap":
|
||||
cm, err := unstructuredToConfigmap(u)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return ConfigMapHash(cm)
|
||||
case "Secret":
|
||||
sec, err := unstructuredToSecret(u)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return SecretHash(sec)
|
||||
default:
|
||||
return "", fmt.Errorf("type %s is supported for hashing in %v", kind, m)
|
||||
}
|
||||
}
|
||||
|
||||
// ConfigMapHash returns a hash of the ConfigMap.
|
||||
// The Data, Kind, and Name are taken into account.
|
||||
func ConfigMapHash(cm *v1.ConfigMap) (string, error) {
|
||||
encoded, err := encodeConfigMap(cm)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
h, err := encodeHash(hash(encoded))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
|
||||
// SecretHash returns a hash of the Secret.
|
||||
// The Data, Kind, Name, and Type are taken into account.
|
||||
func SecretHash(sec *v1.Secret) (string, error) {
|
||||
encoded, err := encodeSecret(sec)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
h, err := encodeHash(hash(encoded))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
|
||||
// encodeConfigMap encodes a ConfigMap.
|
||||
// Data, Kind, and Name are taken into account.
|
||||
func encodeConfigMap(cm *v1.ConfigMap) (string, error) {
|
||||
// json.Marshal sorts the keys in a stable order in the encoding
|
||||
m := map[string]interface{}{"kind": "ConfigMap", "name": cm.Name, "data": cm.Data}
|
||||
if len(cm.BinaryData) > 0 {
|
||||
m["binaryData"] = cm.BinaryData
|
||||
}
|
||||
data, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
// encodeSecret encodes a Secret.
|
||||
// Data, Kind, Name, and Type are taken into account.
|
||||
func encodeSecret(sec *v1.Secret) (string, error) {
|
||||
// json.Marshal sorts the keys in a stable order in the encoding
|
||||
data, err := json.Marshal(map[string]interface{}{"kind": "Secret", "type": sec.Type, "name": sec.Name, "data": sec.Data})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
// encodeHash extracts the first 40 bits of the hash from the hex string
|
||||
// (1 hex char represents 4 bits), and then maps vowels and vowel-like hex
|
||||
// characters to consonants to prevent bad words from being formed (the theory
|
||||
// is that no vowels makes it really hard to make bad words). Since the string
|
||||
// is hex, the only vowels it can contain are 'a' and 'e'.
|
||||
// We picked some arbitrary consonants to map to from the same character set as GenerateName.
|
||||
// See: https://github.com/kubernetes/apimachinery/blob/dc1f89aff9a7509782bde3b68824c8043a3e58cc/pkg/util/rand/rand.go#L75
|
||||
// If the hex string contains fewer than ten characters, returns an error.
|
||||
func encodeHash(hex string) (string, error) {
|
||||
if len(hex) < 10 {
|
||||
return "", fmt.Errorf("the hex string must contain at least 10 characters")
|
||||
}
|
||||
enc := []rune(hex[:10])
|
||||
for i := range enc {
|
||||
switch enc[i] {
|
||||
case '0':
|
||||
enc[i] = 'g'
|
||||
case '1':
|
||||
enc[i] = 'h'
|
||||
case '3':
|
||||
enc[i] = 'k'
|
||||
case 'a':
|
||||
enc[i] = 'm'
|
||||
case 'e':
|
||||
enc[i] = 't'
|
||||
}
|
||||
}
|
||||
return string(enc), nil
|
||||
}
|
||||
|
||||
// hash hashes `data` with sha256 and returns the hex string
|
||||
func hash(data string) string {
|
||||
return fmt.Sprintf("%x", sha256.Sum256([]byte(data)))
|
||||
}
|
||||
|
||||
func unstructuredToConfigmap(u unstructured.Unstructured) (*v1.ConfigMap, error) {
|
||||
marshaled, err := json.Marshal(u.Object)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out v1.ConfigMap
|
||||
err = json.Unmarshal(marshaled, &out)
|
||||
return &out, err
|
||||
}
|
||||
|
||||
func unstructuredToSecret(u unstructured.Unstructured) (*v1.Secret, error) {
|
||||
marshaled, err := json.Marshal(u.Object)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out v1.Secret
|
||||
err = json.Unmarshal(marshaled, &out)
|
||||
return &out, err
|
||||
}
|
||||
208
k8sdeps/transformer/hash/hash_test.go
Normal file
208
k8sdeps/transformer/hash/hash_test.go
Normal file
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
Copyright 2017 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 hash
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
"sigs.k8s.io/kustomize/pkg/gvk"
|
||||
)
|
||||
|
||||
var service = gvk.Gvk{Version: "v1", Kind: "Service"}
|
||||
var secret = gvk.Gvk{Version: "v1", Kind: "Secret"}
|
||||
var cmap = gvk.Gvk{Version: "v1", Kind: "ConfigMap"}
|
||||
var deploy = gvk.Gvk{Group: "apps", Version: "v1", Kind: "Deployment"}
|
||||
|
||||
func TestConfigMapHash(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
cm *v1.ConfigMap
|
||||
hash string
|
||||
err string
|
||||
}{
|
||||
// empty map
|
||||
{"empty data", &v1.ConfigMap{Data: map[string]string{}, BinaryData: map[string][]byte{}}, "42745tchd9", ""},
|
||||
// one key
|
||||
{"one key", &v1.ConfigMap{Data: map[string]string{"one": ""}}, "9g67k2htb6", ""},
|
||||
// three keys (tests sorting order)
|
||||
{"three keys", &v1.ConfigMap{Data: map[string]string{"two": "2", "one": "", "three": "3"}}, "f5h7t85m9b", ""},
|
||||
// empty binary data map
|
||||
{"empty binary data", &v1.ConfigMap{BinaryData: map[string][]byte{}}, "dk855m5d49", ""},
|
||||
// one key with binary data
|
||||
{"one key with binary data", &v1.ConfigMap{BinaryData: map[string][]byte{"one": []byte("")}}, "mk79584b8c", ""},
|
||||
// three keys with binary data (tests sorting order)
|
||||
{"three keys with binary data", &v1.ConfigMap{BinaryData: map[string][]byte{"two": []byte("2"), "one": []byte(""), "three": []byte("3")}}, "t458mc6db2", ""},
|
||||
// two keys, one with string and another with binary data
|
||||
{"two keys with one each", &v1.ConfigMap{Data: map[string]string{"one": ""}, BinaryData: map[string][]byte{"two": []byte("")}}, "698h7c7t9m", ""},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
h, err := ConfigMapHash(c.cm)
|
||||
if SkipRest(t, c.desc, err, c.err) {
|
||||
continue
|
||||
}
|
||||
if c.hash != h {
|
||||
t.Errorf("case %q, expect hash %q but got %q", c.desc, c.hash, h)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecretHash(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
secret *v1.Secret
|
||||
hash string
|
||||
err string
|
||||
}{
|
||||
// empty map
|
||||
{"empty data", &v1.Secret{Type: "my-type", Data: map[string][]byte{}}, "t75bgf6ctb", ""},
|
||||
// one key
|
||||
{"one key", &v1.Secret{Type: "my-type", Data: map[string][]byte{"one": []byte("")}}, "74bd68bm66", ""},
|
||||
// three keys (tests sorting order)
|
||||
{"three keys", &v1.Secret{Type: "my-type", Data: map[string][]byte{"two": []byte("2"), "one": []byte(""), "three": []byte("3")}}, "dgcb6h9tmk", ""},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
h, err := SecretHash(c.secret)
|
||||
if SkipRest(t, c.desc, err, c.err) {
|
||||
continue
|
||||
}
|
||||
if c.hash != h {
|
||||
t.Errorf("case %q, expect hash %q but got %q", c.desc, c.hash, h)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeConfigMap(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
cm *v1.ConfigMap
|
||||
expect string
|
||||
err string
|
||||
}{
|
||||
// empty map
|
||||
{"empty data", &v1.ConfigMap{Data: map[string]string{}}, `{"data":{},"kind":"ConfigMap","name":""}`, ""},
|
||||
// one key
|
||||
{"one key", &v1.ConfigMap{Data: map[string]string{"one": ""}}, `{"data":{"one":""},"kind":"ConfigMap","name":""}`, ""},
|
||||
// three keys (tests sorting order)
|
||||
{"three keys", &v1.ConfigMap{Data: map[string]string{"two": "2", "one": "", "three": "3"}},
|
||||
`{"data":{"one":"","three":"3","two":"2"},"kind":"ConfigMap","name":""}`, ""},
|
||||
// empty binary map
|
||||
{"empty data", &v1.ConfigMap{BinaryData: map[string][]byte{}}, `{"data":null,"kind":"ConfigMap","name":""}`, ""},
|
||||
// one key with binary data
|
||||
{"one key", &v1.ConfigMap{BinaryData: map[string][]byte{"one": []byte("")}},
|
||||
`{"binaryData":{"one":""},"data":null,"kind":"ConfigMap","name":""}`, ""},
|
||||
// three keys with binary data (tests sorting order)
|
||||
{"three keys", &v1.ConfigMap{BinaryData: map[string][]byte{"two": []byte("2"), "one": []byte(""), "three": []byte("3")}},
|
||||
`{"binaryData":{"one":"","three":"Mw==","two":"Mg=="},"data":null,"kind":"ConfigMap","name":""}`, ""},
|
||||
// two keys, one string and one binary values
|
||||
{"two keys with one each", &v1.ConfigMap{Data: map[string]string{"one": ""}, BinaryData: map[string][]byte{"two": []byte("")}},
|
||||
`{"binaryData":{"two":""},"data":{"one":""},"kind":"ConfigMap","name":""}`, ""},
|
||||
}
|
||||
for _, c := range cases {
|
||||
s, err := encodeConfigMap(c.cm)
|
||||
if SkipRest(t, c.desc, err, c.err) {
|
||||
continue
|
||||
}
|
||||
if s != c.expect {
|
||||
t.Errorf("case %q, expect %q but got %q from encode %#v", c.desc, c.expect, s, c.cm)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeSecret(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
secret *v1.Secret
|
||||
expect string
|
||||
err string
|
||||
}{
|
||||
// empty map
|
||||
{"empty data", &v1.Secret{Type: "my-type", Data: map[string][]byte{}}, `{"data":{},"kind":"Secret","name":"","type":"my-type"}`, ""},
|
||||
// one key
|
||||
{"one key", &v1.Secret{Type: "my-type", Data: map[string][]byte{"one": []byte("")}}, `{"data":{"one":""},"kind":"Secret","name":"","type":"my-type"}`, ""},
|
||||
// three keys (tests sorting order) - note json.Marshal base64 encodes the values because they come in as []byte
|
||||
{"three keys", &v1.Secret{
|
||||
Type: "my-type",
|
||||
Data: map[string][]byte{"two": []byte("2"), "one": []byte(""), "three": []byte("3")},
|
||||
},
|
||||
`{"data":{"one":"","three":"Mw==","two":"Mg=="},"kind":"Secret","name":"","type":"my-type"}`, ""},
|
||||
}
|
||||
for _, c := range cases {
|
||||
s, err := encodeSecret(c.secret)
|
||||
if SkipRest(t, c.desc, err, c.err) {
|
||||
continue
|
||||
}
|
||||
if s != c.expect {
|
||||
t.Errorf("case %q, expect %q but got %q from encode %#v", c.desc, c.expect, s, c.secret)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHash(t *testing.T) {
|
||||
// hash the empty string to be sure that sha256 is being used
|
||||
expect := "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
sum := hash("")
|
||||
if expect != sum {
|
||||
t.Errorf("expected hash %q but got %q", expect, sum)
|
||||
}
|
||||
}
|
||||
|
||||
// warn devs who change types that they might have to update a hash function
|
||||
// not perfect, as it only checks the number of top-level fields
|
||||
func TestTypeStability(t *testing.T) {
|
||||
errfmt := `case %q, expected %d fields but got %d
|
||||
Depending on the field(s) you added, you may need to modify the hash function for this type.
|
||||
To guide you: the hash function targets fields that comprise the contents of objects,
|
||||
not their metadata (e.g. the Data of a ConfigMap, but nothing in ObjectMeta).
|
||||
`
|
||||
cases := []struct {
|
||||
typeName string
|
||||
obj interface{}
|
||||
expect int
|
||||
}{
|
||||
{"ConfigMap", v1.ConfigMap{}, 4},
|
||||
{"Secret", v1.Secret{}, 5},
|
||||
}
|
||||
for _, c := range cases {
|
||||
val := reflect.ValueOf(c.obj)
|
||||
if num := val.NumField(); c.expect != num {
|
||||
t.Errorf(errfmt, c.typeName, c.expect, num)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SkipRest returns true if there was a non-nil error or if we expected an error that didn't happen,
|
||||
// and logs the appropriate error on the test object.
|
||||
// The return value indicates whether we should skip the rest of the test case due to the error result.
|
||||
func SkipRest(t *testing.T, desc string, err error, contains string) bool {
|
||||
if err != nil {
|
||||
if len(contains) == 0 {
|
||||
t.Errorf("case %q, expect nil error but got %q", desc, err.Error())
|
||||
} else if !strings.Contains(err.Error(), contains) {
|
||||
t.Errorf("case %q, expect error to contain %q but got %q", desc, contains, err.Error())
|
||||
}
|
||||
return true
|
||||
} else if len(contains) > 0 {
|
||||
t.Errorf("case %q, expect error to contain %q but got nil error", desc, contains)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
57
k8sdeps/transformer/hash/namehash.go
Normal file
57
k8sdeps/transformer/hash/namehash.go
Normal file
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
Copyright 2018 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 hash
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"sigs.k8s.io/kustomize/pkg/resmap"
|
||||
"sigs.k8s.io/kustomize/pkg/resource"
|
||||
"sigs.k8s.io/kustomize/pkg/transformers"
|
||||
)
|
||||
|
||||
type nameHashTransformer struct{}
|
||||
|
||||
var _ transformers.Transformer = &nameHashTransformer{}
|
||||
|
||||
// NewNameHashTransformer construct a nameHashTransformer.
|
||||
func NewNameHashTransformer() transformers.Transformer {
|
||||
return &nameHashTransformer{}
|
||||
}
|
||||
|
||||
// Transform appends hash to configmaps and secrets.
|
||||
func (o *nameHashTransformer) Transform(m resmap.ResMap) error {
|
||||
for _, res := range m {
|
||||
if res.IsGenerated() {
|
||||
err := o.appendHash(res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *nameHashTransformer) appendHash(res *resource.Resource) error {
|
||||
h, err := NewKustHash().Hash(res.Map())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nameWithHash := fmt.Sprintf("%s-%s", res.GetName(), h)
|
||||
res.SetName(nameWithHash)
|
||||
return nil
|
||||
}
|
||||
162
k8sdeps/transformer/hash/namehash_test.go
Normal file
162
k8sdeps/transformer/hash/namehash_test.go
Normal file
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
Copyright 2018 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 hash
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"sigs.k8s.io/kustomize/k8sdeps/kunstruct"
|
||||
"sigs.k8s.io/kustomize/pkg/ifc"
|
||||
"sigs.k8s.io/kustomize/pkg/resid"
|
||||
"sigs.k8s.io/kustomize/pkg/resmap"
|
||||
"sigs.k8s.io/kustomize/pkg/resource"
|
||||
)
|
||||
|
||||
func TestNameHashTransformer(t *testing.T) {
|
||||
rf := resource.NewFactory(
|
||||
kunstruct.NewKunstructuredFactoryImpl())
|
||||
objs := resmap.ResMap{
|
||||
resid.NewResId(cmap, "cm1"): rf.FromMap(
|
||||
map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "cm1",
|
||||
},
|
||||
}),
|
||||
resid.NewResId(deploy, "deploy1"): rf.FromMap(
|
||||
map[string]interface{}{
|
||||
"group": "apps",
|
||||
"apiVersion": "v1",
|
||||
"kind": "Deployment",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "deploy1",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"template": map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"labels": map[string]interface{}{
|
||||
"old-label": "old-value",
|
||||
},
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"containers": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "nginx",
|
||||
"image": "nginx:1.7.9",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
resid.NewResId(service, "svc1"): rf.FromMap(
|
||||
map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Service",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "svc1",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"ports": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "port1",
|
||||
"port": "12345",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
resid.NewResId(secret, "secret1"): rf.FromMap(
|
||||
map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Secret",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "secret1",
|
||||
},
|
||||
}).SetBehavior(ifc.BehaviorCreate),
|
||||
}
|
||||
|
||||
expected := resmap.ResMap{
|
||||
resid.NewResId(cmap, "cm1"): rf.FromMap(
|
||||
map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "cm1",
|
||||
},
|
||||
}),
|
||||
resid.NewResId(deploy, "deploy1"): rf.FromMap(
|
||||
map[string]interface{}{
|
||||
"group": "apps",
|
||||
"apiVersion": "v1",
|
||||
"kind": "Deployment",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "deploy1",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"template": map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"labels": map[string]interface{}{
|
||||
"old-label": "old-value",
|
||||
},
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"containers": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "nginx",
|
||||
"image": "nginx:1.7.9",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
resid.NewResId(service, "svc1"): rf.FromMap(
|
||||
map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Service",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "svc1",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"ports": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "port1",
|
||||
"port": "12345",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
resid.NewResId(secret, "secret1"): rf.FromMap(
|
||||
map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Secret",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "secret1-7kc45hd5f7",
|
||||
},
|
||||
}).SetBehavior(ifc.BehaviorCreate),
|
||||
}
|
||||
|
||||
tran := NewNameHashTransformer()
|
||||
tran.Transform(objs)
|
||||
|
||||
if !reflect.DeepEqual(objs, expected) {
|
||||
err := expected.ErrorIfNotEqual(objs)
|
||||
t.Fatalf("actual doesn't match expected: %v", err)
|
||||
}
|
||||
}
|
||||
174
k8sdeps/transformer/patch/patch.go
Normal file
174
k8sdeps/transformer/patch/patch.go
Normal file
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
Copyright 2018 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 patch
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/evanphx/json-patch"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/strategicpatch"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"sigs.k8s.io/kustomize/pkg/gvk"
|
||||
"sigs.k8s.io/kustomize/pkg/resmap"
|
||||
"sigs.k8s.io/kustomize/pkg/resource"
|
||||
"sigs.k8s.io/kustomize/pkg/transformers"
|
||||
)
|
||||
|
||||
// patchTransformer applies patches.
|
||||
type patchTransformer struct {
|
||||
patches []*resource.Resource
|
||||
rf *resource.Factory
|
||||
}
|
||||
|
||||
var _ transformers.Transformer = &patchTransformer{}
|
||||
|
||||
// NewPatchTransformer constructs a patchTransformer.
|
||||
func NewPatchTransformer(
|
||||
slice []*resource.Resource, rf *resource.Factory) (transformers.Transformer, error) {
|
||||
if len(slice) == 0 {
|
||||
return transformers.NewNoOpTransformer(), nil
|
||||
}
|
||||
return &patchTransformer{patches: slice, rf: rf}, nil
|
||||
}
|
||||
|
||||
// Transform apply the patches on top of the base resources.
|
||||
func (pt *patchTransformer) Transform(baseResourceMap resmap.ResMap) error {
|
||||
// Merge and then index the patches by Id.
|
||||
patches, err := pt.mergePatches()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Strategic merge the resources exist in both base and patches.
|
||||
for _, patch := range patches {
|
||||
// Merge patches with base resource.
|
||||
id := patch.Id()
|
||||
matchedIds := baseResourceMap.FindByGVKN(id)
|
||||
if len(matchedIds) == 0 {
|
||||
return fmt.Errorf("failed to find an object with %#v to apply the patch", id.Gvk())
|
||||
}
|
||||
if len(matchedIds) > 1 {
|
||||
return fmt.Errorf("found multiple objects %#v targeted by patch %#v (ambiguous)", matchedIds, id)
|
||||
}
|
||||
id = matchedIds[0]
|
||||
base := baseResourceMap[id]
|
||||
merged := map[string]interface{}{}
|
||||
versionedObj, err := scheme.Scheme.New(toSchemaGvk(id.Gvk()))
|
||||
baseName := base.GetName()
|
||||
switch {
|
||||
case runtime.IsNotRegisteredError(err):
|
||||
// Use JSON merge patch to handle types w/o schema
|
||||
baseBytes, err := json.Marshal(base.Map())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
patchBytes, err := json.Marshal(patch.Map())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mergedBytes, err := jsonpatch.MergePatch(baseBytes, patchBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = json.Unmarshal(mergedBytes, &merged)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case err != nil:
|
||||
return err
|
||||
default:
|
||||
// Use Strategic-Merge-Patch to handle types w/ schema
|
||||
// TODO: Change this to use the new Merge package.
|
||||
// Store the name of the base object, because this name may have been munged.
|
||||
// Apply this name to the patched object.
|
||||
lookupPatchMeta, err := strategicpatch.NewPatchMetaFromStruct(versionedObj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
merged, err = strategicpatch.StrategicMergeMapPatchUsingLookupPatchMeta(
|
||||
base.Map(),
|
||||
patch.Map(),
|
||||
lookupPatchMeta)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
base.SetName(baseName)
|
||||
baseResourceMap[id].SetMap(merged)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// mergePatches merge and index patches by Id.
|
||||
// It errors out if there is conflict between patches.
|
||||
func (pt *patchTransformer) mergePatches() (resmap.ResMap, error) {
|
||||
rc := resmap.ResMap{}
|
||||
for ix, patch := range pt.patches {
|
||||
id := patch.Id()
|
||||
existing, found := rc[id]
|
||||
if !found {
|
||||
rc[id] = patch
|
||||
continue
|
||||
}
|
||||
|
||||
versionedObj, err := scheme.Scheme.New(toSchemaGvk(id.Gvk()))
|
||||
if err != nil && !runtime.IsNotRegisteredError(err) {
|
||||
return nil, err
|
||||
}
|
||||
var cd conflictDetector
|
||||
if err != nil {
|
||||
cd = newJMPConflictDetector(pt.rf)
|
||||
} else {
|
||||
cd, err = newSMPConflictDetector(versionedObj, pt.rf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
conflict, err := cd.hasConflict(existing, patch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if conflict {
|
||||
conflictingPatch, err := cd.findConflict(ix, pt.patches)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, fmt.Errorf(
|
||||
"conflict between %#v and %#v",
|
||||
conflictingPatch.Map(), patch.Map())
|
||||
}
|
||||
merged, err := cd.mergePatches(existing, patch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rc[id] = merged
|
||||
}
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
// toSchemaGvk converts to a schema.GroupVersionKind.
|
||||
func toSchemaGvk(x gvk.Gvk) schema.GroupVersionKind {
|
||||
return schema.GroupVersionKind{
|
||||
Group: x.Group,
|
||||
Version: x.Version,
|
||||
Kind: x.Kind,
|
||||
}
|
||||
}
|
||||
563
k8sdeps/transformer/patch/patch_test.go
Normal file
563
k8sdeps/transformer/patch/patch_test.go
Normal file
@@ -0,0 +1,563 @@
|
||||
/*
|
||||
Copyright 2018 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 patch
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sigs.k8s.io/kustomize/k8sdeps/kunstruct"
|
||||
"sigs.k8s.io/kustomize/pkg/gvk"
|
||||
"sigs.k8s.io/kustomize/pkg/resid"
|
||||
"sigs.k8s.io/kustomize/pkg/resmap"
|
||||
"sigs.k8s.io/kustomize/pkg/resource"
|
||||
)
|
||||
|
||||
var rf = resource.NewFactory(
|
||||
kunstruct.NewKunstructuredFactoryImpl())
|
||||
var deploy = gvk.Gvk{Group: "apps", Version: "v1", Kind: "Deployment"}
|
||||
var foo = gvk.Gvk{Group: "example.com", Version: "v1", Kind: "Foo"}
|
||||
|
||||
func TestOverlayRun(t *testing.T) {
|
||||
base := resmap.ResMap{
|
||||
resid.NewResId(deploy, "deploy1"): rf.FromMap(
|
||||
map[string]interface{}{
|
||||
"apiVersion": "apps/v1",
|
||||
"kind": "Deployment",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "deploy1",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"template": map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"labels": map[string]interface{}{
|
||||
"old-label": "old-value",
|
||||
},
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"containers": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "nginx",
|
||||
"image": "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
patch := []*resource.Resource{
|
||||
rf.FromMap(map[string]interface{}{
|
||||
"apiVersion": "apps/v1",
|
||||
"kind": "Deployment",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "deploy1",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"template": map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"labels": map[string]interface{}{
|
||||
"another-label": "foo",
|
||||
},
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"containers": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "nginx",
|
||||
"image": "nginx:latest",
|
||||
"env": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "SOMEENV",
|
||||
"value": "BAR",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
}
|
||||
expected := resmap.ResMap{
|
||||
resid.NewResId(deploy, "deploy1"): rf.FromMap(
|
||||
map[string]interface{}{
|
||||
"apiVersion": "apps/v1",
|
||||
"kind": "Deployment",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "deploy1",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"template": map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"labels": map[string]interface{}{
|
||||
"old-label": "old-value",
|
||||
"another-label": "foo",
|
||||
},
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"containers": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "nginx",
|
||||
"image": "nginx:latest",
|
||||
"env": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "SOMEENV",
|
||||
"value": "BAR",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
lt, err := NewPatchTransformer(patch, rf)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
err = lt.Transform(base)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(base, expected) {
|
||||
err = expected.ErrorIfNotEqual(base)
|
||||
t.Fatalf("actual doesn't match expected: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiplePatches(t *testing.T) {
|
||||
base := resmap.ResMap{
|
||||
resid.NewResId(deploy, "deploy1"): rf.FromMap(
|
||||
map[string]interface{}{
|
||||
"apiVersion": "apps/v1",
|
||||
"kind": "Deployment",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "deploy1",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"template": map[string]interface{}{
|
||||
"spec": map[string]interface{}{
|
||||
"containers": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "nginx",
|
||||
"image": "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
patch := []*resource.Resource{
|
||||
rf.FromMap(map[string]interface{}{
|
||||
"apiVersion": "apps/v1",
|
||||
"kind": "Deployment",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "deploy1",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"template": map[string]interface{}{
|
||||
"spec": map[string]interface{}{
|
||||
"containers": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "nginx",
|
||||
"image": "nginx:latest",
|
||||
"env": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "SOMEENV",
|
||||
"value": "BAR",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
rf.FromMap(map[string]interface{}{
|
||||
"apiVersion": "apps/v1",
|
||||
"kind": "Deployment",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "deploy1",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"template": map[string]interface{}{
|
||||
"spec": map[string]interface{}{
|
||||
"containers": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "nginx",
|
||||
"env": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "ANOTHERENV",
|
||||
"value": "HELLO",
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"name": "busybox",
|
||||
"image": "busybox",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
}
|
||||
expected := resmap.ResMap{
|
||||
resid.NewResId(deploy, "deploy1"): rf.FromMap(
|
||||
map[string]interface{}{
|
||||
"apiVersion": "apps/v1",
|
||||
"kind": "Deployment",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "deploy1",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"template": map[string]interface{}{
|
||||
"spec": map[string]interface{}{
|
||||
"containers": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "nginx",
|
||||
"image": "nginx:latest",
|
||||
"env": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "ANOTHERENV",
|
||||
"value": "HELLO",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"name": "SOMEENV",
|
||||
"value": "BAR",
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"name": "busybox",
|
||||
"image": "busybox",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
lt, err := NewPatchTransformer(patch, rf)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
err = lt.Transform(base)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(base, expected) {
|
||||
err = expected.ErrorIfNotEqual(base)
|
||||
t.Fatalf("actual doesn't match expected: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiplePatchesWithConflict(t *testing.T) {
|
||||
base := resmap.ResMap{
|
||||
resid.NewResId(deploy, "deploy1"): rf.FromMap(
|
||||
map[string]interface{}{
|
||||
"apiVersion": "apps/v1",
|
||||
"kind": "Deployment",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "deploy1",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"template": map[string]interface{}{
|
||||
"spec": map[string]interface{}{
|
||||
"containers": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "nginx",
|
||||
"image": "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
patch := []*resource.Resource{
|
||||
rf.FromMap(map[string]interface{}{
|
||||
"apiVersion": "apps/v1",
|
||||
"kind": "Deployment",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "deploy1",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"template": map[string]interface{}{
|
||||
"spec": map[string]interface{}{
|
||||
"containers": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "nginx",
|
||||
"image": "nginx:latest",
|
||||
"env": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "SOMEENV",
|
||||
"value": "BAR",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
rf.FromMap(map[string]interface{}{
|
||||
"apiVersion": "apps/v1",
|
||||
"kind": "Deployment",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "deploy1",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"template": map[string]interface{}{
|
||||
"spec": map[string]interface{}{
|
||||
"containers": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "nginx",
|
||||
"image": "nginx:1.7.9",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
lt, err := NewPatchTransformer(patch, rf)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
err = lt.Transform(base)
|
||||
if err == nil {
|
||||
t.Fatalf("did not get expected error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "conflict") {
|
||||
t.Fatalf("expected error to contain %q but get %v", "conflict", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoSchemaOverlayRun(t *testing.T) {
|
||||
base := resmap.ResMap{
|
||||
resid.NewResId(foo, "my-foo"): rf.FromMap(
|
||||
map[string]interface{}{
|
||||
"apiVersion": "example.com/v1",
|
||||
"kind": "Foo",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "my-foo",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"bar": map[string]interface{}{
|
||||
"A": "X",
|
||||
"B": "Y",
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
patch := []*resource.Resource{
|
||||
rf.FromMap(map[string]interface{}{
|
||||
"apiVersion": "example.com/v1",
|
||||
"kind": "Foo",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "my-foo",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"bar": map[string]interface{}{
|
||||
"B": nil,
|
||||
"C": "Z",
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
}
|
||||
expected := resmap.ResMap{
|
||||
resid.NewResId(foo, "my-foo"): rf.FromMap(
|
||||
map[string]interface{}{
|
||||
"apiVersion": "example.com/v1",
|
||||
"kind": "Foo",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "my-foo",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"bar": map[string]interface{}{
|
||||
"A": "X",
|
||||
"C": "Z",
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
lt, err := NewPatchTransformer(patch, rf)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
err = lt.Transform(base)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if err = expected.ErrorIfNotEqual(base); err != nil {
|
||||
t.Fatalf("actual doesn't match expected: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoSchemaMultiplePatches(t *testing.T) {
|
||||
base := resmap.ResMap{
|
||||
resid.NewResId(foo, "my-foo"): rf.FromMap(
|
||||
map[string]interface{}{
|
||||
"apiVersion": "example.com/v1",
|
||||
"kind": "Foo",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "my-foo",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"bar": map[string]interface{}{
|
||||
"A": "X",
|
||||
"B": "Y",
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
patch := []*resource.Resource{
|
||||
rf.FromMap(map[string]interface{}{
|
||||
"apiVersion": "example.com/v1",
|
||||
"kind": "Foo",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "my-foo",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"bar": map[string]interface{}{
|
||||
"B": nil,
|
||||
"C": "Z",
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
rf.FromMap(map[string]interface{}{
|
||||
"apiVersion": "example.com/v1",
|
||||
"kind": "Foo",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "my-foo",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"bar": map[string]interface{}{
|
||||
"C": "Z",
|
||||
"D": "W",
|
||||
},
|
||||
"baz": map[string]interface{}{
|
||||
"hello": "world",
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
}
|
||||
expected := resmap.ResMap{
|
||||
resid.NewResId(foo, "my-foo"): rf.FromMap(
|
||||
map[string]interface{}{
|
||||
"apiVersion": "example.com/v1",
|
||||
"kind": "Foo",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "my-foo",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"bar": map[string]interface{}{
|
||||
"A": "X",
|
||||
"C": "Z",
|
||||
"D": "W",
|
||||
},
|
||||
"baz": map[string]interface{}{
|
||||
"hello": "world",
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
lt, err := NewPatchTransformer(patch, rf)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
err = lt.Transform(base)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if err = expected.ErrorIfNotEqual(base); err != nil {
|
||||
t.Fatalf("actual doesn't match expected: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoSchemaMultiplePatchesWithConflict(t *testing.T) {
|
||||
base := resmap.ResMap{
|
||||
resid.NewResId(foo, "my-foo"): rf.FromMap(
|
||||
map[string]interface{}{
|
||||
"apiVersion": "example.com/v1",
|
||||
"kind": "Foo",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "my-foo",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"bar": map[string]interface{}{
|
||||
"A": "X",
|
||||
"B": "Y",
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
patch := []*resource.Resource{
|
||||
rf.FromMap(map[string]interface{}{
|
||||
"apiVersion": "example.com/v1",
|
||||
"kind": "Foo",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "my-foo",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"bar": map[string]interface{}{
|
||||
"B": nil,
|
||||
"C": "Z",
|
||||
},
|
||||
},
|
||||
}),
|
||||
rf.FromMap(map[string]interface{}{
|
||||
"apiVersion": "example.com/v1",
|
||||
"kind": "Foo",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "my-foo",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"bar": map[string]interface{}{
|
||||
"C": "NOT_Z",
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
lt, err := NewPatchTransformer(patch, rf)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
err = lt.Transform(base)
|
||||
if err == nil {
|
||||
t.Fatalf("did not get expected error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "conflict") {
|
||||
t.Fatalf("expected error to contain %q but get %v", "conflict", err)
|
||||
}
|
||||
}
|
||||
137
k8sdeps/transformer/patch/patchconflictdetector.go
Normal file
137
k8sdeps/transformer/patch/patchconflictdetector.go
Normal file
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
Copyright 2018 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 patch
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/evanphx/json-patch"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/mergepatch"
|
||||
"k8s.io/apimachinery/pkg/util/strategicpatch"
|
||||
"sigs.k8s.io/kustomize/pkg/resource"
|
||||
)
|
||||
|
||||
type conflictDetector interface {
|
||||
hasConflict(patch1, patch2 *resource.Resource) (bool, error)
|
||||
findConflict(conflictingPatchIdx int, patches []*resource.Resource) (*resource.Resource, error)
|
||||
mergePatches(patch1, patch2 *resource.Resource) (*resource.Resource, error)
|
||||
}
|
||||
|
||||
type jsonMergePatch struct {
|
||||
rf *resource.Factory
|
||||
}
|
||||
|
||||
var _ conflictDetector = &jsonMergePatch{}
|
||||
|
||||
func newJMPConflictDetector(rf *resource.Factory) conflictDetector {
|
||||
return &jsonMergePatch{rf: rf}
|
||||
}
|
||||
|
||||
func (jmp *jsonMergePatch) hasConflict(
|
||||
patch1, patch2 *resource.Resource) (bool, error) {
|
||||
return mergepatch.HasConflicts(patch1.Map(), patch2.Map())
|
||||
}
|
||||
|
||||
func (jmp *jsonMergePatch) findConflict(
|
||||
conflictingPatchIdx int, patches []*resource.Resource) (*resource.Resource, error) {
|
||||
for i, patch := range patches {
|
||||
if i == conflictingPatchIdx {
|
||||
continue
|
||||
}
|
||||
if !patches[conflictingPatchIdx].Id().GvknEquals(patch.Id()) {
|
||||
continue
|
||||
}
|
||||
conflict, err := mergepatch.HasConflicts(
|
||||
patch.Map(),
|
||||
patches[conflictingPatchIdx].Map())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if conflict {
|
||||
return patch, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (jmp *jsonMergePatch) mergePatches(
|
||||
patch1, patch2 *resource.Resource) (*resource.Resource, error) {
|
||||
baseBytes, err := json.Marshal(patch1.Map())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
patchBytes, err := json.Marshal(patch2.Map())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mergedBytes, err := jsonpatch.MergeMergePatches(baseBytes, patchBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mergedMap := make(map[string]interface{})
|
||||
err = json.Unmarshal(mergedBytes, &mergedMap)
|
||||
return jmp.rf.FromMap(mergedMap), err
|
||||
}
|
||||
|
||||
type strategicMergePatch struct {
|
||||
lookupPatchMeta strategicpatch.LookupPatchMeta
|
||||
rf *resource.Factory
|
||||
}
|
||||
|
||||
var _ conflictDetector = &strategicMergePatch{}
|
||||
|
||||
func newSMPConflictDetector(
|
||||
versionedObj runtime.Object,
|
||||
rf *resource.Factory) (conflictDetector, error) {
|
||||
lookupPatchMeta, err := strategicpatch.NewPatchMetaFromStruct(versionedObj)
|
||||
return &strategicMergePatch{lookupPatchMeta: lookupPatchMeta, rf: rf}, err
|
||||
}
|
||||
|
||||
func (smp *strategicMergePatch) hasConflict(p1, p2 *resource.Resource) (bool, error) {
|
||||
return strategicpatch.MergingMapsHaveConflicts(
|
||||
p1.Map(), p2.Map(), smp.lookupPatchMeta)
|
||||
}
|
||||
|
||||
func (smp *strategicMergePatch) findConflict(
|
||||
conflictingPatchIdx int, patches []*resource.Resource) (*resource.Resource, error) {
|
||||
for i, patch := range patches {
|
||||
if i == conflictingPatchIdx {
|
||||
continue
|
||||
}
|
||||
if !patches[conflictingPatchIdx].Id().GvknEquals(patch.Id()) {
|
||||
continue
|
||||
}
|
||||
conflict, err := strategicpatch.MergingMapsHaveConflicts(
|
||||
patch.Map(),
|
||||
patches[conflictingPatchIdx].Map(),
|
||||
smp.lookupPatchMeta)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if conflict {
|
||||
return patch, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (smp *strategicMergePatch) mergePatches(patch1, patch2 *resource.Resource) (*resource.Resource, error) {
|
||||
mergeJSONMap, err := strategicpatch.MergeStrategicMergeMapPatchUsingLookupPatchMeta(
|
||||
smp.lookupPatchMeta, patch1.Map(), patch2.Map())
|
||||
return smp.rf.FromMap(mergeJSONMap), err
|
||||
}
|
||||
Reference in New Issue
Block a user