Skip to content

Commit

Permalink
Put all resources in specified provider namespace (#538)
Browse files Browse the repository at this point in the history
  • Loading branch information
lblackstone committed Apr 23, 2019
1 parent d0f00cf commit e1beb1e
Show file tree
Hide file tree
Showing 8 changed files with 257 additions and 15 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

### Improvements

- None
- Put all resources in specified provider namespace (https://github.com/pulumi/pulumi-kubernetes/pull/538)

### Bug fixes

Expand Down
35 changes: 30 additions & 5 deletions pkg/clients/clients.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func (dcs *DynamicClientSet) ResourceClient(gvk schema.GroupVersionKind, namespa
}

// For namespaced Kinds, create a namespaced client. If no namespace is provided, use the "default" namespace.
namespaced, err := dcs.namespaced(gvk)
namespaced, err := dcs.IsNamespacedKind(gvk)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -132,10 +132,15 @@ func (dcs *DynamicClientSet) gvkForKind(kind kinds.Kind) (*schema.GroupVersionKi
return nil, fmt.Errorf("failed to find gvk for Kind: %q", kind)
}

func (dcs *DynamicClientSet) namespaced(gvk schema.GroupVersionKind) (bool, error) {
resourceList, err := dcs.DiscoveryClientCached.ServerResourcesForGroupVersion(gvk.GroupVersion().String())
func (dcs *DynamicClientSet) IsNamespacedKind(gvk schema.GroupVersionKind) (bool, error) {
gv := gvk.GroupVersion().String()
if strings.Contains(gv, "core/v1") {
gv = "v1"
}

resourceList, err := dcs.DiscoveryClientCached.ServerResourcesForGroupVersion(gv)
if err != nil {
return false, err
return false, &NoNamespaceInfoErr{gvk}
}

for _, resource := range resourceList.APIResources {
Expand All @@ -144,7 +149,27 @@ func (dcs *DynamicClientSet) namespaced(gvk schema.GroupVersionKind) (bool, erro
}
}

return true, fmt.Errorf("failed to discover namespace info for %s", gvk)
return false, &NoNamespaceInfoErr{gvk}
}

type NoNamespaceInfoErr struct {
gvk schema.GroupVersionKind
}

func (e *NoNamespaceInfoErr) Error() string {
return fmt.Sprintf("failed to determine if the following GVK is namespaced: %s", e.gvk)
}

func IsNoNamespaceInfoErr(err error) bool {
if err == nil {
return false
}
switch err.(type) {
case *NoNamespaceInfoErr:
return true
default:
return false
}
}

// namespaceOrDefault returns `ns` or the the default namespace `"default"` if `ns` is empty.
Expand Down
55 changes: 46 additions & 9 deletions pkg/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,13 @@ type kubeOpts struct {
}

type kubeProvider struct {
host *provider.HostClient
canceler *cancellationContext
name string
version string
providerPrefix string
opts kubeOpts
host *provider.HostClient
canceler *cancellationContext
name string
version string
providerPrefix string
opts kubeOpts
overrideNamespace string

clientSet *clients.DynamicClientSet
}
Expand Down Expand Up @@ -138,6 +139,10 @@ func (k *kubeProvider) Configure(_ context.Context, req *pulumirpc.ConfigureRequ
CurrentContext: vars["kubernetes:config:context"],
}

if overrides.Context.Namespace != "" {
k.overrideNamespace = overrides.Context.Namespace
}

var kubeconfig clientcmd.ClientConfig
if configJSON, ok := vars["kubernetes:config:kubeconfig"]; ok {
config, err := clientcmd.Load([]byte(configJSON))
Expand Down Expand Up @@ -272,6 +277,27 @@ func (k *kubeProvider) Check(ctx context.Context, req *pulumirpc.CheckRequest) (
return nil, err
}

// If an override namespace is set on the provider for this resource, check if the resource has Namespaced
// or Global scope. For namespaced resources, set the namespace to the override value, ignoring any value
// currently set on the resource. Global-scope resources are unaffected by the override.
if k.overrideNamespace != "" {
namespacedKind, err := k.clientSet.IsNamespacedKind(gvk)
if err != nil {
if clients.IsNoNamespaceInfoErr(err) {
// This is probably a CustomResource without a registered CustomResourceDefinition.
// Since we can't tell for sure at this point, assume it is namespaced, and correct if
// required during the Create step.
namespacedKind = true
} else {
return nil, err
}
}

if namespacedKind {
newInputs.SetNamespace(k.overrideNamespace)
}
}

// HACK: Do not validate against OpenAPI spec if there is a computed value. The OpenAPI spec
// does not know how to deal with the placeholder values for computed values.
if !hasComputedValue(newInputs) {
Expand Down Expand Up @@ -364,9 +390,20 @@ func (k *kubeProvider) Diff(
return nil, err
}

// Explicitly set the "default" namespace if unset so that the diff ignores it.
oldInputs.SetNamespace(canonicalNamespace(oldInputs.GetNamespace()))
newInputs.SetNamespace(canonicalNamespace(newInputs.GetNamespace()))
namespacedKind, err := k.clientSet.IsNamespacedKind(gvk)
if err != nil {
return nil, err
}

if namespacedKind {
// Explicitly set the "default" namespace if unset so that the diff ignores it.
oldInputs.SetNamespace(canonicalNamespace(oldInputs.GetNamespace()))
newInputs.SetNamespace(canonicalNamespace(newInputs.GetNamespace()))
} else {
// Clear the namespace if it was set erroneously.
oldInputs.SetNamespace("")
newInputs.SetNamespace("")
}

// Decide whether to replace the resource.
replaces, err := forceNewProperties(oldInputs.Object, newInputs.Object, gvk)
Expand Down
81 changes: 81 additions & 0 deletions tests/integration/provider/provider_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright 2016-2019, Pulumi Corporation.
//
// 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 ints

import (
"os"
"testing"

"github.com/pulumi/pulumi/pkg/tokens"

"github.com/pulumi/pulumi-kubernetes/pkg/openapi"
"github.com/pulumi/pulumi-kubernetes/tests"
"github.com/pulumi/pulumi/pkg/resource"
"github.com/pulumi/pulumi/pkg/resource/deploy/providers"
"github.com/pulumi/pulumi/pkg/testing/integration"
"github.com/stretchr/testify/assert"
)

func TestProvider(t *testing.T) {
kubectx := os.Getenv("KUBERNETES_CONTEXT")

if kubectx == "" {
t.Skipf("Skipping test due to missing KUBERNETES_CONTEXT variable")
}

integration.ProgramTest(t, &integration.ProgramTestOptions{
Dir: "step1",
Dependencies: []string{"@pulumi/kubernetes"},
Quick: true,
ExtraRuntimeValidation: func(t *testing.T, stackInfo integration.RuntimeValidationStackInfo) {
assert.NotNil(t, stackInfo.Deployment)
assert.Equal(t, 7, len(stackInfo.Deployment.Resources))

tests.SortResourcesByURN(stackInfo)

stackRes := stackInfo.Deployment.Resources[6]
assert.Equal(t, resource.RootStackType, stackRes.URN.Type())

k8sProvider := stackInfo.Deployment.Resources[5]
assert.True(t, providers.IsProviderType(k8sProvider.URN.Type()))

defaultProvider := stackInfo.Deployment.Resources[4]
assert.True(t, providers.IsProviderType(defaultProvider.URN.Type()))

// Assert the provider Namespace was created
providerNamespace := stackInfo.Deployment.Resources[0]
assert.Equal(t, tokens.Type("kubernetes:core/v1:Namespace"), providerNamespace.URN.Type())
providerNsName, _ := openapi.Pluck(providerNamespace.Outputs, "metadata", "name")

// Assert the other Namespace was created despite the provider override.
otherNamespace := stackInfo.Deployment.Resources[1]
assert.Equal(t, tokens.Type("kubernetes:core/v1:Namespace"), otherNamespace.URN.Type())
nsName, _ := openapi.Pluck(otherNamespace.Outputs, "metadata", "name")
assert.NotEqual(t, nsName, providerNsName)

// Assert the Pod was created in the provider namespace.
pod := stackInfo.Deployment.Resources[3]
assert.Equal(t, "nginx", string(pod.URN.Name()))
podNamespace, _ := openapi.Pluck(pod.Outputs, "metadata", "namespace")
assert.Equal(t, providerNamespace.ID.String(), podNamespace.(string))

// Assert the Pod was created in the provider namespace rather than the specified namespace.
namespacedPod := stackInfo.Deployment.Resources[2]
assert.Equal(t, "namespaced-nginx", string(namespacedPod.URN.Name()))
namespacedPodNamespace, _ := openapi.Pluck(namespacedPod.Outputs, "metadata", "namespace")
assert.Equal(t, providerNamespace.ID.String(), namespacedPodNamespace.(string))
},
})
}
3 changes: 3 additions & 0 deletions tests/integration/provider/step1/Pulumi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
name: provider
description: Tests first-class provider support.
runtime: nodejs
60 changes: 60 additions & 0 deletions tests/integration/provider/step1/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright 2016-2019, Pulumi Corporation.
//
// 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.

import * as k8s from "@pulumi/kubernetes";
import * as fs from "fs";
import * as os from "os";
import * as path from "path";

// Use the existing ~/.kube/config kubeconfig
const kubeconfig = fs.readFileSync(path.join(os.homedir(), ".kube", "config")).toString();

const ns = new k8s.core.v1.Namespace("ns");

// Create a new provider
const myk8s = new k8s.Provider("myk8s", {
kubeconfig: kubeconfig,
namespace: ns.metadata.name,
});

// Create a Pod using the custom provider.
// The namespace should be automatically set by the provider override.
new k8s.core.v1.Pod("nginx", {
spec: {
containers: [{
image: "nginx:1.7.9",
name: "nginx",
ports: [{ containerPort: 80 }],
}],
},
}, { provider: myk8s });

// Create a Pod using the custom provider with a specified namespace.
// The namespace should be overridden by the provider override.
new k8s.core.v1.Pod("namespaced-nginx", {
metadata: { namespace: ns.metadata.name },
spec: {
containers: [{
image: "nginx:1.7.9",
name: "nginx",
ports: [{ containerPort: 80 }],
}],
},
}, { provider: myk8s });

// Create a Namespace using the custom provider
// The namespace should not be affected by the provider override since it is a non-namespaceable kind.
new k8s.core.v1.Namespace("other-ns",
{},
{ provider: myk8s });
14 changes: 14 additions & 0 deletions tests/integration/provider/step1/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "steps",
"version": "0.1.0",
"dependencies": {
"@pulumi/pulumi": "dev",
"@pulumi/random": "dev"
},
"devDependencies": {
"typescript": "^3.0.0"
},
"peerDependencies": {
"@pulumi/kubernetes": "latest"
}
}
22 changes: 22 additions & 0 deletions tests/integration/provider/step1/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"outDir": "bin",
"target": "es6",
"module": "commonjs",
"moduleResolution": "node",
"declaration": true,
"sourceMap": true,
"stripInternal": true,
"experimentalDecorators": true,
"pretty": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"forceConsistentCasingInFileNames": true,
"strictNullChecks": true
},
"files": [
"index.ts"
]
}

0 comments on commit e1beb1e

Please sign in to comment.