Skip to content

Commit

Permalink
Implement core of pure client-go-based Kubernetes provider
Browse files Browse the repository at this point in the history
This commit will introduce several important pieces that we will need to
implement the gRPC provider interface:

- [x] A discovery client, which "discovers" which version of the
      Kubernetes API a running instance of the API server supports and
      dynamically configures itself to use that API. This means that
      this one Kubernetes provider will work for (we believe) all
      versions of Kubernetes that support OpenAPI (rather than having
      one client per version, as is true of Terraform). It also frees us
      to use regular Kubernetes API objects, freeing us of the need to
      map "our" types to the underlying Kubernetes API (again, as is the
      case with Terraform.
- [x] Simple utilities for creating pools of Kubernetes clients, parsing
      and creating canonical names for resources, and so on.
- [x] A small validation library, which allows us to dynamically see
      which version of the Kubernetes API a particular API server
      supports, and then validate that some API object against it. In
      particular, this includes all Kubernetes API objects, even CRDs
      (which, again, is not supported by Terraform).
- [x] A stub gRPC server. (This panics when you call it but we will fix
      that soon enough.)
- [x] A simple build system that makes it easy to propagate version
      information into the client code.
- [ ] Implementations of the gRPC server's core functions (e.g.,
      `Update`, etc.)
- [ ] Tests. Once we've finished bootstrapping we will port the tests
      for `pulumi/pulumi-kubernetes` to the new provider.

Follow up commits will address the last, unfinished bullet points.
  • Loading branch information
hausdorff committed May 2, 2018
1 parent d583926 commit e85e033
Show file tree
Hide file tree
Showing 8 changed files with 745 additions and 168 deletions.
53 changes: 4 additions & 49 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,68 +1,23 @@
PROJECT_NAME := Kubernetes Package
PROJECT_NAME := Pulumi Kubernetes Resource Provider
include build/common.mk

PACK := kubernetes
PACKDIR := pack
PROJECT := github.com/pulumi/pulumi-kubernetes
NODE_MODULE_NAME := @pulumi/kubernetes

TFGEN := pulumi-tfgen-${PACK}
PROVIDER := pulumi-resource-${PACK}
VERSION := $(shell scripts/get-version)

VERSION_FLAGS := -ldflags "-X github.com/pulumi/pulumi-kubernetes/pkg/version.Version=${VERSION}"

GOMETALINTERBIN=gometalinter
GOMETALINTER=${GOMETALINTERBIN} --config=Gometalinter.json

TESTPARALLELISM := 10

build::
go install -ldflags "-X github.com/pulumi/pulumi-kubernetes/pkg/version.Version=${VERSION}" ${PROJECT}/cmd/${TFGEN}
go install -ldflags "-X github.com/pulumi/pulumi-kubernetes/pkg/version.Version=${VERSION}" ${PROJECT}/cmd/${PROVIDER}
for LANGUAGE in "nodejs" "python" ; do \
$(TFGEN) $$LANGUAGE --out ${PACKDIR}/$$LANGUAGE/ || exit 3 ; \
done
cd ${PACKDIR}/nodejs/ && \
yarn install && \
yarn link @pulumi/pulumi && \
yarn run tsc
cp README.md LICENSE ${PACKDIR}/nodejs/package.json ${PACKDIR}/nodejs/yarn.lock ${PACKDIR}/nodejs/bin/
cd ${PACKDIR}/python/ && \
python setup.py clean --all 2>/dev/null && \
rm -rf ./bin/ ../python.bin/ && cp -R . ../python.bin && mv ../python.bin ./bin && \
sed -i.bak "s/\$${VERSION}/$(VERSION)/g" ./bin/setup.py && rm ./bin/setup.py.bak && \
cd ./bin && python setup.py build sdist
go install $(VERSION_FLAGS) ${PROJECT}/cmd/${PROVIDER}

lint::
$(GOMETALINTER) ./cmd/... resources.go | sort ; exit "$${PIPESTATUS[0]}"

install::
GOBIN=$(PULUMI_BIN) go install -ldflags "-X github.com/pulumi/pulumi-kubernetes/pkg/version.Version=${VERSION}" ${PROJECT}/cmd/${PROVIDER}
[ ! -e "$(PULUMI_NODE_MODULES)/$(NODE_MODULE_NAME)" ] || rm -rf "$(PULUMI_NODE_MODULES)/$(NODE_MODULE_NAME)"
mkdir -p "$(PULUMI_NODE_MODULES)/$(NODE_MODULE_NAME)"
cp -r pack/nodejs/bin/. "$(PULUMI_NODE_MODULES)/$(NODE_MODULE_NAME)"
rm -rf "$(PULUMI_NODE_MODULES)/$(NODE_MODULE_NAME)/node_modules"
cd "$(PULUMI_NODE_MODULES)/$(NODE_MODULE_NAME)" && \
yarn install --offline --production && \
(yarn unlink > /dev/null 2>&1 || true) && \
yarn link
cd ${PACKDIR}/python && pip install --upgrade --user -e .

test_all::
PATH=$(PULUMI_BIN):$(PATH) go test -v -cover -timeout 1h -parallel ${TESTPARALLELISM} ./examples

.PHONY: publish_tgz
publish_tgz:
$(call STEP_MESSAGE)
./scripts/publish_tgz.sh

.PHONY: publish_packages
publish_packages:
$(call STEP_MESSAGE)
./scripts/publish_packages.sh

# The travis_* targets are entrypoints for CI.
.PHONY: travis_cron travis_push travis_pull_request travis_api
travis_cron: all
travis_push: only_build publish_tgz only_test publish_packages
travis_pull_request: all
travis_api: all
9 changes: 4 additions & 5 deletions cmd/pulumi-resource-kubernetes/main.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
// Copyright 2016-2018, Pulumi Corporation. All rights reserved.

package main

import (
kubernetes "github.com/pulumi/pulumi-kubernetes"
"github.com/pulumi/pulumi-kubernetes/pkg/provider"
"github.com/pulumi/pulumi-kubernetes/pkg/version"
"github.com/pulumi/pulumi-terraform/pkg/tfbridge"
)

var providerName = "kubernetes"

func main() {
tfbridge.Main("kubernetes", version.Version, kubernetes.Provider())
provider.Serve(providerName, version.Version)
}
13 changes: 0 additions & 13 deletions cmd/pulumi-tfgen-kubernetes/main.go

This file was deleted.

245 changes: 245 additions & 0 deletions pkg/provider/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
package provider

import (
"fmt"
"strings"
"sync"

"github.com/golang/glog"

"github.com/emicklei/go-restful-swagger12"
"github.com/googleapis/gnostic/OpenAPIv2"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
apiVers "k8s.io/apimachinery/pkg/version"
"k8s.io/client-go/discovery"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
)

// --------------------------------------------------------------------------

// In-memory, caching Kubernetes discovery client.
//
// The Kubernetes discovery client "discovers" the API server's capabilities, and opaquely handles
// the mapping of unstructured property bag -> typed API objects, regardless (in theory) of the
// version of the API server, greatly simplifying the logic required to interface with the cluster.
//
// This code implements the in-memory caching mechanism for this client, so that we do not have to
// retrieve this information multiple times to satisfy some set of requests.

// --------------------------------------------------------------------------

type memcachedDiscoveryClient struct {
cl discovery.DiscoveryInterface
lock sync.RWMutex
servergroups *metav1.APIGroupList
serverresources map[string]*metav1.APIResourceList
schemas map[string]*swagger.ApiDeclaration
schema *openapi_v2.Document
}

var _ discovery.CachedDiscoveryInterface = &memcachedDiscoveryClient{}

// NewMemcachedDiscoveryClient creates a new DiscoveryClient that
// caches results in memory
func NewMemcachedDiscoveryClient(cl discovery.DiscoveryInterface) discovery.CachedDiscoveryInterface {
c := &memcachedDiscoveryClient{cl: cl}
c.Invalidate()
return c
}

func (c *memcachedDiscoveryClient) Fresh() bool {
return true
}

func (c *memcachedDiscoveryClient) Invalidate() {
c.lock.Lock()
defer c.lock.Unlock()

c.servergroups = nil
c.serverresources = make(map[string]*metav1.APIResourceList)
c.schemas = make(map[string]*swagger.ApiDeclaration)
}

func (c *memcachedDiscoveryClient) RESTClient() rest.Interface {
return c.cl.RESTClient()
}

func (c *memcachedDiscoveryClient) ServerGroups() (*metav1.APIGroupList, error) {
c.lock.Lock()
defer c.lock.Unlock()

var err error
if c.servergroups != nil {
return c.servergroups, nil
}
c.servergroups, err = c.cl.ServerGroups()
return c.servergroups, err
}

func (c *memcachedDiscoveryClient) ServerResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error) {
c.lock.Lock()
defer c.lock.Unlock()

var err error
if v := c.serverresources[groupVersion]; v != nil {
return v, nil
}
c.serverresources[groupVersion], err = c.cl.ServerResourcesForGroupVersion(groupVersion)
return c.serverresources[groupVersion], err
}

func (c *memcachedDiscoveryClient) ServerResources() ([]*metav1.APIResourceList, error) {
return c.cl.ServerResources()
}

func (c *memcachedDiscoveryClient) ServerPreferredResources() ([]*metav1.APIResourceList, error) {
return c.cl.ServerPreferredResources()
}

func (c *memcachedDiscoveryClient) ServerPreferredNamespacedResources() ([]*metav1.APIResourceList, error) {
return c.cl.ServerPreferredNamespacedResources()
}

func (c *memcachedDiscoveryClient) ServerVersion() (*apiVers.Info, error) {
return c.cl.ServerVersion()
}

func (c *memcachedDiscoveryClient) SwaggerSchema(version schema.GroupVersion) (*swagger.ApiDeclaration, error) {
key := version.String()

c.lock.Lock()
defer c.lock.Unlock()

if c.schemas[key] != nil {
return c.schemas[key], nil
}

schema, err := c.cl.SwaggerSchema(version)
if err != nil {
return nil, err
}

c.schemas[key] = schema
return schema, nil
}

func (c *memcachedDiscoveryClient) OpenAPISchema() (*openapi_v2.Document, error) {
c.lock.Lock()
defer c.lock.Unlock()

if c.schema != nil {
return c.schema, nil
}

schema, err := c.cl.OpenAPISchema()
if err != nil {
return nil, err
}

c.schema = schema
return schema, nil
}

// --------------------------------------------------------------------------
// Client utilities.
// --------------------------------------------------------------------------

// clientForResource returns the ResourceClient for a given object
func clientForResource(
pool dynamic.ClientPool, disco discovery.DiscoveryInterface, obj runtime.Object, defNs string,
) (dynamic.ResourceInterface, error) {
gvk := obj.GetObjectKind().GroupVersionKind()
meta, err := meta.Accessor(obj)
if err != nil {
return nil, err
}

namespace := meta.GetNamespace()
if namespace == "" {
namespace = defNs
}

return clientForGVK(pool, disco, gvk, namespace)
}

// clientForResource returns the ResourceClient for a given object
func clientForGVK(
pool dynamic.ClientPool, disco discovery.DiscoveryInterface, gvk schema.GroupVersionKind,
namespace string,
) (dynamic.ResourceInterface, error) {
client, err := pool.ClientForGroupVersionKind(gvk)
if err != nil {
return nil, err
}

resource, err := serverResourceForGVK(disco, gvk)
if err != nil {
return nil, err
}

glog.V(3).Infof("Fetching client for %s namespace=%s", resource, namespace)
rc := client.Resource(resource, namespace)
return rc, nil
}

func serverResourceForGVK(
disco discovery.ServerResourcesInterface, gvk schema.GroupVersionKind,
) (*metav1.APIResource, error) {
resources, err := disco.ServerResourcesForGroupVersion(gvk.GroupVersion().String())
if err != nil {
return nil, fmt.Errorf("unable to fetch resource description for %s: %v", gvk.GroupVersion(), err)
}

for _, r := range resources.APIResources {
if r.Kind == gvk.Kind {
glog.V(3).Infof("Using resource '%s' for %s", r.Name, gvk)
return &r, nil
}
}

return nil, fmt.Errorf("Server is unable to handle %s", gvk)
}

// resourceNameForGVK returns a lowercase plural form of a type, for
// human messages. Returns lowercased kind if discovery lookup fails.
func resourceNameForObj(disco discovery.ServerResourcesInterface, o runtime.Object) string {
return resourceNameForGVK(disco, o.GetObjectKind().GroupVersionKind())
}

// resourceNameForGVK returns a lowercase plural form of a type, for
// human messages. Returns lowercased kind if discovery lookup fails.
func resourceNameForGVK(
disco discovery.ServerResourcesInterface, gvk schema.GroupVersionKind,
) string {
rls, err := disco.ServerResourcesForGroupVersion(gvk.GroupVersion().String())
if err != nil {
glog.V(3).Infof("Discovery failed for %s: %s, falling back to kind", gvk, err)
return strings.ToLower(gvk.Kind)
}

for _, rl := range rls.APIResources {
if rl.Kind == gvk.Kind {
return rl.Name
}
}

glog.V(3).Infof("Discovery failed to find %s, falling back to kind", gvk)
return strings.ToLower(gvk.Kind)
}

// fqObjName returns "namespace.name"
func fqObjName(o metav1.Object) string {
return fqName(o.GetNamespace(), o.GetName())
}

// fqName returns "namespace.name"
func fqName(namespace, name string) string {
if namespace == "" {
return name
}
return fmt.Sprintf("%s.%s", namespace, name)
}
Loading

0 comments on commit e85e033

Please sign in to comment.