mirror of
https://github.com/kubernetes-sigs/kustomize.git
synced 2026-06-11 17:12:51 +00:00
namespace transformer implementation using kyaml
This commit is contained in:
9
api/filters/namespace/doc.go
Normal file
9
api/filters/namespace/doc.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// Copyright 2020 The Kubernetes Authors.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
// Package namespace contains a kio.Filter implementation of the kustomize
|
||||||
|
// namespace transformer.
|
||||||
|
//
|
||||||
|
// Special cases for known Kubernetes resources have been hardcoded in addition
|
||||||
|
// to those defined by the FsSlice.
|
||||||
|
package namespace
|
||||||
50
api/filters/namespace/example_test.go
Normal file
50
api/filters/namespace/example_test.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// Copyright 2020 The Kubernetes Authors.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package namespace_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"sigs.k8s.io/kustomize/api/filters/namespace"
|
||||||
|
"sigs.k8s.io/kustomize/api/internal/plugins/builtinconfig"
|
||||||
|
"sigs.k8s.io/kustomize/kyaml/kio"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleFilter() {
|
||||||
|
fss := builtinconfig.MakeDefaultConfig().NameSpace
|
||||||
|
err := kio.Pipeline{
|
||||||
|
Inputs: []kio.Reader{&kio.ByteReader{Reader: bytes.NewBufferString(`
|
||||||
|
apiVersion: example.com/v1
|
||||||
|
kind: Foo
|
||||||
|
metadata:
|
||||||
|
name: instance
|
||||||
|
---
|
||||||
|
apiVersion: example.com/v1
|
||||||
|
kind: Bar
|
||||||
|
metadata:
|
||||||
|
name: instance
|
||||||
|
namespace: bar
|
||||||
|
`)}},
|
||||||
|
Filters: []kio.Filter{namespace.Filter{Namespace: "app", FsSlice: fss}},
|
||||||
|
Outputs: []kio.Writer{kio.ByteWriter{Writer: os.Stdout}},
|
||||||
|
}.Execute()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// apiVersion: example.com/v1
|
||||||
|
// kind: Foo
|
||||||
|
// metadata:
|
||||||
|
// name: instance
|
||||||
|
// namespace: app
|
||||||
|
// ---
|
||||||
|
// apiVersion: example.com/v1
|
||||||
|
// kind: Bar
|
||||||
|
// metadata:
|
||||||
|
// name: instance
|
||||||
|
// namespace: app
|
||||||
|
}
|
||||||
171
api/filters/namespace/namespace.go
Normal file
171
api/filters/namespace/namespace.go
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
// Copyright 2019 The Kubernetes Authors.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package namespace
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sigs.k8s.io/kustomize/api/filters/fsslice"
|
||||||
|
"sigs.k8s.io/kustomize/api/types"
|
||||||
|
"sigs.k8s.io/kustomize/kyaml/kio"
|
||||||
|
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Filter struct {
|
||||||
|
// Namespace is the namespace to apply to the inputs
|
||||||
|
Namespace string `yaml:"namespace,omitempty"`
|
||||||
|
|
||||||
|
// FsSlice contains the FieldSpecs to locate the namespace field
|
||||||
|
FsSlice types.FsSlice
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ kio.Filter = Filter{}
|
||||||
|
|
||||||
|
func (ns Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
|
||||||
|
for i := range nodes {
|
||||||
|
if err := ns.run(nodes[i]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nodes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run runs the filter on a single node rather than a slice
|
||||||
|
func (ns Filter) run(node *yaml.RNode) error {
|
||||||
|
// hacks for hardcoded types -- :(
|
||||||
|
if err := ns.hacks(node); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the fieldspecs that are for hardcoded fields. The fieldspecs
|
||||||
|
// exist for backwards compatibility with other implementations
|
||||||
|
// of this transformation.
|
||||||
|
// This implementation of the namespace transformation
|
||||||
|
// Does not use the fieldspecs for implementing cases which
|
||||||
|
// require hardcoded logic.
|
||||||
|
ns.FsSlice = ns.removeFieldSpecsForHacks(ns.FsSlice)
|
||||||
|
|
||||||
|
// transformations based on data -- :)
|
||||||
|
return node.PipeE(fsslice.Filter{
|
||||||
|
FsSlice: ns.FsSlice,
|
||||||
|
SetValue: fsslice.SetScalar(ns.Namespace),
|
||||||
|
CreateKind: yaml.ScalarNode, // Namespace is a ScalarNode
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// hacks applies the namespace transforms that are hardcoded rather
|
||||||
|
// than specified through FieldSpecs.
|
||||||
|
func (ns Filter) hacks(obj *yaml.RNode) error {
|
||||||
|
meta, err := obj.GetMeta()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ns.metaNamespaceHack(obj, meta); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ns.roleBindingHack(obj, meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
// metaNamespaceHack is a hack for implementing the namespace transform
|
||||||
|
// for the metadata.namespace field on namespace scoped resources.
|
||||||
|
// namespace scoped resources are determined by NOT being present
|
||||||
|
// in a blacklist of cluster-scoped resource types (by apiVersion and kind).
|
||||||
|
//
|
||||||
|
// This hack should be updated to allow individual resources to specify
|
||||||
|
// if they are cluster scoped through either an annotation on the resources,
|
||||||
|
// or through inlined OpenAPI on the resource as a YAML comment.
|
||||||
|
func (ns Filter) metaNamespaceHack(obj *yaml.RNode, meta yaml.ResourceMeta) error {
|
||||||
|
gvk := fsslice.GetGVK(meta)
|
||||||
|
if !gvk.IsNamespaceableKind() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
f := fsslice.Filter{
|
||||||
|
FsSlice: []types.FieldSpec{
|
||||||
|
{Path: metaNamespaceField, CreateIfNotPresent: true},
|
||||||
|
},
|
||||||
|
SetValue: fsslice.SetScalar(ns.Namespace),
|
||||||
|
CreateKind: yaml.ScalarNode, // Namespace is a ScalarNode
|
||||||
|
}
|
||||||
|
_, err := f.Filter(obj)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// roleBindingHack is a hack for implementing the namespace transform
|
||||||
|
// for RoleBinding and ClusterRoleBinding resource types.
|
||||||
|
// RoleBinding and ClusterRoleBinding have namespace set on
|
||||||
|
// elements of the "subjects" field if and only if the subject elements
|
||||||
|
// "name" is "default". Otherwise the namespace is not set.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// kind: RoleBinding
|
||||||
|
// subjects:
|
||||||
|
// - name: "default" # this will have the namespace set
|
||||||
|
// ...
|
||||||
|
// - name: "something-else" # this will not have the namespace set
|
||||||
|
// ...
|
||||||
|
func (ns Filter) roleBindingHack(obj *yaml.RNode, meta yaml.ResourceMeta) error {
|
||||||
|
if meta.Kind != roleBindingKind && meta.Kind != clusterRoleBindingKind {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup the namespace field on all elements.
|
||||||
|
// We should change the fieldspec so this isn't necessary.
|
||||||
|
obj, err := obj.Pipe(yaml.Lookup(subjectsField))
|
||||||
|
if err != nil || yaml.IsEmpty(obj) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the namespace to each "subject" with name: default
|
||||||
|
err = obj.VisitElements(func(o *yaml.RNode) error {
|
||||||
|
// copied from kunstruct based kustomize NamespaceTransformer plugin
|
||||||
|
// The only case we need to force the namespace
|
||||||
|
// if for the "service account". "default" is
|
||||||
|
// kind of hardcoded here for right now.
|
||||||
|
name, err := o.Pipe(
|
||||||
|
yaml.Lookup("name"), yaml.Match("default"),
|
||||||
|
)
|
||||||
|
if err != nil || yaml.IsEmpty(name) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the namespace for the default account
|
||||||
|
v := yaml.NewScalarRNode(ns.Namespace)
|
||||||
|
return o.PipeE(
|
||||||
|
yaml.LookupCreate(yaml.ScalarNode, "namespace"),
|
||||||
|
yaml.FieldSetter{Value: v},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeFieldSpecsForHacks removes from the list fieldspecs that
|
||||||
|
// have hardcoded implementations
|
||||||
|
func (ns Filter) removeFieldSpecsForHacks(fs types.FsSlice) types.FsSlice {
|
||||||
|
var val types.FsSlice
|
||||||
|
for i := range fs {
|
||||||
|
// implemented by metaNamespaceHack
|
||||||
|
if fs[i].Path == metaNamespaceField {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// implemented by roleBindingHack
|
||||||
|
if fs[i].Kind == roleBindingKind && fs[i].Path == subjectsField {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// implemented by roleBindingHack
|
||||||
|
if fs[i].Kind == clusterRoleBindingKind && fs[i].Path == subjectsField {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val = append(val, fs[i])
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
metaNamespaceField = "metadata/namespace"
|
||||||
|
subjectsField = "subjects"
|
||||||
|
roleBindingKind = "RoleBinding"
|
||||||
|
clusterRoleBindingKind = "ClusterRoleBinding"
|
||||||
|
)
|
||||||
298
api/filters/namespace/namespace_test.go
Normal file
298
api/filters/namespace/namespace_test.go
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
// Copyright 2019 The Kubernetes Authors.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package namespace_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"sigs.k8s.io/kustomize/api/filters/namespace"
|
||||||
|
"sigs.k8s.io/kustomize/api/internal/plugins/builtinconfig"
|
||||||
|
"sigs.k8s.io/kustomize/api/types"
|
||||||
|
"sigs.k8s.io/kustomize/kyaml/kio"
|
||||||
|
)
|
||||||
|
|
||||||
|
var tests = []TestCase{
|
||||||
|
{
|
||||||
|
name: "add",
|
||||||
|
input: `
|
||||||
|
apiVersion: example.com/v1
|
||||||
|
kind: Foo
|
||||||
|
metadata:
|
||||||
|
name: instance
|
||||||
|
---
|
||||||
|
apiVersion: example.com/v1
|
||||||
|
kind: Bar
|
||||||
|
metadata:
|
||||||
|
name: instance
|
||||||
|
`,
|
||||||
|
expected: `
|
||||||
|
apiVersion: example.com/v1
|
||||||
|
kind: Foo
|
||||||
|
metadata:
|
||||||
|
name: instance
|
||||||
|
namespace: foo
|
||||||
|
---
|
||||||
|
apiVersion: example.com/v1
|
||||||
|
kind: Bar
|
||||||
|
metadata:
|
||||||
|
name: instance
|
||||||
|
namespace: foo
|
||||||
|
`,
|
||||||
|
filter: namespace.Filter{Namespace: "foo"},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "add-recurse",
|
||||||
|
input: `
|
||||||
|
apiVersion: example.com/v1
|
||||||
|
kind: Foo
|
||||||
|
---
|
||||||
|
apiVersion: example.com/v1
|
||||||
|
kind: Bar
|
||||||
|
`,
|
||||||
|
expected: `
|
||||||
|
apiVersion: example.com/v1
|
||||||
|
kind: Foo
|
||||||
|
metadata:
|
||||||
|
namespace: foo
|
||||||
|
---
|
||||||
|
apiVersion: example.com/v1
|
||||||
|
kind: Bar
|
||||||
|
metadata:
|
||||||
|
namespace: foo
|
||||||
|
`,
|
||||||
|
filter: namespace.Filter{Namespace: "foo"},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "update",
|
||||||
|
input: `
|
||||||
|
apiVersion: example.com/v1
|
||||||
|
kind: Foo
|
||||||
|
metadata:
|
||||||
|
name: instance
|
||||||
|
# update this namespace
|
||||||
|
namespace: bar
|
||||||
|
---
|
||||||
|
apiVersion: example.com/v1
|
||||||
|
kind: Bar
|
||||||
|
metadata:
|
||||||
|
name: instance
|
||||||
|
namespace: bar
|
||||||
|
`,
|
||||||
|
expected: `
|
||||||
|
apiVersion: example.com/v1
|
||||||
|
kind: Foo
|
||||||
|
metadata:
|
||||||
|
name: instance
|
||||||
|
# update this namespace
|
||||||
|
namespace: foo
|
||||||
|
---
|
||||||
|
apiVersion: example.com/v1
|
||||||
|
kind: Bar
|
||||||
|
metadata:
|
||||||
|
name: instance
|
||||||
|
namespace: foo
|
||||||
|
`,
|
||||||
|
filter: namespace.Filter{Namespace: "foo"},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "update-rolebinding",
|
||||||
|
input: `
|
||||||
|
apiVersion: example.com/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
subjects:
|
||||||
|
- name: default
|
||||||
|
---
|
||||||
|
apiVersion: example.com/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
subjects:
|
||||||
|
- name: default
|
||||||
|
namespace: foo
|
||||||
|
---
|
||||||
|
apiVersion: example.com/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
subjects:
|
||||||
|
- name: something
|
||||||
|
---
|
||||||
|
apiVersion: example.com/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
subjects:
|
||||||
|
- name: something
|
||||||
|
namespace: foo
|
||||||
|
`,
|
||||||
|
expected: `
|
||||||
|
apiVersion: example.com/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
subjects:
|
||||||
|
- name: default
|
||||||
|
namespace: bar
|
||||||
|
metadata:
|
||||||
|
namespace: bar
|
||||||
|
---
|
||||||
|
apiVersion: example.com/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
subjects:
|
||||||
|
- name: default
|
||||||
|
namespace: bar
|
||||||
|
metadata:
|
||||||
|
namespace: bar
|
||||||
|
---
|
||||||
|
apiVersion: example.com/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
subjects:
|
||||||
|
- name: something
|
||||||
|
metadata:
|
||||||
|
namespace: bar
|
||||||
|
---
|
||||||
|
apiVersion: example.com/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
subjects:
|
||||||
|
- name: something
|
||||||
|
namespace: foo
|
||||||
|
metadata:
|
||||||
|
namespace: bar
|
||||||
|
`,
|
||||||
|
filter: namespace.Filter{Namespace: "bar"},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "update-clusterrolebinding",
|
||||||
|
input: `
|
||||||
|
apiVersion: example.com/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
subjects:
|
||||||
|
- name: default
|
||||||
|
---
|
||||||
|
apiVersion: example.com/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
subjects:
|
||||||
|
- name: default
|
||||||
|
namespace: foo
|
||||||
|
---
|
||||||
|
apiVersion: example.com/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
subjects:
|
||||||
|
- name: something
|
||||||
|
---
|
||||||
|
apiVersion: example.com/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
subjects:
|
||||||
|
- name: something
|
||||||
|
namespace: foo
|
||||||
|
`,
|
||||||
|
expected: `
|
||||||
|
apiVersion: example.com/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
subjects:
|
||||||
|
- name: default
|
||||||
|
namespace: bar
|
||||||
|
---
|
||||||
|
apiVersion: example.com/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
subjects:
|
||||||
|
- name: default
|
||||||
|
namespace: bar
|
||||||
|
---
|
||||||
|
apiVersion: example.com/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
subjects:
|
||||||
|
- name: something
|
||||||
|
---
|
||||||
|
apiVersion: example.com/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
subjects:
|
||||||
|
- name: something
|
||||||
|
namespace: foo
|
||||||
|
`,
|
||||||
|
filter: namespace.Filter{Namespace: "bar"},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "data-fieldspecs",
|
||||||
|
input: `
|
||||||
|
apiVersion: example.com/v1
|
||||||
|
kind: Foo
|
||||||
|
metadata:
|
||||||
|
name: instance
|
||||||
|
---
|
||||||
|
apiVersion: example.com/v1
|
||||||
|
kind: Bar
|
||||||
|
metadata:
|
||||||
|
name: instance
|
||||||
|
`,
|
||||||
|
expected: `
|
||||||
|
apiVersion: example.com/v1
|
||||||
|
kind: Foo
|
||||||
|
metadata:
|
||||||
|
name: instance
|
||||||
|
namespace: foo
|
||||||
|
a:
|
||||||
|
b:
|
||||||
|
c: foo
|
||||||
|
---
|
||||||
|
apiVersion: example.com/v1
|
||||||
|
kind: Bar
|
||||||
|
metadata:
|
||||||
|
name: instance
|
||||||
|
namespace: foo
|
||||||
|
a:
|
||||||
|
b:
|
||||||
|
c: foo
|
||||||
|
`,
|
||||||
|
filter: namespace.Filter{Namespace: "foo"},
|
||||||
|
fsslice: []types.FieldSpec{
|
||||||
|
{
|
||||||
|
Path: "a/b/c",
|
||||||
|
CreateIfNotPresent: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestCase struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
filter namespace.Filter
|
||||||
|
fsslice types.FsSlice
|
||||||
|
}
|
||||||
|
|
||||||
|
var config = builtinconfig.MakeDefaultConfig()
|
||||||
|
|
||||||
|
func TestNamespace_Filter(t *testing.T) {
|
||||||
|
for i := range tests {
|
||||||
|
test := tests[i]
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
test.filter.FsSlice = append(config.NameSpace, test.fsslice...)
|
||||||
|
|
||||||
|
out := &bytes.Buffer{}
|
||||||
|
rw := &kio.ByteReadWriter{
|
||||||
|
Reader: bytes.NewBufferString(test.input),
|
||||||
|
Writer: out,
|
||||||
|
}
|
||||||
|
|
||||||
|
// run the filter
|
||||||
|
err := kio.Pipeline{
|
||||||
|
Inputs: []kio.Reader{rw},
|
||||||
|
Filters: []kio.Filter{test.filter},
|
||||||
|
Outputs: []kio.Writer{rw},
|
||||||
|
}.Execute()
|
||||||
|
if !assert.NoError(t, err) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
// check results
|
||||||
|
if !assert.Equal(t,
|
||||||
|
strings.TrimSpace(test.expected),
|
||||||
|
strings.TrimSpace(out.String())) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user