generate configmap for pruning

This commit is contained in:
Jingfang Liu
2019-04-05 16:13:15 -07:00
parent 4937b1c75e
commit 826affb8dd
17 changed files with 673 additions and 4 deletions

View File

@@ -20,8 +20,10 @@ package transformer
import (
"sigs.k8s.io/kustomize/k8sdeps/transformer/hash"
"sigs.k8s.io/kustomize/k8sdeps/transformer/patch"
"sigs.k8s.io/kustomize/k8sdeps/transformer/prune"
"sigs.k8s.io/kustomize/pkg/resource"
"sigs.k8s.io/kustomize/pkg/transformers"
"sigs.k8s.io/kustomize/pkg/types"
)
// FactoryImpl makes patch transformer and name hash transformer
@@ -41,3 +43,7 @@ func (p *FactoryImpl) MakePatchTransformer(slice []*resource.Resource, rf *resou
func (p *FactoryImpl) MakeHashTransformer() transformers.Transformer {
return hash.NewNameHashTransformer()
}
func (p *FactoryImpl) MakePruneTransformer(arg *types.Prune, namespace string, append bool) transformers.Transformer {
return prune.NewPruneTransformer(arg, namespace, append)
}

View File

@@ -20,6 +20,7 @@ import (
"crypto/sha256"
"encoding/json"
"fmt"
"sort"
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -86,6 +87,21 @@ func SecretHash(sec *v1.Secret) (string, error) {
return h, nil
}
// SortArrayAndComputeHash sorts a string array and
// returns a hash for it
func SortArrayAndComputeHash(s []string) (string, error) {
sort.Strings(s)
data, err := json.Marshal(s)
if err != nil {
return "", err
}
h, err := encodeHash(hash(string(data)))
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) {

View File

@@ -90,6 +90,28 @@ func TestSecretHash(t *testing.T) {
}
}
func TestArrayHash(t *testing.T) {
array1 := []string{"a", "b", "c"}
array2 := []string{"c", "b", "a"}
h1, err := SortArrayAndComputeHash(array1)
if err != nil {
t.Errorf("unexpected error %v", err)
}
if h1 == "" {
t.Errorf("failed to hash %v", array1)
}
h2, err := SortArrayAndComputeHash(array2)
if err != nil {
t.Errorf("unexpected error %v", err)
}
if h2 == "" {
t.Errorf("failed to hash %v", array2)
}
if h1 != h2 {
t.Errorf("hash is not consistent with reordered list: %s %s", h1, h2)
}
}
func TestEncodeConfigMap(t *testing.T) {
cases := []struct {
desc string

View File

@@ -0,0 +1,110 @@
/*
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 prune
import (
"fmt"
"sigs.k8s.io/kustomize/k8sdeps/kunstruct"
"sigs.k8s.io/kustomize/k8sdeps/transformer/hash"
"sigs.k8s.io/kustomize/pkg/gvk"
"sigs.k8s.io/kustomize/pkg/resid"
"sigs.k8s.io/kustomize/pkg/resmap"
"sigs.k8s.io/kustomize/pkg/resource"
"sigs.k8s.io/kustomize/pkg/transformers"
"sigs.k8s.io/kustomize/pkg/types"
)
//const PruneAnnotation = "kustomize.k8s.io/PruneRevision"
const PruneAnnotation = "current"
// pruneTransformer compute the ConfigMap used in prune
type pruneTransformer struct {
append bool
cmName string
cmNamespace string
}
var _ transformers.Transformer = &pruneTransformer{}
// NewPruneTransformer makes a pruneTransformer.
func NewPruneTransformer(p *types.Prune, namespace string, append bool) transformers.Transformer {
if p == nil || p.Type != "alphaConfigMap" || p.AlphaConfigMap.Namespace != namespace {
return transformers.NewNoOpTransformer()
}
return &pruneTransformer{
append: append,
cmName: p.AlphaConfigMap.Name,
cmNamespace: p.AlphaConfigMap.Namespace,
}
}
// Transform generates a prune ConfigMap based on the input ResMap.
// this tranformer doesn't change existing resources -
// it just visits resources and accumulates information to make a new ConfigMap.
// The prune ConfigMap is used to support the pruning command in the client side tool,
// which is proposed in https://github.com/kubernetes/enhancements/pull/810
func (o *pruneTransformer) Transform(m resmap.ResMap) error {
keys := []string{}
for id, r := range m {
s := id.PruneString()
keys = append(keys, s)
for _, refid := range r.GetRefBy() {
keys = append(keys, s+"---"+refid.PruneString())
}
}
h, err := hash.SortArrayAndComputeHash(keys)
if err != nil {
return err
}
args := &types.ConfigMapArgs{}
args.Name = o.cmName
args.Namespace = o.cmNamespace
for _, key := range keys {
args.LiteralSources = append(args.LiteralSources,
key+"="+h)
}
opts := &types.GeneratorOptions{
Annotations: make(map[string]string),
}
opts.Annotations[PruneAnnotation] = h
kf := kunstruct.NewKunstructuredFactoryImpl()
k, err := kf.MakeConfigMap(nil, opts, args)
if err != nil {
return err
}
if !o.append {
for k := range m {
delete(m, k)
}
}
id := resid.NewResIdWithPrefixNamespace(
gvk.Gvk{
Version: "v1",
Kind: "ConfigMap",
},
o.cmName,
"", o.cmNamespace)
if _, ok := m[id]; ok {
return fmt.Errorf("id %v is already used, please use a different name in the prune field", id)
}
m[id] = resource.NewFactory(kf).FromKunstructured(k)
return nil
}

View File

@@ -0,0 +1,171 @@
/*
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 prune
import (
"reflect"
"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"
"sigs.k8s.io/kustomize/pkg/types"
)
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 makeResMap() resmap.ResMap {
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(secret, "secret1"): rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "secret1",
},
}),
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{}{
"spec": map[string]interface{}{
"containers": []interface{}{
map[string]interface{}{
"name": "nginx",
"image": "nginx:1.7.9",
"env": []interface{}{
map[string]interface{}{
"name": "CM_FOO",
"valueFrom": map[string]interface{}{
"configMapKeyRef": map[string]interface{}{
"name": "cm1",
"key": "somekey",
},
},
},
},
"envFrom": []interface{}{
map[string]interface{}{
"configMapRef": map[string]interface{}{
"name": "cm1",
"key": "somekey",
},
},
map[string]interface{}{
"secretRef": map[string]interface{}{
"name": "secret1",
"key": "somekey",
},
},
},
},
},
},
},
},
}),
}
objs[resid.NewResId(cmap, "cm1")].AppendRefBy(resid.NewResId(deploy, "deploy1"))
objs[resid.NewResId(secret, "secret1")].AppendRefBy(resid.NewResId(deploy, "deploy1"))
return objs
}
func TestPruneTransformer(t *testing.T) {
rf := resource.NewFactory(
kunstruct.NewKunstructuredFactoryImpl())
// hash is derived based on all keys in the ConfigMap data field.
// It is added to annotations as
// current: hash
// When seeing the same annotation, prune binary assumes no
// clean up is needed
hash := "k777d7h45b"
// This is the root or inventory object which tracks all
// the applied resources - this is the thing we expect the transformer to create.
pruneMap := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "pruneCM",
"namespace": "default",
"annotations": map[string]interface{}{
"current": hash,
},
},
"data": map[string]interface{}{
"_ConfigMap__cm1": hash,
"_Secret__secret1": hash,
"apps_Deployment__deploy1": hash,
"_ConfigMap__cm1---apps_Deployment__deploy1": hash,
"_Secret__secret1---apps_Deployment__deploy1": hash,
},
})
expected := resmap.ResMap{
resid.NewResIdWithPrefixNamespace(cmap, "pruneCM", "", "default"): pruneMap,
}
p := &types.Prune{
Type: "alphaConfigMap",
AlphaConfigMap: types.NameArgs{
Name: "pruneCM",
Namespace: "default",
},
}
objs := makeResMap()
// include the original resmap; only return the ConfigMap for pruning
tran := NewPruneTransformer(p, "default", false)
tran.Transform(objs)
if !reflect.DeepEqual(objs, expected) {
err := expected.ErrorIfNotEqual(objs)
t.Fatalf("actual doesn't match expected: %v", err)
}
objs = makeResMap()
expected = objs.DeepCopy(rf)
expected[resid.NewResIdWithPrefixNamespace(cmap, "pruneCM", "", "default")] = pruneMap
// append the ConfigMap for pruning to the original resmap
tran = NewPruneTransformer(p, "default", true)
tran.Transform(objs)
if !reflect.DeepEqual(objs, expected) {
err := expected.ErrorIfNotEqual(objs)
t.Fatalf("actual doesn't match expected: %v", err)
}
}

View File

@@ -84,6 +84,8 @@ func NewCmdBuild(
&o.outputPath,
"output", "o", "",
"If specified, write the build output to this path.")
cmd.AddCommand(NewCmdBuildPrune(out, fs, rf, ptf, b))
return cmd
}
@@ -130,3 +132,55 @@ func (o *Options) RunBuild(
_, err = out.Write(res)
return err
}
func (o *Options) RunBuildPrune(
out io.Writer, fSys fs.FileSystem,
rf *resmap.Factory, ptf transformer.Factory,
b bool) error {
ldr, err := loader.NewLoader(o.kustomizationPath, fSys)
if err != nil {
return err
}
defer ldr.Cleanup()
kt, err := target.NewKustTarget(ldr, rf, ptf, b)
if err != nil {
return err
}
allResources, err := kt.MakePruneConfigMap()
if err != nil {
return err
}
// Output the objects.
res, err := allResources.EncodeAsYaml()
if err != nil {
return err
}
if o.outputPath != "" {
return fSys.WriteFile(o.outputPath, res)
}
_, err = out.Write(res)
return err
}
func NewCmdBuildPrune(
out io.Writer, fs fs.FileSystem,
rf *resmap.Factory,
ptf transformer.Factory,
b bool) *cobra.Command {
var o Options
cmd := &cobra.Command{
Use: "prune [path]",
Short: "Print configmap to prune previous applied objects",
Example: examples,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
err := o.Validate(args)
if err != nil {
return err
}
return o.RunBuildPrune(out, fs, rf, ptf, b)
},
}
return cmd
}

View File

@@ -69,6 +69,7 @@ func determineFieldOrder() []string {
"Configurations",
"Generators",
"Transformers",
"Prune",
}
// Add deprecated fields here.

View File

@@ -48,6 +48,7 @@ func TestFieldOrder(t *testing.T) {
"Configurations",
"Generators",
"Transformers",
"Prune",
}
actual := determineFieldOrder()
if len(expected) != len(actual) {

View File

@@ -20,10 +20,12 @@ package transformer
import (
"sigs.k8s.io/kustomize/pkg/resource"
"sigs.k8s.io/kustomize/pkg/transformers"
"sigs.k8s.io/kustomize/pkg/types"
)
// Factory makes transformers
type Factory interface {
MakePatchTransformer(slice []*resource.Resource, rf *resource.Factory) (transformers.Transformer, error)
MakeHashTransformer() transformers.Transformer
MakePruneTransformer(p *types.Prune, namespace string, append bool) transformers.Transformer
}

View File

@@ -205,3 +205,13 @@ func (n ResId) prefixList() []string {
func (n ResId) suffixList() []string {
return strings.Split(n.suffix, ":")
}
// PruneString returns a string which can be used
// as a key in a Prune ConfigMap
func (n ResId) PruneString() string {
name := n.prefix + n.name + n.suffix
return n.gvKind.Group +
"_" + n.gvKind.Kind +
"_" + n.namespace +
"_" + name
}

View File

@@ -30,6 +30,7 @@ import (
type Resource struct {
ifc.Kunstructured
options *types.GenArgs
refBy []resid.ResId
}
// String returns resource as JSON.
@@ -43,10 +44,16 @@ func (r *Resource) String() string {
// DeepCopy returns a new copy of resource
func (r *Resource) DeepCopy() *Resource {
return &Resource{
rc := &Resource{
Kunstructured: r.Kunstructured.Copy(),
options: r.options,
}
if len(r.refBy) > 0 {
refby := make([]resid.ResId, len(r.refBy))
copy(refby, r.refBy)
rc.refBy = refby
}
return rc
}
// Behavior returns the behavior for the resource.
@@ -65,12 +72,26 @@ func (r *Resource) Id() resid.ResId {
return resid.NewResIdWithPrefixNamespace(r.GetGvk(), r.GetName(), "", namespace)
}
// GetRefBy returns the ResIds that referred to current resource
func (r *Resource) GetRefBy() []resid.ResId {
return r.refBy
}
// AppendRefBy appends a ResId into the refBy list
func (r *Resource) AppendRefBy(id resid.ResId) {
r.refBy = append(r.refBy, id)
}
// Merge performs merge with other resource.
func (r *Resource) Merge(other *Resource) {
r.Replace(other)
mergeConfigmap(r.Map(), other.Map(), r.Map())
}
func (r *Resource) PruneString() string {
return r.Id().PruneString()
}
// Replace performs replace with other resource.
func (r *Resource) Replace(other *Resource) {
r.SetLabels(mergeStringMaps(other.GetLabels(), r.GetLabels()))

View File

@@ -17,6 +17,7 @@ limitations under the License.
package resource_test
import (
"reflect"
"testing"
"sigs.k8s.io/kustomize/k8sdeps/kunstruct"
@@ -94,3 +95,20 @@ func TestResourceId(t *testing.T) {
}
}
}
func TestDeepCopy(t *testing.T) {
r := factory.FromMap(
map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "pooh",
},
})
r.AppendRefBy(resid.NewResId(gvk.Gvk{Group: "somegroup", Kind: "MyKind"}, "random"))
cr := r.DeepCopy()
if !reflect.DeepEqual(r, cr) {
t.Errorf("expected %v\nbut got%v", r, cr)
}
}

View File

@@ -143,7 +143,47 @@ func (kt *KustTarget) MakeCustomizedResMap() (resmap.ResMap, error) {
}
// With all the back references fixed, it's OK to resolve Vars.
err = ra.ResolveVars()
return ra.ResMap(), err
if err != nil {
return nil, err
}
rm := ra.ResMap()
pt := kt.tFactory.MakePruneTransformer(kt.kustomization.Prune, kt.kustomization.Namespace, true)
err = pt.Transform(rm)
if err != nil {
return nil, err
}
return rm, nil
}
func (kt *KustTarget) MakePruneConfigMap() (resmap.ResMap, error) {
ra, err := kt.AccumulateTarget()
if err != nil {
return nil, err
}
err = ra.Transform(kt.tFactory.MakeHashTransformer())
if err != nil {
return nil, err
}
// Given that names have changed (prefixs/suffixes added),
// fix all the back references to those names.
err = ra.FixBackReferences()
if err != nil {
return nil, err
}
// With all the back references fixed, it's OK to resolve Vars.
err = ra.ResolveVars()
if err != nil {
return nil, err
}
rm := ra.ResMap()
pt := kt.tFactory.MakePruneTransformer(kt.kustomization.Prune, kt.kustomization.Namespace, false)
err = pt.Transform(rm)
if err != nil {
return nil, err
}
return rm, nil
}
func (kt *KustTarget) shouldAddHashSuffixesToGeneratedResources() bool {

View File

@@ -0,0 +1,180 @@
/*
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 target_test
import (
"testing"
)
func TestPruneConfigMap(t *testing.T) {
th := NewKustTestHarness(t, "/app/base")
th.writeK("/app/base", `
resources:
- deployment.yaml
- service.yaml
- secret.yaml
prune:
type: alphaConfigMap
alphaConfigMap:
name: haha
namespace: default
namePrefix: my-
namespace: default
`)
th.writeF("/app/base/deployment.yaml", `
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: mysql
labels:
app: mysql
spec:
selector:
matchLabels:
app: mysql
strategy:
type: Recreate
template:
metadata:
labels:
app: mysql
spec:
containers:
- image: mysql:5.6
name: mysql
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: pass
key: password
ports:
- containerPort: 3306
name: mysql
volumeMounts:
- name: mysql-persistent-storage
mountPath: /var/lib/mysql
volumes:
- name: mysql-persistent-storage
emptyDir: {}
`)
th.writeF("/app/base/service.yaml", `
apiVersion: v1
kind: Service
metadata:
name: mmmysql
labels:
app: mysql
spec:
ports:
- port: 3306
selector:
app: mysql
`)
th.writeF("/app/base/secret.yaml", `
apiVersion: v1
kind: Secret
metadata:
name: pass
type: Opaque
data:
# Default password is "admin".
password: YWRtaW4=
username: jingfang
`)
m, err := th.makeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.assertActualEqualsExpected(m, `
apiVersion: v1
data:
_Secret_default_my-pass: 54f87m6fd6
_Secret_default_my-pass---apps_Deployment_default_my-mysql: 54f87m6fd6
_Service_default_my-mmmysql: 54f87m6fd6
apps_Deployment_default_my-mysql: 54f87m6fd6
kind: ConfigMap
metadata:
annotations:
current: 54f87m6fd6
name: haha
namespace: default
---
apiVersion: v1
data:
password: YWRtaW4=
username: jingfang
kind: Secret
metadata:
name: my-pass
namespace: default
type: Opaque
---
apiVersion: v1
kind: Service
metadata:
labels:
app: mysql
name: my-mmmysql
namespace: default
spec:
ports:
- port: 3306
selector:
app: mysql
---
apiVersion: apps/v1beta2
kind: Deployment
metadata:
labels:
app: mysql
name: my-mysql
namespace: default
spec:
selector:
matchLabels:
app: mysql
strategy:
type: Recreate
template:
metadata:
labels:
app: mysql
spec:
containers:
- env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
key: password
name: my-pass
image: mysql:5.6
name: mysql
ports:
- containerPort: 3306
name: mysql
volumeMounts:
- mountPath: /var/lib/mysql
name: mysql-persistent-storage
volumes:
- emptyDir: {}
name: mysql-persistent-storage
`)
}

View File

@@ -21,6 +21,7 @@ import (
"log"
"sigs.k8s.io/kustomize/pkg/gvk"
"sigs.k8s.io/kustomize/pkg/resid"
"sigs.k8s.io/kustomize/pkg/resmap"
"sigs.k8s.io/kustomize/pkg/transformers/config"
)
@@ -70,7 +71,7 @@ func (o *nameReferenceTransformer) Transform(m resmap.ResMap) error {
m[id].Map(), fSpec.PathSlice(),
fSpec.CreateIfNotPresent,
o.updateNameReference(
backRef.Gvk, m.FilterBy(id)))
id, backRef.Gvk, m.FilterBy(id)))
if err != nil {
return err
}
@@ -82,7 +83,7 @@ func (o *nameReferenceTransformer) Transform(m resmap.ResMap) error {
}
func (o *nameReferenceTransformer) updateNameReference(
backRef gvk.Gvk, m resmap.ResMap) func(in interface{}) (interface{}, error) {
rid resid.ResId, backRef gvk.Gvk, m resmap.ResMap) func(in interface{}) (interface{}, error) {
return func(in interface{}) (interface{}, error) {
switch in.(type) {
case string:
@@ -98,6 +99,7 @@ func (o *nameReferenceTransformer) updateNameReference(
}
// Return transformed name of the object,
// complete with prefixes, hashes, etc.
res.AppendRefBy(rid)
return res.GetName(), nil
}
}
@@ -123,6 +125,7 @@ func (o *nameReferenceTransformer) updateNameReference(
for _, index := range indexes {
l[index] = res.GetName()
}
res.AppendRefBy(rid)
return l, nil
}
}

View File

@@ -618,6 +618,7 @@ func TestNameReferencePersistentVolumeHappyRun(t *testing.T) {
},
}),
}
expected[resid.NewResId(pv, "volume1")].AppendRefBy(resid.NewResId(pvc, "claim1"))
nrt := NewNameReferenceTransformer(defaultTransformerConfig.NameReference)
err := nrt.Transform(m)
if err != nil {

View File

@@ -136,6 +136,9 @@ type Kustomization struct {
// Transformers is a list of files containing transformers
Transformers []string `json:"transformers,omitempty" yaml:"transformers,omitempty"`
// Name of the ConfigMap used in Prune
Prune *Prune `json:"prune,omitempty" yaml:"prune:omitempty"`
}
// DealWithMissingFields fills the missing fields
@@ -289,3 +292,13 @@ type KVSource struct {
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Args []string `json:"args,omitempty" yaml:"args,omitempty"`
}
type Prune struct {
Type string `json:"type,omitempty" yaml:"type,omitempty"`
AlphaConfigMap NameArgs `json:"alphaConfigMap,omitempty" yaml:"alphaConfigMap,omitempty"`
}
type NameArgs struct {
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
}