Skip to content

Commit

Permalink
Provider Unit Tests (Provider Lifecycle) (#2768)
Browse files Browse the repository at this point in the history
<!--Thanks for your contribution. See [CONTRIBUTING](CONTRIBUTING.md)
    for Pulumi's contribution guidelines.

    Help us merge your changes more quickly by adding more details such
    as labels, milestones, and reviewers.-->

### Proposed changes

This PR implements a suite of unit tests for the provider implementation
code in `provider/pkg/provider/provider.go`. Unlike the test suites in
`tests/`, this suite uses a mock Kubernetes client and executes the
provider code directly.

This PR focuses on the provider lifecycle (e.g. plugin info and provider
configuration):
- [x] Suite
- [x] RPC:CheckConfig
  - [x] Strict Mode
  - [x] Yaml Rendering Mode
- [x] RPC:DiffConfig
  - [x] Kubeconfig Parsing
  - [x] Cluster Change Detection 
- [x] RPC:Configure
  - [x] Secrets Support
  - [x] Connectivity
- [x] RPC:GetPluginInfo
- [x] RPC:GetSchema
- [x] RPC:GetMapping
- [x] RPC:Cancel

In follow-up PR(s), tests will be developed for the resource lifecycle
(eg. `Check`, `Diff`, `Read`, `Create`, `Update`, `Delete`), and for
invokes (`Invoke`, `StreamInvoke`).

### Changes to Implementation Code
Some minor refactoring was necessary to make it possible to substitute
fake Kubernetes clients.
- the type of the `RESTMapper` field of `DynamicClientSet` was changed
to an interface.
- the type of the `client` field of `LogClient` was changed to an
interface.
- the low-level logic for making a Kubernetes client from a kubeconfig
was moved into a function called `makeClient`.
- add a `makeClient` field to `kubeProvider` as an indirection for test
purposes.

Closes #2767
  • Loading branch information
EronWright authored Jan 26, 2024
1 parent eaf6fb9 commit df718a6
Show file tree
Hide file tree
Showing 13 changed files with 1,074 additions and 43 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,7 @@ sdk/java/gradlew
sdk/java/gradlew.bat

sdk/python/venv

coverage/
coverage.txt
ginkgo.report
7 changes: 3 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ JAVA_GEN := pulumi-java-gen
JAVA_GEN_VERSION := v0.8.0

WORKING_DIR := $(shell pwd)
TESTPARALLELISM := 4

openapi_file::
@mkdir -p $(OPENAPI_DIR)
Expand Down Expand Up @@ -53,7 +52,7 @@ k8sprovider_debug::
(cd provider && CGO_ENABLED=0 go build -o $(WORKING_DIR)/bin/${PROVIDER} -gcflags="all=-N -l" -ldflags "-X ${PROJECT}/${VERSION_PATH}=${VERSION}" $(PROJECT)/${PROVIDER_PATH}/cmd/$(PROVIDER))

test_provider::
cd provider/pkg && go test -short -v -count=1 -coverprofile="coverage.txt" -coverpkg=./... -timeout 2h -parallel ${TESTPARALLELISM} ./...
cd provider/pkg && go test -short -v -coverprofile="coverage.txt" -coverpkg=./... -timeout 2h ./...

dotnet_sdk:: DOTNET_VERSION := $(shell pulumictl get version --language dotnet)
dotnet_sdk::
Expand Down Expand Up @@ -115,8 +114,8 @@ install_provider::

install:: install_nodejs_sdk install_dotnet_sdk install_provider

GO_TEST_FAST := go test -short -v -count=1 -cover -timeout 2h -parallel ${TESTPARALLELISM}
GO_TEST := go test -v -count=1 -cover -timeout 2h -parallel ${TESTPARALLELISM}
GO_TEST_FAST := go test -short -v -cover -timeout 2h
GO_TEST := go test -v -cover -timeout 2h

# Required for the codegen action that runs in pulumi/pulumi
test:: test_all
Expand Down
6 changes: 5 additions & 1 deletion provider/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ require (
github.com/google/gnostic-models v0.6.8
github.com/imdario/mergo v0.3.16
github.com/mitchellh/mapstructure v1.5.0
github.com/onsi/ginkgo/v2 v2.15.0
github.com/onsi/gomega v1.30.0
github.com/pkg/errors v0.9.1
github.com/pulumi/cloud-ready-checks v1.1.0
github.com/pulumi/pulumi/pkg/v3 v3.103.1
Expand Down Expand Up @@ -254,6 +256,8 @@ require (
github.com/go-git/go-billy/v5 v5.5.0 // indirect
github.com/go-git/go-git/v5 v5.11.0 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/google/pprof v0.0.0-20230406165453-00490a63f317 // indirect
github.com/google/s2a-go v0.1.4 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.4 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
Expand Down Expand Up @@ -292,7 +296,7 @@ require (
go.uber.org/zap v1.26.0 // indirect
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/tools v0.15.0 // indirect
golang.org/x/tools v0.16.1 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230920204549-e6e6cdab5c13 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 // indirect
k8s.io/cli-runtime v0.29.0 // indirect
Expand Down
13 changes: 6 additions & 7 deletions provider/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1503,10 +1503,9 @@ github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0=
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4=
github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o=
github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY=
github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM=
github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
Expand All @@ -1517,8 +1516,8 @@ github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoT
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0=
github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=
github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8=
github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
Expand Down Expand Up @@ -2505,8 +2504,8 @@ golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E
golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8=
golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk=
golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA=
golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
Expand Down
18 changes: 12 additions & 6 deletions provider/pkg/clients/clients.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ import (

"github.com/pulumi/pulumi-kubernetes/provider/v4/pkg/kinds"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/discovery"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
clientcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/rest"
"k8s.io/client-go/restmapper"
)
Expand All @@ -49,7 +51,7 @@ func ResourceClient(kind kinds.Kind, namespace string, client *DynamicClientSet)
type DynamicClientSet struct {
GenericClient dynamic.Interface
DiscoveryClientCached discovery.CachedDiscoveryInterface
RESTMapper *restmapper.DeferredDiscoveryRESTMapper
RESTMapper meta.ResettableRESTMapper
}

func NewDynamicClientSet(clientConfig *rest.Config) (*DynamicClientSet, error) {
Expand Down Expand Up @@ -192,23 +194,27 @@ func IsNamespacedKind(gvk schema.GroupVersionKind, clientSet *DynamicClientSet)
}

type LogClient struct {
clientset *kubernetes.Clientset
ctx context.Context
client clientcorev1.CoreV1Interface
ctx context.Context
}

func NewLogClient(ctx context.Context, clientConfig *rest.Config) (*LogClient, error) {
func NewLogClient(ctx context.Context, client clientcorev1.CoreV1Interface) *LogClient {
return &LogClient{client: client, ctx: ctx}
}

func MakeLogClient(ctx context.Context, clientConfig *rest.Config) (*LogClient, error) {
// creates the clientset
clientset, err := kubernetes.NewForConfig(clientConfig)
if err != nil {
return nil, err
}

return &LogClient{clientset: clientset, ctx: ctx}, nil
return NewLogClient(ctx, clientset.CoreV1()), nil
}

func (lc *LogClient) Logs(namespace, name string) (io.ReadCloser, error) {
podLogOpts := corev1.PodLogOptions{Follow: true}
req := lc.clientset.CoreV1().Pods(namespace).GetLogs(name, &podLogOpts)
req := lc.client.Pods(namespace).GetLogs(name, &podLogOpts)
return req.Stream(lc.ctx)
}

Expand Down
45 changes: 28 additions & 17 deletions provider/pkg/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,7 @@ type kubeProvider struct {
clusterUnreachable bool // Kubernetes cluster is unreachable.
clusterUnreachableReason string // Detailed error message if cluster is unreachable.

config *rest.Config // Cluster config, e.g., through $KUBECONFIG file.
kubeconfig clientcmd.ClientConfig
makeClient func(context.Context, *rest.Config) (*clients.DynamicClientSet, *clients.LogClient, error)
clientSet *clients.DynamicClientSet
logClient *clients.LogClient
k8sVersion cluster.ServerVersion
Expand All @@ -159,7 +158,7 @@ var _ pulumirpc.ResourceProviderServer = (*kubeProvider)(nil)

func makeKubeProvider(
host *provider.HostClient, name, version string, pulumiSchema, terraformMapping []byte,
) (pulumirpc.ResourceProviderServer, error) {
) (*kubeProvider, error) {
return &kubeProvider{
host: host,
canceler: makeCancellationContext(),
Expand All @@ -172,9 +171,24 @@ func makeKubeProvider(
suppressDeprecationWarnings: false,
deleteUnreachable: false,
skipUpdateUnreachable: false,
makeClient: makeClient,
}, nil
}

// makeClient makes a client to connect to a Kubernetes cluster using the given config.
// ctx is a cancellation context that may be used to cancel any subsequent requests made by the clients.
func makeClient(ctx context.Context, config *rest.Config) (*clients.DynamicClientSet, *clients.LogClient, error) {
cs, err := clients.NewDynamicClientSet(config)
if err != nil {
return nil, nil, err
}
lc, err := clients.MakeLogClient(ctx, config)
if err != nil {
return nil, nil, err
}
return cs, lc, nil
}

func (k *kubeProvider) getResources() (k8sopenapi.Resources, error) {
k.resourcesMutex.RLock()
rs := k.resources
Expand Down Expand Up @@ -742,12 +756,16 @@ func (k *kubeProvider) Configure(_ context.Context, req *pulumirpc.ConfigureRequ
}

// Attempt to load the configuration from the provided kubeconfig. If this fails, mark the cluster as unreachable.
var config *rest.Config
if !k.clusterUnreachable {
config, err := kubeconfig.ClientConfig()
contract.Assertf(kubeconfig != nil, "expected kubeconfig to be initialized")
var err error
config, err = kubeconfig.ClientConfig()
if err != nil {
k.clusterUnreachable = true
k.clusterUnreachableReason = fmt.Sprintf("unable to load Kubernetes client configuration from kubeconfig file. Make sure you have: \n\n"+
" \t • set up the provider as per https://www.pulumi.com/registry/packages/kubernetes/installation-configuration/ \n\n %v", err)
config = nil
} else {
if kubeClientSettings.Burst != nil {
config.Burst = *kubeClientSettings.Burst
Expand All @@ -761,27 +779,20 @@ func (k *kubeProvider) Configure(_ context.Context, req *pulumirpc.ConfigureRequ
config.Timeout = time.Duration(*kubeClientSettings.Timeout) * time.Second
logger.V(9).Infof("kube client timeout set to %v", config.Timeout)
}
warningConfig := rest.CopyConfig(config)
warningConfig.WarningHandler = rest.NoWarnings{}
k.config = warningConfig
k.kubeconfig = kubeconfig
config.WarningHandler = rest.NoWarnings{}
}
}

// These operations require a reachable cluster.
if !k.clusterUnreachable {
cs, err := clients.NewDynamicClientSet(k.config)
if err != nil {
return nil, err
}
k.clientSet = cs
lc, err := clients.NewLogClient(k.canceler.context, k.config)
contract.Assertf(config != nil, "expected config to be initialized")
var err error
k.clientSet, k.logClient, err = k.makeClient(k.canceler.context, config)
if err != nil {
return nil, err
}
k.logClient = lc

k.k8sVersion = cluster.TryGetServerVersion(cs.DiscoveryClientCached)
k.k8sVersion = cluster.TryGetServerVersion(k.clientSet.DiscoveryClientCached)

if k.k8sVersion.Compare(cluster.ServerVersion{Major: 1, Minor: 13}) < 0 {
return nil, fmt.Errorf("minimum supported cluster version is v1.13. found v%s", k.k8sVersion)
Expand All @@ -801,7 +812,7 @@ func (k *kubeProvider) Configure(_ context.Context, req *pulumirpc.ConfigureRequ
k.canceler,
apiConfig,
overrides,
k.config,
config,
k.clientSet,
k.helmDriver,
k.defaultNamespace,
Expand Down
Loading

0 comments on commit df718a6

Please sign in to comment.