diff --git a/cluster/kubernetes/cached_disco_test.go b/cluster/kubernetes/cached_disco_test.go index d7bf0a74a..6c56cb2dd 100644 --- a/cluster/kubernetes/cached_disco_test.go +++ b/cluster/kubernetes/cached_disco_test.go @@ -78,7 +78,7 @@ func TestCachedDiscovery(t *testing.T) { t.Fatal(err) } - namespaced, err := namespacer.lookupNamespaced("foo/v1", "Custom") + namespaced, err := namespacer.lookupNamespaced("foo/v1", "Custom", nil) if err != nil { t.Fatal(err) } @@ -124,7 +124,7 @@ func TestCachedDiscovery(t *testing.T) { t.Error("does not exist") } - namespaced, err = namespacer.lookupNamespaced("foo/v1", "Custom") + namespaced, err = namespacer.lookupNamespaced("foo/v1", "Custom", nil) if err != nil { t.Fatal(err) } diff --git a/cluster/kubernetes/manifests.go b/cluster/kubernetes/manifests.go index 576a09bc1..e7cb5602a 100644 --- a/cluster/kubernetes/manifests.go +++ b/cluster/kubernetes/manifests.go @@ -1,12 +1,19 @@ package kubernetes import ( + "gopkg.in/yaml.v2" + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + "k8s.io/apimachinery/pkg/runtime/schema" + "github.com/weaveworks/flux" kresource "github.com/weaveworks/flux/cluster/kubernetes/resource" "github.com/weaveworks/flux/image" "github.com/weaveworks/flux/resource" ) +// ResourceScopes maps resource definitions (GroupVersionKind) to whether they are namespaced or not +type ResourceScopes map[schema.GroupVersionKind]v1beta1.ResourceScope + // namespacer assigns namespaces to manifests that need it (or "" if // the manifest should not have a namespace. type namespacer interface { @@ -14,7 +21,7 @@ type namespacer interface { // the manifest to be applied. This may be "", indicating that it // should not have a namespace (i.e., it's a cluster-level // resource). - EffectiveNamespace(kresource.KubeManifest) (string, error) + EffectiveNamespace(manifest kresource.KubeManifest, knownScopes ResourceScopes) (string, error) } // Manifests is an implementation of cluster.Manifests, particular to @@ -26,11 +33,39 @@ type Manifests struct { Namespacer namespacer } +func getCRDScopes(manifests map[string]kresource.KubeManifest) ResourceScopes { + result := ResourceScopes{} + for _, km := range manifests { + if km.GetKind() == "CustomResourceDefinition" { + var crd v1beta1.CustomResourceDefinition + if err := yaml.Unmarshal(km.Bytes(), &crd); err != nil { + // The CRD can't be parsed, so we (intentionally) ignore it and + // just hope for EffectiveNamespace() to find its scope in the cluster if needed. + continue + } + crdVersions := crd.Spec.Versions + if len(crdVersions) == 0 { + crdVersions = []v1beta1.CustomResourceDefinitionVersion{{Name: crd.Spec.Version}} + } + for _, crdVersion := range crdVersions { + gvk := schema.GroupVersionKind{ + Group: crd.Spec.Group, + Version: crdVersion.Name, + Kind: crd.Spec.Names.Kind, + } + result[gvk] = crd.Spec.Scope + } + } + } + return result +} + func postProcess(manifests map[string]kresource.KubeManifest, nser namespacer) (map[string]resource.Resource, error) { + knownScopes := getCRDScopes(manifests) result := map[string]resource.Resource{} for _, km := range manifests { if nser != nil { - ns, err := nser.EffectiveNamespace(km) + ns, err := nser.EffectiveNamespace(km, knownScopes) if err != nil { return nil, err } diff --git a/cluster/kubernetes/manifests_test.go b/cluster/kubernetes/manifests_test.go new file mode 100644 index 000000000..7e4494edc --- /dev/null +++ b/cluster/kubernetes/manifests_test.go @@ -0,0 +1,62 @@ +package kubernetes + +import ( + "io/ioutil" + "path/filepath" + "testing" + + "github.com/weaveworks/flux/cluster/kubernetes/testfiles" +) + +func TestKnownCRDScope(t *testing.T) { + coreClient := makeFakeClient() + + nser, err := NewNamespacer(coreClient.Discovery()) + if err != nil { + t.Fatal(err) + } + manifests := Manifests{nser} + + dir, cleanup := testfiles.TempDir(t) + defer cleanup() + const defs = `--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: foo +spec: + group: foo.example.com + names: + kind: Foo + listKind: FooList + plural: foos + shortNames: + - foo + scope: Namespaced + version: v1beta1 + versions: + - name: v1beta1 + served: true + storage: true +--- +apiVersion: foo.example.com/v1beta1 +kind: Foo +metadata: + name: fooinstance + namespace: bar +` + + if err = ioutil.WriteFile(filepath.Join(dir, "test.yaml"), []byte(defs), 0600); err != nil { + t.Fatal(err) + } + + resources, err := manifests.LoadManifests(dir, []string{dir}) + if err != nil { + t.Fatal(err) + } + + if _, ok := resources["bar:foo/fooinstance"]; !ok { + t.Fatal("couldn't find crd instance") + } + +} diff --git a/cluster/kubernetes/namespacer.go b/cluster/kubernetes/namespacer.go index 8386e7425..d2f9e06a9 100644 --- a/cluster/kubernetes/namespacer.go +++ b/cluster/kubernetes/namespacer.go @@ -3,6 +3,8 @@ package kubernetes import ( "fmt" + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/discovery" "k8s.io/client-go/tools/clientcmd" @@ -55,8 +57,8 @@ var getDefaultNamespace = func() (string, error) { // effectiveNamespace yields the namespace that would be used for this // resource were it applied, taking into account the kind of the // resource, and local configuration. -func (n *namespaceViaDiscovery) EffectiveNamespace(m kresource.KubeManifest) (string, error) { - namespaced, err := n.lookupNamespaced(m.GroupVersion(), m.GetKind()) +func (n *namespaceViaDiscovery) EffectiveNamespace(m kresource.KubeManifest, knownScopes ResourceScopes) (string, error) { + namespaced, err := n.lookupNamespaced(m.GroupVersion(), m.GetKind(), knownScopes) switch { case err != nil: return "", err @@ -68,7 +70,24 @@ func (n *namespaceViaDiscovery) EffectiveNamespace(m kresource.KubeManifest) (st return m.GetNamespace(), nil } -func (n *namespaceViaDiscovery) lookupNamespaced(groupVersion, kind string) (bool, error) { +func (n *namespaceViaDiscovery) lookupNamespaced(groupVersion string, kind string, knownScopes ResourceScopes) (bool, error) { + namespaced, clusterErr := n.lookupNamespacedInCluster(groupVersion, kind) + if clusterErr == nil || knownScopes == nil { + return namespaced, nil + } + // Not found in the cluster, let's try the known scopes + gv, err := schema.ParseGroupVersion(groupVersion) + if err != nil { + return false, clusterErr + } + scope, found := knownScopes[gv.WithKind(kind)] + if !found { + return false, clusterErr + } + return scope == v1beta1.NamespaceScoped, nil +} + +func (n *namespaceViaDiscovery) lookupNamespacedInCluster(groupVersion, kind string) (bool, error) { resourceList, err := n.disco.ServerResourcesForGroupVersion(groupVersion) if err != nil { return false, fmt.Errorf("error looking up API resources for %s.%s: %s", kind, groupVersion, err.Error()) diff --git a/cluster/kubernetes/namespacer_test.go b/cluster/kubernetes/namespacer_test.go index 6a1a97476..95871a5c4 100644 --- a/cluster/kubernetes/namespacer_test.go +++ b/cluster/kubernetes/namespacer_test.go @@ -27,6 +27,12 @@ func makeFakeClient() *corefake.Clientset { {Name: "namespaces", SingularName: "namespace", Namespaced: false, Kind: "Namespace", Verbs: getAndList}, }, }, + { + GroupVersion: "apiextensions.k8s.io/v1beta1", + APIResources: []metav1.APIResource{ + {Name: "customresourcedefinitions", SingularName: "customresourcedefinition", Namespaced: false, Kind: "CustomResourceDefinition", Verbs: getAndList}, + }, + }, } coreClient := corefake.NewSimpleClientset() @@ -101,7 +107,7 @@ metadata: t.Errorf("manifest for %q not found", id) return } - got, err := nser.EffectiveNamespace(res) + got, err := nser.EffectiveNamespace(res, nil) if err != nil { t.Errorf("error getting effective namespace for %q: %s", id, err.Error()) return diff --git a/daemon/daemon_test.go b/daemon/daemon_test.go index d2e040699..8e603800f 100644 --- a/daemon/daemon_test.go +++ b/daemon/daemon_test.go @@ -634,7 +634,7 @@ func mustParseImageRef(ref string) image.Ref { type anonNamespacer func(kresource.KubeManifest) string -func (fn anonNamespacer) EffectiveNamespace(m kresource.KubeManifest) (string, error) { +func (fn anonNamespacer) EffectiveNamespace(m kresource.KubeManifest, _ kubernetes.ResourceScopes) (string, error) { return fn(m), nil } diff --git a/release/releaser_test.go b/release/releaser_test.go index 5a0fe5989..f90f5aae3 100644 --- a/release/releaser_test.go +++ b/release/releaser_test.go @@ -24,7 +24,7 @@ import ( type constNamespacer string -func (ns constNamespacer) EffectiveNamespace(kresource.KubeManifest) (string, error) { +func (ns constNamespacer) EffectiveNamespace(manifest kresource.KubeManifest, _ kubernetes.ResourceScopes) (string, error) { return string(ns), nil }