Add create subcommand

This commit is contained in:
Richard Marshall
2019-07-29 21:49:32 -07:00
parent e904f612f3
commit 5dfa929906
6 changed files with 450 additions and 0 deletions

View File

@@ -13,6 +13,7 @@ import (
"sigs.k8s.io/kustomize/v3/k8sdeps/transformer" "sigs.k8s.io/kustomize/v3/k8sdeps/transformer"
"sigs.k8s.io/kustomize/v3/k8sdeps/validator" "sigs.k8s.io/kustomize/v3/k8sdeps/validator"
"sigs.k8s.io/kustomize/v3/pkg/commands/build" "sigs.k8s.io/kustomize/v3/pkg/commands/build"
"sigs.k8s.io/kustomize/v3/pkg/commands/create"
"sigs.k8s.io/kustomize/v3/pkg/commands/edit" "sigs.k8s.io/kustomize/v3/pkg/commands/edit"
"sigs.k8s.io/kustomize/v3/pkg/commands/misc" "sigs.k8s.io/kustomize/v3/pkg/commands/misc"
"sigs.k8s.io/kustomize/v3/pkg/fs" "sigs.k8s.io/kustomize/v3/pkg/fs"
@@ -44,6 +45,7 @@ See https://sigs.k8s.io/kustomize
stdOut, fSys, v, stdOut, fSys, v,
rf, pf), rf, pf),
edit.NewCmdEdit(fSys, v, uf), edit.NewCmdEdit(fSys, v, uf),
create.NewCmdCreate(fSys),
misc.NewCmdConfig(fSys), misc.NewCmdConfig(fSys),
misc.NewCmdVersion(stdOut), misc.NewCmdVersion(stdOut),
) )

View File

@@ -0,0 +1,181 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package create
import (
"fmt"
"os"
"path/filepath"
"github.com/spf13/cobra"
"sigs.k8s.io/kustomize/v3/k8sdeps/kunstruct"
"sigs.k8s.io/kustomize/v3/pkg/commands/kustfile"
"sigs.k8s.io/kustomize/v3/pkg/commands/util"
"sigs.k8s.io/kustomize/v3/pkg/fs"
"sigs.k8s.io/kustomize/v3/pkg/pgmconfig"
)
type createFlags struct {
resources []string
namespace string
annotations string
labels string
prefix string
suffix string
detectResources bool
detectRecursive bool
path string
}
// NewCmdCreate returns an instance of 'create' subcommand.
func NewCmdCreate(fSys fs.FileSystem) *cobra.Command {
opts := createFlags{path: "."}
c := &cobra.Command{
Use: "create",
Short: "Create a new kustomization in the current directory",
Long: "",
Example: `
# Create a new overlay from the base '../base".
kustomize create --resource ../base
# Create a new kustomization detecting resources in the current directory.
kustomize create --autodetect
# Create a new kustomization with multiple resources and fields set.
kustomize create --resource depoyment.yaml --resource service.yaml --namespace staging --nameprefix acme-
`,
RunE: func(cmd *cobra.Command, args []string) error {
return runCreate(opts, fSys)
},
}
c.Flags().StringSliceVar(
&opts.resources,
"resource",
[]string{},
"Name of a file containing a file to add to the kustomization file.")
c.Flags().StringVar(
&opts.namespace,
"namespace",
"",
"Set the value of the namespace field in the customization file.")
c.Flags().StringVar(
&opts.annotations,
"annotation",
"",
"Add one or more common annotations.")
c.Flags().StringVar(
&opts.labels,
"label",
"",
"Add one or more common labels.")
c.Flags().StringVar(
&opts.prefix,
"nameprefix",
"",
"Sets the value of the namePrefix field in the kustomization file.")
c.Flags().StringVar(
&opts.suffix,
"namesuffix",
"",
"Sets the value of the nameSuffix field in the kustomization file.")
c.Flags().BoolVar(
&opts.detectResources,
"autodetect",
false,
"Search for kubernetes resources in the current directory to be added to the kustomization file.")
c.Flags().BoolVar(
&opts.detectRecursive,
"recursive",
false,
"Enable recursive directory searching for resource auto-detection.")
return c
}
func runCreate(opts createFlags, fSys fs.FileSystem) error {
resources, err := util.GlobPatterns(fSys, opts.resources)
if err != nil {
return err
}
if _, err := kustfile.NewKustomizationFile(fSys); err == nil {
return fmt.Errorf("kustomization file already exists")
}
if opts.detectResources {
detected, err := detectResources(fSys, opts.path, opts.detectRecursive)
if err != nil {
return err
}
for _, resource := range detected {
if kustfile.StringInSlice(resource, resources) {
continue
}
resources = append(resources, resource)
}
}
f, err := fSys.Create("kustomization.yaml")
if err != nil {
return err
}
f.Close()
mf, err := kustfile.NewKustomizationFile(fSys)
if err != nil {
return err
}
m, err := mf.Read()
if err != nil {
return err
}
m.Resources = resources
m.Namespace = opts.namespace
m.NamePrefix = opts.prefix
m.NameSuffix = opts.suffix
annotations, err := util.ConvertToMap(opts.annotations, "annotation")
if err != nil {
return err
}
m.CommonAnnotations = annotations
labels, err := util.ConvertToMap(opts.labels, "label")
if err != nil {
return err
}
m.CommonLabels = labels
return mf.Write(m)
}
func detectResources(fSys fs.FileSystem, base string, recursive bool) ([]string, error) {
var paths []string
factory := kunstruct.NewKunstructuredFactoryImpl()
err := fSys.Walk(base, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if path == base {
return nil
}
if info.IsDir() {
if !recursive {
return filepath.SkipDir
}
// If a sub-directory contains an existing kustomization file add the
// directory as a resource and do not decend into it.
for _, kfilename := range pgmconfig.KustomizationFileNames {
if fSys.Exists(filepath.Join(path, kfilename)) {
paths = append(paths, path)
return filepath.SkipDir
}
}
return nil
}
fContents, err := fSys.ReadFile(path)
if err != nil {
return err
}
if _, err := factory.SliceFromBytes(fContents); err != nil {
return nil
}
paths = append(paths, path)
return nil
})
return paths, err
}

View File

@@ -0,0 +1,179 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package create
import (
"reflect"
"testing"
"sigs.k8s.io/kustomize/v3/pkg/commands/kustfile"
"sigs.k8s.io/kustomize/v3/pkg/fs"
"sigs.k8s.io/kustomize/v3/pkg/types"
)
func readKustomizationFS(t *testing.T, fakeFS fs.FileSystem) *types.Kustomization {
kf, err := kustfile.NewKustomizationFile(fakeFS)
if err != nil {
t.Errorf("unexpected new error %v", err)
}
m, err := kf.Read()
if err != nil {
t.Errorf("unexpected read error %v", err)
}
return m
}
func TestCreateNoArgs(t *testing.T) {
fakeFS := fs.MakeFakeFS()
cmd := NewCmdCreate(fakeFS)
err := cmd.RunE(cmd, []string{})
if err != nil {
t.Errorf("unexpected cmd error: %v", err)
}
readKustomizationFS(t, fakeFS)
}
func TestCreateWithResources(t *testing.T) {
fakeFS := fs.MakeFakeFS()
fakeFS.WriteFile("foo.yaml", []byte(""))
opts := createFlags{resources: []string{"foo.yaml"}}
err := runCreate(opts, fakeFS)
if err != nil {
t.Errorf("unexpected cmd error: %v", err)
}
m := readKustomizationFS(t, fakeFS)
expected := []string{"foo.yaml"}
if !reflect.DeepEqual(m.Resources, expected) {
t.Fatalf("expected %+v but got %+v", expected, m.Resources)
}
}
func TestCreateWithNamespace(t *testing.T) {
fakeFS := fs.MakeFakeFS()
want := "foo"
opts := createFlags{namespace: want}
err := runCreate(opts, fakeFS)
if err != nil {
t.Errorf("unexpected cmd error: %v", err)
}
m := readKustomizationFS(t, fakeFS)
got := m.Namespace
if got != want {
t.Errorf("want: %s, got: %s", want, got)
}
}
func TestCreateWithLabels(t *testing.T) {
fakeFS := fs.MakeFakeFS()
opts := createFlags{labels: "foo:bar"}
err := runCreate(opts, fakeFS)
if err != nil {
t.Errorf("unexpected cmd error: %v", err)
}
m := readKustomizationFS(t, fakeFS)
expected := map[string]string{"foo": "bar"}
if !reflect.DeepEqual(m.CommonLabels, expected) {
t.Fatalf("expected %+v but got %+v", expected, m.CommonLabels)
}
}
func TestCreateWithAnnotations(t *testing.T) {
fakeFS := fs.MakeFakeFS()
opts := createFlags{annotations: "foo:bar"}
err := runCreate(opts, fakeFS)
if err != nil {
t.Errorf("unexpected cmd error: %v", err)
}
m := readKustomizationFS(t, fakeFS)
expected := map[string]string{"foo": "bar"}
if !reflect.DeepEqual(m.CommonAnnotations, expected) {
t.Fatalf("expected %+v but got %+v", expected, m.CommonAnnotations)
}
}
func TestCreateWithNamePrefix(t *testing.T) {
fakeFS := fs.MakeFakeFS()
want := "foo-"
opts := createFlags{prefix: want}
err := runCreate(opts, fakeFS)
if err != nil {
t.Errorf("unexpected cmd error: %v", err)
}
m := readKustomizationFS(t, fakeFS)
got := m.NamePrefix
if got != want {
t.Errorf("want: %s, got: %s", want, got)
}
}
func TestCreateWithNameSuffix(t *testing.T) {
fakeFS := fs.MakeFakeFS()
opts := createFlags{suffix: "-foo"}
err := runCreate(opts, fakeFS)
if err != nil {
t.Errorf("unexpected cmd error: %v", err)
}
m := readKustomizationFS(t, fakeFS)
if m.NameSuffix != "-foo" {
t.Errorf("want: -foo, got: %s", m.NameSuffix)
}
}
func writeDetectContent(fakeFS fs.FileSystem) {
fakeFS.WriteFile("/test.yaml", []byte(`
apiVersion: v1
kind: Service
metadata:
name: test`))
fakeFS.WriteFile("/README.md", []byte(`
# Not a k8s resource
This file is not a valid kubernetes object.`))
fakeFS.Mkdir("/sub")
fakeFS.WriteFile("/sub/test.yaml", []byte(`
apiVersion: v1
kind: Service
metadata:
name: test2`))
fakeFS.WriteFile("/sub/README.md", []byte(`
# Not a k8s resource
This file in a subdirectory is not a valid kubernetes object.`))
fakeFS.Mkdir("/overlay")
fakeFS.WriteFile("/overlay/test.yaml", []byte(`
apiVersion: v1
kind: Service
metadata:
name: test3`))
fakeFS.WriteFile("/overlay/kustomization.yaml", []byte(`
resources:
- test.yaml`))
}
func TestCreateWithDetect(t *testing.T) {
fakeFS := fs.MakeFakeFS()
writeDetectContent(fakeFS)
opts := createFlags{path: "/", detectResources: true}
err := runCreate(opts, fakeFS)
if err != nil {
t.Fatalf("unexpected cmd error: %v", err)
}
m := readKustomizationFS(t, fakeFS)
expected := []string{"/test.yaml"}
if !reflect.DeepEqual(m.Resources, expected) {
t.Fatalf("expected %+v but got %+v", expected, m.Resources)
}
}
func TestCreateWithDetectRecursive(t *testing.T) {
fakeFS := fs.MakeFakeFS()
writeDetectContent(fakeFS)
opts := createFlags{path: "/", detectResources: true, detectRecursive: true}
err := runCreate(opts, fakeFS)
if err != nil {
t.Fatalf("unexpected cmd error: %v", err)
}
m := readKustomizationFS(t, fakeFS)
expected := []string{"/overlay", "/sub/test.yaml", "/test.yaml"}
if !reflect.DeepEqual(m.Resources, expected) {
t.Fatalf("expected %+v but got %+v", expected, m.Resources)
}
}

View File

@@ -18,6 +18,7 @@ package fs
import ( import (
"fmt" "fmt"
"os"
"path/filepath" "path/filepath"
"sort" "sort"
"strings" "strings"
@@ -179,7 +180,87 @@ func (fs *fakeFs) WriteTestKustomizationWith(bytes []byte) {
fs.WriteFile(pgmconfig.KustomizationFileNames[0], bytes) fs.WriteFile(pgmconfig.KustomizationFileNames[0], bytes)
} }
// Walk implements filepath.Walk using the fake filesystem.
func (fs *fakeFs) Walk(path string, walkFn filepath.WalkFunc) error {
info, err := fs.lstat(path)
if err != nil {
err = walkFn(path, info, err)
} else {
err = fs.walk(path, info, walkFn)
}
if err == filepath.SkipDir {
return nil
}
return err
}
func (fs *fakeFs) pathMatch(path, pattern string) bool { func (fs *fakeFs) pathMatch(path, pattern string) bool {
match, _ := filepath.Match(pattern, path) match, _ := filepath.Match(pattern, path)
return match return match
} }
func (fs *fakeFs) lstat(path string) (*Fakefileinfo, error) {
f, found := fs.m[path]
if !found {
return nil, os.ErrNotExist
}
return &Fakefileinfo{f}, nil
}
func (fs *fakeFs) join(elem ...string) string {
for i, e := range elem {
if e != "" {
return strings.Replace(strings.Join(elem[i:], "/"), "//", "/", -1)
}
}
return ""
}
func (fs *fakeFs) readDirNames(path string) []string {
var names []string
if !strings.HasSuffix(path, "/") {
path += "/"
}
pathSegments := strings.Count(path, "/")
for name := range fs.m {
if name == path {
continue
}
if strings.Count(name, "/") > pathSegments {
continue
}
if strings.HasPrefix(name, path) {
names = append(names, filepath.Base(name))
}
}
sort.Strings(names)
return names
}
func (fs *fakeFs) walk(path string, info os.FileInfo, walkFn filepath.WalkFunc) error {
if !info.IsDir() {
return walkFn(path, info, nil)
}
names := fs.readDirNames(path)
if err := walkFn(path, info, nil); err != nil {
return err
}
for _, name := range names {
filename := fs.join(path, name)
fileInfo, err := fs.lstat(filename)
if err != nil {
if err := walkFn(filename, fileInfo, os.ErrNotExist); err != nil && err != filepath.SkipDir {
return err
}
} else {
err = fs.walk(filename, fileInfo, walkFn)
if err != nil {
if !fileInfo.IsDir() || err != filepath.SkipDir {
return err
}
}
}
}
return nil
}

View File

@@ -20,6 +20,7 @@ package fs
import ( import (
"io" "io"
"os" "os"
"path/filepath"
) )
// FileSystem groups basic os filesystem methods. // FileSystem groups basic os filesystem methods.
@@ -35,6 +36,7 @@ type FileSystem interface {
Glob(pattern string) ([]string, error) Glob(pattern string) ([]string, error)
ReadFile(name string) ([]byte, error) ReadFile(name string) ([]byte, error)
WriteFile(name string, data []byte) error WriteFile(name string, data []byte) error
Walk(path string, walkFn filepath.WalkFunc) error
} }
// File groups the basic os.File methods. // File groups the basic os.File methods.

View File

@@ -120,3 +120,8 @@ func (realFS) ReadFile(name string) ([]byte, error) { return ioutil.ReadFile(nam
func (realFS) WriteFile(name string, c []byte) error { func (realFS) WriteFile(name string, c []byte) error {
return ioutil.WriteFile(name, c, 0666) return ioutil.WriteFile(name, c, 0666)
} }
// Walk delegates to filepath.Walk.
func (realFS) Walk(path string, walkFn filepath.WalkFunc) error {
return filepath.Walk(path, walkFn)
}