kyaml: initial support for yaml and resource manipulation

This commit is contained in:
Phillip Wittrock
2019-11-04 11:27:47 -08:00
parent 588297f1f9
commit efd7c8e3f7
92 changed files with 13733 additions and 0 deletions

View File

@@ -0,0 +1,171 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package filters
import (
"bytes"
"os"
"os/exec"
"regexp"
"strings"
"sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
// GrepFilter filters Resources using a container image.
// The container must start a process that reads the list of
// input Resources from stdin, reads the Configuration from the env
// API_CONFIG, and writes the filtered Resources to stdout.
// If there is a error or validation failure, the process must exit
// non-zero.
// The full set of environment variables from the parent process
// are passed to the container.
type ContainerFilter struct {
// Image is the container image to use to create a container.
Image string `yaml:"image,omitempty"`
// Config is the API configuration for the container and passed through the
// API_CONFIG env var to the container.
// Typically a Kubernetes style Resource Config.
Config *yaml.RNode `yaml:"config,omitempty"`
// args may be specified by tests to override how a container is spawned
args []string
checkInput func(string)
}
// GrepFilter implements kio.GrepFilter
func (c *ContainerFilter) Filter(input []*yaml.RNode) ([]*yaml.RNode, error) {
// get the command to filter the Resources
cmd, err := c.getCommand()
if err != nil {
return nil, err
}
in := &bytes.Buffer{}
out := &bytes.Buffer{}
// write the input
err = kio.ByteWriter{
WrappingApiVersion: kio.ResourceListApiVersion,
WrappingKind: kio.ResourceListKind,
Writer: in, KeepReaderAnnotations: true, FunctionConfig: c.Config}.Write(input)
if err != nil {
return nil, err
}
// capture the command stdout for the return value
r := &kio.ByteReader{Reader: out}
// do the filtering
if c.checkInput != nil {
c.checkInput(in.String())
}
cmd.Stdin = in
cmd.Stdout = out
if err := cmd.Run(); err != nil {
return nil, err
}
return r.Read()
}
// getArgs returns the command + args to run to spawn the container
func (c *ContainerFilter) getArgs() []string {
// run the container using docker. this is simpler than using the docker
// libraries, and ensures things like auth work the same as if the container
// was run from the cli.
args := []string{"docker", "run",
"--rm", // delete the container afterward
"-i", "-a", "STDIN", "-a", "STDOUT", "-a", "STDERR", // attach stdin, stdout, stderr
// added security options
"--network", "none", // disable the network
"--user", "nobody", // run as nobody
// don't make fs readonly because things like heredoc rely on writing tmp files
"--security-opt=no-new-privileges", // don't allow the user to escalate privileges
}
// export the local environment vars to the container
for _, pair := range os.Environ() {
args = append(args, "-e", strings.Split(pair, "=")[0])
}
return append(args, c.Image)
}
// getCommand returns a command which will apply the GrepFilter using the container image
func (c *ContainerFilter) getCommand() (*exec.Cmd, error) {
// encode the filter command API configuration
cfg := &bytes.Buffer{}
if err := func() error {
e := yaml.NewEncoder(cfg)
defer e.Close()
// make it fit on a single line
c.Config.YNode().Style = yaml.FlowStyle
return e.Encode(c.Config.YNode())
}(); err != nil {
return nil, err
}
if len(c.args) == 0 {
c.args = c.getArgs()
}
cmd := exec.Command(c.args[0], c.args[1:]...)
cmd.Stderr = os.Stderr
cmd.Env = os.Environ()
// set stderr for err messaging
return cmd, nil
}
// IsReconcilerFilter filters Resources based on whether or not they are Reconciler Resource.
// Resources with an apiVersion starting with '*.gcr.io', 'gcr.io' or 'docker.io' are considered
// Reconciler Resources.
type IsReconcilerFilter struct {
// ExcludeReconcilers if set to true, then Reconcilers will be excluded -- e.g.
// Resources with a reconcile container through the apiVersion (gcr.io prefix) or
// through the annotations
ExcludeReconcilers bool `yaml:"excludeReconcilers,omitempty"`
// IncludeNonReconcilers if set to true, the NonReconciler will be included.
IncludeNonReconcilers bool `yaml:"includeNonReconcilers,omitempty"`
}
// Filter implements kio.Filter
func (c *IsReconcilerFilter) Filter(inputs []*yaml.RNode) ([]*yaml.RNode, error) {
var out []*yaml.RNode
for i := range inputs {
isContainerResource := GetContainerName(inputs[i]) != ""
if isContainerResource && !c.ExcludeReconcilers {
out = append(out, inputs[i])
}
if !isContainerResource && c.IncludeNonReconcilers {
out = append(out, inputs[i])
}
}
return out, nil
}
// GetContainerName returns the container image for an API if one exists
func GetContainerName(n *yaml.RNode) string {
meta, _ := n.GetMeta()
container := meta.Annotations["kyaml.kustomize.dev/container"]
if container != "" {
return container
}
if match.MatchString(meta.ApiVersion) {
return meta.ApiVersion
}
return ""
}
// match specifies the set of apiVersions to recognize as being container images
var match = regexp.MustCompile(`(docker\.io|.*\.?gcr\.io)/.*(:.*)?`)

View File

@@ -0,0 +1,281 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package filters
import (
"bytes"
"os"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
func TestFilter_command(t *testing.T) {
cfg, err := yaml.Parse(`apiversion: apps/v1
kind: Deployment
metadata:
name: foo
`)
if !assert.NoError(t, err) {
return
}
instance := &ContainerFilter{
Image: "example.com:version",
Config: cfg,
}
os.Setenv("KYAML_TEST", "FOO")
cmd, err := instance.getCommand()
if !assert.NoError(t, err) {
return
}
expected := []string{
"docker", "run",
"--rm",
"-i", "-a", "STDIN", "-a", "STDOUT", "-a", "STDERR",
"--network", "none",
"--user", "nobody",
"--security-opt=no-new-privileges",
}
for _, e := range os.Environ() {
// the process env
expected = append(expected, "-e", strings.Split(e, "=")[0])
}
expected = append(expected, "example.com:version")
assert.Equal(t, expected, cmd.Args)
foundKyaml := false
for _, e := range cmd.Env {
// verify the command has the right environment variables to pass to the container
split := strings.Split(e, "=")
if split[0] == "KYAML_TEST" {
assert.Equal(t, "FOO", split[1])
foundKyaml = true
}
}
assert.True(t, foundKyaml)
}
func TestFilter_Filter(t *testing.T) {
cfg, err := yaml.Parse(`apiVersion: apps/v1
kind: Deployment
metadata:
name: foo
`)
if !assert.NoError(t, err) {
return
}
input, err := (&kio.ByteReader{Reader: bytes.NewBufferString(`
apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-foo
---
apiVersion: v1
kind: Service
metadata:
name: service-foo
`)}).Read()
if !assert.NoError(t, err) {
return
}
called := false
result, err := (&ContainerFilter{
Image: "example.com:version",
Config: cfg,
args: []string{"sed", "s/Deployment/StatefulSet/g"},
checkInput: func(s string) {
called = true
if !assert.Equal(t, `apiVersion: kyaml.kustomize.dev/v1alpha1
kind: ResourceList
items:
- apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-foo
annotations:
kyaml.kustomize.dev/kio/index: 0
- apiVersion: v1
kind: Service
metadata:
name: service-foo
annotations:
kyaml.kustomize.dev/kio/index: 1
functionConfig: {apiVersion: apps/v1, kind: Deployment, metadata: {name: foo}}
`, s) {
t.FailNow()
}
},
}).Filter(input)
if !assert.NoError(t, err) {
return
}
if !assert.True(t, called) {
return
}
b := &bytes.Buffer{}
err = kio.ByteWriter{Writer: b, KeepReaderAnnotations: true}.Write(result)
if !assert.NoError(t, err) {
return
}
assert.Equal(t, `apiVersion: apps/v1
kind: StatefulSet
metadata:
name: deployment-foo
annotations:
kyaml.kustomize.dev/kio/index: 0
---
apiVersion: v1
kind: Service
metadata:
name: service-foo
annotations:
kyaml.kustomize.dev/kio/index: 1
`, b.String())
}
func TestFilter_Filter_noChange(t *testing.T) {
cfg, err := yaml.Parse(`apiversion: apps/v1
kind: Deployment
metadata:
name: foo
`)
if !assert.NoError(t, err) {
return
}
input, err := (&kio.ByteReader{Reader: bytes.NewBufferString(`
apiversion: apps/v1
kind: Deployment
metadata:
name: deployment-foo
---
apiVersion: v1
kind: Service
metadata:
name: service-foo
`)}).Read()
if !assert.NoError(t, err) {
return
}
called := false
result, err := (&ContainerFilter{
Image: "example.com:version",
Config: cfg,
args: []string{"sh", "-c", "cat <&0"},
checkInput: func(s string) {
called = true
if !assert.Equal(t, `apiVersion: kyaml.kustomize.dev/v1alpha1
kind: ResourceList
items:
- apiversion: apps/v1
kind: Deployment
metadata:
name: deployment-foo
annotations:
kyaml.kustomize.dev/kio/index: 0
- apiVersion: v1
kind: Service
metadata:
name: service-foo
annotations:
kyaml.kustomize.dev/kio/index: 1
functionConfig: {apiversion: apps/v1, kind: Deployment, metadata: {name: foo}}
`, s) {
t.FailNow()
}
},
}).Filter(input)
if !assert.NoError(t, err) {
return
}
if !assert.True(t, called) {
return
}
b := &bytes.Buffer{}
err = kio.ByteWriter{Writer: b, KeepReaderAnnotations: true}.Write(result)
if !assert.NoError(t, err) {
return
}
assert.Equal(t, `apiversion: apps/v1
kind: Deployment
metadata:
name: deployment-foo
annotations:
kyaml.kustomize.dev/kio/index: 0
---
apiVersion: v1
kind: Service
metadata:
name: service-foo
annotations:
kyaml.kustomize.dev/kio/index: 1
`, b.String())
}
func Test_GetContainerName(t *testing.T) {
// make sure gcr.io works
n, err := yaml.Parse(`apiVersion: gcr.io/foo/bar:something
kind: MyThing
`)
if !assert.NoError(t, err) {
return
}
c := GetContainerName(n)
assert.Equal(t, "gcr.io/foo/bar:something", c)
// make sure regional gcr.io works
n, err = yaml.Parse(`apiVersion: us.gcr.io/foo/bar:something
kind: MyThing
`)
if !assert.NoError(t, err) {
return
}
c = GetContainerName(n)
assert.Equal(t, "us.gcr.io/foo/bar:something", c)
// container from annotation
n, err = yaml.Parse(`apiVersion: v1
kind: MyThing
metadata:
annotations:
kyaml.kustomize.dev/container: gcr.io/foo/bar:something
`)
if !assert.NoError(t, err) {
return
}
c = GetContainerName(n)
assert.Equal(t, "gcr.io/foo/bar:something", c)
// doesn't have a container
n, err = yaml.Parse(`apiVersion: v1
kind: MyThing
metadata:
`)
if !assert.NoError(t, err) {
return
}
c = GetContainerName(n)
assert.Equal(t, "", c)
// make sure docker.io works
n, err = yaml.Parse(`apiVersion: docker.io/foo/bar:something
kind: MyThing
`)
if !assert.NoError(t, err) {
return
}
c = GetContainerName(n)
assert.Equal(t, "docker.io/foo/bar:something", c)
}

View File

@@ -0,0 +1,198 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package filters
import (
"fmt"
"sort"
"strings"
"sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
// Filters are the list of known filters for unmarshalling a filter into a concrete
// implementation.
var Filters = map[string]func() kio.Filter{
"FileSetter": func() kio.Filter { return &FileSetter{} },
"FormatFilter": func() kio.Filter { return &FormatFilter{} },
"GrepFilter": func() kio.Filter { return GrepFilter{} },
"MatchModifier": func() kio.Filter { return &MatchModifyFilter{} },
"Modifier": func() kio.Filter { return &Modifier{} },
}
// filter wraps a kio.filter so that it can be unmarshalled from yaml.
type KFilter struct {
kio.Filter
}
func (t KFilter) MarshalYAML() (interface{}, error) {
return t.Filter, nil
}
func (t *KFilter) UnmarshalYAML(unmarshal func(interface{}) error) error {
i := map[string]interface{}{}
if err := unmarshal(i); err != nil {
return err
}
meta := &yaml.ResourceMeta{}
if err := unmarshal(meta); err != nil {
return err
}
if filter, found := Filters[meta.Kind]; !found {
var knownFilters []string
for k := range Filters {
knownFilters = append(knownFilters, k)
}
sort.Strings(knownFilters)
return fmt.Errorf("unsupported filter Kind %v: may be one of: [%s]",
meta, strings.Join(knownFilters, ","))
} else {
t.Filter = filter()
}
return unmarshal(t.Filter)
}
// Modifier modifies the input Resources by invoking the provided pipeline.
// Modifier will return any Resources for which the pipeline does not return an error.
type Modifier struct {
Kind string `yaml:"kind,omitempty"`
Filters yaml.YFilters `yaml:"pipeline,omitempty"`
}
var _ kio.Filter = &Modifier{}
func (f Modifier) Filter(input []*yaml.RNode) ([]*yaml.RNode, error) {
for i := range input {
if _, err := input[i].Pipe(f.Filters.Filters()...); err != nil {
return nil, err
}
}
return input, nil
}
type MatchModifyFilter struct {
Kind string `yaml:"kind,omitempty"`
MatchFilters []yaml.YFilters `yaml:"match,omitempty"`
ModifyFilters yaml.YFilters `yaml:"modify,omitempty"`
}
var _ kio.Filter = &MatchModifyFilter{}
func (f MatchModifyFilter) Filter(input []*yaml.RNode) ([]*yaml.RNode, error) {
var matches = input
var err error
for _, filter := range f.MatchFilters {
matches, err = MatchFilter{Filters: filter}.Filter(matches)
if err != nil {
return nil, err
}
}
_, err = Modifier{Filters: f.ModifyFilters}.Filter(matches)
if err != nil {
return nil, err
}
return input, nil
}
type MatchFilter struct {
Kind string `yaml:"kind,omitempty"`
Filters yaml.YFilters `yaml:"pipeline,omitempty"`
}
var _ kio.Filter = &MatchFilter{}
func (f MatchFilter) Filter(input []*yaml.RNode) ([]*yaml.RNode, error) {
var output []*yaml.RNode
for i := range input {
if v, err := input[i].Pipe(f.Filters.Filters()...); err != nil {
return nil, err
} else if v == nil {
continue
}
output = append(output, input[i])
}
return output, nil
}
type FilenameFmtVerb string
const (
// KindFmt substitutes kind
KindFmt FilenameFmtVerb = "%k"
// NameFmt substitutes metadata.name
NameFmt FilenameFmtVerb = "%n"
// NamespaceFmt substitutes metdata.namespace
NamespaceFmt FilenameFmtVerb = "%s"
)
// FileSetter sets the file name and mode annotations on Resources.
type FileSetter struct {
Kind string `yaml:"kind,omitempty"`
// FilenamePattern is the pattern to use for generating filenames. FilenameFmtVerb
// FielnameFmtVerbs may be specified to substitute Resource metadata into the filename.
FilenamePattern string `yaml:"filenamePattern,omitempty"`
// Mode is the filemode to write.
Mode string `yaml:"mode,omitempty"`
// Override will override the existing filename if it is set on the pattern.
// Otherwise the existing filename is kept.
Override bool `yaml:"override,omitempty"`
}
var _ kio.Filter = &FileSetter{}
const DefaultFilenamePattern = "%n_%k.yaml"
func (f *FileSetter) Filter(input []*yaml.RNode) ([]*yaml.RNode, error) {
if f.Mode == "" {
f.Mode = fmt.Sprintf("%d", 0600)
}
if f.FilenamePattern == "" {
f.FilenamePattern = DefaultFilenamePattern
}
resources := map[string][]*yaml.RNode{}
for i := range input {
m, err := input[i].GetMeta()
if err != nil {
return nil, err
}
file := f.FilenamePattern
file = strings.Replace(file, string(KindFmt), strings.ToLower(m.Kind), -1)
file = strings.Replace(file, string(NameFmt), strings.ToLower(m.Name), -1)
file = strings.Replace(file, string(NamespaceFmt), strings.ToLower(m.Namespace), -1)
if _, found := m.Annotations[kioutil.PathAnnotation]; !found || f.Override {
if _, err := input[i].Pipe(yaml.SetAnnotation(kioutil.PathAnnotation, file)); err != nil {
return nil, err
}
}
resources[file] = append(resources[file], input[i])
}
var output []*yaml.RNode
for i := range resources {
if err := kioutil.SortNodes(resources[i]); err != nil {
return nil, err
}
for j := range resources[i] {
if _, err := resources[i][j].Pipe(
yaml.SetAnnotation(kioutil.IndexAnnotation, fmt.Sprintf("%d", j))); err != nil {
return nil, err
}
output = append(output, resources[i][j])
}
}
return output, nil
}

View File

@@ -0,0 +1,170 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package filters_test
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
. "sigs.k8s.io/kustomize/kyaml/kio"
. "sigs.k8s.io/kustomize/kyaml/kio/filters"
)
var r = `
apiVersion: apps/v1
kind: Deployment
metadata:
name: foo1
namespace: bar
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: foo2
---
apiVersion: v1
kind: Service
metadata:
name: foo2
namespace: bar
---
apiVersion: v1
kind: Service
metadata:
name: foo1
`
func TestFileSetter_Filter(t *testing.T) {
in := bytes.NewBufferString(r)
out := &bytes.Buffer{}
err := Pipeline{
Inputs: []Reader{&ByteReader{Reader: in}},
Filters: []Filter{&FileSetter{}},
Outputs: []Writer{ByteWriter{Sort: true, Writer: out}},
}.Execute()
if !assert.NoError(t, err) {
return
}
assert.Equal(t, `apiVersion: apps/v1
kind: Deployment
metadata:
name: foo1
namespace: bar
annotations:
kyaml.kustomize.dev/kio/path: foo1_deployment.yaml
---
apiVersion: v1
kind: Service
metadata:
name: foo1
annotations:
kyaml.kustomize.dev/kio/path: foo1_service.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: foo2
annotations:
kyaml.kustomize.dev/kio/path: foo2_deployment.yaml
---
apiVersion: v1
kind: Service
metadata:
name: foo2
namespace: bar
annotations:
kyaml.kustomize.dev/kio/path: foo2_service.yaml
`, out.String())
}
func TestFileSetter_Filter_pattern(t *testing.T) {
in := bytes.NewBufferString(r)
out := &bytes.Buffer{}
err := Pipeline{
Inputs: []Reader{&ByteReader{Reader: in}},
Filters: []Filter{&FileSetter{
FilenamePattern: "%n_%s_%k.yaml",
}},
Outputs: []Writer{ByteWriter{Sort: true, Writer: out}},
}.Execute()
if !assert.NoError(t, err) {
return
}
assert.Equal(t, `apiVersion: v1
kind: Service
metadata:
name: foo1
annotations:
kyaml.kustomize.dev/kio/path: foo1__service.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: foo1
namespace: bar
annotations:
kyaml.kustomize.dev/kio/path: foo1_bar_deployment.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: foo2
annotations:
kyaml.kustomize.dev/kio/path: foo2__deployment.yaml
---
apiVersion: v1
kind: Service
metadata:
name: foo2
namespace: bar
annotations:
kyaml.kustomize.dev/kio/path: foo2_bar_service.yaml
`, out.String())
}
func TestFileSetter_Filter_empty(t *testing.T) {
in := bytes.NewBufferString(r)
out := &bytes.Buffer{}
err := Pipeline{
Inputs: []Reader{&ByteReader{Reader: in}},
Filters: []Filter{&FileSetter{
FilenamePattern: "resource.yaml",
}},
Outputs: []Writer{ByteWriter{Writer: out}},
}.Execute()
if !assert.NoError(t, err) {
return
}
assert.Equal(t, `apiVersion: apps/v1
kind: Deployment
metadata:
name: foo1
namespace: bar
annotations:
kyaml.kustomize.dev/kio/path: resource.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: foo2
annotations:
kyaml.kustomize.dev/kio/path: resource.yaml
---
apiVersion: v1
kind: Service
metadata:
name: foo2
namespace: bar
annotations:
kyaml.kustomize.dev/kio/path: resource.yaml
---
apiVersion: v1
kind: Service
metadata:
name: foo1
annotations:
kyaml.kustomize.dev/kio/path: resource.yaml
`, out.String())
}

220
kyaml/kio/filters/fmtr.go Normal file
View File

@@ -0,0 +1,220 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
// Package yamlfmt contains libraries for formatting yaml files containing
// Kubernetes Resource configuration.
//
// Yaml files are formatted by:
// - Sorting fields and map values
// - Sorting unordered lists for whitelisted types
// - Applying a canonical yaml Style
//
// Fields are ordered using a relative ordering applied to commonly
// encountered Resource fields. All Resources, including non-builtin
// Resources such as CRDs, share the same field precedence.
//
// Fields that do not appear in the explicit ordering are ordered
// lexicographically.
//
// A subset of well known known unordered lists are sorted by element field
// values.
package filters
import (
"bytes"
"fmt"
"io"
"sort"
"sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
// FormatInput returns the formatted input.
func FormatInput(input io.Reader) (*bytes.Buffer, error) {
buff := &bytes.Buffer{}
err := kio.Pipeline{
Inputs: []kio.Reader{&kio.ByteReader{Reader: input}},
Filters: []kio.Filter{FormatFilter{}},
Outputs: []kio.Writer{kio.ByteWriter{Writer: buff}},
}.Execute()
return buff, err
}
// FormatFileOrDirectory reads the file or directory and formats each file's
// contents by writing it back to the file.
func FormatFileOrDirectory(path string) error {
return kio.Pipeline{
Inputs: []kio.Reader{kio.LocalPackageReader{
PackagePath: path,
}},
Filters: []kio.Filter{FormatFilter{}},
Outputs: []kio.Writer{kio.LocalPackageWriter{PackagePath: path}},
}.Execute()
}
type FormatFilter struct{}
var _ kio.Filter = FormatFilter{}
func (f FormatFilter) Filter(slice []*yaml.RNode) ([]*yaml.RNode, error) {
for i := range slice {
kindNode, err := slice[i].Pipe(yaml.Get("kind"))
if err != nil {
return nil, err
}
if kindNode == nil {
continue
}
apiVersionNode, err := slice[i].Pipe(yaml.Get("apiVersion"))
if err != nil {
return nil, err
}
if apiVersionNode == nil {
continue
}
kind, apiVersion := kindNode.YNode().Value, apiVersionNode.YNode().Value
err = (&formatter{apiVersion: apiVersion, kind: kind}).fmtNode(slice[i].YNode(), "")
if err != nil {
return nil, err
}
}
return slice, nil
}
type formatter struct {
apiVersion string
kind string
}
// fmtNode recursively formats the Document Contents.
func (f *formatter) fmtNode(n *yaml.Node, path string) error {
n.Style = 0
// sort the order of mapping fields
if n.Kind == yaml.MappingNode {
sort.Sort(sortedMapContents(*n))
}
// sort the order of sequence elements if it is whitelisted
if n.Kind == yaml.SequenceNode {
if yaml.WhitelistedListSortKinds.Has(f.kind) &&
yaml.WhitelistedListSortApis.Has(f.apiVersion) {
if sortField, found := yaml.WhitelistedListSortFields[path]; found {
sort.Sort(sortedSeqContents{Node: *n, sortField: sortField})
}
}
}
for i := range n.Content {
p := path
if n.Kind == yaml.MappingNode && i%2 == 1 {
p = fmt.Sprintf("%s.%s", path, n.Content[i-1].Value)
}
err := f.fmtNode(n.Content[i], p)
if err != nil {
return err
}
}
return nil
}
// sortedMapContents sorts the Contents field of a MappingNode by the field names using a statically
// defined field precedence, and falling back on lexicographical sorting
type sortedMapContents yaml.Node
func (s sortedMapContents) Len() int {
return len(s.Content) / 2
}
func (s sortedMapContents) Swap(i, j int) {
// yaml MappingNode Contents are a list of field names followed by
// field values, rather than a list of field <name, value> pairs.
// increment.
//
// e.g. ["field1Name", "field1Value", "field2Name", "field2Value"]
iFieldNameIndex := i * 2
jFieldNameIndex := j * 2
iFieldValueIndex := iFieldNameIndex + 1
jFieldValueIndex := jFieldNameIndex + 1
// swap field names
s.Content[iFieldNameIndex], s.Content[jFieldNameIndex] =
s.Content[jFieldNameIndex], s.Content[iFieldNameIndex]
// swap field values
s.Content[iFieldValueIndex], s.Content[jFieldValueIndex] = s.
Content[jFieldValueIndex], s.Content[iFieldValueIndex]
}
func (s sortedMapContents) Less(i, j int) bool {
iFieldNameIndex := i * 2
jFieldNameIndex := j * 2
iFieldName := s.Content[iFieldNameIndex].Value
jFieldName := s.Content[jFieldNameIndex].Value
// order by their precedence values looked up from the index
iOrder, foundI := yaml.FieldOrder[iFieldName]
jOrder, foundJ := yaml.FieldOrder[jFieldName]
if foundI && foundJ {
return iOrder < jOrder
}
// known fields come before unknown fields
if foundI {
return true
}
if foundJ {
return false
}
// neither field is known, sort them lexicographically
return iFieldName < jFieldName
}
// sortedSeqContents sorts the Contents field of a SequenceNode by the value of
// the elements sortField.
// e.g. it will sort spec.template.spec.containers by the value of the container `name` field
type sortedSeqContents struct {
yaml.Node
sortField string
}
func (s sortedSeqContents) Len() int {
return len(s.Content)
}
func (s sortedSeqContents) Swap(i, j int) {
s.Content[i], s.Content[j] = s.Content[j], s.Content[i]
}
func (s sortedSeqContents) Less(i, j int) bool {
// primitive lists -- sort by the element's primitive values
if s.sortField == "" {
iValue := s.Content[i].Value
jValue := s.Content[j].Value
return iValue < jValue
}
// map lists -- sort by the element's sortField values
var iValue, jValue string
for a := range s.Content[i].Content {
if a%2 != 0 {
continue // not a fieldNameIndex
}
// locate the index of the sortField field
if s.Content[i].Content[a].Value == s.sortField {
// a is the yaml node for the field key, a+1 is the node for the field value
iValue = s.Content[i].Content[a+1].Value
}
}
for a := range s.Content[j].Content {
if a%2 != 0 {
continue // not a fieldNameIndex
}
// locate the index of the sortField field
if s.Content[j].Content[a].Value == s.sortField {
// a is the yaml node for the field key, a+1 is the node for the field value
jValue = s.Content[j].Content[a+1].Value
}
}
// compare the field values
return iValue < jValue
}

View File

@@ -0,0 +1,623 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package filters_test
import (
"bytes"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
. "sigs.k8s.io/kustomize/kyaml/kio/filters"
"sigs.k8s.io/kustomize/kyaml/kio/filters/testyaml"
)
// TestFormatInput_configMap verifies a ConfigMap yaml is formatted correctly
func TestFormatInput_configMap(t *testing.T) {
y := `
# this formatting is intentionally weird
apiVersion: v1
# this is data
data:
# this is color
color: purple
# that was color
# this is textmode
textmode: "true"
# this is how
how: fairlyNice
kind: ConfigMap
metadata:
selfLink: /api/v1/namespaces/default/configmaps/config-multi-env-files
namespace: default
creationTimestamp: 2017-12-27T18:38:34Z
name: config-multi-env-files
resourceVersion: "810136"
uid: 252c4572-eb35-11e7-887b-42010a8002b8 # keep no trailing linefeed`
expected := `# this formatting is intentionally weird
apiVersion: v1
kind: ConfigMap
metadata:
name: config-multi-env-files
namespace: default
creationTimestamp: 2017-12-27T18:38:34Z
resourceVersion: "810136"
selfLink: /api/v1/namespaces/default/configmaps/config-multi-env-files
uid: 252c4572-eb35-11e7-887b-42010a8002b8 # keep no trailing linefeed
# this is data
data:
# this is color
color: purple
# that was color
# this is how
how: fairlyNice
# this is textmode
textmode: "true"
`
s, err := FormatInput(strings.NewReader(y))
assert.NoError(t, err)
assert.Equal(t, expected, s.String())
}
// TestFormatInput_deployment verifies a Deployment yaml is formatted correctly
func TestFormatInput_deployment(t *testing.T) {
y := `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
selector:
matchLabels:
app: nginx
replicas: 3
template:
metadata:
labels:
app: nginx
spec:
containers:
# this is a container
- ports:
# this is a port
- containerPort: 80
name: b-nginx
image: nginx:1.7.9
# this is another container
- name: a-nginx
image: nginx:1.7.9
ports:
- containerPort: 80
`
expected := `apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- # this is another container
name: a-nginx
image: nginx:1.7.9
ports:
- containerPort: 80
- name: b-nginx
image: nginx:1.7.9
# this is a container
ports:
- # this is a port
containerPort: 80
`
s, err := FormatInput(strings.NewReader(y))
assert.NoError(t, err)
assert.Equal(t, expected, s.String())
}
// TestFormatInput_service verifies a Service yaml is formatted correctly
func TestFormatInput_service(t *testing.T) {
y := `
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app: MyApp
ports:
- protocol: TCP
port: 80
targetPort: 9376
`
expected := `apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app: MyApp
ports:
- protocol: TCP
port: 80
targetPort: 9376
`
s, err := FormatInput(strings.NewReader(y))
assert.NoError(t, err)
assert.Equal(t, expected, s.String())
}
// TestFormatInput_service verifies a Service yaml is formatted correctly
func TestFormatInput_validatingWebhookConfiguration(t *testing.T) {
y := `
apiVersion: admissionregistration.k8s.io/v1beta1
kind: ValidatingWebhookConfiguration
metadata:
name: <name of this configuration object>
webhooks:
- name: <webhook name, e.g., pod-policy.example.io>
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- UPDATE # this list is indented by 2
- CREATE
- CONNECT
resources:
- pods # this list is not indented by 2
scope: "Namespaced"
clientConfig:
service:
namespace: <namespace of the front-end service>
name: <name of the front-end service>
caBundle: <pem encoded ca cert that signs the server cert used by the webhook>
admissionReviewVersions:
- v1beta1
timeoutSeconds: 1
`
expected := `apiVersion: admissionregistration.k8s.io/v1beta1
kind: ValidatingWebhookConfiguration
metadata:
name: <name of this configuration object>
webhooks:
- name: <webhook name, e.g., pod-policy.example.io>
admissionReviewVersions:
- v1beta1
clientConfig:
service:
name: <name of the front-end service>
namespace: <namespace of the front-end service>
caBundle: <pem encoded ca cert that signs the server cert used by the webhook>
rules:
- resources:
- pods # this list is not indented by 2
apiGroups:
- ""
apiVersions:
- v1
operations:
- CONNECT
- CREATE
- UPDATE # this list is indented by 2
scope: Namespaced
timeoutSeconds: 1
`
s, err := FormatInput(strings.NewReader(y))
assert.NoError(t, err)
assert.Equal(t, expected, s.String())
}
// TestFormatInput_unKnownType verifies an unknown type yaml is formatted correctly
func TestFormatInput_unKnownType(t *testing.T) {
y := `
spec:
template:
spec:
# these shouldn't be sorted because the type isn't whitelisted
containers:
- name: b
- name: a
replicas: 1
status:
conditions:
- 3
- 1
- 2
other:
b: a1
a: b1
apiVersion: example.com/v1beta1
kind: MyType
`
expected := `apiVersion: example.com/v1beta1
kind: MyType
spec:
replicas: 1
template:
spec:
# these shouldn't be sorted because the type isn't whitelisted
containers:
- name: b
- name: a
status:
conditions:
- 3
- 1
- 2
other:
a: b1
b: a1
`
s, err := FormatInput(strings.NewReader(y))
assert.NoError(t, err)
assert.Equal(t, expected, s.String())
}
// TestFormatInput_deployment verifies a Deployment yaml is formatted correctly
func TestFormatInput_resources(t *testing.T) {
input := &bytes.Buffer{}
_, err := io.Copy(input, bytes.NewReader(testyaml.UnformattedYaml1))
assert.NoError(t, err)
_, err = io.Copy(input, strings.NewReader("---\n"))
assert.NoError(t, err)
_, err = io.Copy(input, bytes.NewReader(testyaml.UnformattedYaml2))
assert.NoError(t, err)
expectedOutput := &bytes.Buffer{}
_, err = io.Copy(expectedOutput, bytes.NewReader(testyaml.FormattedYaml1))
assert.NoError(t, err)
_, err = io.Copy(expectedOutput, strings.NewReader("---\n"))
assert.NoError(t, err)
_, err = io.Copy(expectedOutput, bytes.NewReader(testyaml.FormattedYaml2))
assert.NoError(t, err)
s, err := FormatInput(input)
assert.NoError(t, err)
assert.Equal(t, expectedOutput.String(), s.String())
}
//
func TestFormatInput_failMissingKind(t *testing.T) {
y := `
spec:
template:
spec:
containers:
- b
- a
replicas: 1
status:
conditions:
- 3
- 1
- 2
other:
b: a1
a: b1
apiVersion: example.com/v1beta1
`
b, err := FormatInput(strings.NewReader(y))
assert.NoError(t, err)
assert.Equal(t, strings.TrimLeft(y, "\n"), b.String())
}
func TestFormatInput_failMissingApiVersion(t *testing.T) {
y := `
spec:
template:
spec:
containers:
- a
- b
replicas: 1
status:
conditions:
- 3
- 1
- 2
other:
b: a1
a: b1
kind: MyKind
`
b, err := FormatInput(strings.NewReader(y))
assert.NoError(t, err)
assert.Equal(t, strings.TrimLeft(y, "\n"), b.String())
}
func TestFormatInput_failUnmarshal(t *testing.T) {
y := `
spec:
template:
spec:
containers:
- a
- b
replicas: 1
status:
conditions:
- 3
- 1
- 2
other:
b: a1
a: b1
kind: MyKind
apiVersion: example.com/v1beta1
`
_, err := FormatInput(strings.NewReader(y))
assert.EqualError(t, err, "yaml: line 15: found character that cannot start any token")
}
// TestFormatFileOrDirectory_yamlExtFile verifies that FormatFileOrDirectory will format a file
// with a .yaml extension.
func TestFormatFileOrDirectory_yamlExtFile(t *testing.T) {
// write the unformatted file
f, err := ioutil.TempFile("", "yamlfmt*.yaml")
if !assert.NoError(t, err) {
return
}
defer os.Remove(f.Name())
err = ioutil.WriteFile(f.Name(), testyaml.UnformattedYaml1, 0600)
if !assert.NoError(t, err) {
return
}
// format the file
err = FormatFileOrDirectory(f.Name())
if !assert.NoError(t, err) {
return
}
// check the result is formatted
b, err := ioutil.ReadFile(f.Name())
if !assert.NoError(t, err) {
return
}
assert.Equal(t, string(testyaml.FormattedYaml1), string(b))
}
func TestFormatFileOrDirectory_multipleYamlEntries(t *testing.T) {
// write the unformatted file
f, err := ioutil.TempFile("", "yamlfmt*.yaml")
assert.NoError(t, err)
defer os.Remove(f.Name())
err = ioutil.WriteFile(f.Name(),
[]byte(string(testyaml.UnformattedYaml1)+"---\n"+string(testyaml.UnformattedYaml2)), 0600)
assert.NoError(t, err)
// format the file
err = FormatFileOrDirectory(f.Name())
assert.NoError(t, err)
// check the result is formatted
b, err := ioutil.ReadFile(f.Name())
assert.NoError(t, err)
assert.Equal(t, string(testyaml.FormattedYaml1)+"---\n"+string(testyaml.FormattedYaml2), string(b))
}
// TestFormatFileOrDirectory_ymlExtFile verifies that FormatFileOrDirectory will format a file
// with a .yml extension.
func TestFormatFileOrDirectory_ymlExtFile(t *testing.T) {
// write the unformatted file
f, err := ioutil.TempFile("", "yamlfmt*.yml")
assert.NoError(t, err)
defer os.Remove(f.Name())
err = ioutil.WriteFile(f.Name(), testyaml.UnformattedYaml1, 0600)
assert.NoError(t, err)
// format the file
err = FormatFileOrDirectory(f.Name())
assert.NoError(t, err)
// check the result is formatted
b, err := ioutil.ReadFile(f.Name())
assert.NoError(t, err)
assert.Equal(t, string(testyaml.FormattedYaml1), string(b))
}
// TestFormatFileOrDirectory_skipYamlExtFileWithJson verifies that the json content is formatted
// as yaml
func TestFormatFileOrDirectory_YamlExtFileWithJson(t *testing.T) {
// write the unformatted JSON file contents
f, err := ioutil.TempFile("", "yamlfmt*.yaml")
assert.NoError(t, err)
defer os.Remove(f.Name())
err = ioutil.WriteFile(f.Name(), testyaml.UnformattedJson1, 0600)
assert.NoError(t, err)
// format the file
err = FormatFileOrDirectory(f.Name())
assert.NoError(t, err)
// check the result is formatted as yaml
b, err := ioutil.ReadFile(f.Name())
assert.NoError(t, err)
assert.Equal(t, string(testyaml.FormattedYaml1), string(b))
}
// TestFormatFileOrDirectory_partialKubernetesYamlFile verifies that if a yaml file contains both
// Kubernetes and non-Kubernetes documents, it will only format the Kubernetes documents
func TestFormatFileOrDirectory_partialKubernetesYamlFile(t *testing.T) {
// write the unformatted file
f, err := ioutil.TempFile("", "yamlfmt*.yaml")
assert.NoError(t, err)
defer os.Remove(f.Name())
err = ioutil.WriteFile(f.Name(), []byte(string(testyaml.UnformattedYaml1)+`---
status:
conditions:
- 3
- 1
- 2
spec: a
---
`+string(testyaml.UnformattedYaml2)), 0600)
assert.NoError(t, err)
// format the file
err = FormatFileOrDirectory(f.Name())
assert.NoError(t, err)
// check the result is NOT formatted
b, err := ioutil.ReadFile(f.Name())
assert.NoError(t, err)
assert.Equal(t, string(testyaml.FormattedYaml1)+`---
status:
conditions:
- 3
- 1
- 2
spec: a
---
`+string(testyaml.FormattedYaml2), string(b))
}
// TestFormatFileOrDirectory_nonKubernetesYamlFile verifies that if a yaml file does not contain
// kubernetes
func TestFormatFileOrDirectory_skipNonKubernetesYamlFile(t *testing.T) {
// write the unformatted JSON file contents
f, err := ioutil.TempFile("", "yamlfmt*.yaml")
assert.NoError(t, err)
defer os.Remove(f.Name())
err = ioutil.WriteFile(f.Name(), []byte(`
status:
conditions:
- 3
- 1
- 2
spec: a
`), 0600)
assert.NoError(t, err)
// format the file
err = FormatFileOrDirectory(f.Name())
assert.NoError(t, err)
// check the result is formatted as yaml
b, err := ioutil.ReadFile(f.Name())
assert.NoError(t, err)
assert.Equal(t, `status:
conditions:
- 3
- 1
- 2
spec: a
`, string(b))
}
// TestFormatFileOrDirectory_jsonFile should not fmt the file even though it contains yaml.
func TestFormatFileOrDirectory_skipJsonExtFile(t *testing.T) {
f, err := ioutil.TempFile("", "yamlfmt*.json")
assert.NoError(t, err)
defer os.Remove(f.Name())
err = ioutil.WriteFile(f.Name(), testyaml.UnformattedYaml1, 0600)
assert.NoError(t, err)
err = FormatFileOrDirectory(f.Name())
assert.NoError(t, err)
b, err := ioutil.ReadFile(f.Name())
assert.NoError(t, err)
assert.Equal(t, string(testyaml.UnformattedYaml1), string(b))
}
// TestFormatFileOrDirectory_directory verifies that yaml files will be formatted,
// and other files will be ignored
func TestFormatFileOrDirectory_directory(t *testing.T) {
d, err := ioutil.TempDir("", "yamlfmt")
assert.NoError(t, err)
err = os.Mkdir(filepath.Join(d, "config"), 0700)
assert.NoError(t, err)
err = ioutil.WriteFile(filepath.Join(d, "c1.yaml"), testyaml.UnformattedYaml1, 0600)
assert.NoError(t, err)
err = ioutil.WriteFile(filepath.Join(d, "config", "c2.yaml"), testyaml.UnformattedYaml2, 0600)
assert.NoError(t, err)
err = ioutil.WriteFile(filepath.Join(d, "README.md"), []byte(`# Markdown`), 0600)
assert.NoError(t, err)
err = FormatFileOrDirectory(d)
assert.NoError(t, err)
b, err := ioutil.ReadFile(filepath.Join(d, "c1.yaml"))
assert.NoError(t, err)
assert.Equal(t, string(testyaml.FormattedYaml1), string(b))
b, err = ioutil.ReadFile(filepath.Join(d, "config", "c2.yaml"))
assert.NoError(t, err)
assert.Equal(t, string(testyaml.FormattedYaml2), string(b))
b, err = ioutil.ReadFile(filepath.Join(d, "README.md"))
assert.NoError(t, err)
assert.Equal(t, `# Markdown`, string(b))
// verify no additional files were created
files := []string{
".", "c1.yaml", "README.md", "config", filepath.Join("config", "c2.yaml")}
err = filepath.Walk(d, func(path string, info os.FileInfo, err error) error {
assert.NoError(t, err)
path, err = filepath.Rel(d, path)
assert.NoError(t, err)
assert.Contains(t, files, path)
return nil
})
assert.NoError(t, err)
}
// TestFormatFileOrDirectory_trimWhiteSpace verifies that trailling and leading whitespace is
// trimmed
func TestFormatFileOrDirectory_trimWhiteSpace(t *testing.T) {
f, err := ioutil.TempFile("", "yamlfmt*.yaml")
assert.NoError(t, err)
defer os.Remove(f.Name())
err = ioutil.WriteFile(f.Name(), []byte("\n\n"+string(testyaml.UnformattedYaml1)+"\n\n"), 0600)
assert.NoError(t, err)
err = FormatFileOrDirectory(f.Name())
assert.NoError(t, err)
b, err := ioutil.ReadFile(f.Name())
assert.NoError(t, err)
assert.Equal(t, string(testyaml.FormattedYaml1), string(b))
}

117
kyaml/kio/filters/grep.go Normal file
View File

@@ -0,0 +1,117 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package filters
import (
"regexp"
"strings"
"sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
type GrepType int
const (
Regexp GrepType = 1 << iota
GreaterThanEq
GreaterThan
LessThan
LessThanEq
)
// GrepFilter filters RNodes with a matching field
type GrepFilter struct {
Path []string `yaml:"path,omitempty"`
Value string `yaml:"value,omitempty"`
MatchType GrepType `yaml:"matchType,omitempty"`
InvertMatch bool `yaml:"invertMatch,omitempty"`
Compare func(a, b string) (int, error)
}
var _ kio.Filter = GrepFilter{}
func (f GrepFilter) Filter(input []*yaml.RNode) ([]*yaml.RNode, error) {
// compile the regular expression 1 time if we are matching using regex
var reg *regexp.Regexp
var err error
if f.MatchType == Regexp || f.MatchType == 0 {
reg, err = regexp.Compile(f.Value)
if err != nil {
return nil, err
}
}
var output kio.ResourceNodeSlice
for i := range input {
node := input[i]
val, err := node.Pipe(&yaml.PathMatcher{Path: f.Path})
if err != nil {
return nil, err
}
if val == nil || len(val.Content()) == 0 {
if f.InvertMatch {
output = append(output, input[i])
}
continue
}
found := false
err = val.VisitElements(func(elem *yaml.RNode) error {
// get the value
var str string
if f.MatchType == Regexp {
style := elem.YNode().Style
defer func() { elem.YNode().Style = style }()
elem.YNode().Style = yaml.FlowStyle
str, err = elem.String()
if err != nil {
return err
}
str = strings.TrimSpace(strings.Replace(str, `"`, "", -1))
} else {
// if not regexp, then it needs to parse into a quantity and comments will
// break that
str = elem.YNode().Value
if str == "" {
return nil
}
}
if f.MatchType == Regexp || f.MatchType == 0 {
if reg.MatchString(str) {
found = true
}
return nil
}
comp, err := f.Compare(str, f.Value)
if err != nil {
return err
}
if f.MatchType == GreaterThan && comp > 0 {
found = true
}
if f.MatchType == GreaterThanEq && comp >= 0 {
found = true
}
if f.MatchType == LessThan && comp < 0 {
found = true
}
if f.MatchType == LessThanEq && comp <= 0 {
found = true
}
return nil
})
if err != nil {
return nil, err
}
if found == f.InvertMatch {
continue
}
output = append(output, input[i])
}
return output, nil
}

View File

@@ -0,0 +1,157 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package filters_test
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/kustomize/kyaml/kio"
. "sigs.k8s.io/kustomize/kyaml/kio/filters"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
func TestGrepFilter_Filter(t *testing.T) {
in := `kind: Deployment
metadata:
labels:
app: nginx2
name: foo
annotations:
app: nginx2
spec:
replicas: 1
---
kind: Deployment
metadata:
labels:
app: nginx
annotations:
app: nginx
name: bar
spec:
replicas: 3
---
kind: Service
metadata:
name: foo
annotations:
app: nginx
spec:
selector:
app: nginx
`
out := &bytes.Buffer{}
err := kio.Pipeline{
Inputs: []kio.Reader{&kio.ByteReader{Reader: bytes.NewBufferString(in)}},
Filters: []kio.Filter{GrepFilter{Path: []string{"metadata", "name"}, Value: "foo"}},
Outputs: []kio.Writer{kio.ByteWriter{Writer: out}},
}.Execute()
if !assert.NoError(t, err) {
t.FailNow()
}
if !assert.Equal(t, `kind: Deployment
metadata:
labels:
app: nginx2
name: foo
annotations:
app: nginx2
spec:
replicas: 1
---
kind: Service
metadata:
name: foo
annotations:
app: nginx
spec:
selector:
app: nginx
`, out.String()) {
t.FailNow()
}
out = &bytes.Buffer{}
err = kio.Pipeline{
Inputs: []kio.Reader{&kio.ByteReader{Reader: bytes.NewBufferString(in)}},
Filters: []kio.Filter{GrepFilter{Path: []string{"kind"}, Value: "Deployment"}},
Outputs: []kio.Writer{kio.ByteWriter{Writer: out}},
}.Execute()
if !assert.NoError(t, err) {
t.FailNow()
}
if !assert.Equal(t, `kind: Deployment
metadata:
labels:
app: nginx2
name: foo
annotations:
app: nginx2
spec:
replicas: 1
---
kind: Deployment
metadata:
labels:
app: nginx
annotations:
app: nginx
name: bar
spec:
replicas: 3
`, out.String()) {
t.FailNow()
}
out = &bytes.Buffer{}
err = kio.Pipeline{
Inputs: []kio.Reader{&kio.ByteReader{Reader: bytes.NewBufferString(in)}},
Filters: []kio.Filter{GrepFilter{Path: []string{"spec", "replicas"}, Value: "3"}},
Outputs: []kio.Writer{kio.ByteWriter{Writer: out}},
}.Execute()
if !assert.NoError(t, err) {
t.FailNow()
}
if !assert.Equal(t, `kind: Deployment
metadata:
labels:
app: nginx
annotations:
app: nginx
name: bar
spec:
replicas: 3
`, out.String()) {
t.FailNow()
}
out = &bytes.Buffer{}
err = kio.Pipeline{
Inputs: []kio.Reader{&kio.ByteReader{Reader: bytes.NewBufferString(in)}},
Filters: []kio.Filter{GrepFilter{Path: []string{"spec", "not-present"}, Value: "3"}},
Outputs: []kio.Writer{kio.ByteWriter{Writer: out}},
}.Execute()
if !assert.NoError(t, err) {
t.FailNow()
}
if !assert.Equal(t, ``, out.String()) {
t.FailNow()
}
}
func TestGrepFilter_init(t *testing.T) {
assert.Equal(t, GrepFilter{}, Filters["GrepFilter"]())
}
func TestGrepFilter_error(t *testing.T) {
v, err := GrepFilter{Path: []string{"metadata", "name"},
Value: "foo"}.Filter([]*yaml.RNode{{}})
if !assert.NoError(t, err) {
t.FailNow()
}
assert.Nil(t, v)
}

View File

@@ -0,0 +1,76 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
// Package merge contains libraries for merging Resources and Patches
package filters
import (
"sigs.k8s.io/kustomize/kyaml/yaml"
"sigs.k8s.io/kustomize/kyaml/yaml/merge2"
)
// GrepFilter merges Resources with the Group/Version/Kind/Namespace/Name together using
// a 2-way merge strategy.
//
// - Fields set to null in the source will be cleared from the destination
// - Fields with matching keys will be merged recursively
// - Lists with an associative key (e.g. name) will have their elements merged using the key
// - List without an associative key will have the dest list replaced by the source list
type MergeFilter struct {
Reverse bool
}
type mergeKey struct {
apiVersion string
kind string
namespace string
name string
}
// GrepFilter implements kio.GrepFilter by merge Resources with the same G/V/K/NS/N
func (c MergeFilter) Filter(input []*yaml.RNode) ([]*yaml.RNode, error) {
// invert the merge precedence
if c.Reverse {
for i, j := 0, len(input)-1; i < j; i, j = i+1, j-1 {
input[i], input[j] = input[j], input[i]
}
}
// index the Resources by G/V/K/NS/N
index := map[mergeKey][]*yaml.RNode{}
for i := range input {
meta, err := input[i].GetMeta()
if err != nil {
return nil, err
}
key := mergeKey{
apiVersion: meta.ApiVersion,
kind: meta.Kind,
namespace: meta.Namespace,
name: meta.Name,
}
index[key] = append(index[key], input[i])
}
// merge each of the G/V/K/NS/N lists
var output []*yaml.RNode
var err error
for k := range index {
var merged *yaml.RNode
resources := index[k]
for i := range resources {
patch := resources[i]
if merged == nil {
// first resources, don't merge it
merged = resources[i]
} else {
merged, err = merge2.Merge(patch, merged)
if err != nil {
return nil, err
}
}
}
output = append(output, merged)
}
return output, nil
}

167
kyaml/kio/filters/merge3.go Normal file
View File

@@ -0,0 +1,167 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package filters
import (
"fmt"
"sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/yaml"
"sigs.k8s.io/kustomize/kyaml/yaml/merge3"
)
const (
mergeSourceAnnotation = "kyaml.kustomize.dev/merge-source"
mergeSourceOriginal = "original"
mergeSourceUpdated = "updated"
mergeSourceDest = "dest"
)
// Merge3 performs a 3-way merge on the original, updated, and destination packages.
type Merge3 struct {
OriginalPath string
UpdatedPath string
DestPath string
MatchFilesGlob []string
}
func (m Merge3) Merge() error {
// Read the destination package. The ReadWriter will take take of deleting files
// for removed resources.
var inputs []kio.Reader
dest := &kio.LocalPackageReadWriter{
PackagePath: m.DestPath,
MatchFilesGlob: m.MatchFilesGlob,
SetAnnotations: map[string]string{mergeSourceAnnotation: mergeSourceDest},
}
inputs = append(inputs, dest)
// Read the original package
inputs = append(inputs, kio.LocalPackageReader{
PackagePath: m.OriginalPath,
MatchFilesGlob: m.MatchFilesGlob,
SetAnnotations: map[string]string{mergeSourceAnnotation: mergeSourceOriginal},
})
// Read the updated package
inputs = append(inputs, kio.LocalPackageReader{
PackagePath: m.UpdatedPath,
MatchFilesGlob: m.MatchFilesGlob,
SetAnnotations: map[string]string{mergeSourceAnnotation: mergeSourceUpdated},
})
return kio.Pipeline{
Inputs: inputs,
Filters: []kio.Filter{m, FormatFilter{}}, // format the merged output
Outputs: []kio.Writer{dest},
}.Execute()
}
// Filter combines Resources with the same GVK + N + NS into tuples, and then merges them
func (m Merge3) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
// index the nodes by their identity
tl := tuples{}
for i := range nodes {
if err := tl.add(nodes[i]); err != nil {
return nil, err
}
}
// iterate over the inputs, merging as needed
var output []*yaml.RNode
for i := range tl.list {
t := tl.list[i]
switch {
case t.original == nil && t.updated == nil && t.dest != nil:
// added locally -- keep dest
output = append(output, t.dest)
case t.original == nil && t.updated != nil && t.dest == nil:
// added in the update -- add update
output = append(output, t.updated)
case t.original != nil && t.updated == nil:
// deleted in the update
// don't include the resource in the output
case t.original != nil && t.dest == nil:
// deleted locally
// don't include the resource in the output
default:
// dest and updated are non-nil -- merge them
node, err := t.merge()
if err != nil {
return nil, err
}
if node != nil {
output = append(output, node)
}
}
}
return output, nil
}
// tuples combines nodes with the same GVK + N + NS
type tuples struct {
list []*tuple
}
// add adds a node to the list, combining it with an existing matching Resource if found
func (ts *tuples) add(node *yaml.RNode) error {
nodeMeta, err := node.GetMeta()
if err != nil {
return err
}
for i := range ts.list {
t := ts.list[i]
if t.meta.Name == nodeMeta.Name && t.meta.Namespace == nodeMeta.Namespace &&
t.meta.ApiVersion == nodeMeta.ApiVersion && t.meta.Kind == nodeMeta.Kind {
return t.add(node)
}
}
t := &tuple{meta: nodeMeta}
if err := t.add(node); err != nil {
return err
}
ts.list = append(ts.list, t)
return nil
}
// tuple wraps an original, updated, and dest tuple for a given Resource
type tuple struct {
meta yaml.ResourceMeta
original *yaml.RNode
updated *yaml.RNode
dest *yaml.RNode
}
// add sets the corresponding tuple field for the node
func (t *tuple) add(node *yaml.RNode) error {
meta, err := node.GetMeta()
if err != nil {
return err
}
switch meta.Annotations[mergeSourceAnnotation] {
case mergeSourceDest:
if t.dest != nil {
return fmt.Errorf("dest source already specified")
}
t.dest = node
case mergeSourceOriginal:
if t.original != nil {
return fmt.Errorf("original source already specified")
}
t.original = node
case mergeSourceUpdated:
if t.updated != nil {
return fmt.Errorf("updated source already specified")
}
t.updated = node
default:
return fmt.Errorf("no source annotation for Resource")
}
return nil
}
// merge performs a 3-way merge on the tuple
func (t *tuple) merge() (*yaml.RNode, error) {
return merge3.Merge(t.dest, t.original, t.updated)
}

View File

@@ -0,0 +1,56 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package filters_test
import (
"io/ioutil"
"os"
"path/filepath"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/kustomize/kyaml/copyutil"
"sigs.k8s.io/kustomize/kyaml/kio/filters"
)
func TestMerge3_Merge(t *testing.T) {
_, datadir, _, ok := runtime.Caller(0)
if !assert.True(t, ok) {
t.FailNow()
}
datadir = filepath.Join(filepath.Dir(datadir), "testdata")
// setup the local directory
dir, err := ioutil.TempDir("", "kyaml-test")
if !assert.NoError(t, err) {
t.FailNow()
}
defer os.RemoveAll(dir)
if !assert.NoError(t, copyutil.CopyDir(
filepath.Join(datadir, "dataset1-localupdates"),
filepath.Join(dir, "dataset1"))) {
t.FailNow()
}
err = filters.Merge3{
OriginalPath: filepath.Join(datadir, "dataset1"),
UpdatedPath: filepath.Join(datadir, "dataset1-remoteupdates"),
DestPath: filepath.Join(dir, "dataset1"),
}.Merge()
if !assert.NoError(t, err) {
t.FailNow()
}
diffs, err := copyutil.Diff(
filepath.Join(dir, "dataset1"),
filepath.Join(datadir, "dataset1-expected"))
if !assert.NoError(t, err) {
t.FailNow()
}
if !assert.Empty(t, diffs.List()) {
t.FailNow()
}
}

View File

@@ -0,0 +1,4 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package filters

View File

@@ -0,0 +1,43 @@
// Copyright 2019 Google LLC
//
// 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 filters
import (
"sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
type StripCommentsFilter struct{}
var _ kio.Filter = StripCommentsFilter{}
func (f StripCommentsFilter) Filter(slice []*yaml.RNode) ([]*yaml.RNode, error) {
for i := range slice {
stripComments(slice[i].YNode())
}
return slice, nil
}
func stripComments(node *yaml.Node) {
if node == nil {
return
}
node.HeadComment = ""
node.LineComment = ""
node.FootComment = ""
for i := range node.Content {
stripComments(node.Content[i])
}
}

View File

@@ -0,0 +1,11 @@
# Copyright 2019 The Kubernetes Authors.
# SPDX-License-Identifier: Apache-2.0
#
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
labels:
app.kubernetes.io/component: undefined
app.kubernetes.io/instance: undefined
data: {}

View File

@@ -0,0 +1,43 @@
# Copyright 2019 The Kubernetes Authors.
# SPDX-License-Identifier: Apache-2.0
#
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
labels:
app: java
new-local: label
new-remote: label
spec:
replicas: 3
selector:
matchLabels:
app: java
template:
metadata:
labels:
app: java
spec:
restartPolicy: Always
containers:
- name: app
image: gcr.io/project/app:version
command:
- java
- -jar
- /app.jar
- otherstuff
args:
- foo
ports:
- containerPort: 8080
envFrom:
- configMapRef:
name: app-config
env:
- name: JAVA_OPTS
value: -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap
-Djava.security.egd=file:/dev/./urandom
imagePullPolicy: Always
minReadySeconds: 20

View File

@@ -0,0 +1,16 @@
# Copyright 2019 The Kubernetes Authors.
# SPDX-License-Identifier: Apache-2.0
#
apiVersion: v1
kind: Service
metadata:
name: app
labels:
app: java
spec:
selector:
app: java
ports:
- name: "8080"
port: 8080
targetPort: 8080

View File

@@ -0,0 +1,23 @@
# Copyright 2019 The Kubernetes Authors.
# SPDX-License-Identifier: Apache-2.0
#
apiVersion: v1
kind: ConfigMap
metadata:
name: mysql
labels:
app: mysql
annotations:
file/index: "0"
file/path: mysql-configmap.resource.yaml
package/name: mysql
package/original-name: mysql
data:
master.cnf: |
# Apply this config only on the master.
[mysqld]
log-bin
slave.cnf: |
# Apply this config only on slaves.
[mysqld]
super-read-only

View File

@@ -0,0 +1,29 @@
# Copyright 2019 The Kubernetes Authors.
# SPDX-License-Identifier: Apache-2.0
#
apiVersion: v1
kind: Service
metadata:
name: mysql
labels:
app: mysql
spec:
selector:
app: mysql
ports:
- name: mysql
port: 3306
clusterIP: None
---
apiVersion: v1
kind: Service
metadata:
name: mysql-read
labels:
app: mysql
spec:
selector:
app: mysql
ports:
- name: mysql
port: 3306

View File

@@ -0,0 +1,173 @@
# Copyright 2019 The Kubernetes Authors.
# SPDX-License-Identifier: Apache-2.0
#
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
replicas: 3
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
initContainers:
- name: init-mysql
image: mysql:5.7
command:
- bash
- -c
- |
set -ex
# Generate mysql server-id from pod ordinal index.
[[ `hostname` =~ -([0-9]+)$ ]] || exit 1
ordinal=${BASH_REMATCH[1]}
echo [mysqld] > /mnt/conf.d/server-id.cnf
# Add an offset to avoid reserved server-id=0 value.
echo server-id=$((100 + $ordinal)) >> /mnt/conf.d/server-id.cnf
# Copy appropriate conf.d files from config-map to emptyDir.
if [[ $ordinal -eq 0 ]]; then
cp /mnt/config-map/master.cnf /mnt/conf.d/
else
cp /mnt/config-map/slave.cnf /mnt/conf.d/
fi
volumeMounts:
- name: conf
mountPath: /mnt/conf.d
- name: config-map
mountPath: /mnt/config-map
- name: clone-mysql
image: gcr.io/google-samples/xtrabackup:1.0
command:
- bash
- -c
- |
set -ex
# Skip the clone if data already exists.
[[ -d /var/lib/mysql/mysql ]] && exit 0
# Skip the clone on master (ordinal index 0).
[[ `hostname` =~ -([0-9]+)$ ]] || exit 1
ordinal=${BASH_REMATCH[1]}
[[ $ordinal -eq 0 ]] && exit 0
# Clone data from previous peer.
ncat --recv-only mysql-$(($ordinal-1)).mysql 3307 | xbstream -x -C /var/lib/mysql
# Prepare the backup.
xtrabackup --prepare --target-dir=/var/lib/mysql
volumeMounts:
- name: data
mountPath: /var/lib/mysql
subPath: mysql
- name: conf
mountPath: /etc/mysql/conf.d
containers:
- name: mysql
image: mysql:5.7
ports:
- name: mysql
containerPort: 3306
env:
- name: MYSQL_ALLOW_EMPTY_PASSWORD
value: "1"
resources:
requests:
cpu: 500m
memory: 1Gi
volumeMounts:
- name: data
mountPath: /var/lib/mysql
subPath: mysql
- name: conf
mountPath: /etc/mysql/conf.d
livenessProbe:
exec:
command:
- mysqladmin
- ping
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
readinessProbe:
exec:
command:
- mysql
- -h
- 127.0.0.1
- -e
- SELECT 1
initialDelaySeconds: 5
periodSeconds: 2
timeoutSeconds: 1
- name: xtrabackup
image: gcr.io/google-samples/xtrabackup:1.0
command:
- bash
- -c
- |
set -ex
cd /var/lib/mysql
# Determine binlog position of cloned data, if any.
if [[ -f xtrabackup_slave_info ]]; then
# XtraBackup already generated a partial "CHANGE MASTER TO" query
# because we're cloning from an existing slave.
mv xtrabackup_slave_info change_master_to.sql.in
# Ignore xtrabackup_binlog_info in this case (it's useless).
rm -f xtrabackup_binlog_info
elif [[ -f xtrabackup_binlog_info ]]; then
# We're cloning directly from master. Parse binlog position.
[[ `cat xtrabackup_binlog_info` =~ ^(.*?)[[:space:]]+(.*?)$ ]] || exit 1
rm xtrabackup_binlog_info
echo "CHANGE MASTER TO MASTER_LOG_FILE='${BASH_REMATCH[1]}',\
MASTER_LOG_POS=${BASH_REMATCH[2]}" > change_master_to.sql.in
fi
# Check if we need to complete a clone by starting replication.
if [[ -f change_master_to.sql.in ]]; then
echo "Waiting for mysqld to be ready (accepting connections)"
until mysql -h 127.0.0.1 -e "SELECT 1"; do sleep 1; done
echo "Initializing replication from clone position"
# In case of container restart, attempt this at-most-once.
mv change_master_to.sql.in change_master_to.sql.orig
mysql -h 127.0.0.1 <<EOF
$(<change_master_to.sql.orig),
MASTER_HOST='mysql-0.mysql',
MASTER_USER='root',
MASTER_PASSWORD='',
MASTER_CONNECT_RETRY=10;
START SLAVE;
EOF
fi
# Start a server to send backups when requested by peers.
exec ncat --listen --keep-open --send-only --max-conns=1 3307 -c \
"xtrabackup --backup --slave-info --stream=xbstream --host=127.0.0.1 --user=root"
ports:
- name: xtrabackup
containerPort: 3307
resources:
requests:
cpu: 100m
memory: 100Mi
volumeMounts:
- name: data
mountPath: /var/lib/mysql
subPath: mysql
- name: conf
mountPath: /etc/mysql/conf.d
volumes:
- name: conf
emptyDir: {}
- name: config-map
configMap:
name: mysql
volumeClaimTemplates:
- metadata:
name: data
spec:
resources:
requests:
storage: 10Gi
accessModes:
- ReadWriteOnce
serviceName: mysql

View File

@@ -0,0 +1,31 @@
# Copyright 2019 The Kubernetes Authors.
# SPDX-License-Identifier: Apache-2.0
#
apiVersion: v1
kind: Service
metadata:
name: wordpress
labels:
app: wordpress
spec:
type: NodePort
selector:
app: wordpress
tier: frontend
ports:
- port: 80
nodePort: 30000
---
apiVersion: v1
kind: Service
metadata:
name: wordpress-identity
labels:
app: wordpress-identity
spec:
selector:
app: wordpress
tier: frontend
ports:
- port: 80
nodePort: 30000

View File

@@ -0,0 +1,42 @@
# Copyright 2019 The Kubernetes Authors.
# SPDX-License-Identifier: Apache-2.0
#
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: wordpress
labels:
app: wordpress
spec:
selector:
matchLabels:
app: wordpress
tier: frontend
template:
metadata:
labels:
app: wordpress
tier: frontend
spec:
containers:
- name: wordpress
image: buddy/wordpress:latest
ports:
- name: wordpress
containerPort: 80
env:
- name: WORDPRESS_DB_HOST
value: wordpress-mysql
- name: WORDPRESS_DB_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-pass
key: password
volumeMounts:
- name: wordpress-persistent-storage
mountPath: /var/www/html
volumes:
- name: wordpress-persistent-storage
persistentVolumeClaim:
claimName: wp-pv-claim
serviceName: wordpress-identity

View File

@@ -0,0 +1,11 @@
# Copyright 2019 The Kubernetes Authors.
# SPDX-License-Identifier: Apache-2.0
#
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
labels:
app.kubernetes.io/component: undefined
app.kubernetes.io/instance: undefined
data: {}

View File

@@ -0,0 +1,41 @@
# Copyright 2019 The Kubernetes Authors.
# SPDX-License-Identifier: Apache-2.0
#
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
labels:
app: java
new-local: label
spec:
replicas: 2
selector:
matchLabels:
app: java
template:
metadata:
labels:
app: java
spec:
restartPolicy: Always
containers:
- name: app
image: gcr.io/project/app:version
command:
- java
- -jar
- /app.jar
args:
- foo
ports:
- containerPort: 8080
envFrom:
- configMapRef:
name: app-config
env:
- name: JAVA_OPTS
value: -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap
-Djava.security.egd=file:/dev/./urandom
imagePullPolicy: Always
minReadySeconds: 20

View File

@@ -0,0 +1,16 @@
# Copyright 2019 The Kubernetes Authors.
# SPDX-License-Identifier: Apache-2.0
#
apiVersion: v1
kind: Service
metadata:
name: app
labels:
app: java
spec:
selector:
app: java
ports:
- name: "8080"
port: 8080
targetPort: 8080

View File

@@ -0,0 +1,31 @@
# Copyright 2019 The Kubernetes Authors.
# SPDX-License-Identifier: Apache-2.0
#
apiVersion: v1
kind: Service
metadata:
name: wordpress
labels:
app: wordpress
spec:
type: NodePort
selector:
app: wordpress
tier: frontend
ports:
- port: 80
nodePort: 30000
---
apiVersion: v1
kind: Service
metadata:
name: wordpress-identity
labels:
app: wordpress-identity
spec:
selector:
app: wordpress
tier: frontend
ports:
- port: 80
nodePort: 30000

View File

@@ -0,0 +1,42 @@
# Copyright 2019 The Kubernetes Authors.
# SPDX-License-Identifier: Apache-2.0
#
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: wordpress
labels:
app: wordpress
spec:
selector:
matchLabels:
app: wordpress
tier: frontend
template:
metadata:
labels:
app: wordpress
tier: frontend
spec:
containers:
- name: wordpress
image: buddy/wordpress:latest
ports:
- name: wordpress
containerPort: 80
env:
- name: WORDPRESS_DB_HOST
value: wordpress-mysql
- name: WORDPRESS_DB_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-pass
key: password
volumeMounts:
- name: wordpress-persistent-storage
mountPath: /var/www/html
volumes:
- name: wordpress-persistent-storage
persistentVolumeClaim:
claimName: wp-pv-claim
serviceName: wordpress-identity

View File

@@ -0,0 +1,11 @@
# Copyright 2019 The Kubernetes Authors.
# SPDX-License-Identifier: Apache-2.0
#
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
labels:
app.kubernetes.io/component: undefined
app.kubernetes.io/instance: undefined
data: {}

View File

@@ -0,0 +1,40 @@
# Copyright 2019 The Kubernetes Authors.
# SPDX-License-Identifier: Apache-2.0
#
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
labels:
app: java
new-remote: label
spec:
replicas: 3
selector:
matchLabels:
app: java
template:
metadata:
labels:
app: java
spec:
restartPolicy: Always
containers:
- name: app
image: gcr.io/project/app:version
command:
- java
- -jar
- /app.jar
- otherstuff
ports:
- containerPort: 8080
envFrom:
- configMapRef:
name: app-config
env:
- name: JAVA_OPTS
value: -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap
-Djava.security.egd=file:/dev/./urandom
imagePullPolicy: Always
minReadySeconds: 5

View File

@@ -0,0 +1,16 @@
# Copyright 2019 The Kubernetes Authors.
# SPDX-License-Identifier: Apache-2.0
#
apiVersion: v1
kind: Service
metadata:
name: app
labels:
app: java
spec:
selector:
app: java
ports:
- name: "8080"
port: 8080
targetPort: 8080

View File

@@ -0,0 +1,23 @@
# Copyright 2019 The Kubernetes Authors.
# SPDX-License-Identifier: Apache-2.0
#
apiVersion: v1
kind: ConfigMap
metadata:
name: mysql
labels:
app: mysql
annotations:
file/index: "0"
file/path: mysql-configmap.resource.yaml
package/name: mysql
package/original-name: mysql
data:
master.cnf: |
# Apply this config only on the master.
[mysqld]
log-bin
slave.cnf: |
# Apply this config only on slaves.
[mysqld]
super-read-only

View File

@@ -0,0 +1,29 @@
# Copyright 2019 The Kubernetes Authors.
# SPDX-License-Identifier: Apache-2.0
#
apiVersion: v1
kind: Service
metadata:
name: mysql
labels:
app: mysql
spec:
selector:
app: mysql
ports:
- name: mysql
port: 3306
clusterIP: None
---
apiVersion: v1
kind: Service
metadata:
name: mysql-read
labels:
app: mysql
spec:
selector:
app: mysql
ports:
- name: mysql
port: 3306

View File

@@ -0,0 +1,173 @@
# Copyright 2019 The Kubernetes Authors.
# SPDX-License-Identifier: Apache-2.0
#
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
replicas: 3
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
initContainers:
- name: init-mysql
image: mysql:5.7
command:
- bash
- -c
- |
set -ex
# Generate mysql server-id from pod ordinal index.
[[ `hostname` =~ -([0-9]+)$ ]] || exit 1
ordinal=${BASH_REMATCH[1]}
echo [mysqld] > /mnt/conf.d/server-id.cnf
# Add an offset to avoid reserved server-id=0 value.
echo server-id=$((100 + $ordinal)) >> /mnt/conf.d/server-id.cnf
# Copy appropriate conf.d files from config-map to emptyDir.
if [[ $ordinal -eq 0 ]]; then
cp /mnt/config-map/master.cnf /mnt/conf.d/
else
cp /mnt/config-map/slave.cnf /mnt/conf.d/
fi
volumeMounts:
- name: conf
mountPath: /mnt/conf.d
- name: config-map
mountPath: /mnt/config-map
- name: clone-mysql
image: gcr.io/google-samples/xtrabackup:1.0
command:
- bash
- -c
- |
set -ex
# Skip the clone if data already exists.
[[ -d /var/lib/mysql/mysql ]] && exit 0
# Skip the clone on master (ordinal index 0).
[[ `hostname` =~ -([0-9]+)$ ]] || exit 1
ordinal=${BASH_REMATCH[1]}
[[ $ordinal -eq 0 ]] && exit 0
# Clone data from previous peer.
ncat --recv-only mysql-$(($ordinal-1)).mysql 3307 | xbstream -x -C /var/lib/mysql
# Prepare the backup.
xtrabackup --prepare --target-dir=/var/lib/mysql
volumeMounts:
- name: data
mountPath: /var/lib/mysql
subPath: mysql
- name: conf
mountPath: /etc/mysql/conf.d
containers:
- name: mysql
image: mysql:5.7
ports:
- name: mysql
containerPort: 3306
env:
- name: MYSQL_ALLOW_EMPTY_PASSWORD
value: "1"
resources:
requests:
cpu: 500m
memory: 1Gi
volumeMounts:
- name: data
mountPath: /var/lib/mysql
subPath: mysql
- name: conf
mountPath: /etc/mysql/conf.d
livenessProbe:
exec:
command:
- mysqladmin
- ping
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
readinessProbe:
exec:
command:
- mysql
- -h
- 127.0.0.1
- -e
- SELECT 1
initialDelaySeconds: 5
periodSeconds: 2
timeoutSeconds: 1
- name: xtrabackup
image: gcr.io/google-samples/xtrabackup:1.0
command:
- bash
- -c
- |
set -ex
cd /var/lib/mysql
# Determine binlog position of cloned data, if any.
if [[ -f xtrabackup_slave_info ]]; then
# XtraBackup already generated a partial "CHANGE MASTER TO" query
# because we're cloning from an existing slave.
mv xtrabackup_slave_info change_master_to.sql.in
# Ignore xtrabackup_binlog_info in this case (it's useless).
rm -f xtrabackup_binlog_info
elif [[ -f xtrabackup_binlog_info ]]; then
# We're cloning directly from master. Parse binlog position.
[[ `cat xtrabackup_binlog_info` =~ ^(.*?)[[:space:]]+(.*?)$ ]] || exit 1
rm xtrabackup_binlog_info
echo "CHANGE MASTER TO MASTER_LOG_FILE='${BASH_REMATCH[1]}',\
MASTER_LOG_POS=${BASH_REMATCH[2]}" > change_master_to.sql.in
fi
# Check if we need to complete a clone by starting replication.
if [[ -f change_master_to.sql.in ]]; then
echo "Waiting for mysqld to be ready (accepting connections)"
until mysql -h 127.0.0.1 -e "SELECT 1"; do sleep 1; done
echo "Initializing replication from clone position"
# In case of container restart, attempt this at-most-once.
mv change_master_to.sql.in change_master_to.sql.orig
mysql -h 127.0.0.1 <<EOF
$(<change_master_to.sql.orig),
MASTER_HOST='mysql-0.mysql',
MASTER_USER='root',
MASTER_PASSWORD='',
MASTER_CONNECT_RETRY=10;
START SLAVE;
EOF
fi
# Start a server to send backups when requested by peers.
exec ncat --listen --keep-open --send-only --max-conns=1 3307 -c \
"xtrabackup --backup --slave-info --stream=xbstream --host=127.0.0.1 --user=root"
ports:
- name: xtrabackup
containerPort: 3307
resources:
requests:
cpu: 100m
memory: 100Mi
volumeMounts:
- name: data
mountPath: /var/lib/mysql
subPath: mysql
- name: conf
mountPath: /etc/mysql/conf.d
volumes:
- name: conf
emptyDir: {}
- name: config-map
configMap:
name: mysql
volumeClaimTemplates:
- metadata:
name: data
spec:
resources:
requests:
storage: 10Gi
accessModes:
- ReadWriteOnce
serviceName: mysql

View File

@@ -0,0 +1,11 @@
# Copyright 2019 The Kubernetes Authors.
# SPDX-License-Identifier: Apache-2.0
#
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
labels:
app.kubernetes.io/component: undefined
app.kubernetes.io/instance: undefined
data: {}

View File

@@ -0,0 +1,38 @@
# Copyright 2019 The Kubernetes Authors.
# SPDX-License-Identifier: Apache-2.0
#
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
labels:
app: java
spec:
replicas: 1
selector:
matchLabels:
app: java
template:
metadata:
labels:
app: java
spec:
restartPolicy: Always
containers:
- name: app
image: gcr.io/project/app:version
command:
- java
- -jar
- /app.jar
ports:
- containerPort: 8080
envFrom:
- configMapRef:
name: app-config
env:
- name: JAVA_OPTS
value: -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap
-Djava.security.egd=file:/dev/./urandom
imagePullPolicy: Always
minReadySeconds: 5

View File

@@ -0,0 +1,16 @@
# Copyright 2019 The Kubernetes Authors.
# SPDX-License-Identifier: Apache-2.0
#
apiVersion: v1
kind: Service
metadata:
name: app
labels:
app: java
spec:
selector:
app: java
ports:
- name: "8080"
port: 8080
targetPort: 8080

View File

@@ -0,0 +1,11 @@
# Copyright 2019 The Kubernetes Authors.
# SPDX-License-Identifier: Apache-2.0
#
apiVersion: v1
kind: ConfigMap
metadata:
name: rails-app-config
labels:
app.kubernetes.io/component: undefined
app.kubernetes.io/instance: undefined
data: {}

View File

@@ -0,0 +1,36 @@
# Copyright 2019 The Kubernetes Authors.
# SPDX-License-Identifier: Apache-2.0
#
apiVersion: apps/v1
kind: Deployment
metadata:
name: rails-app
labels:
app: rails
spec:
replicas: 1
selector:
matchLabels:
app: rails
template:
metadata:
labels:
app: rails
spec:
restartPolicy: Always
containers:
- name: rails
image: gcr.io/project/app:version
command:
- rails
ports:
- containerPort: 8080
envFrom:
- configMapRef:
name: rails-app-config
env:
- name: JAVA_OPTS
value: -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap
-Djava.security.egd=file:/dev/./urandom
imagePullPolicy: Always
minReadySeconds: 5

View File

@@ -0,0 +1,16 @@
# Copyright 2019 The Kubernetes Authors.
# SPDX-License-Identifier: Apache-2.0
#
apiVersion: v1
kind: Service
metadata:
name: rails-app
labels:
app: rails
spec:
selector:
app: rails
ports:
- name: "8080"
port: 8080
targetPort: 8080

View File

@@ -0,0 +1,57 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
// Package testyaml contains test data and libraries for formatting
// Kubernetes configuration
package testyaml
var UnformattedYaml1 = []byte(`
spec: a
status:
conditions:
- 3
- 1
- 2
apiVersion: example.com/v1beta1
kind: MyType
`)
var UnformattedYaml2 = []byte(`
spec2: a
status2:
conditions:
- 3
- 1
- 2
apiVersion: example.com/v1beta1
kind: MyType2
`)
var UnformattedJson1 = []byte(`
{
"spec": "a",
"status": {"conditions": [3, 1, 2]},
"apiVersion": "example.com/v1beta1",
"kind": "MyType"
}
`)
var FormattedYaml1 = []byte(`apiVersion: example.com/v1beta1
kind: MyType
spec: a
status:
conditions:
- 3
- 1
- 2
`)
var FormattedYaml2 = []byte(`apiVersion: example.com/v1beta1
kind: MyType2
spec2: a
status2:
conditions:
- 3
- 1
- 2
`)