-
Notifications
You must be signed in to change notification settings - Fork 115
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement core of pure client-go-based Kubernetes provider
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
Showing
8 changed files
with
745 additions
and
168 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
Oops, something went wrong.