Commit 94db53d0 authored by Victor Agababov's avatar Victor Agababov

Initial commit for DeploymentManager on k8s.

parents
expandybird/pkg/*
expandybird/expansion/*.pyc
resourcifier/pkg/*
resourcifier/bin/*
manager/pkg/*
# Contributing guidelines
## How to become a contributor and submit your own code
### Contributor License Agreements
We'd love to accept your patches! Before we can take them, we have to jump a couple of legal hurdles.
Please fill out either the individual or corporate Contributor License Agreement (CLA).
* If you are an individual writing original source code and you're sure you own the intellectual property, then you'll need to sign an [individual CLA](http://code.google.com/legal/individual-cla-v1.0.html).
* If you work for a company that wants to allow you to contribute your work, then you'll need to sign a [corporate CLA](http://code.google.com/legal/corporate-cla-v1.0.html).
Follow either of the two links above to access the appropriate CLA and instructions for how to sign and return it. Once we receive it, we'll be able to accept your pull requests.
***NOTE***: Only original source code from you and other people that have signed the CLA can be accepted into the main repository.
### Contributing A Patch
1. Submit an issue describing your proposed change to the repo in question.
1. The repo owner will respond to your issue promptly.
1. If your proposed change is accepted, and you haven't already done so, sign a Contributor License Agreement (see details above).
1. Fork the desired repo, develop and test your code changes.
1. Submit a pull request.
This diff is collapsed.
SUBDIRS := expandybird/. resourcifier/. manager/. client/.
TARGETS := all build test push container clean
SUBDIRS_TARGETS := \
$(foreach t,$(TARGETS),$(addsuffix $t,$(SUBDIRS)))
GO_DEPS := util/... version/... expandybird/... resourcifier/... manager/... client/...
.PHONY : all build test clean $(TARGETS) $(SUBDIRS_TARGETS) .project .docker
all: build
clean:
go clean -v $(GO_DEPS)
test: build
-go test -v $(GO_DEPS)
build:
go get -v $(GO_DEPS)
go install -v $(GO_DEPS)
push: container
container: .project .docker
.project:
@if [[ -z "${PROJECT}" ]]; then echo "PROJECT variable must be set"; exit 1; fi
.docker:
@if [[ -z `which docker` ]] || ! docker version &> /dev/null; then echo "docker is not installed correctly"; exit 1; fi
$(TARGETS) : % : $(addsuffix %,$(SUBDIRS))
$(SUBDIRS_TARGETS) :
$(MAKE) -C $(@D) $(@F:.%=%)
# Deployment Manager
Deployment Manager lets you define and deploy simple declarative configuration
for your Kubernetes resources.
You can also use Python or [Jinja](http://jinja.pocoo.org/) to create powerful
parameterizable abstract types called **Templates**. You can create general
abstract building blocks to reuse, like a
[Replicated Service](examples/guestbook/replicatedservice.py), or create
more concrete types like a [Redis cluster](examples/guestbook/redis.jinja).
You can find more examples of Templates and configurations in our
[examples](examples).
Deployment Manager uses the same concepts and languages as
[Google Cloud Deployment Manager](https://cloud.google.com/deployment-manager/overview),
but works directly within your Kubernetes cluster.
## Getting started
For the following steps, it is assumed that you have a Kubernetes cluster up
and running, and that you can run kubectl commands against it. It is also
assumed that you're working with a clone of the repository installed in the src
folder of your GOPATH, and that your PATH contains $GOPATH/bin, per convention.
Since Deployment Manager uses Python and will be running locally on your
machine, you will first need to make sure the necessary Python packages are
installed. This assumes that you have already installed the pip package
management system on your machine.
```
pip install -r expandybird/requirements.txt
```
Next, you'll build and install the binaries, and bootstrap Deployment Manager
into the cluster. Finally, you'll deploy an example application on the
cluster using Deployment Manager.
### Building and installing the binaries
In this step, you're going to build and install the Deployment Manager binaries.
You can do this by running make in the repository root.
```
make
```
### Bootstrapping Deployment Manager
In this step, you're going to bootstrap Deployment Manager into the cluster.
Next, start the three Deployment Manager binaries on localhost using the supplied
bootstrap script.
```
./examples/bootstrap/bootstrap.sh
```
The script starts the following binaries:
* manager (frontend service) running on port 8080
* expandybird (expands templates) running on port 8081
* resourcifier (reifies primitive Kubernetes resources) running on port 8082
It also starts kubectl proxy on port 8001.
Next, use the Deployment Manager running on localhost to deploy itself onto the
cluster using the supplied command line tool and template.
```
client --name test --service=http://localhost:8080 examples/bootstrap/bootstrap.yaml
```
You should now have Deployment Manager running on your cluster, and it should be
visible using kubectl (kubectl get pod,rc,service).
### Deploying your first application (Guestbook)
In this step, you're going to deploy the canonical guestbook example to your
Kubernetes cluster.
```
client --name guestbook --service=http://localhost:8001/api/v1/proxy/namespaces/default/services/manager-service:manager examples/guestbook/guestbook.yaml
```
You should now have guestbook up and running. To verify, get the list of services
running on the cluster:
```
kubectl get service
```
You should see frontend-service running. If your cluster supports external
load balancing, it will have an external IP assigned to it, and you should be
able to navigate to it to see the guestbook in action.
## Building the container images
This project runs Deployment Manager on Kubernetes as three replicated services.
By default, prebuilt images stored in Google Container Registry are used to create
them. However, you can build your own container images and push them to your own
project in the registry.
To build and push your own images to Google Container Registry, first set the
environment variable PROJECT to the name of a project known to gcloud. Then, run
the following command:
```
make push
```
# Makefile for the Docker image gcr.io/$(PROJECT)/expandybird
# MAINTAINER: Jack Greenfield <jackgr@google.com>
# If you update this image please check the tag value before pushing.
.PHONY : all build test push container clean
test: client
client --action=expand test/guestbook.yaml test/replicatedservice.py test/redis.jinja > /dev/null
client:
go get -v ./...
go install -v ./...
/*
Copyright 2015 The Kubernetes Authors All rights reserved.
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 main
import (
"expandybird/expander"
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"strings"
"time"
)
var (
action = flag.String("action", "deploy", "expand | deploy | list | get | delete | update")
name = flag.String("name", "", "Name of template or deployment")
service = flag.String("service", "http://localhost:8080", "URL for deployment manager")
binary = flag.String("binary", "../expandybird/expansion/expansion.py",
"Path to template expansion binary")
)
var usage = func() {
message := "usage: %s [<flags>] (name | (<template> [<import1>...<importN>]))\n"
fmt.Fprintf(os.Stderr, message, os.Args[0])
flag.PrintDefaults()
}
func main() {
flag.Parse()
name := getNameArgument()
switch *action {
case "expand":
backend := expander.NewExpander(*binary)
template := loadTemplate(name)
output, err := backend.ExpandTemplate(template)
if err != nil {
log.Fatalf("cannot expand %s: %s\n", name, err)
}
fmt.Println(output)
case "deploy":
callService("deployments", "POST", name, readTemplate(name))
case "list":
callService("deployments", "GET", name, nil)
case "get":
path := fmt.Sprintf("deployments/%s", name)
callService(path, "GET", name, nil)
case "delete":
path := fmt.Sprintf("deployments/%s", name)
callService(path, "DELETE", name, nil)
case "update":
path := fmt.Sprintf("deployments/%s", name)
callService(path, "PUT", name, readTemplate(name))
}
}
func callService(path, method, name string, reader io.ReadCloser) {
action := strings.ToLower(method)
if action == "post" {
action = "deploy"
}
url := fmt.Sprintf("%s/%s", *service, path)
request, err := http.NewRequest(method, url, reader)
request.Header.Add("Content-Type", "application/json")
response, err := http.DefaultClient.Do(request)
if err != nil {
log.Fatalf("cannot %s template named %s: %s\n", action, name, err)
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
log.Fatalf("cannot %s template named %s: %s\n", action, name, err)
}
if response.StatusCode < http.StatusOK ||
response.StatusCode >= http.StatusMultipleChoices {
message := fmt.Sprintf("status code: %d status: %s", response.StatusCode, response.Status)
log.Fatalf("cannot %s template named %s: %s\n", action, name, message)
}
fmt.Println(string(body))
}
func readTemplate(name string) io.ReadCloser {
return marshalTemplate(loadTemplate(name))
}
func loadTemplate(name string) *expander.Template {
args := flag.Args()
if len(args) < 1 {
usage()
os.Exit(1)
}
var template *expander.Template
var err error
if len(args) == 1 {
template, err = expander.NewTemplateFromRootTemplate(args[0])
} else {
template, err = expander.NewTemplateFromFileNames(args[0], args[1:])
}
if err != nil {
log.Fatalf("cannot create template from supplied file names: %s\n", err)
}
if name != "" {
template.Name = name
}
return template
}
func marshalTemplate(template *expander.Template) io.ReadCloser {
j, err := json.Marshal(template)
if err != nil {
log.Fatalf("cannot deploy template %s: %s\n", template.Name, err)
}
return ioutil.NopCloser(bytes.NewReader(j))
}
func getNameArgument() string {
if *name == "" {
*name = fmt.Sprintf("manifest-%d", time.Now().UTC().UnixNano())
}
return *name
}
######################################################################
# Copyright 2015 The Kubernetes Authors All rights reserved.
# 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.
######################################################################
imports:
- path: replicatedservice.py
resources:
- name: expandybird
type: replicatedservice.py
properties:
service_port: 8081
target_port: 8080
container_port: 8080
external_service: false
replicas: 2
image: gcr.io/PROJECT/expandybird:latest
labels:
app: dm
- name: resourcifier
type: replicatedservice.py
properties:
service_port: 8082
target_port: 8080
container_port: 8080
external_service: false
replicas: 2
image: gcr.io/PROJECT/resourcifier:latest
labels:
app: dm
- name: manager
type: replicatedservice.py
properties:
service_port: 8080
target_port: 8080
container_port: 8080
external_service: true
replicas: 1
image: gcr.io/PROJECT/manager:latest
labels:
app: dm
######################################################################
# Copyright 2015 The Kubernetes Authors All rights reserved.
# 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.
######################################################################
imports:
- path: redis.jinja
- path: replicatedservice.py
resources:
- name: frontend
type: replicatedservice.py
properties:
service_port: 80
container_port: 80
external_service: true
replicas: 3
image: gcr.io/google_containers/example-guestbook-php-redis:v3
- name: redis
type: redis.jinja
properties: null
{% set REDIS_PORT = 6379 %}
{% set WORKERS = properties['workers'] or 2 %}
resources:
- name: redis-master
type: replicatedservice.py
properties:
# This has to be overwritten since service names are hard coded in the code
service_name: redis-master
service_port: {{ REDIS_PORT }}
target_port: {{ REDIS_PORT }}
container_port: {{ REDIS_PORT }}
replicas: 1
container_name: master
image: redis
- name: redis-slave
type: replicatedservice.py
properties:
# This has to be overwritten since service names are hard coded in the code
service_name: redis-slave
service_port: {{ REDIS_PORT }}
container_port: {{ REDIS_PORT }}
replicas: {{ WORKERS }}
container_name: worker
image: kubernetes/redis-slave:v2
# An example of how to specify env variables.
env:
- name: GET_HOSTS_FROM
value: env
- name: REDIS_MASTER_SERVICE_HOST
value: redis-master
######################################################################
# Copyright 2015 The Kubernetes Authors All rights reserved.
# 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.
######################################################################
"""Defines a ReplicatedService type by creating both a Service and an RC.
This module creates a typical abstraction for running a service in a
Kubernetes cluster, namely a replication controller and a service packaged
together into a single unit.
"""
import yaml
SERVICE_TYPE_COLLECTION = 'Service'
RC_TYPE_COLLECTION = 'ReplicationController'
def GenerateConfig(context):
"""Generates a Replication Controller and a matching Service.
Args:
context: Template context, which can contain the following properties:
container_name - Name to use for container. If omitted, name is
used.
namespace - Namespace to create the resources in. If omitted,
'default' is used.
service_name - Name to use for service. If omitted name-service is
used.
protocol - Protocol to use for the service
service_port - Port to use for the service
target_port - Target port for the service
container_port - Container port to use
replicas - Number of replicas to create in RC
image - Docker image to use for replicas. Required.
labels - labels to apply.
env - Environmental variables to apply (list of maps). Format
should be:
[{'name': ENV_VAR_NAME, 'value':'ENV_VALUE'},
{'name': ENV_VAR_NAME_2, 'value':'ENV_VALUE_2'}]
external_service - If set to true, enable external Load Balancer
Returns:
A Container Manifest as a YAML string.
"""
# YAML config that we're going to create for both RC & Service
config = {'resources': []}
name = context.env['name']
container_name = context.properties.get('container_name', name)
namespace = context.properties.get('namespace', 'default')
# Define things that the Service cares about
service_name = context.properties.get('service_name', name + '-service')
service_type = SERVICE_TYPE_COLLECTION
# Define things that the Replication Controller (rc) cares about
rc_name = name + '-rc'
rc_type = RC_TYPE_COLLECTION
service = {
'name': service_name,
'type': service_type,
'properties': {
'apiVersion': 'v1',
'kind': 'Service',
'namespace': namespace,
'metadata': {
'name': service_name,
'labels': GenerateLabels(context, service_name),
},
'spec': {
'ports': [GenerateServicePorts(context, container_name)],
'selector': GenerateLabels(context, name)
}
}
}
set_up_external_lb = context.properties.get('external_service', None)
if set_up_external_lb:
service['properties']['spec']['type'] = 'LoadBalancer'
config['resources'].append(service)
rc = {
'name': rc_name,
'type': rc_type,
'properties': {
'apiVersion': 'v1',
'kind': 'ReplicationController',
'namespace': namespace,
'metadata': {
'name': rc_name,
'labels': GenerateLabels(context, rc_name),
},
'spec': {
'replicas': context.properties['replicas'],
'selector': GenerateLabels(context, name),
'template': {
'metadata': {
'labels': GenerateLabels(context, name),
},
'spec': {
'containers': [
{
'env': GenerateEnv(context),
'name': container_name,
'image': context.properties['image'],
'ports': [
{
'name': container_name,
'containerPort': context.properties['container_port'],
}
]
}
]
}
}
}
}
}
config['resources'].append(rc)
return yaml.dump(config)
# Generates labels either from the context.properties['labels'] or generates
# a default label 'name':name
def GenerateLabels(context, name):
"""Generates labels from context.properties['labels'] or creates default.
We make a deep copy of the context.properties['labels'] section to avoid
linking in the yaml document, which I believe reduces readability of the
expanded template. If no labels are given, generate a default 'name':name.
Args:
context: Template context, which can contain the following properties:
labels - Labels to generate
Returns:
A dict containing labels in a name:value format
"""
tmp_labels = context.properties.get('labels', None)
ret_labels = {'name': name}
if isinstance(tmp_labels, dict):
for key, value in tmp_labels.iteritems():
ret_labels[key] = value
return ret_labels
def GenerateServicePorts(context, name):
"""Generates a ports section for a service.
Args:
context: Template context, which can contain the following properties:
service_port - Port to use for the service
target_port - Target port for the service
protocol - Protocol to use.
Returns:
A dict containing a port definition
"""
service_port = context.properties.get('service_port', None)
target_port = context.properties.get('target_port', None)
protocol = context.properties.get('protocol')
ports = {}
if name:
ports['name'] = name
if service_port:
ports['port'] = service_port
if target_port:
ports['targetPort'] = target_port
if protocol:
ports['protocol'] = protocol
return ports
def GenerateEnv(context):
"""Generates environmental variables for a pod.
Args:
context: Template context, which can contain the following properties:
env - Environment variables to set.
Returns:
A list containing env variables in dict format {name: 'name', value: 'value'}
"""
env = []
tmp_env = context.properties.get('env', [])
for entry in tmp_env:
if isinstance(entry, dict):
env.append({'name': entry.get('name'), 'value': entry.get('value')})
return env
This diff is collapsed.
#!/bin/bash
KUBECTL=`which kubectl`
if [[ -z $KUBECTL ]] ; then
echo Cannot find kubectl
exit 1
fi
echo "Starting resourcifier..."
RESOURCIFIER=`which resourcifier`
if [[ -z $RESOURCIFIER ]] ; then
echo Cannot find resourcifier
exit 1
fi
pkill -f $RESOURCIFIER
$RESOURCIFIER > resourcifier.log 2>&1 --kubectl=$KUBECTL --port=8082 &
echo
echo "Starting expandybird..."
EXPANDYBIRD=`which expandybird`
if [[ -z $EXPANDYBIRD ]] ; then
echo Cannot find expandybird
exit 1
fi
pkill -f $EXPANDYBIRD
$EXPANDYBIRD > expandybird.log 2>&1 --port=8081 --expansion_binary=expandybird/expansion/expansion.py &
echo
echo "Starting deployment manager..."
MANAGER=`which manager`
if [[ -z $MANAGER ]] ; then
echo Cannot find manager
exit 1
fi
pkill -f $MANAGER
$MANAGER > manager.log 2>&1 --port=8080 --expanderURL=http://localhost:8081 --deployerURL=http://localhost:8082 &
echo
echo "Starting kubectl proxy..."
pkill -f "$KUBECTL proxy"
$KUBECTL proxy --port=8001 &
sleep 1s
echo
echo "Done."
imports:
- path: replicatedservice.py
resources:
- name: expandybird
type: replicatedservice.py
properties:
service_port: 8081
target_port: 8080
container_port: 8080
external_service: false
replicas: 2
image: gcr.io/dm-k8s-testing/expandybird:latest
labels:
app: dm
- name: resourcifier
type: replicatedservice.py
properties:
service_port: 8082
target_port: 8080
container_port: 8080
external_service: false
replicas: 2
image: gcr.io/dm-k8s-testing/resourcifier:latest
labels:
app: dm
- name: manager
type: replicatedservice.py
properties:
service_port: 8080
target_port: 8080
container_port: 8080
external_service: false
replicas: 1
image: gcr.io/dm-k8s-testing/manager:latest
labels:
app: dm
"""Defines a ReplicatedService type by creating both a Service and an RC.
This module creates a typical abstraction for running a service in a
Kubernetes cluster, namely a replication controller and a service packaged
together into a single unit.
"""
import yaml
SERVICE_TYPE_COLLECTION = 'Service'
RC_TYPE_COLLECTION = 'ReplicationController'
def GenerateConfig(context):
"""Generates a Replication Controller and a matching Service.
Args:
context: Template context, which can contain the following properties:
container_name - Name to use for container. If omitted, name is
used.
namespace - Namespace to create the resources in. If omitted,
'default' is used.
service_name - Name to use for service. If omitted name-service is
used.
protocol - Protocol to use for the service
service_port - Port to use for the service
target_port - Target port for the service
container_port - Container port to use
replicas - Number of replicas to create in RC
image - Docker image to use for replicas. Required.
labels - labels to apply.
env - Environmental variables to apply (list of maps). Format
should be:
[{'name': ENV_VAR_NAME, 'value':'ENV_VALUE'},
{'name': ENV_VAR_NAME_2, 'value':'ENV_VALUE_2'}]
external_service - If set to true, enable external Load Balancer
Returns:
A Container Manifest as a YAML string.
"""
# YAML config that we're going to create for both RC & Service
config = {'resources': []}
name = context.env['name']
container_name = context.properties.get('container_name', name)
namespace = context.properties.get('namespace', 'default')
# Define things that the Service cares about
service_name = context.properties.get('service_name', name + '-service')
service_type = SERVICE_TYPE_COLLECTION
# Define things that the Replication Controller (rc) cares about
rc_name = name + '-rc'
rc_type = RC_TYPE_COLLECTION
service = {
'name': service_name,
'type': service_type,
'properties': {
'apiVersion': 'v1',
'kind': 'Service',
'namespace': namespace,
'metadata': {
'name': service_name,
'labels': GenerateLabels(context, service_name),
},
'spec': {
'ports': [GenerateServicePorts(context, container_name)],
'selector': GenerateLabels(context, name)
}
}
}
set_up_external_lb = context.properties.get('external_service', None)
if set_up_external_lb:
service['properties']['spec']['type'] = 'LoadBalancer'
config['resources'].append(service)
rc = {
'name': rc_name,
'type': rc_type,
'properties': {
'apiVersion': 'v1',
'kind': 'ReplicationController',
'namespace': namespace,
'metadata': {
'name': rc_name,
'labels': GenerateLabels(context, rc_name),
},
'spec': {
'replicas': context.properties['replicas'],
'selector': GenerateLabels(context, name),
'template': {
'metadata': {
'labels': GenerateLabels(context, name),
},
'spec': {
'containers': [
{
'env': GenerateEnv(context),
'name': container_name,
'image': context.properties['image'],
'ports': [
{
'name': container_name,
'containerPort': context.properties['container_port'],
}
]
}
]
}
}
}
}
}
config['resources'].append(rc)
return yaml.dump(config)
# Generates labels either from the context.properties['labels'] or generates
# a default label 'name':name
def GenerateLabels(context, name):
"""Generates labels from context.properties['labels'] or creates default.
We make a deep copy of the context.properties['labels'] section to avoid
linking in the yaml document, which I believe reduces readability of the
expanded template. If no labels are given, generate a default 'name':name.
Args:
context: Template context, which can contain the following properties:
labels - Labels to generate
Returns:
A dict containing labels in a name:value format
"""
tmp_labels = context.properties.get('labels', None)
ret_labels = {'name': name}
if isinstance(tmp_labels, dict):
for key, value in tmp_labels.iteritems():
ret_labels[key] = value
return ret_labels
def GenerateServicePorts(context, name):
"""Generates a ports section for a service.
Args:
context: Template context, which can contain the following properties:
service_port - Port to use for the service
target_port - Target port for the service
protocol - Protocol to use.
Returns:
A dict containing a port definition
"""
service_port = context.properties.get('service_port', None)
target_port = context.properties.get('target_port', None)
protocol = context.properties.get('protocol')
ports = {}
if name:
ports['name'] = name
if service_port:
ports['port'] = service_port
if target_port:
ports['targetPort'] = target_port
if protocol:
ports['protocol'] = protocol
return ports
def GenerateEnv(context):
"""Generates environmental variables for a pod.
Args:
context: Template context, which can contain the following properties:
env - Environment variables to set.
Returns:
A list containing env variables in dict format {name: 'name', value: 'value'}
"""
env = []
tmp_env = context.properties.get('env', [])
for entry in tmp_env:
if isinstance(entry, dict):
env.append({'name': entry.get('name'), 'value': entry.get('value')})
return env
imports:
- path: redis.jinja
- path: replicatedservice.py
resources:
- name: frontend
type: replicatedservice.py
properties:
service_port: 80
container_port: 80
external_service: true
replicas: 3
image: gcr.io/google_containers/example-guestbook-php-redis:v3
- name: redis
type: redis.jinja
properties: null
{% set REDIS_PORT = 6379 %}
{% set WORKERS = properties['workers'] or 2 %}
resources:
- name: redis-master
type: replicatedservice.py
properties:
# This has to be overwritten since service names are hard coded in the code
service_name: redis-master
service_port: {{ REDIS_PORT }}
target_port: {{ REDIS_PORT }}
container_port: {{ REDIS_PORT }}
replicas: 1
container_name: master
image: redis
- name: redis-slave
type: replicatedservice.py
properties:
# This has to be overwritten since service names are hard coded in the code
service_name: redis-slave
service_port: {{ REDIS_PORT }}
container_port: {{ REDIS_PORT }}
replicas: {{ WORKERS }}
container_name: worker
image: kubernetes/redis-slave:v2
# An example of how to specify env variables.
env:
- name: GET_HOSTS_FROM
value: env
- name: REDIS_MASTER_SERVICE_HOST
value: redis-master
"""Defines a ReplicatedService type by creating both a Service and an RC.
This module creates a typical abstraction for running a service in a
Kubernetes cluster, namely a replication controller and a service packaged
together into a single unit.
"""
import yaml
SERVICE_TYPE_COLLECTION = 'Service'
RC_TYPE_COLLECTION = 'ReplicationController'
def GenerateConfig(context):
"""Generates a Replication Controller and a matching Service.
Args:
context: Template context, which can contain the following properties:
container_name - Name to use for container. If omitted, name is
used.
namespace - Namespace to create the resources in. If omitted,
'default' is used.
service_name - Name to use for service. If omitted name-service is
used.
protocol - Protocol to use for the service
service_port - Port to use for the service
target_port - Target port for the service
container_port - Container port to use
replicas - Number of replicas to create in RC
image - Docker image to use for replicas. Required.
labels - labels to apply.
env - Environmental variables to apply (list of maps). Format
should be:
[{'name': ENV_VAR_NAME, 'value':'ENV_VALUE'},
{'name': ENV_VAR_NAME_2, 'value':'ENV_VALUE_2'}]
external_service - If set to true, enable external Load Balancer
Returns:
A Container Manifest as a YAML string.
"""
# YAML config that we're going to create for both RC & Service
config = {'resources': []}
name = context.env['name']
container_name = context.properties.get('container_name', name)
namespace = context.properties.get('namespace', 'default')
# Define things that the Service cares about
service_name = context.properties.get('service_name', name + '-service')
service_type = SERVICE_TYPE_COLLECTION
# Define things that the Replication Controller (rc) cares about
rc_name = name + '-rc'
rc_type = RC_TYPE_COLLECTION
service = {
'name': service_name,
'type': service_type,
'properties': {
'apiVersion': 'v1',
'kind': 'Service',
'namespace': namespace,
'metadata': {
'name': service_name,
'labels': GenerateLabels(context, service_name),
},
'spec': {
'ports': [GenerateServicePorts(context, container_name)],
'selector': GenerateLabels(context, name)
}
}
}
set_up_external_lb = context.properties.get('external_service', None)
if set_up_external_lb:
service['properties']['spec']['type'] = 'LoadBalancer'
config['resources'].append(service)
rc = {
'name': rc_name,
'type': rc_type,
'properties': {
'apiVersion': 'v1',
'kind': 'ReplicationController',
'namespace': namespace,
'metadata': {
'name': rc_name,
'labels': GenerateLabels(context, rc_name),
},
'spec': {
'replicas': context.properties['replicas'],
'selector': GenerateLabels(context, name),
'template': {
'metadata': {
'labels': GenerateLabels(context, name),
},
'spec': {
'containers': [
{
'env': GenerateEnv(context),
'name': container_name,
'image': context.properties['image'],
'ports': [
{
'name': container_name,
'containerPort': context.properties['container_port'],
}
]
}
]
}
}
}
}
}
config['resources'].append(rc)
return yaml.dump(config)
# Generates labels either from the context.properties['labels'] or generates
# a default label 'name':name
def GenerateLabels(context, name):
"""Generates labels from context.properties['labels'] or creates default.
We make a deep copy of the context.properties['labels'] section to avoid
linking in the yaml document, which I believe reduces readability of the
expanded template. If no labels are given, generate a default 'name':name.
Args:
context: Template context, which can contain the following properties:
labels - Labels to generate
Returns:
A dict containing labels in a name:value format
"""
tmp_labels = context.properties.get('labels', None)
ret_labels = {'name': name}
if isinstance(tmp_labels, dict):
for key, value in tmp_labels.iteritems():
ret_labels[key] = value
return ret_labels
def GenerateServicePorts(context, name):
"""Generates a ports section for a service.
Args:
context: Template context, which can contain the following properties:
service_port - Port to use for the service
target_port - Target port for the service
protocol - Protocol to use.
Returns:
A dict containing a port definition
"""
service_port = context.properties.get('service_port', None)
target_port = context.properties.get('target_port', None)
protocol = context.properties.get('protocol')
ports = {}
if name:
ports['name'] = name
if service_port:
ports['port'] = service_port
if target_port:
ports['targetPort'] = target_port
if protocol:
ports['protocol'] = protocol
return ports
def GenerateEnv(context):
"""Generates environmental variables for a pod.
Args:
context: Template context, which can contain the following properties:
env - Environment variables to set.
Returns:
A list containing env variables in dict format {name: 'name', value: 'value'}
"""
env = []
tmp_env = context.properties.get('env', [])
for entry in tmp_env:
if isinstance(entry, dict):
env.append({'name': entry.get('name'), 'value': entry.get('value')})
return env
FROM python:2-onbuild
RUN ln -s /usr/local/bin/python /usr/bin/python
RUN mkdir -p /var/expandybird/expansion
WORKDIR /var/expandybird
ADD expandybird ./expandybird
ADD expansion ./expansion
ADD requirements.txt ./requirements.txt
RUN pip install -r ./requirements.txt
EXPOSE 8080
ENTRYPOINT ["./expandybird"]
# Makefile for the Docker image gcr.io/$(PROJECT)/expandybird
# MAINTAINER: Jack Greenfield <jackgr@google.com>
# If you update this image please check the tag value before pushing.
.PHONY : all build test push container clean
PREFIX := gcr.io/$(PROJECT)
IMAGE := expandybird
TAG := latest
DIR := .
push: container
gcloud docker push $(PREFIX)/$(IMAGE):$(TAG)
container: expandybird
cp $(shell which expandybird) .
docker build -t $(PREFIX)/$(IMAGE):$(TAG) $(DIR)
rm -f expandybird
expandybird:
go get -v ./...
go install -v ./...
clean:
-docker rmi $(PREFIX)/$(IMAGE):$(TAG)
rm -f expandybird
/*
Copyright 2015 The Kubernetes Authors All rights reserved.
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 expander
import (
"path/filepath"
"bytes"
"fmt"
"io/ioutil"
"log"
"os/exec"
"path"
"github.com/ghodss/yaml"
)
// Expander abstracts interactions with the expander and deployer services.
type Expander interface {
ExpandTemplate(template *Template) (string, error)
}
type expander struct {
ExpansionBinary string
}
// NewExpander returns a new initialized Expander.
func NewExpander(binary string) Expander {
return &expander{binary}
}
// ImportFile describes a file that we import into our templates
// TODO: Encode the Content so that it doesn't get mangled.
type ImportFile struct {
Name string `json:"name,omitempty"`
Content string `json:"content"`
}
// A Template defines a single deployment.
type Template struct {
Name string `json:"name"`
Content string `json:"content"`
Imports []*ImportFile `json:"imports"`
}
// NewTemplateFromRootTemplate creates and returns a new template whose content
// and imported files are constructed from reading the root template, parsing out
// the imports section and reading the imports from there
func NewTemplateFromRootTemplate(templateFileName string) (*Template, error) {
templateDir := filepath.Dir(templateFileName)
content, err := ioutil.ReadFile(templateFileName)
if err != nil {
return nil, fmt.Errorf("cannot read template file (%s): %s", err, templateFileName)
}
var c map[string]interface{}
err = yaml.Unmarshal([]byte(content), &c)
if err != nil {
log.Fatalf("Cannot parse template: %v", err)
}
// For each of the imports, grab the import file
var imports []string
if c["imports"] != nil {
for _, importFile := range c["imports"].([]interface{}) {
var fileName = importFile.(map[string]interface{})["path"].(string)
imports = append(imports, templateDir+"/"+fileName)
}
}
return NewTemplateFromFileNames(templateFileName, imports[0:])
}
// NewTemplateFromFileNames creates and returns a new template whose content
// and imported files are read from the supplied file names.
func NewTemplateFromFileNames(
templateFileName string,
importFileNames []string,
) (*Template, error) {
name := path.Base(templateFileName)
content, err := ioutil.ReadFile(templateFileName)
if err != nil {
return nil, fmt.Errorf("cannot read template file (%s): %s", err, templateFileName)
}
imports := []*ImportFile{}
for _, importFileName := range importFileNames {
importFileData, err := ioutil.ReadFile(importFileName)
if err != nil {
return nil, fmt.Errorf("cannot read import file (%s): %s", err, importFileName)
}
imports = append(imports,
&ImportFile{
Name: path.Base(importFileName),
Content: string(importFileData),
})
}
return &Template{
Name: name,
Content: string(content),
Imports: imports,
}, nil
}
// ExpansionResult describes the unmarshalled output of ExpandTemplate.
type ExpansionResult struct {
Config map[string]interface{}
Layout map[string]interface{}
}
// NewExpansionResult creates and returns a new expansion result from
// the raw output of ExpandTemplate.
func NewExpansionResult(output string) (*ExpansionResult, error) {
eResponse := &ExpansionResult{}
if err := yaml.Unmarshal([]byte(output), eResponse); err != nil {
return nil, fmt.Errorf("cannot unmarshal expansion result (%s):\n%s", err, output)
}
return eResponse, nil
}
// Marshal creates and returns an ExpansionResponse from an ExpansionResult.
func (eResult *ExpansionResult) Marshal() (*ExpansionResponse, error) {
configYaml, err := yaml.Marshal(eResult.Config)
if err != nil {
return nil, fmt.Errorf("cannot marshal manifest template (%s):\n%s", err, eResult.Config)
}
layoutYaml, err := yaml.Marshal(eResult.Layout)
if err != nil {
return nil, fmt.Errorf("cannot marshal manifest layout (%s):\n%s", err, eResult.Layout)
}
return &ExpansionResponse{
Config: string(configYaml),
Layout: string(layoutYaml),
}, nil
}
// ExpansionResponse describes the results of marshaling an ExpansionResult.
type ExpansionResponse struct {
Config string `json:"config"`
Layout string `json:"layout"`
}
// NewExpansionResponse creates and returns a new expansion response from
// the raw output of ExpandTemplate.
func NewExpansionResponse(output string) (*ExpansionResponse, error) {
eResult, err := NewExpansionResult(output)
if err != nil {
return nil, err
}
eResponse, err := eResult.Marshal()
if err != nil {
return nil, err
}
return eResponse, nil
}
// Unmarshal creates and returns an ExpansionResult from an ExpansionResponse.
func (eResponse *ExpansionResponse) Unmarshal() (*ExpansionResult, error) {
var config map[string]interface{}
if err := yaml.Unmarshal([]byte(eResponse.Config), &config); err != nil {
return nil, fmt.Errorf("cannot unmarshal config (%s):\n%s", err, eResponse.Config)
}
var layout map[string]interface{}
if err := yaml.Unmarshal([]byte(eResponse.Layout), &layout); err != nil {
return nil, fmt.Errorf("cannot unmarshal layout (%s):\n%s", err, eResponse.Layout)
}
return &ExpansionResult{
Config: config,
Layout: layout,
}, nil
}
// ExpandTemplate passes the given configuration to the expander and returns the
// expanded configuration as a string on success.
func (e *expander) ExpandTemplate(template *Template) (string, error) {
if e.ExpansionBinary == "" {
message := fmt.Sprintf("expansion binary cannot be empty")
return "", fmt.Errorf("error expanding template %s: %s", template.Name, message)
}
// Those are automatically increasing buffers, so writing arbitrary large
// data here won't block the child process.
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd := &exec.Cmd{
Path: e.ExpansionBinary,
// Note, that binary name still has to be passed argv[0].
Args: []string{e.ExpansionBinary, template.Content},
// TODO(vagababov): figure out whether do we even need "PROJECT" and
// "DEPLOYMENT_NAME" variables here.
Env: []string{
"PROJECT=" + template.Name,
"DEPLOYMENT_NAME=" + template.Name,
},
Stdout: &stdout,
Stderr: &stderr,
}
for _, imp := range template.Imports {
cmd.Args = append(cmd.Args, imp.Name, imp.Content)
}
if err := cmd.Start(); err != nil {
log.Printf("error starting expansion process: %s", err)
return "", err
}
cmd.Wait()
log.Printf("Expansion process: pid: %d SysTime: %v UserTime: %v", cmd.ProcessState.Pid(),
cmd.ProcessState.SystemTime(), cmd.ProcessState.UserTime())
if stderr.String() != "" {
return "", fmt.Errorf("error expanding template %s: %s", template.Name, stderr.String())
}
return stdout.String(), nil
}
/*
Copyright 2015 The Kubernetes Authors All rights reserved.
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 expander
import (
"fmt"
"io/ioutil"
"reflect"
"strings"
"testing"
)
const invalidFileName = "afilethatdoesnotexist"
var importFileNames = []string{
"../test/replicatedservice.py",
}
var outputFileName = "../test/ExpectedOutput.yaml"
type ExpanderTestCase struct {
Description string
TemplateFileName string
ImportFileNames []string
ExpectedError string
}
func (etc *ExpanderTestCase) GetTemplate(t *testing.T) *Template {
template, err := NewTemplateFromFileNames(etc.TemplateFileName, etc.ImportFileNames)
if err != nil {
t.Errorf("cannot create template for test case '%s': %s\n", etc.Description, err)
}
return template
}
func GetOutputString(t *testing.T, description string) string {
output, err := ioutil.ReadFile(outputFileName)
if err != nil {
t.Errorf("cannot read output file for test case '%s': %s\n", description, err)
}
return string(output)
}
func TestNewTemplateFromFileNames(t *testing.T) {
if _, err := NewTemplateFromFileNames(invalidFileName, importFileNames); err == nil {
t.Errorf("expected error did not occur for invalid template file name")
}
_, err := NewTemplateFromFileNames(invalidFileName, []string{"afilethatdoesnotexist"})
if err == nil {
t.Errorf("expected error did not occur for invalid import file names")
}
}
var ExpanderTestCases = []ExpanderTestCase{
{
"expect error for invalid file name",
"../test/InvalidFileName.yaml",
importFileNames,
"ExpansionError: Exception",
},
{
"expect error for invalid property",
"../test/InvalidProperty.yaml",
importFileNames,
"ExpansionError: Exception",
},
{
"expect error for malformed content",
"../test/MalformedContent.yaml",
importFileNames,
"ExpansionError: Error parsing YAML: mapping values are not allowed here",
},
{
"expect error for missing imports",
"../test/MissingImports.yaml",
importFileNames,
"ExpansionError: Exception",
},
{
"expect error for missing resource name",
"../test/MissingResourceName.yaml",
importFileNames,
"ExpansionError: Resource does not have a name",
},
{
"expect error for missing type name",
"../test/MissingTypeName.yaml",
importFileNames,
"ExpansionError: Resource does not have type defined",
},
{
"expect success",
"../test/ValidContent.yaml",
importFileNames,
"",
},
}
func TestExpandTemplate(t *testing.T) {
backend := NewExpander("../expansion/expansion.py")
for _, etc := range ExpanderTestCases {
template := etc.GetTemplate(t)
actualOutput, err := backend.ExpandTemplate(template)
if err != nil {
message := err.Error()
if !strings.Contains(message, etc.ExpectedError) {
t.Errorf("error in test case '%s': %s\n", etc.Description, message)
}
} else {
if etc.ExpectedError != "" {
t.Errorf("expected error did not occur in test case '%s': %s\n",
etc.Description, etc.ExpectedError)
}
actualResult, err := NewExpansionResult(actualOutput)
if err != nil {
t.Errorf("error in test case '%s': %s\n", etc.Description, err)
}
expectedOutput := GetOutputString(t, etc.Description)
expectedResult, err := NewExpansionResult(expectedOutput)
if err != nil {
t.Errorf("error in test case '%s': %s\n", etc.Description, err)
}
if !reflect.DeepEqual(actualResult, expectedResult) {
message := fmt.Sprintf("want: %s\nhave: %s\n", expectedOutput, actualOutput)
t.Errorf("error in test case '%s': %s\n", etc.Description, message)
}
}
}
}
This diff is collapsed.
######################################################################
# Copyright 2015 The Kubernetes Authors All rights reserved.
# 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.
######################################################################
"""App allowing expansion from file names instead of cmdline arguments."""
import os.path
import sys
from expansion import Expand
def main():
if len(sys.argv) < 2:
print >>sys.stderr, 'No template specified.'
sys.exit(1)
template = ''
imports = {}
try:
with open(sys.argv[1]) as f:
template = f.read()
for imp in sys.argv[2:]:
import_contents = ''
with open(imp) as f:
import_contents = f.read()
import_name = os.path.basename(imp)
imports[import_name] = import_contents
except IOError as e:
print 'IOException: ', str(e)
sys.exit(1)
env = {}
env['deployment'] = os.environ['DEPLOYMENT_NAME']
env['project'] = os.environ['PROJECT']
validate_schema = 'VALIDATE_SCHEMA' in os.environ
print Expand(template, imports, env=env, validate_schema=validate_schema)
if __name__ == '__main__':
main()
######################################################################
# Copyright 2015 The Kubernetes Authors All rights reserved.
# 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.
######################################################################
"""Loader for loading modules from a user provided dictionary of imports."""
import imp
from os import sep
import os.path
import sys
class AllowedImportsLoader(object):
# Dictionary with modules loaded from user provided imports
user_modules = {}
@staticmethod
def get_filename(name):
return '%s.py' % name.replace('.', '/')
def load_module(self, name, etc=None): # pylint: disable=unused-argument
"""Implements loader.load_module() for loading user provided imports."""
if name in AllowedImportsLoader.user_modules:
return AllowedImportsLoader.user_modules[name]
module = imp.new_module(name)
try:
data = FileAccessRedirector.allowed_imports[self.get_filename(name)]
except Exception: # pylint: disable=broad-except
return None
# Run the module code.
exec data in module.__dict__ # pylint: disable=exec-used
AllowedImportsLoader.user_modules[name] = module
# We need to register the module in module registry, since new_module
# doesn't do this, but we need it for hierarchical references.
sys.modules[name] = module
# If this module has children load them recursively.
if name in FileAccessRedirector.parents:
for child in FileAccessRedirector.parents[name]:
full_name = name + '.' + child
self.load_module(full_name)
# If we have helpers/common.py package, then for it to be successfully
# resolved helpers.common name must resolvable, hence, once we load
# child package we attach it to parent module immeadiately.
module.__dict__[child] = AllowedImportsLoader.user_modules[full_name]
return module
class AllowedImportsHandler(object):
def find_module(self, name, path=None): # pylint: disable=unused-argument
filename = AllowedImportsLoader.get_filename(name)
if filename in FileAccessRedirector.allowed_imports:
return AllowedImportsLoader()
else:
return None
def process_imports(imports):
"""Processes the imports by copying them and adding necessary parent packages.
Copies the imports and then for all the hierarchical packages creates
dummy entries for those parent packages, so that hierarchical imports
can be resolved. In the process parent child relationship map is built.
For example: helpers/extra/common.py will generate helpers, helpers.extra
and helpers.extra.common packages along with related .py files.
Args:
imports: map of files to their relative paths.
Returns:
dictionary of imports to their contents and parent-child pacakge
relationship map.
"""
# First clone all the existing ones.
ret = {}
parents = {}
for k in imports:
ret[k] = imports[k]
# Now build the hierarchical modules.
for k in imports.keys():
if imports[k].endswith('.jinja'):
continue
# Normalize paths and trim .py extension, if any.
normalized = os.path.splitext(os.path.normpath(k))[0]
# If this is actually a path and not an absolute name, split it and process
# the hierarchical packages.
if sep in normalized:
parts = normalized.split(sep)
# Create dummy file entries for package levels and also retain
# parent-child relationships.
for i in xrange(0, len(parts)-1):
# Generate the partial package path.
path = os.path.join(parts[0], *parts[1:i+1])
# __init__.py file might have been provided and non-empty by the user.
if path not in ret:
# exec requires at least new line to be present to successfully
# compile the file.
ret[path + '.py'] = '\n'
else:
# To simplify our code, we'll store both versions in that case, since
# loader code expects files with .py extension.
ret[path + '.py'] = ret[path]
# Generate fully qualified package name.
fqpn = '.'.join(parts[0:i+1])
if fqpn in parents:
parents[fqpn].append(parts[i+1])
else:
parents[fqpn] = [parts[i+1]]
return ret, parents
class FileAccessRedirector(object):
# Dictionary with user provided imports.
allowed_imports = {}
# Dictionary that shows parent child relationships, key is the parent, value
# is the list of child packages.
parents = {}
@staticmethod
def redirect(imports):
"""Restricts imports and builtin 'open' to the set of user provided imports.
Imports already available in sys.modules will continue to be available.
Args:
imports: map from string to string, the map of imported files names
and contents.
"""
if imports is not None:
imps, parents = process_imports(imports)
FileAccessRedirector.allowed_imports = imps
FileAccessRedirector.parents = parents
# Prepend our module handler before standard ones.
sys.meta_path = [AllowedImportsHandler()] + sys.meta_path
######################################################################
# Copyright 2015 The Kubernetes Authors All rights reserved.
# 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.
######################################################################
"""Validation of Template properties for deployment manager v2."""
import jsonschema
import yaml
import schema_validation_utils
IMPORTS = "imports"
PROPERTIES = "properties"
# This validator will set default values in properties.
# This does not return a complete set of errors; use only for setting defaults.
# Pass this object a schema to get a validator for that schema.
DEFAULT_VALIDATOR = schema_validation_utils.OnlyValidateProperties(
schema_validation_utils.ExtendWithDefault(jsonschema.Draft4Validator))
# This is a regular validator, use after using the DEFAULT_VALIDATOR
# Pass this object a schema to get a validator for that schema.
VALIDATOR = schema_validation_utils.OnlyValidateProperties(
jsonschema.Draft4Validator)
# This is a validator using the default Draft4 metaschema,
# use it to validate user schemas.
SCHEMA_VALIDATOR = jsonschema.Draft4Validator(
jsonschema.Draft4Validator.META_SCHEMA)
# JsonSchema to be used to validate the user's "imports:" section
IMPORT_SCHEMA = """
properties:
imports:
type: array
items:
type: object
required:
- path
properties:
path:
type: string
name:
type: string
additionalProperties: false
uniqueItems: true
"""
# Validator to be used against the "imports:" section of a schema
IMPORT_SCHEMA_VALIDATOR = jsonschema.Draft4Validator(
yaml.safe_load(IMPORT_SCHEMA))
def Validate(properties, schema_name, template_name, imports):
"""Given a set of properties, validates it against the given schema.
Args:
properties: dict, the properties to be validated
schema_name: name of the schema file to validate
template_name: name of the template whose's properties are being validated
imports: map from string to string, the map of imported files names
and contents
Returns:
Dict containing the validated properties, with defaults filled in
Raises:
ValidationErrors: A list of ValidationError errors that occurred when
validating the properties and schema
"""
if schema_name not in imports:
raise ValidationErrors(schema_name, template_name,
["Could not find schema file '"
+ schema_name + "'."])
else:
raw_schema = imports[schema_name]
if properties is None:
properties = {}
schema = yaml.safe_load(raw_schema)
# If the schema is empty, do nothing.
if schema is None:
return properties
schema_errors = []
validating_imports = IMPORTS in schema and schema[IMPORTS]
# Validate the syntax of the optional "imports:" section of the schema
if validating_imports:
schema_errors.extend(list(IMPORT_SCHEMA_VALIDATOR.iter_errors(schema)))
# Validate the syntax of the optional "properties:" section of the schema
if PROPERTIES in schema and schema[PROPERTIES]:
try:
schema_errors.extend(
list(SCHEMA_VALIDATOR.iter_errors(schema[PROPERTIES])))
except jsonschema.RefResolutionError as e:
# Calls to iter_errors could throw a RefResolution exception
raise ValidationErrors(schema_name, template_name,
list(e), is_schema_error=True)
if schema_errors:
raise ValidationErrors(schema_name, template_name,
schema_errors, is_schema_error=True)
######
# Assume we have a valid schema
######
errors = []
# Validate that all files specified as "imports:" were included
if validating_imports:
# We have already validated that "imports:"
# is a list of unique "path/name" maps
for import_object in schema[IMPORTS]:
if "name" in import_object:
import_name = import_object["name"]
else:
import_name = import_object["path"]
if import_name not in imports:
errors.append(("File '" + import_name + "' requested in schema '"
+ schema_name + "' but not included with imports."))
try:
# This code block uses DEFAULT_VALIDATOR and VALIDATOR for two very
# different purposes.
# DEFAULT_VALIDATOR is based on JSONSchema 4, but uses modified validators:
# - The 'required' validator does nothing
# - The 'properties' validator sets default values on user properties
# With these changes, the validator does not report errors correctly.
#
# So, we do error reporting in two steps:
# 1) Use DEFAULT_VALIDATOR to set default values in the user's properties
# 2) Use the unmodified VALIDATOR to report all of the errors
# Calling iter_errors mutates properties in place, adding default values.
# You must call list()! This is a generator, not a function!
list(DEFAULT_VALIDATOR(schema).iter_errors(properties))
# Now that we have default values, validate the properties
errors.extend(list(VALIDATOR(schema).iter_errors(properties)))
if errors:
raise ValidationErrors(schema_name, template_name, errors)
except jsonschema.RefResolutionError as e:
# Calls to iter_errors could throw a RefResolution exception
raise ValidationErrors(schema_name, template_name,
list(e), is_schema_error=True)
except TypeError as e:
raise ValidationErrors(
schema_name, template_name,
[e, "Perhaps you forgot to put 'quotes' around your reference."],
is_schema_error=True)
return properties
class ValidationErrors(Exception):
"""Exception raised for errors during validation process.
The errors could have occured either in the schema xor in the properties
Attributes:
is_schema_error: Boolean, either an invalid schema, or invalid properties
errors: List of ValidationError type objects
"""
def BuildMessage(self):
"""Builds a human readable message from a list of jsonschema errors.
Returns:
A string in a human readable message format.
"""
if self.is_schema_error:
message = "Invalid schema '%s':\n" % self.schema_name
else:
message = "Invalid properties for '%s':\n" % self.template_name
for error in self.errors:
if type(error) is jsonschema.exceptions.ValidationError:
error_message = error.message
location = list(error.path)
if location and len(location):
error_message += " at " + str(location)
# If location is empty the error happened at the root of the schema
else:
error_message = str(error)
message += error_message + "\n"
return message
def __init__(self, schema_name, template_name, errors, is_schema_error=False):
self.schema_name = schema_name
self.template_name = template_name
self.errors = errors
self.is_schema_error = is_schema_error
self.message = self.BuildMessage()
super(ValidationErrors, self).__init__(self.message)
######################################################################
# Copyright 2015 The Kubernetes Authors All rights reserved.
# 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.
######################################################################
"""Helper functions for Schema Validation."""
import jsonschema
DEFAULT = "default"
PROPERTIES = "properties"
REF = "$ref"
REQUIRED = "required"
def OnlyValidateProperties(validator_class):
"""Takes a validator and makes it process only the 'properties' top level.
Args:
validator_class: A class to add a new validator to
Returns:
A validator_class that will validate properties against things
under the top level "properties" field
"""
def PropertiesValidator(unused_validator, inputs, instance, schema):
if inputs is None:
inputs = {}
for error in validator_class(schema).iter_errors(instance, inputs):
yield error
# This makes sure the only keyword jsonschema will validate is 'properties'
new_validators = ClearValidatorMap(validator_class.VALIDATORS)
new_validators.update({PROPERTIES: PropertiesValidator})
return jsonschema.validators.extend(
validator_class, new_validators)
def ExtendWithDefault(validator_class):
"""Takes a validator and makes it set default values on properties.
Args:
validator_class: A class to add our overridden validators to
Returns:
A validator_class that will set default values and ignore required fields
"""
def SetDefaultsInProperties(validator, properties, instance, unused_schema):
if properties is None:
properties = {}
SetDefaults(validator, properties, instance)
return jsonschema.validators.extend(
validator_class, {PROPERTIES: SetDefaultsInProperties,
REQUIRED: IgnoreKeyword})
def SetDefaults(validator, properties, instance):
"""Populate the default values of properties.
Args:
validator: A generator that validates the "properties" keyword
properties: User properties on which to set defaults
instance: Piece of user schema containing "properties"
"""
if not properties:
return
for dm_property, subschema in properties.iteritems():
# If the property already has a value, we don't need it's default
if dm_property in instance:
return
# The ordering of these conditions assumes that '$ref' blocks override
# all other schema info, which is what the jsonschema library assumes.
# If the subschema has a reference,
# see if that reference defines a 'default' value
if REF in subschema:
out = ResolveReferencedDefault(validator, subschema[REF])
instance.setdefault(dm_property, out)
# Otherwise, see if the subschema has a 'default' value
elif DEFAULT in subschema:
instance.setdefault(dm_property, subschema[DEFAULT])
def ResolveReferencedDefault(validator, ref):
"""Resolves a reference, and returns any default value it defines.
Args:
validator: A generator the validates the "$ref" keyword
ref: The target of the "$ref" keyword
Returns:
The value of the 'default' field found in the referenced schema, or None
"""
with validator.resolver.resolving(ref) as resolved:
if DEFAULT in resolved:
return resolved[DEFAULT]
def ClearValidatorMap(validators):
"""Remaps all JsonSchema validators to make them do nothing."""
ignore_validators = {}
for keyword in validators:
ignore_validators.update({keyword: IgnoreKeyword})
return ignore_validators
def IgnoreKeyword(
unused_validator, unused_required, unused_instance, unused_schema):
"""Validator for JsonSchema that does nothing."""
pass
/*
Copyright 2015 The Kubernetes Authors All rights reserved.
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 main
import (
"expandybird/expander"
"expandybird/service"
"version"
"flag"
"fmt"
"log"
"net/http"
restful "github.com/emicklei/go-restful"
)
// port that we are going to listen on
var port = flag.Int("port", 8080, "Port to listen on")
// path to expansion binary
var expansionBinary = flag.String("expansion_binary", "../expansion/expansion.py",
"The path to the expansion binary that will be used to expand the template.")
func main() {
flag.Parse()
backend := expander.NewExpander(*expansionBinary)
wrapper := service.NewService(service.NewExpansionHandler(backend))
address := fmt.Sprintf(":%d", *port)
container := restful.DefaultContainer
server := &http.Server{
Addr: address,
Handler: container,
}
wrapper.Register(container)
log.Printf("Version: %s", version.DeploymentManagerVersion)
log.Printf("Listening on %s...", address)
log.Fatal(server.ListenAndServe())
}
pyyaml
Jinja2
Jsonschema
/*
Copyright 2015 The Kubernetes Authors All rights reserved.
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 service
import (
"expandybird/expander"
"util"
"errors"
"fmt"
"net/http"
restful "github.com/emicklei/go-restful"
)
// A Service wraps a web service that performs template expansion.
type Service struct {
*restful.WebService
}
// NewService creates and returns a new Service, initalized with a new
// restful.WebService configured with a route that dispatches to the supplied
// handler. The new Service must be registered before accepting traffic by
// calling Register.
func NewService(handler restful.RouteFunction) *Service {
restful.EnableTracing(true)
webService := new(restful.WebService)
webService.Consumes(restful.MIME_JSON, restful.MIME_XML)
webService.Produces(restful.MIME_JSON, restful.MIME_XML)
webService.Route(webService.POST("/expand").To(handler).
Doc("Expand a template.").
Reads(&expander.Template{}))
return &Service{webService}
}
// Register adds the web service wrapped by the Service to the supplied
// container. If the supplied container is nil, then the default container is
// used, instead.
func (s *Service) Register(container *restful.Container) {
if container == nil {
container = restful.DefaultContainer
}
container.Add(s.WebService)
}
// NewExpansionHandler returns a route function that handles an incoming
// template expansion request, bound to the supplied expander.
func NewExpansionHandler(backend expander.Expander) restful.RouteFunction {
return func(req *restful.Request, resp *restful.Response) {
util.LogHandlerEntry("expandybird: expand", req.Request)
template := &expander.Template{}
if err := req.ReadEntity(&template); err != nil {
logAndReturnErrorFromHandler(http.StatusBadRequest, err.Error(), resp)
return
}
output, err := backend.ExpandTemplate(template)
if err != nil {
message := fmt.Sprintf("error (%s) expanding template:\n%v\n", err, template)
logAndReturnErrorFromHandler(http.StatusBadRequest, message, resp)
return
}
response, err := expander.NewExpansionResponse(output)
if err != nil {
message := fmt.Sprintf("error (%s) marshaling output:\n%v\n", err, output)
logAndReturnErrorFromHandler(http.StatusBadRequest, message, resp)
return
}
util.LogHandlerExit("expandybird", http.StatusOK, "OK", resp.ResponseWriter)
resp.WriteEntity(response)
}
}
func logAndReturnErrorFromHandler(statusCode int, message string, resp *restful.Response) {
util.LogHandlerExit("expandybird: expand", statusCode, message, resp.ResponseWriter)
resp.WriteError(statusCode, errors.New(message))
}
/*
Copyright 2015 The Kubernetes Authors All rights reserved.
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 service
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"reflect"
"testing"
"expandybird/expander"
"util"
restful "github.com/emicklei/go-restful"
)
func GetTemplateReader(t *testing.T, description string, templateFileName string) io.Reader {
template, err := expander.NewTemplateFromFileNames(templateFileName, importFileNames)
if err != nil {
t.Errorf("cannot create template for test case (%s): %s\n", err, description)
}
templateData, err := json.Marshal(template)
if err != nil {
t.Errorf("cannot marshal template for test case (%s): %s\n", err, description)
}
reader := bytes.NewReader(templateData)
return reader
}
func GetOutputString(t *testing.T, description string) string {
output, err := ioutil.ReadFile(outputFileName)
if err != nil {
t.Errorf("cannot read output file for test case (%s): %s\n", err, description)
}
return string(output)
}
const (
httpGETMethod = "GET"
httpPOSTMethod = "POST"
validServiceURL = "/expand"
invalidServiceURL = "http://localhost:8080/invalidurlpath"
jsonContentType = "application/json"
invalidContentType = "invalid/content-type"
inputFileName = "../test/ValidContent.yaml"
outputFileName = "../test/ExpectedOutput.yaml"
)
var importFileNames = []string{
"../test/replicatedservice.py",
}
type ServiceWrapperTestCase struct {
Description string
HTTPMethod string
ServiceURLPath string
ContentType string
StatusCode int
}
var ServiceWrapperTestCases = []ServiceWrapperTestCase{
{
"expect error for invalid HTTP verb",
httpGETMethod,
validServiceURL,
jsonContentType,
http.StatusMethodNotAllowed,
},
{
"expect error for invalid URL path",
httpPOSTMethod,
invalidServiceURL,
jsonContentType,
http.StatusNotFound,
},
{
"expect error for invalid content type",
httpPOSTMethod,
validServiceURL,
invalidContentType,
http.StatusUnsupportedMediaType,
},
{
"expect success",
httpPOSTMethod,
validServiceURL,
jsonContentType,
http.StatusOK,
},
}
func TestServiceWrapper(t *testing.T) {
backend := expander.NewExpander("../expansion/expansion.py")
wrapper := NewService(NewExpansionHandler(backend))
container := restful.DefaultContainer
wrapper.Register(container)
defer container.Remove(wrapper.WebService)
handlerTester := util.NewHandlerTester(container)
for _, swtc := range ServiceWrapperTestCases {
reader := GetTemplateReader(t, swtc.Description, inputFileName)
w, err := handlerTester(swtc.HTTPMethod, swtc.ServiceURLPath, swtc.ContentType, reader)
if err != nil {
t.Errorf("error in test case '%s': %s\n", swtc.Description, err)
}
if w.Code != http.StatusOK {
if w.Code != swtc.StatusCode {
message := fmt.Sprintf("test returned code:%d, status: %s", w.Code, w.Body.String())
t.Errorf("error in test case '%s': %s\n", swtc.Description, message)
}
} else {
if swtc.StatusCode != http.StatusOK {
t.Errorf("expected error did not occur in test case '%s': want: %d have: %d\n",
swtc.Description, swtc.StatusCode, w.Code)
}
body := w.Body.Bytes()
actualResponse := &expander.ExpansionResponse{}
if err := json.Unmarshal(body, actualResponse); err != nil {
t.Errorf("error in test case '%s': %s\n", swtc.Description, err)
}
actualResult, err := actualResponse.Unmarshal()
if err != nil {
t.Errorf("error in test case '%s': %s\n", swtc.Description, err)
}
expectedOutput := GetOutputString(t, swtc.Description)
expectedResult := expandOutputOrDie(t, expectedOutput, swtc.Description)
if !reflect.DeepEqual(expectedResult, actualResult) {
message := fmt.Sprintf("want: %s\nhave: %s\n",
util.ToYAMLOrError(expectedResult), util.ToYAMLOrError(actualResult))
t.Errorf("error in test case '%s':\n%s\n", swtc.Description, message)
}
}
}
}
type ExpansionHandlerTestCase struct {
Description string
TemplateFileName string
}
var ExpansionHandlerTestCases = []ExpansionHandlerTestCase{
{
"expect error while expanding template",
"../test/InvalidFileName.yaml",
},
{
"expect error while marshaling output",
"../test/InvalidTypeName.yaml",
},
}
var malformedExpansionOutput = []byte(`
this is malformed output
`)
type mockExpander struct {
}
// ExpandTemplate passes the given configuration to the expander and returns the
// expanded configuration as a string on success.
func (e *mockExpander) ExpandTemplate(template *expander.Template) (string, error) {
switch template.Name {
case "InvalidFileName":
return "", fmt.Errorf("expansion error")
case "InvalidTypeName":
return string(malformedExpansionOutput), nil
}
panic("unknown test case")
}
func TestExpansionHandler(t *testing.T) {
backend := &mockExpander{}
wrapper := NewService(NewExpansionHandler(backend))
container := restful.DefaultContainer
wrapper.Register(container)
defer container.Remove(wrapper.WebService)
handlerTester := util.NewHandlerTester(container)
for _, ehtc := range ExpansionHandlerTestCases {
reader := GetTemplateReader(t, ehtc.Description, ehtc.TemplateFileName)
w, err := handlerTester(httpPOSTMethod, validServiceURL, jsonContentType, reader)
if err != nil {
t.Errorf("error in test case '%s': %s\n", ehtc.Description, err)
}
if w.Code != http.StatusBadRequest {
t.Errorf("expected error did not occur in test case '%s': want: %d have: %d\n",
ehtc.Description, http.StatusBadRequest, w.Code)
}
}
}
func expandOutputOrDie(t *testing.T, output, description string) *expander.ExpansionResult {
result, err := expander.NewExpansionResult(output)
if err != nil {
t.Errorf("cannot expand output for test case '%s': %s\n", description, err)
}
return result
}
######################################################################
# Copyright 2015 The Kubernetes Authors All rights reserved.
# 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.
######################################################################
config:
resources:
- name: expandybird-service
properties:
apiVersion: v1
kind: Service
metadata:
labels:
app: expandybird
name: expandybird-service
name: expandybird-service
namespace: default
spec:
ports:
- name: expandybird
port: 8080
targetPort: 8080
selector:
app: expandybird
name: expandybird
type: LoadBalancer
type: Service
- name: expandybird-rc
properties:
apiVersion: v1
kind: ReplicationController
metadata:
labels:
app: expandybird
name: expandybird-rc
name: expandybird-rc
namespace: default
spec:
replicas: 3
selector:
app: expandybird
name: expandybird
template:
metadata:
labels:
app: expandybird
name: expandybird
spec:
containers:
- image: b.gcr.io/dm-k8s-testing/expandybird
name: expandybird
ports:
- containerPort: 8080
name: expandybird
type: ReplicationController
layout:
resources:
- name: expandybird
properties:
container_port: 8080
external_service: true
image: b.gcr.io/dm-k8s-testing/expandybird
labels:
app: expandybird
replicas: 3
service_port: 8080
target_port: 8080
resources:
- name: expandybird-service
type: Service
- name: expandybird-rc
type: ReplicationController
type: replicatedservice.py
######################################################################
# Copyright 2015 The Kubernetes Authors All rights reserved.
# 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.
######################################################################
imports:
- path: invalidfilename.py
resources:
- name: expandybird
type: replicatedservice.py
properties:
service_port: 8080
target_port: 8080
image: b.gcr.io/dm-k8s-testing/expandybird
######################################################################
# Copyright 2015 The Kubernetes Authors All rights reserved.
# 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.
######################################################################
imports:
- path: replicatedservice.py
resources:
- name: expandybird
type: replicatedservice.py
properties:
service_port: 8080
target_port: 8080
invalidproperty: b.gcr.io/dm-k8s-testing/expandybird
######################################################################
# Copyright 2015 The Kubernetes Authors All rights reserved.
# 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.
######################################################################
imports:
- path: replicatedservice.py
resources:
- name: expandybird
type: invalidtypename.py
properties:
service_port: 8080
target_port: 8080
image: b.gcr.io/dm-k8s-testing/expandybird
######################################################################
# Copyright 2015 The Kubernetes Authors All rights reserved.
# 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.
######################################################################
imports:
- path: replicatedservice.py
resources:
- name: expandybird
type: replicatedservice.py
thisisnotalist: somevalue
shouldnotbehere: anothervalue
######################################################################
# Copyright 2015 The Kubernetes Authors All rights reserved.
# 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.
######################################################################
imports:
resources:
- name: expandybird
type: replicatedservice.py
properties:
service_port: 8080
target_port: 8080
image: b.gcr.io/dm-k8s-testing/expandybird
######################################################################
# Copyright 2015 The Kubernetes Authors All rights reserved.
# 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.
######################################################################
imports:
- path: replicatedservice.py
resources:
- type: replicatedservice.py
properties:
service_port: 8080
target_port: 8080
image: b.gcr.io/dm-k8s-testing/expandybird
######################################################################
# Copyright 2015 The Kubernetes Authors All rights reserved.
# 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.
######################################################################
imports:
- path: replicatedservice.py
resources:
- name: expandybird
properties:
service_port: 8080
target_port: 8080
image: b.gcr.io/dm-k8s-testing/expandybird
######################################################################
# Copyright 2015 The Kubernetes Authors All rights reserved.
# 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.
######################################################################
imports:
- path: replicatedservice.py
resources:
- name: expandybird
type: replicatedservice.py
properties:
service_port: 8080
target_port: 8080
container_port: 8080
external_service: true
replicas: 3
image: b.gcr.io/dm-k8s-testing/expandybird
labels:
app: expandybird
\ No newline at end of file
######################################################################
# Copyright 2015 The Kubernetes Authors All rights reserved.
# 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.
######################################################################
"""Defines a ReplicatedService type by creating both a Service and an RC.
This module creates a typical abstraction for running a service in a
Kubernetes cluster, namely a replication controller and a service packaged
together into a single unit.
"""
import yaml
SERVICE_TYPE_COLLECTION = 'Service'
RC_TYPE_COLLECTION = 'ReplicationController'
def GenerateConfig(context):
"""Generates a Replication Controller and a matching Service.
Args:
context: Template context, which can contain the following properties:
container_name - Name to use for container. If omitted, name is
used.
namespace - Namespace to create the resources in. If omitted,
'default' is used.
protocol - Protocol to use for the service
service_port - Port to use for the service
target_port - Target port for the service
container_port - Container port to use
replicas - Number of replicas to create in RC
image - Docker image to use for replicas. Required.
labels - labels to apply.
external_service - If set to true, enable external Load Balancer
Returns:
A Container Manifest as a YAML string.
"""
# YAML config that we're going to create for both RC & Service
config = {'resources': []}
name = context.env['name']
container_name = context.properties.get('container_name', name)
namespace = context.properties.get('namespace', 'default')
# Define things that the Service cares about
service_name = name + '-service'
service_type = SERVICE_TYPE_COLLECTION
# Define things that the Replication Controller (rc) cares about
rc_name = name + '-rc'
rc_type = RC_TYPE_COLLECTION
service = {
'name': service_name,
'type': service_type,
'properties': {
'apiVersion': 'v1',
'kind': 'Service',
'namespace': namespace,
'metadata': {
'name': service_name,
'labels': GenerateLabels(context, service_name),
},
'spec': {
'ports': [GenerateServicePorts(context, container_name)],
'selector': GenerateLabels(context, name)
}
}
}
set_up_external_lb = context.properties.get('external_service', None)
if set_up_external_lb:
service['properties']['spec']['type'] = 'LoadBalancer'
config['resources'].append(service)
rc = {
'name': rc_name,
'type': rc_type,
'properties': {
'apiVersion': 'v1',
'kind': 'ReplicationController',
'namespace': namespace,
'metadata': {
'name': rc_name,
'labels': GenerateLabels(context, rc_name),
},
'spec': {
'replicas': context.properties['replicas'],
'selector': GenerateLabels(context, name),
'template': {
'metadata': {
'labels': GenerateLabels(context, name),
},
'spec': {
'containers': [
{
'name': container_name,
'image': context.properties['image'],
'ports': [
{
'name': container_name,
'containerPort': context.properties['container_port'],
}
]
}
]
}
}
}
}
}
config['resources'].append(rc)
return yaml.dump(config)
# Generates labels either from the context.properties['labels'] or generates
# a default label 'name':name
def GenerateLabels(context, name):
"""Generates labels from context.properties['labels'] or creates default.
We make a deep copy of the context.properties['labels'] section to avoid
linking in the yaml document, which I believe reduces readability of the
expanded template. If no labels are given, generate a default 'name':name.
Args:
context: Template context, which can contain the following properties:
labels - Labels to generate
Returns:
A dict containing labels in a name:value format
"""
tmp_labels = context.properties.get('labels', None)
ret_labels = {'name': name}
if isinstance(tmp_labels, dict):
for key, value in tmp_labels.iteritems():
ret_labels[key] = value
return ret_labels
def GenerateServicePorts(context, name):
"""Generates a ports section for a service.
Args:
context: Template context, which can contain the following properties:
service_port - Port to use for the service
target_port - Target port for the service
protocol - Protocol to use.
Returns:
A dict containing a port definition
"""
service_port = context.properties.get('service_port', None)
target_port = context.properties.get('target_port', None)
protocol = context.properties.get('protocol')
ports = {}
if name:
ports['name'] = name
if service_port:
ports['port'] = service_port
if target_port:
ports['targetPort'] = target_port
if protocol:
ports['protocol'] = protocol
return ports
# Copyright 2015 Google, Inc. All Rights Reserved
#
# 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.
FROM golang:1.4
MAINTAINER Jack Greenfield <jackgr@google.com>
WORKDIR /go/src
RUN mkdir -p manager
COPY manager manager
RUN mkdir -p util
COPY util util
RUN mkdir -p version
COPY version version
RUN go-wrapper download manager/...
RUN go-wrapper install manager/...
EXPOSE 8080
ENTRYPOINT ["go-wrapper", "run"]
# Makefile for the Docker image gcr.io/$(PROJECT)/manager
# MAINTAINER: Jack Greenfield <jackgr@google.com>
# If you update this image please check the tag value before pushing.
.PHONY : all build test push container clean .project
PREFIX := gcr.io/$(PROJECT)
IMAGE := manager
TAG := latest
ROOT_DIR := $(abspath ./..)
DIR = $(ROOT_DIR)
push: container
gcloud docker push $(PREFIX)/$(IMAGE):$(TAG)
container:
docker build -t $(PREFIX)/$(IMAGE):$(TAG) -f Dockerfile $(DIR)
clean:
-docker rmi $(PREFIX)/$(IMAGE):$(TAG)
\ No newline at end of file
/*
Copyright 2015 The Kubernetes Authors All rights reserved.
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 main
import (
"encoding/json"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
"os"
"strings"
"github.com/ghodss/yaml"
"github.com/gorilla/mux"
"manager/manager"
"manager/repository"
"util"
)
var deployments = []Route{
{"ListDeployments", "/deployments", "GET", listDeploymentsHandlerFunc, ""},
{"GetDeployment", "/deployments/{deployment}", "GET", getDeploymentHandlerFunc, ""},
{"CreateDeployment", "/deployments", "POST", createDeploymentHandlerFunc, "JSON"},
{"DeleteDeployment", "/deployments/{deployment}", "DELETE", deleteDeploymentHandlerFunc, ""},
{"PutDeployment", "/deployments/{deployment}", "PUT", putDeploymentHandlerFunc, "JSON"},
{"ListManifests", "/deployments/{deployment}/manifests", "GET", listManifestsHandlerFunc, ""},
{"GetManifest", "/deployments/{deployment}/manifests/{manifest}", "GET", getManifestHandlerFunc, ""},
{"ListTypes", "/types", "GET", listTypesHandlerFunc, ""},
{"ListTypeInstances", "/types/{type}/instances", "GET", listTypeInstancesHandlerFunc, ""},
}
var (
maxLength = flag.Int64("maxLength", 1024, "The maximum length (KB) of a template.")
expanderName = flag.String("expander", "expandybird-service", "The DNS name of the expander service.")
expanderURL = flag.String("expanderURL", "", "The URL for the expander service.")
deployerName = flag.String("deployer", "resourcifier-service", "The DNS name of the deployer service.")
deployerURL = flag.String("deployerURL", "", "The URL for the deployer service.")
)
var backend manager.Manager
func init() {
if !flag.Parsed() {
flag.Parse()
}
routes = append(routes, deployments...)
backend = newManager()
}
func newManager() manager.Manager {
expander := manager.NewExpander(getServiceURL(*expanderURL, *expanderName), manager.NewTypeResolver())
deployer := manager.NewDeployer(getServiceURL(*deployerURL, *deployerName))
r := repository.NewMapBasedRepository()
return manager.NewManager(expander, deployer, r)
}
func getServiceURL(serviceURL, serviceName string) string {
if serviceURL == "" {
serviceURL = makeEnvVariableURL(serviceName)
if serviceURL == "" {
addrs, err := net.LookupHost(serviceName)
if err != nil || len(addrs) < 1 {
log.Fatalf("cannot resolve service:%v. environment:%v", serviceName, os.Environ())
}
serviceURL = fmt.Sprintf("https://%s", addrs[0])
}
}
return serviceURL
}
// makeEnvVariableURL takes a service name and returns the value of the
// environment variable that identifies its URL, if it exists, or the empty
// string, if it doesn't.
func makeEnvVariableURL(str string) string {
prefix := makeEnvVariableName(str)
url := os.Getenv(prefix + "_PORT")
return strings.Replace(url, "tcp", "http", 1)
}
// makeEnvVariableName is copied from the Kubernetes source,
// which is referenced by the documentation for service environment variables.
func makeEnvVariableName(str string) string {
// TODO: If we simplify to "all names are DNS1123Subdomains" this
// will need two tweaks:
// 1) Handle leading digits
// 2) Handle dots
return strings.ToUpper(strings.Replace(str, "-", "_", -1))
}
func listDeploymentsHandlerFunc(w http.ResponseWriter, r *http.Request) {
handler := "manager: list deployments"
util.LogHandlerEntry(handler, r)
l, err := backend.ListDeployments()
if err != nil {
util.LogAndReturnError(handler, http.StatusInternalServerError, err, w)
return
}
var names []string
for _, d := range l {
names = append(names, d.Name)
}
util.LogHandlerExitWithJSON(handler, w, names, http.StatusOK)
}
func getDeploymentHandlerFunc(w http.ResponseWriter, r *http.Request) {
handler := "manager: get deployment"
util.LogHandlerEntry(handler, r)
name, err := getPathVariable(w, r, "deployment", handler)
if err != nil {
return
}
d, err := backend.GetDeployment(name)
if err != nil {
util.LogAndReturnError(handler, http.StatusBadRequest, err, w)
return
}
util.LogHandlerExitWithJSON(handler, w, d, http.StatusOK)
}
func createDeploymentHandlerFunc(w http.ResponseWriter, r *http.Request) {
handler := "manager: create deployment"
util.LogHandlerEntry(handler, r)
defer r.Body.Close()
t := getTemplate(w, r, handler)
if t != nil {
d, err := backend.CreateDeployment(t)
if err != nil {
util.LogAndReturnError(handler, http.StatusBadRequest, err, w)
return
}
util.LogHandlerExitWithJSON(handler, w, d, http.StatusCreated)
return
}
}
func deleteDeploymentHandlerFunc(w http.ResponseWriter, r *http.Request) {
handler := "manager: delete deployment"
util.LogHandlerEntry(handler, r)
defer r.Body.Close()
name, err := getPathVariable(w, r, "deployment", handler)
if err != nil {
return
}
d, err := backend.DeleteDeployment(name, true)
if err != nil {
util.LogAndReturnError(handler, http.StatusBadRequest, err, w)
return
}
util.LogHandlerExitWithJSON(handler, w, d, http.StatusOK)
}
func putDeploymentHandlerFunc(w http.ResponseWriter, r *http.Request) {
handler := "manager: update deployment"
util.LogHandlerEntry(handler, r)
defer r.Body.Close()
name, err := getPathVariable(w, r, "deployment", handler)
if err != nil {
return
}
t := getTemplate(w, r, handler)
if t != nil {
d, err := backend.PutDeployment(name, t)
if err != nil {
util.LogAndReturnError(handler, http.StatusBadRequest, err, w)
return
}
util.LogHandlerExitWithJSON(handler, w, d, http.StatusCreated)
}
}
func getPathVariable(w http.ResponseWriter, r *http.Request, variable, handler string) (string, error) {
vars := mux.Vars(r)
variable, ok := vars[variable]
if !ok {
e := fmt.Errorf("%s parameter not found in URL", variable)
util.LogAndReturnError(handler, http.StatusBadRequest, e, w)
return "", e
}
return variable, nil
}
func getTemplate(w http.ResponseWriter, r *http.Request, handler string) *manager.Template {
util.LogHandlerEntry(handler, r)
b := io.LimitReader(r.Body, *maxLength*1024)
y, err := ioutil.ReadAll(b)
if err != nil {
util.LogAndReturnError(handler, http.StatusBadRequest, err, w)
return nil
}
// Reject the input if it exceeded the length limit,
// since we may not have read all of it into the buffer.
if _, err = b.Read(make([]byte, 0, 1)); err != io.EOF {
e := fmt.Errorf("template exceeds maximum length of %d KB", *maxLength)
util.LogAndReturnError(handler, http.StatusBadRequest, e, w)
return nil
}
if err := r.Body.Close(); err != nil {
util.LogAndReturnError(handler, http.StatusInternalServerError, err, w)
return nil
}
j, err := yaml.YAMLToJSON(y)
if err != nil {
e := fmt.Errorf("%v\n%v", err, string(y))
util.LogAndReturnError(handler, http.StatusBadRequest, e, w)
return nil
}
t := &manager.Template{}
if err := json.Unmarshal(j, t); err != nil {
e := fmt.Errorf("%v\n%v", err, string(j))
util.LogAndReturnError(handler, http.StatusBadRequest, e, w)
return nil
}
return t
}
func listManifestsHandlerFunc(w http.ResponseWriter, r *http.Request) {
handler := "manager: list manifests"
util.LogHandlerEntry(handler, r)
deploymentName, err := getPathVariable(w, r, "deployment", handler)
if err != nil {
return
}
m, err := backend.ListManifests(deploymentName)
if err != nil {
util.LogAndReturnError(handler, http.StatusInternalServerError, err, w)
return
}
var manifestNames []string
for _, manifest := range m {
manifestNames = append(manifestNames, manifest.Name)
}
util.LogHandlerExitWithJSON(handler, w, manifestNames, http.StatusOK)
}
func getManifestHandlerFunc(w http.ResponseWriter, r *http.Request) {
handler := "manager: get manifest"
util.LogHandlerEntry(handler, r)
deploymentName, err := getPathVariable(w, r, "deployment", handler)
if err != nil {
return
}
manifestName, err := getPathVariable(w, r, "manifest", handler)
if err != nil {
return
}
m, err := backend.GetManifest(deploymentName, manifestName)
if err != nil {
util.LogAndReturnError(handler, http.StatusBadRequest, err, w)
return
}
util.LogHandlerExitWithJSON(handler, w, m, http.StatusOK)
}
// Putting Type handlers here for now because deployments.go
// currently owns its own Manager backend and doesn't like to share.
func listTypesHandlerFunc(w http.ResponseWriter, r *http.Request) {
handler := "manager: list types"
util.LogHandlerEntry(handler, r)
util.LogHandlerExitWithJSON(handler, w, backend.ListTypes(), http.StatusOK)
}
func listTypeInstancesHandlerFunc(w http.ResponseWriter, r *http.Request) {
handler := "manager: list instances"
util.LogHandlerEntry(handler, r)
typeName, err := getPathVariable(w, r, "type", handler)
if err != nil {
return
}
util.LogHandlerExitWithJSON(handler, w, backend.ListInstances(typeName), http.StatusOK)
}
/*
Copyright 2015 The Kubernetes Authors All rights reserved.
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 main
import (
"version"
"flag"
"fmt"
"log"
"net/http"
"os"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
)
// Route defines a routing table entry to be registered with gorilla/mux.
type Route struct {
Name string
Path string
Methods string
HandlerFunc http.HandlerFunc
Type string
}
var routes = []Route{}
// port to listen on
var port = flag.Int("port", 8080, "The port to listen on")
func main() {
if !flag.Parsed() {
flag.Parse()
}
router := mux.NewRouter()
router.StrictSlash(true)
for _, route := range routes {
handler := http.Handler(http.HandlerFunc(route.HandlerFunc))
switch route.Type {
case "JSON":
handler = handlers.ContentTypeHandler(handler, "application/json")
case "":
break
default:
log.Fatalf("invalid route type: %v", route.Type)
}
r := router.NewRoute()
r.Name(route.Name).
Path(route.Path).
Methods(route.Methods).
Handler(handler)
}
address := fmt.Sprintf(":%d", *port)
handler := handlers.CombinedLoggingHandler(os.Stderr, router)
log.Printf("Version: %s", version.DeploymentManagerVersion)
log.Printf("Listening on port %d...", *port)
log.Fatal(http.ListenAndServe(address, handler))
}
/*
Copyright 2015 The Kubernetes Authors All rights reserved.
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 manager
import (
"bytes"
"fmt"
"github.com/ghodss/yaml"
"io"
"io/ioutil"
"log"
"net/http"
"net/url"
"strings"
)
// Deployer abstracts interactions with the expander and deployer services.
type Deployer interface {
GetConfiguration(cached *Configuration) (*Configuration, error)
CreateConfiguration(configuration *Configuration) error
DeleteConfiguration(configuration *Configuration) error
PutConfiguration(configuration *Configuration) error
}
// NewDeployer returns a new initialized Deployer.
func NewDeployer(url string) Deployer {
return &deployer{url}
}
type deployer struct {
deployerURL string
}
func (d *deployer) getBaseURL() string {
return fmt.Sprintf("%s/configurations", d.deployerURL)
}
type formatter func(err error) error
// GetConfiguration reads and returns the actual configuration
// of the resources described by a cached configuration.
func (d *deployer) GetConfiguration(cached *Configuration) (*Configuration, error) {
errors := &Error{}
actual := &Configuration{}
for _, resource := range cached.Resources {
rtype := url.QueryEscape(resource.Type)
rname := url.QueryEscape(resource.Name)
url := fmt.Sprintf("%s/%s/%s", d.getBaseURL(), rtype, rname)
body, err := d.callService("GET", url, nil, func(e error) error {
return fmt.Errorf("cannot get configuration for resource (%s)", e)
})
if err != nil {
log.Println(errors.appendError(err))
continue
}
if len(body) != 0 {
result := &Resource{Name: resource.Name, Type: resource.Type}
if err := yaml.Unmarshal(body, &result.Properties); err != nil {
return nil, fmt.Errorf("cannot get configuration for resource (%v)", err)
}
actual.Resources = append(actual.Resources, result)
}
}
if len(errors.errors) > 0 {
return nil, errors
}
return actual, nil
}
// CreateConfiguration deploys the set of resources described by a configuration.
func (d *deployer) CreateConfiguration(configuration *Configuration) error {
return d.callServiceWithConfiguration("POST", "create", configuration)
}
// DeleteConfiguration deletes the set of resources described by a configuration.
func (d *deployer) DeleteConfiguration(configuration *Configuration) error {
return d.callServiceWithConfiguration("DELETE", "delete", configuration)
}
// PutConfiguration replaces the set of resources described by a configuration.
func (d *deployer) PutConfiguration(configuration *Configuration) error {
return d.callServiceWithConfiguration("PUT", "replace", configuration)
}
func (d *deployer) callServiceWithConfiguration(method, operation string, configuration *Configuration) error {
callback := func(e error) error {
return fmt.Errorf("cannot %s configuration (%s)", operation, e)
}
y, err := yaml.Marshal(configuration)
if err != nil {
return callback(err)
}
reader := ioutil.NopCloser(bytes.NewReader(y))
_, err = d.callService(method, d.getBaseURL(), reader, callback)
return err
}
func (d *deployer) callService(method, url string, reader io.Reader, callback formatter) ([]byte, error) {
request, err := http.NewRequest(method, url, reader)
if err != nil {
return nil, callback(err)
}
if method != "GET" {
request.Header.Add("Content-Type", "application/json")
}
response, err := http.DefaultClient.Do(request)
if err != nil {
return nil, callback(err)
}
defer response.Body.Close()
if response.StatusCode < http.StatusOK ||
response.StatusCode >= http.StatusMultipleChoices {
err := fmt.Errorf("deployer service response:\n%v\n", response)
return nil, callback(err)
}
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, callback(err)
}
return body, nil
}
// Error is an error type that captures errors from the multiple calls to kubectl
// made for a single configuration.
type Error struct {
errors []error
}
// Error returns the string value of an Error.
func (e *Error) Error() string {
errs := []string{}
for _, err := range e.errors {
errs = append(errs, err.Error())
}
return strings.Join(errs, "\n")
}
func (e *Error) appendError(err error) error {
e.errors = append(e.errors, err)
return err
}
/*
Copyright 2015 The Kubernetes Authors All rights reserved.
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 manager
import (
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"path"
"reflect"
"strings"
"testing"
"util"
"github.com/ghodss/yaml"
)
var validConfigurationTestCaseData = []byte(`
resources:
- name: test-controller-v1
type: ReplicationController
properties:
kind: ReplicationController
apiVersion: v1
metadata:
name: test-controller-v1
namespace: default
labels:
k8s-app: test
version: v1
spec:
replicas: 1
selector:
k8s-app: test
version: v1
template:
metadata:
labels:
k8s-app: test
version: v1
spec:
containers:
- name: test
image: deployer/test:latest
ports:
- name: test
containerPort: 8080
protocol: TCP
- name: test
type: Service
properties:
apiVersion: v1
kind: Service
metadata:
name: test
namespace: default
labels:
k8s-app: test
version: v1
spec:
type: LoadBalancer
selector:
k8s-app: test
version: v1
ports:
- name: test
port: 8080
targetPort: test
protocol: TCP
`)
type DeployerTestCases struct {
TestCases []DeployerTestCase
}
type DeployerTestCase struct {
Description string
Error string
Handler func(w http.ResponseWriter, r *http.Request)
}
func TestGetConfiguration(t *testing.T) {
valid := getValidConfiguration(t)
tests := []DeployerTestCase{
{
"expect success for GetConfiguration",
"",
func(w http.ResponseWriter, r *http.Request) {
// Get name from path, find in valid, and return its properties.
rtype := path.Base(path.Dir(r.URL.Path))
rname := path.Base(r.URL.Path)
for _, resource := range valid.Resources {
if resource.Type == rtype && resource.Name == rname {
util.LogHandlerExitWithYAML("resourcifier: get configuration", w, resource.Properties, http.StatusOK)
return
}
}
status := fmt.Sprintf("resource %s of type %s not found", rname, rtype)
http.Error(w, status, http.StatusInternalServerError)
},
},
{
"expect error for GetConfiguration",
"cannot get configuration",
deployerErrorHandler,
},
}
for _, dtc := range tests {
ts := httptest.NewServer(http.HandlerFunc(dtc.Handler))
defer ts.Close()
deployer := NewDeployer(ts.URL)
result, err := deployer.GetConfiguration(valid)
if err != nil {
message := err.Error()
if !strings.Contains(message, dtc.Error) {
t.Errorf("error in test case:%s:%s\n", dtc.Description, message)
}
} else {
if dtc.Error != "" {
t.Errorf("expected error:%s\ndid not occur in test case:%s\n",
dtc.Error, dtc.Description)
}
if !reflect.DeepEqual(valid, result) {
t.Errorf("error in test case:%s:\nwant:%s\nhave:%s\n",
dtc.Description, util.ToYAMLOrError(valid), util.ToYAMLOrError(result))
}
}
}
}
func TestCreateConfiguration(t *testing.T) {
valid := getValidConfiguration(t)
tests := []DeployerTestCase{
{
"expect success for CreateConfiguration",
"",
deployerSuccessHandler,
},
{
"expect error for CreateConfiguration",
"cannot create configuration",
deployerErrorHandler,
},
}
for _, dtc := range tests {
ts := httptest.NewServer(http.HandlerFunc(dtc.Handler))
defer ts.Close()
deployer := NewDeployer(ts.URL)
err := deployer.CreateConfiguration(valid)
if err != nil {
message := err.Error()
if !strings.Contains(message, dtc.Error) {
t.Errorf("error in test case:%s:%s\n", dtc.Description, message)
}
} else {
if dtc.Error != "" {
t.Errorf("expected error:%s\ndid not occur in test case:%s\n",
dtc.Error, dtc.Description)
}
}
}
}
func TestDeleteConfiguration(t *testing.T) {
valid := getValidConfiguration(t)
tests := []DeployerTestCase{
{
"expect success for DeleteConfiguration",
"",
deployerSuccessHandler,
},
{
"expect error for DeleteConfiguration",
"cannot delete configuration",
deployerErrorHandler,
},
}
for _, dtc := range tests {
ts := httptest.NewServer(http.HandlerFunc(dtc.Handler))
defer ts.Close()
deployer := NewDeployer(ts.URL)
err := deployer.DeleteConfiguration(valid)
if err != nil {
message := err.Error()
if !strings.Contains(message, dtc.Error) {
t.Errorf("error in test case:%s:%s\n", dtc.Description, message)
}
} else {
if dtc.Error != "" {
t.Errorf("expected error:%s\ndid not occur in test case:%s\n",
dtc.Error, dtc.Description)
}
}
}
}
func TestPutConfiguration(t *testing.T) {
valid := getValidConfiguration(t)
tests := []DeployerTestCase{
{
"expect success for PutConfiguration",
"",
deployerSuccessHandler,
},
{
"expect error for PutConfiguration",
"cannot replace configuration",
deployerErrorHandler,
},
}
for _, dtc := range tests {
ts := httptest.NewServer(http.HandlerFunc(dtc.Handler))
defer ts.Close()
deployer := NewDeployer(ts.URL)
err := deployer.PutConfiguration(valid)
if err != nil {
message := err.Error()
if !strings.Contains(message, dtc.Error) {
t.Errorf("error in test case:%s:%s\n", dtc.Description, message)
}
} else {
if dtc.Error != "" {
t.Errorf("expected error:%s\ndid not occur in test case:%s\n",
dtc.Error, dtc.Description)
}
}
}
}
func getValidConfiguration(t *testing.T) *Configuration {
valid := &Configuration{}
err := yaml.Unmarshal(validConfigurationTestCaseData, valid)
if err != nil {
t.Errorf("cannot unmarshal test case data:%s\n", err)
}
return valid
}
func deployerErrorHandler(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
http.Error(w, "something failed", http.StatusInternalServerError)
}
func deployerSuccessHandler(w http.ResponseWriter, r *http.Request) {
valid := &Configuration{}
err := yaml.Unmarshal(validConfigurationTestCaseData, valid)
if err != nil {
status := fmt.Sprintf("cannot unmarshal test case data:%s", err)
http.Error(w, status, http.StatusInternalServerError)
return
}
defer r.Body.Close()
body, err := ioutil.ReadAll(r.Body)
if err != nil {
status := fmt.Sprintf("cannot read request body:%s", err)
http.Error(w, status, http.StatusInternalServerError)
return
}
result := &Configuration{}
if err := yaml.Unmarshal(body, result); err != nil {
status := fmt.Sprintf("cannot unmarshal request body:%s", err)
http.Error(w, status, http.StatusInternalServerError)
return
}
if !reflect.DeepEqual(valid, result) {
status := fmt.Sprintf("error in http handler:\nwant:%s\nhave:%s\n",
util.ToYAMLOrError(valid), util.ToYAMLOrError(result))
http.Error(w, status, http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
/*
Copyright 2015 The Kubernetes Authors All rights reserved.
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 manager
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"github.com/ghodss/yaml"
)
const (
// TODO (iantw): Align this with a character not allowed to show up in resource names.
layoutNodeKeySeparator = "#"
)
// ExpandedTemplate is the structure returned by the expansion service.
type ExpandedTemplate struct {
Config *Configuration `json:"config"`
Layout *Layout `json:"layout"`
}
// Expander abstracts interactions with the expander and deployer services.
type Expander interface {
ExpandTemplate(t Template) (*ExpandedTemplate, error)
}
// NewExpander returns a new initialized Expander.
func NewExpander(url string, tr TypeResolver) Expander {
return &expander{url, tr}
}
type expander struct {
expanderURL string
typeResolver TypeResolver
}
func (e *expander) getBaseURL() string {
return fmt.Sprintf("%s/expand", e.expanderURL)
}
func expanderError(t *Template, err error) error {
return fmt.Errorf("cannot expand template named %s (%s):\n%s\n", t.Name, err, t.Content)
}
// ExpanderResponse gives back a layout, which has nested structure
// Resource0
// ResourceDefinition
// Resource0, 0
// ResourceDefinition
// Resource0, 0, 0
// ResourceDefinition
// Resource0, 0, 1
// ResourceDefinition
// Resource0, 1
// ResourceDefinition
//
// All the leaf nodes in this tree are either primitives or a currently unexpandable type.
// Next we will resolve all the unexpandable types and re-enter expansion, at which point
// all primitives are untouched and returned as root siblings with no children in the
// resulting layout. The previously unexpandable nodes will become sibling root nodes,
// but with children. We want to replace the leaf nodes that were formerly unexpandable
// with their respective newly created trees.
//
// So, do as follows:
// 1) Do a walk of the tree and find each leaf. Check its Type and place a pointer to it
// into a map with the resource name and type as key if it is non-primitive.
// 2) Re-expand the template with the new imports.
// 3) For each root level sibling, check if its name exists in the hash map from (1)
// 4) Replace the Layout of the node in the hash map with the current node if applicable.
// 5) Return to (1)
// TODO (iantw): There may be a tricky corner case here where a known template could be
// masked by an unknown template, which on the subsequent expansion could allow a collision
// between the name#template key to exist in the layout given a particular choice of naming.
// In practice, it would be nearly impossible to hit, but consider including properties/name/type
// into a hash of sorts to make this robust...
func walkLayout(l *Layout, toReplace map[string]*LayoutResource) map[string]*LayoutResource {
ret := map[string]*LayoutResource{}
toVisit := l.Resources
for len(toVisit) > 0 {
lr := toVisit[0]
nodeKey := lr.Resource.Name + layoutNodeKeySeparator + lr.Resource.Type
if len(lr.Layout.Resources) == 0 && Primitives[lr.Resource.Type] == false {
ret[nodeKey] = lr
} else if toReplace[nodeKey] != nil {
toReplace[nodeKey].Resources = lr.Resources
}
toVisit = append(toVisit, lr.Resources...)
toVisit = toVisit[1:]
}
return ret
}
// ExpandTemplate expands the supplied template, and returns a configuration.
func (e *expander) ExpandTemplate(t Template) (*ExpandedTemplate, error) {
// We have a fencepost problem here.
// 1. Start by trying to resolve any missing templates
// 2. Expand the configuration using all the of the imports available to us at this point
// 3. Expansion may yield additional templates, so we run the type resolution again
// 4. If type resolution resulted in new imports being available, return to 2.
config := &Configuration{}
if err := yaml.Unmarshal([]byte(t.Content), config); err != nil {
e := fmt.Errorf("Unable to unmarshal configuration (%s): %s\n", err, t.Content)
return nil, e
}
var finalLayout *Layout
needResolve := map[string]*LayoutResource{}
// Start things off by attempting to resolve the templates in a first pass.
newImp, err := e.typeResolver.ResolveTypes(config, t.Imports)
if err != nil {
e := fmt.Errorf("type resolution failed:%s\n", err)
return nil, expanderError(&t, e)
}
t.Imports = append(t.Imports, newImp...)
for {
// Now expand with everything imported.
result, err := e.expandTemplate(&t)
if err != nil {
e := fmt.Errorf("template expansion:%s\n", err)
return nil, expanderError(&t, e)
}
// Once we set this layout, we're operating on the "needResolve" *LayoutResources,
// which are pointers into the original layout structure. After each expansion we
// lose the templates in the previous expansion, so we have to keep the first one
// around and keep appending to the pointers in it as we get more layers of expansion.
if finalLayout == nil {
finalLayout = result.Layout
}
needResolve = walkLayout(result.Layout, needResolve)
newImp, err = e.typeResolver.ResolveTypes(result.Config, nil)
if err != nil {
e := fmt.Errorf("type resolution failed:%s\n", err)
return nil, expanderError(&t, e)
}
// If the new imports contain nothing, we are done. Everything is fully expanded.
if len(newImp) == 0 {
result.Layout = finalLayout
return result, nil
}
t.Imports = append(t.Imports, newImp...)
var content []byte
content, err = yaml.Marshal(result.Config)
t.Content = string(content)
if err != nil {
e := fmt.Errorf("Unable to unmarshal response from expander (%s): %s\n",
err, result.Config)
return nil, expanderError(&t, e)
}
}
}
func (e *expander) expandTemplate(t *Template) (*ExpandedTemplate, error) {
j, err := json.Marshal(t)
if err != nil {
return nil, err
}
response, err := http.Post(e.getBaseURL(), "application/json", ioutil.NopCloser(bytes.NewReader(j)))
if err != nil {
e := fmt.Errorf("http POST failed:%s\n", err)
return nil, e
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
err := fmt.Errorf("expander service response:%v", response)
return nil, err
}
body, err := ioutil.ReadAll(response.Body)
if err != nil {
e := fmt.Errorf("error reading response:%s\n", err)
return nil, e
}
er := &ExpansionResponse{}
if err := json.Unmarshal(body, er); err != nil {
e := fmt.Errorf("cannot unmarshal response body (%s):%s\n", err, body)
return nil, e
}
template, err := er.Unmarshal()
if err != nil {
e := fmt.Errorf("cannot unmarshal response yaml (%s):%v\n", err, er)
return nil, e
}
return template, nil
}
// ExpansionResponse describes the results of marshaling an ExpandedTemplate.
type ExpansionResponse struct {
Config string `json:"config"`
Layout string `json:"layout"`
}
// Unmarshal creates and returns an ExpandedTemplate from an ExpansionResponse.
func (er *ExpansionResponse) Unmarshal() (*ExpandedTemplate, error) {
template := &ExpandedTemplate{}
if err := yaml.Unmarshal([]byte(er.Config), &template.Config); err != nil {
return nil, fmt.Errorf("cannot unmarshal config (%s):\n%s", err, er.Config)
}
if err := yaml.Unmarshal([]byte(er.Layout), &template.Layout); err != nil {
return nil, fmt.Errorf("cannot unmarshal layout (%s):\n%s", err, er.Layout)
}
return template, nil
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
# Makefile for the Docker image gcr.io/$(PROJECT)/resourcifier
# MAINTAINER: Jack Greenfield <jackgr@google.com>
# If you update this image please check the tag value before pushing.
.PHONY : all build test push container clean
PREFIX := gcr.io/$(PROJECT)
IMAGE := resourcifier
TAG := latest
ROOT_DIR := $(abspath ./..)
DIR = $(ROOT_DIR)
push: container
gcloud docker push $(PREFIX)/$(IMAGE):$(TAG)
container:
docker build -t $(PREFIX)/$(IMAGE):$(TAG) -f Dockerfile $(DIR)
clean:
-docker rmi $(PREFIX)/$(IMAGE):$(TAG)
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment