Handle the --namespace flag

This commit is contained in:
Morten Torkildsen 2020-08-25 16:45:50 -07:00
parent 5a58b89413
commit e43565ef80
7 changed files with 248 additions and 65 deletions

View File

@ -117,10 +117,21 @@ func (r *ApplyRunner) RunE(cmd *cobra.Command, args []string) error {
return err return err
} }
// Fetch the namespace from the configloader. The source of this
// either the namespace flag or the context. If the namespace is provided
// with the flag, enforceNamespace will be true. In this case, it is
// an error if any of the resources in the package has a different
// namespace set.
namespace, enforceNamespace, err := r.provider.Factory().ToRawKubeConfigLoader().Namespace()
if err != nil {
return err
}
var reader manifestreader.ManifestReader var reader manifestreader.ManifestReader
readerOptions := manifestreader.ReaderOptions{ readerOptions := manifestreader.ReaderOptions{
Factory: r.provider.Factory(), Factory: r.provider.Factory(),
Namespace: metav1.NamespaceDefault, Namespace: namespace,
EnforceNamespace: enforceNamespace,
} }
if len(args) == 0 { if len(args) == 0 {
reader = &manifestreader.StreamManifestReader{ reader = &manifestreader.StreamManifestReader{

View File

@ -6,14 +6,15 @@ package initcmd
import ( import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericclioptions"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/i18n"
"sigs.k8s.io/cli-utils/pkg/config" "sigs.k8s.io/cli-utils/pkg/config"
) )
// NewCmdInit creates the `init` command, which generates the // NewCmdInit creates the `init` command, which generates the
// inventory object template ConfigMap for a package. // inventory object template ConfigMap for a package.
func NewCmdInit(ioStreams genericclioptions.IOStreams) *cobra.Command { func NewCmdInit(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
io := config.NewInitOptions(ioStreams) io := config.NewInitOptions(f, ioStreams)
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "init DIRECTORY", Use: "init DIRECTORY",
DisableFlagsInUseLine: true, DisableFlagsInUseLine: true,
@ -27,6 +28,5 @@ func NewCmdInit(ioStreams genericclioptions.IOStreams) *cobra.Command {
}, },
} }
cmd.Flags().StringVarP(&io.InventoryID, "inventory-id", "i", "", "Identifier for group of applied resources. Must be composed of valid label characters.") cmd.Flags().StringVarP(&io.InventoryID, "inventory-id", "i", "", "Identifier for group of applied resources. Must be composed of valid label characters.")
cmd.Flags().StringVarP(&io.Namespace, "inventory-namespace", "", "", "namespace for the resources to be initialized")
return cmd return cmd
} }

View File

@ -55,7 +55,7 @@ func main() {
} }
names := []string{"init", "apply", "preview", "diff", "destroy", "status"} names := []string{"init", "apply", "preview", "diff", "destroy", "status"}
initCmd := initcmd.NewCmdInit(ioStreams) initCmd := initcmd.NewCmdInit(f, ioStreams)
updateHelp(names, initCmd) updateHelp(names, initCmd)
applyCmd := apply.ApplyCommand(f, ioStreams) applyCmd := apply.ApplyCommand(f, ioStreams)
updateHelp(names, applyCmd) updateHelp(names, applyCmd)

View File

@ -7,7 +7,6 @@ import (
"context" "context"
"github.com/spf13/cobra" "github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericclioptions"
cmdutil "k8s.io/kubectl/pkg/cmd/util" cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/i18n"
@ -117,10 +116,21 @@ func (r *PreviewRunner) RunE(cmd *cobra.Command, args []string) error {
return err return err
} }
// Fetch the namespace from the configloader. The source of this
// either the namespace flag or the context. If the namespace is provided
// with the flag, enforceNamespace will be true. In this case, it is
// an error if any of the resources in the package has a different
// namespace set.
namespace, enforceNamespace, err := r.provider.Factory().ToRawKubeConfigLoader().Namespace()
if err != nil {
return err
}
var reader manifestreader.ManifestReader var reader manifestreader.ManifestReader
readerOptions := manifestreader.ReaderOptions{ readerOptions := manifestreader.ReaderOptions{
Factory: r.provider.Factory(), Factory: r.provider.Factory(),
Namespace: metav1.NamespaceDefault, Namespace: namespace,
EnforceNamespace: enforceNamespace,
} }
if len(args) == 0 { if len(args) == 0 {
reader = &manifestreader.StreamManifestReader{ reader = &manifestreader.StreamManifestReader{

View File

@ -11,10 +11,9 @@ import (
"strings" "strings"
"time" "time"
"github.com/go-errors/errors"
"github.com/google/uuid" "github.com/google/uuid"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericclioptions"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"sigs.k8s.io/cli-utils/pkg/common" "sigs.k8s.io/cli-utils/pkg/common"
"sigs.k8s.io/kustomize/kyaml/kio" "sigs.k8s.io/kustomize/kyaml/kio"
) )
@ -62,6 +61,8 @@ metadata:
// InitOptions contains the fields necessary to generate a // InitOptions contains the fields necessary to generate a
// inventory object template ConfigMap. // inventory object template ConfigMap.
type InitOptions struct { type InitOptions struct {
factory cmdutil.Factory
ioStreams genericclioptions.IOStreams ioStreams genericclioptions.IOStreams
// Package directory argument; must be valid directory. // Package directory argument; must be valid directory.
Dir string Dir string
@ -71,8 +72,9 @@ type InitOptions struct {
InventoryID string InventoryID string
} }
func NewInitOptions(ioStreams genericclioptions.IOStreams) *InitOptions { func NewInitOptions(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *InitOptions {
return &InitOptions{ return &InitOptions{
factory: f,
ioStreams: ioStreams, ioStreams: ioStreams,
} }
} }
@ -89,14 +91,13 @@ func (i *InitOptions) Complete(args []string) error {
return err return err
} }
i.Dir = dir i.Dir = dir
if len(i.Namespace) == 0 {
// Returns default namespace if no namespace found. ns, err := findNamespace(i.factory.ToRawKubeConfigLoader(), i.Dir)
namespace, err := calcPackageNamespace(i.Dir) if err != nil {
if err != nil { return err
return err
}
i.Namespace = namespace
} }
i.Namespace = ns
// Set the default inventory label if one does not exist. // Set the default inventory label if one does not exist.
if len(i.InventoryID) == 0 { if len(i.InventoryID) == 0 {
inventoryID, err := i.defaultInventoryID() inventoryID, err := i.defaultInventoryID()
@ -113,6 +114,35 @@ func (i *InitOptions) Complete(args []string) error {
return nil return nil
} }
type namespaceLoader interface {
Namespace() (string, bool, error)
}
// findNamespace looks up the namespace that should be used for the
// inventory template of the package. If the namespace is specified with
// the --namespace flag, it will be used no matter what. If not, this
// will look at all the resource, and if all belong in the same namespace,
// it will return that namespace. Otherwise, it will return the namespace
// set in the context.
func findNamespace(loader namespaceLoader, dir string) (string, error) {
namespace, enforceNamespace, err := loader.Namespace()
if err != nil {
return "", err
}
if enforceNamespace {
return namespace, nil
}
ns, allInSameNs, err := allInSameNamespace(dir)
if err != nil {
return "", err
}
if allInSameNs {
return ns, nil
}
return namespace, nil
}
// normalizeDir returns full absolute directory path of the // normalizeDir returns full absolute directory path of the
// passed directory or an error. This function cleans up paths // passed directory or an error. This function cleans up paths
// such as current directory (.), relative directories (..), or // such as current directory (.), relative directories (..), or
@ -136,35 +166,36 @@ func isDirectory(path string) bool {
return false return false
} }
// calcPackageNamespace returns the namespace of the package // allInSameNamespace goes through all resources in the package and
// config files. Assumes all namespaced resources are in the // checks the namespace for all of them. If they all have the namespace
// same namespace. Returns the default namespace if none of the // set and they all have the same value, this will return that namespace
// config files has a namespace. // and the second return value will be true. Otherwise, it will not return
func calcPackageNamespace(packageDir string) (string, error) { // a namespace and the second return value will be false.
func allInSameNamespace(packageDir string) (string, bool, error) {
r := kio.LocalPackageReader{PackagePath: packageDir} r := kio.LocalPackageReader{PackagePath: packageDir}
nodes, err := r.Read() nodes, err := r.Read()
if err != nil { if err != nil {
return "", err return "", false, err
} }
// Return the non-empty unique namespace if found. Cluster-scoped var ns string
// resources do not have namespace set.
currentNamespace := metav1.NamespaceDefault
for _, node := range nodes { for _, node := range nodes {
rm, err := node.GetMeta() rm, err := node.GetMeta()
if err != nil || len(rm.ObjectMeta.Namespace) == 0 { if err != nil {
continue return "", false, err
} }
if currentNamespace == metav1.NamespaceDefault { if rm.Namespace == "" {
currentNamespace = rm.ObjectMeta.Namespace return "", false, nil
} }
if currentNamespace != rm.ObjectMeta.Namespace { if ns == "" {
return "", errors.Errorf( ns = rm.Namespace
"resources belong to different namespaces, a namespace is required to create the resource " + } else if rm.Namespace != ns {
"used for keeping track of past apply operations. Please specify ---inventory-namespace.") return "", false, nil
} }
} }
// Return the default namespace if none found. if ns != "" {
return currentNamespace, nil return ns, true, nil
}
return "", false, nil
} }
// defaultInventoryID returns a UUID string as a default unique // defaultInventoryID returns a UUID string as a default unique

View File

@ -14,10 +14,9 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericclioptions"
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
) )
var ioStreams = genericclioptions.IOStreams{}
// writeFile writes a file under the test directory // writeFile writes a file under the test directory
func writeFile(t *testing.T, path string, value []byte) { func writeFile(t *testing.T, path string, value []byte) {
err := ioutil.WriteFile(path, value, 0600) err := ioutil.WriteFile(path, value, 0600)
@ -42,50 +41,171 @@ metadata:
namespace: namespaceB namespace: namespaceB
`) `)
var readFileC = []byte(`
apiVersion: v1
kind: Pod
metadata:
name: objC
`)
func TestComplete(t *testing.T) { func TestComplete(t *testing.T) {
d1, err := ioutil.TempDir("", "test-dir")
if !assert.NoError(t, err) {
assert.FailNow(t, err.Error())
}
defer os.RemoveAll(d1)
writeFile(t, filepath.Join(d1, "a_test.yaml"), readFileA)
writeFile(t, filepath.Join(d1, "b_test.yaml"), readFileB)
tests := map[string]struct { tests := map[string]struct {
args []string args []string
isError bool files map[string][]byte
isError bool
expectedErrMessage string
expectedNamespace string
}{ }{
"Empty args returns error": { "Empty args returns error": {
args: []string{}, args: []string{},
isError: true, isError: true,
expectedErrMessage: "need one 'directory' arg; have 0",
}, },
"More than one argument should fail": { "More than one argument should fail": {
args: []string{"foo", "bar"}, args: []string{"foo", "bar"},
isError: true, isError: true,
expectedErrMessage: "need one 'directory' arg; have 2",
}, },
"Non-directory arg should fail": { "Non-directory arg should fail": {
args: []string{"foo"}, args: []string{"foo"},
isError: true, isError: true,
expectedErrMessage: "invalid directory argument: foo",
}, },
"More than one namespace should fail": { "More than one namespace should fail": {
args: []string{d1}, args: []string{},
isError: true, files: map[string][]byte{
"a_test.yaml": readFileA,
"b_test.yaml": readFileB,
},
isError: true,
expectedErrMessage: "resources belong to different namespaces",
},
"If at least one resource doesn't have namespace, it should use the default": {
args: []string{},
files: map[string][]byte{
"b_test.yaml": readFileB,
"c_test.yaml": readFileC,
},
isError: false,
expectedNamespace: "foo",
},
"No resources without namespace should use the default namespace": {
args: []string{},
files: map[string][]byte{
"c_test.yaml": readFileC,
},
isError: false,
expectedNamespace: "foo",
}, },
} }
for name, tc := range tests { for name, tc := range tests {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
io := NewInitOptions(ioStreams) var err error
err := io.Complete(tc.args) dir, err := ioutil.TempDir("", "test-dir")
if tc.isError && err == nil { if !assert.NoError(t, err) {
t.Errorf("Expected error, but did not receive one") assert.FailNow(t, err.Error())
} }
defer os.RemoveAll(dir)
for fileName, fileContent := range tc.files {
writeFile(t, filepath.Join(dir, fileName), fileContent)
}
if len(tc.files) > 0 {
tc.args = append(tc.args, dir)
}
tf := cmdtesting.NewTestFactory().WithNamespace("foo")
defer tf.Cleanup()
ioStreams, _, out, _ := genericclioptions.NewTestIOStreams()
io := NewInitOptions(tf, ioStreams)
err = io.Complete(tc.args)
if err != nil {
if !tc.isError {
t.Errorf("Expected error, but did not receive one")
return
}
assert.Contains(t, err.Error(), tc.expectedErrMessage)
return
}
assert.Contains(t, out.String(), tc.expectedNamespace)
}) })
} }
} }
func TestFindNamespace(t *testing.T) {
testCases := map[string]struct {
namespace string
enforceNamespace bool
files map[string][]byte
expectedNamespace string
}{
"fallback to default": {
namespace: "foo",
enforceNamespace: false,
files: map[string][]byte{
"a_test.yaml": readFileA,
"b_test.yaml": readFileB,
},
expectedNamespace: "foo",
},
"enforce namespace": {
namespace: "bar",
enforceNamespace: true,
files: map[string][]byte{
"a_test.yaml": readFileA,
},
expectedNamespace: "bar",
},
"use namespace from resource if all the same": {
namespace: "bar",
enforceNamespace: false,
files: map[string][]byte{
"a_test.yaml": readFileA,
},
expectedNamespace: "namespaceA",
},
}
for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) {
var err error
dir, err := ioutil.TempDir("", "test-dir")
if !assert.NoError(t, err) {
assert.FailNow(t, err.Error())
}
defer os.RemoveAll(dir)
for fileName, fileContent := range tc.files {
writeFile(t, filepath.Join(dir, fileName), fileContent)
}
fakeLoader := &fakeNamespaceLoader{
namespace: tc.namespace,
enforceNamespace: tc.enforceNamespace,
}
namespace, err := findNamespace(fakeLoader, dir)
assert.NoError(t, err)
assert.Equal(t, tc.expectedNamespace, namespace)
})
}
}
type fakeNamespaceLoader struct {
namespace string
enforceNamespace bool
}
func (f *fakeNamespaceLoader) Namespace() (string, bool, error) {
return f.namespace, f.enforceNamespace, nil
}
func TestDefaultInventoryID(t *testing.T) { func TestDefaultInventoryID(t *testing.T) {
io := NewInitOptions(ioStreams) tf := cmdtesting.NewTestFactory().WithNamespace("foo")
defer tf.Cleanup()
ioStreams, _, _, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
io := NewInitOptions(tf, ioStreams)
actual, err := io.defaultInventoryID() actual, err := io.defaultInventoryID()
if err != nil { if err != nil {
t.Errorf("Unxpected error during UUID generation: %v", err) t.Errorf("Unxpected error during UUID generation: %v", err)
@ -172,7 +292,10 @@ func TestFillInValues(t *testing.T) {
for name, tc := range tests { for name, tc := range tests {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
io := NewInitOptions(ioStreams) tf := cmdtesting.NewTestFactory().WithNamespace("foo")
defer tf.Cleanup()
ioStreams, _, _, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
io := NewInitOptions(tf, ioStreams)
io.Namespace = tc.namespace io.Namespace = tc.namespace
io.InventoryID = tc.inventoryID io.InventoryID = tc.inventoryID
actual := io.fillInValues() actual := io.fillInValues()

View File

@ -11,6 +11,7 @@ import (
"k8s.io/cli-runtime/pkg/resource" "k8s.io/cli-runtime/pkg/resource"
"k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/cmd/util"
"sigs.k8s.io/cli-utils/pkg/apply/solver" "sigs.k8s.io/cli-utils/pkg/apply/solver"
"sigs.k8s.io/cli-utils/pkg/inventory"
) )
// ManifestReader defines the interface for reading a set // ManifestReader defines the interface for reading a set
@ -53,6 +54,13 @@ func setNamespaces(factory util.Factory, infos []*resource.Info,
for _, inf := range infos { for _, inf := range infos {
accessor, _ := meta.Accessor(inf.Object) accessor, _ := meta.Accessor(inf.Object)
// Exclude any inventory objects here since we don't want to change
// their namespace.
if inventory.IsInventoryObject(inf) {
continue
}
// if the resource already has the namespace set, we don't // if the resource already has the namespace set, we don't
// need to do anything // need to do anything
if ns := accessor.GetNamespace(); ns != "" { if ns := accessor.GetNamespace(); ns != "" {