mirror of https://github.com/helm/helm
commit
94db53d080
@ -0,0 +1,6 @@
|
||||
expandybird/pkg/*
|
||||
expandybird/expansion/*.pyc
|
||||
resourcifier/pkg/*
|
||||
resourcifier/bin/*
|
||||
manager/pkg/*
|
||||
|
@ -0,0 +1,25 @@
|
||||
# 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.
|
||||
|
@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
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.
|
@ -0,0 +1,36 @@
|
||||
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:.%=%)
|
@ -0,0 +1,110 @@
|
||||
# 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
|
||||
```
|
@ -0,0 +1,12 @@
|
||||
# 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 ./...
|
@ -0,0 +1,148 @@
|
||||
/*
|
||||
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
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
######################################################################
|
||||
# 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
|
@ -0,0 +1,28 @@
|
||||
######################################################################
|
||||
# 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
|
@ -0,0 +1,32 @@
|
||||
{% 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
|
@ -0,0 +1,200 @@
|
||||
######################################################################
|
||||
# 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
|
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
@ -0,0 +1,340 @@
|
||||
# Deployment Manager Design
|
||||
|
||||
## Overview
|
||||
Deployment Manager is a service which can be run in a Kubernetes cluster that
|
||||
provides a declarative configuration language to describe Kubernetes
|
||||
resources and a mechanism for deploying, updating, and deleting configurations.
|
||||
This document describes the configuration language, object model, and
|
||||
architecture of the service in detail.
|
||||
|
||||
## Configuration Language
|
||||
The configuration language in Deployment Manager consists of two parts: a
|
||||
YAML-based language for describing resources, and a templating mechanism for
|
||||
creating abstract parameterizable types.
|
||||
|
||||
A configuration consists of a list of resources in YAML. Resources have three
|
||||
properties:
|
||||
|
||||
* name: the name to use when managing the resource
|
||||
* type: the type of the resource being managed
|
||||
* properties: the configuration properties of the resource
|
||||
|
||||
An example snippet of a configuration looks like:
|
||||
|
||||
```
|
||||
resources:
|
||||
- name: my-rc
|
||||
type: ReplicationController
|
||||
properties:
|
||||
metadata:
|
||||
name: my-rc
|
||||
spec:
|
||||
replicas: 1
|
||||
...
|
||||
- name: my-service
|
||||
type: Service
|
||||
properties:
|
||||
...
|
||||
```
|
||||
|
||||
### References
|
||||
Resources can reference values from other resources. The version of Deployment
|
||||
Manager running in the Google Cloud Platform uses references to understand
|
||||
dependencies between resources and properly order the operations it performs on
|
||||
a configuration. This version doesn't yet have this functionality, but will have
|
||||
it shortly.
|
||||
|
||||
A reference follows this syntax: **$(ref.NAME.PATH)**, where _NAME_ is the name
|
||||
of the resource being referenced, and _PATH_ is a JSON path to the value in the
|
||||
resource object.
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
$(ref.my-service.metadata.name)
|
||||
```
|
||||
|
||||
In this case, _my-service_ is the name of the resource, and _metadata.name_ is
|
||||
the JSON path to the value being referenced.
|
||||
|
||||
### Configurable Resources
|
||||
Configurable resources are the primitive resources that can be configured in
|
||||
Deployment Manager, including:
|
||||
|
||||
* Pod
|
||||
* ReplicationController
|
||||
* Service
|
||||
|
||||
Deployment Manager processes configurable resources by passing their
|
||||
configuration properties directly to kubectl on the cluster to create, update,
|
||||
or delete the resource.
|
||||
|
||||
### Templates
|
||||
Templates are abstract types that can be created using Python or
|
||||
[Jinja](http://jinja.pocoo.org/). Templates take a set of properties and must
|
||||
output a valid YAML configuration string. Properties are bound to values when a
|
||||
template is instantiated in a configuration.
|
||||
|
||||
Templates are expanded as a pre-processing step before configurable resources
|
||||
are processed. They can output configurations containing configurable resources,
|
||||
or additional nested templates. Nested templates will be processed recursively.
|
||||
|
||||
An example of a template in python is:
|
||||
|
||||
```
|
||||
import yaml
|
||||
|
||||
def GenerateConfig(context):
|
||||
resources = [{
|
||||
'name': context.env['name'] + '-service',
|
||||
'type': 'Service',
|
||||
'properties': {
|
||||
'prop1': context.properties['prop1'],
|
||||
...
|
||||
}
|
||||
}]
|
||||
|
||||
return yaml.dump({'resources': resources})
|
||||
```
|
||||
|
||||
and in Jinja is:
|
||||
|
||||
```
|
||||
resources:
|
||||
- name: {{ env['name'] }}-service
|
||||
type: Service
|
||||
properties:
|
||||
prop1: {{ properties['prop1'] }}
|
||||
...
|
||||
```
|
||||
|
||||
Templates provide access to several sets of data, which can be used for
|
||||
parameterizing or further customizing a configuration:
|
||||
|
||||
* env: a map of values defined by Deployment Manager, including _deployment_,
|
||||
_name_, and _type_
|
||||
* properties: a map of the key/value pairs passed in the properties section when
|
||||
instantiating the template
|
||||
* imports: a map of import file name to file contents of all imports originally
|
||||
specified for the configuration
|
||||
|
||||
In Python, this data is available from the _context_ object passed into the
|
||||
_GenerateConfig_ method.
|
||||
|
||||
### Template Schemas
|
||||
A schema can be provided for a template. The schema describes the template in
|
||||
more details, including:
|
||||
|
||||
* info: more information about the template, including long description and
|
||||
title
|
||||
* required: properties which are required when instantiating the template
|
||||
* properties: JSON Schema descriptions of each property the template accepts
|
||||
|
||||
An example of a template schema is:
|
||||
|
||||
```
|
||||
info:
|
||||
title: The Example
|
||||
description: A template being used as an example to illustrate concepts.
|
||||
|
||||
required:
|
||||
- prop1
|
||||
|
||||
properties:
|
||||
prop1:
|
||||
description: The first property
|
||||
type: string
|
||||
default: prop-value
|
||||
```
|
||||
|
||||
Schemas are used by Deployment Manager to validate properties being used during
|
||||
template instantiation and provide default value semantics on properties.
|
||||
|
||||
Schemas must be imported along-side the templates which they describe when
|
||||
passing configuration to Deployment Manager.
|
||||
|
||||
### Instantiating Templates
|
||||
Templates can be instantiated in the same way that a configurable resource is
|
||||
used, but must be imported and included as part of the configuration.
|
||||
|
||||
```
|
||||
imports:
|
||||
- path: example.py
|
||||
|
||||
resources:
|
||||
- name: example
|
||||
type: example.py
|
||||
properties:
|
||||
prop1: prop-value
|
||||
```
|
||||
|
||||
The _imports_ list is not understood by the Deployment Manager service, but is a
|
||||
directive to client-side tooling to specify what additional files should be
|
||||
included when passing a configuration to the API.
|
||||
|
||||
## API Model
|
||||
Deployment Manager exposes a set of RESTful collections over HTTP/JSON.
|
||||
|
||||
### Deployments
|
||||
Deployments are the primary resource in the Deployment Manager service. The
|
||||
inputs to a deployment are:
|
||||
|
||||
* name
|
||||
* targetConfig
|
||||
|
||||
When creating a deployment, users pass their YAML configuration, as well as any
|
||||
import files (templates, datafiles, etc.) in as the _targetConfig_.
|
||||
|
||||
Creating, updating and deleting a deployment creates a new manifest for the
|
||||
deployment, and then processes the new configuration. In the case of deleting a
|
||||
deployment, the deployment is first updated to an empty manifest containing no
|
||||
resources, and then is removed from the system.
|
||||
|
||||
Deployments are available at the HTTP endpoint:
|
||||
|
||||
```
|
||||
http://manager-service/deployments
|
||||
```
|
||||
|
||||
### Manifests
|
||||
A manifest is created for a deployment every time it is mutated, including
|
||||
creation, update, and deletion.
|
||||
|
||||
A manifest contains three major pieces of data:
|
||||
|
||||
* inputConfig: the original input configuration for the manifest, including YAML
|
||||
configuration and imports
|
||||
* expandedConfig: the final expanded configuration to be used when processing
|
||||
resources for the manifest
|
||||
* layout: the hierarchical structure of the manifest
|
||||
|
||||
Manifests are available at the HTTP endpoint:
|
||||
|
||||
```
|
||||
http://manager-service/deployments/<deployment>/manifests
|
||||
```
|
||||
|
||||
#### Expanded Configuration
|
||||
Given a new _inputConfig_, Deployment Manager expands all template
|
||||
instantiations recursively until there is a flat set of configurable resources.
|
||||
This final set is stored as the _expandedConfig_ and is used during resource
|
||||
processing.
|
||||
|
||||
#### Layout
|
||||
Users can use templates to build a rich, deep hierarchical architecture in their
|
||||
configuration. Expansion flattens this hierarchy and removes the template
|
||||
relationships from the configuration to create a format optimized for the process
|
||||
of instantiating the resources. However, the structural information contained in
|
||||
the original configuration has many uses, so rather than discard it, Deployment
|
||||
Manager preserves it in the form of a _layout_.
|
||||
|
||||
The _layout_ looks very much like an input configuration. It is a YAML list of
|
||||
resources, where each resource contains the following information:
|
||||
|
||||
* name: name of the resource
|
||||
* type: type of the resource
|
||||
* properties: properties of the resource, set only for templates
|
||||
* resources: sub-resources from expansion, set only for templates
|
||||
|
||||
An example layout is:
|
||||
|
||||
```
|
||||
resources:
|
||||
- name: rs
|
||||
type: replicatedservice.py
|
||||
propertes:
|
||||
replicas: 2
|
||||
resources:
|
||||
- name: rs-rc
|
||||
type: ReplicationController
|
||||
- name: rs-service
|
||||
type: Service
|
||||
```
|
||||
|
||||
The layout can be used for visualizing the architecture of resources, including
|
||||
their hierarchical structure and reference relationships.
|
||||
|
||||
### Types
|
||||
The types API provides information about existing types being used the cluster.
|
||||
|
||||
It can be used to list all known types that are in use in existing deployments:
|
||||
|
||||
```
|
||||
http://manager-service/types
|
||||
```
|
||||
|
||||
It can be used to list all active instances of a specific type in the cluster:
|
||||
|
||||
```
|
||||
http://manager-service/types/<type>/instances
|
||||
```
|
||||
|
||||
Passing _all_ as the type shows all instances of all types in the cluster. Type
|
||||
instances include the following information:
|
||||
|
||||
* name: name of resource
|
||||
* type: type of resource
|
||||
* deployment: name of deployment in which the resource resides
|
||||
* manifest: name of manifest in which the resource configuration resides
|
||||
* path: JSON path to the entry for the resource in the manifest layout
|
||||
|
||||
## Architecture
|
||||
The Deployment Manager service is built to run as a service within a Kubernetes
|
||||
cluster. It has three major components to manage deployments. The following
|
||||
diagram illustrates the relationships between the components, which are described
|
||||
in more detail below.
|
||||
|
||||
![Architecture Diagram](architecture.png "Architecture Diagram")
|
||||
|
||||
Currently there are two caveats in the design of the service:
|
||||
|
||||
* Synchronous API: the API is currently designed to block on all processing for
|
||||
a deployment request. In the future, this design will change to an
|
||||
asynchronous operation-based mode.
|
||||
* Non-persistence: the service currently stores all metadata in memory, so will
|
||||
lose all knowledge of deployments and their metadata on restart. In the
|
||||
future, the service will persist all deployment metadata in the cluster.
|
||||
|
||||
### Manager
|
||||
The **manager** service acts as both the API server and the workflow engine for
|
||||
processing deployments. The process for a deployment is:
|
||||
|
||||
1. Create a new deployment with a manifest containing _inputConfig_ from the
|
||||
user request
|
||||
1. Call out to **expandybird** service to perform expansion on the _inputConfig_
|
||||
1. Store the resulting _expandedConfig_ and _layout_
|
||||
1. Call out to **resourcifier** service to perform processing on resources from
|
||||
the _expandedConfig_
|
||||
1. Respond with success or error messages to the original API request
|
||||
|
||||
The manager is responsible for all persistence of metadata associated with
|
||||
deployments, manifests, type instances, and other resources in the Deployment
|
||||
Manager model.
|
||||
|
||||
### Expandybird
|
||||
The **expandybird** service takes in input configurations, including the YAML
|
||||
configuration and import files, performs all template expansion, and returns the
|
||||
resulting flat configuration and layout. It is completely stateless and handles
|
||||
requests synchronously.
|
||||
|
||||
Because templates are Python or Jinja, the actual expansion process is performed
|
||||
in a sub-process running a Python interpreter. A new sub-process is created for
|
||||
every request to expandybird.
|
||||
|
||||
Currently expansion is not sandboxed, but the intention of templates is to be
|
||||
reproducable hermetically sealed entities, so future designs may
|
||||
introduce a sandbox to limit external interaction like network and disk access
|
||||
during expansion.
|
||||
|
||||
### Resourcifier
|
||||
The **resourcifier** service takes in flat expanded configurations containing
|
||||
only configurable resources, and makes the respective kubectl calls to process
|
||||
each resource. It is completely stateless and handles requests synchronously.
|
||||
|
||||
Processing may be to create, update, or delete a resource,
|
||||
depending on the request. The resourcifier handles references, and is the major
|
||||
workflow engine for resource processing. In the future. it will also handle
|
||||
dependencies between resources, as described earlier.
|
||||
|
||||
The resourcifier service returns either success or error messages encountered
|
||||
during resource processing.
|
@ -0,0 +1,44 @@
|
||||
#!/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."
|
@ -0,0 +1,36 @@
|
||||
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
|
@ -0,0 +1,187 @@
|
||||
"""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
|
@ -0,0 +1,15 @@
|
||||
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
|
@ -0,0 +1,32 @@
|
||||
{% 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
|
@ -0,0 +1,187 @@
|
||||
"""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
|
@ -0,0 +1,16 @@
|
||||
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"]
|
@ -0,0 +1,28 @@
|
||||
# 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
|
||||
|
@ -0,0 +1,237 @@
|
||||
/*
|
||||
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
|
||||
}
|
@ -0,0 +1,146 @@
|
||||
/*
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,372 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
######################################################################
|
||||
# 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.
|
||||
######################################################################
|
||||
|
||||
"""Template expansion utilities."""
|
||||
|
||||
import os.path
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import jinja2
|
||||
import yaml
|
||||
|
||||
from sandbox_loader import FileAccessRedirector
|
||||
|
||||
import schema_validation
|
||||
|
||||
|
||||
def Expand(config, imports=None, env=None, validate_schema=False):
|
||||
"""Expand the configuration with imports.
|
||||
|
||||
Args:
|
||||
config: string, the raw config to be expanded.
|
||||
imports: map from import file name, e.g. "helpers/constants.py" to
|
||||
its contents.
|
||||
env: map from string to string, the map of environment variable names
|
||||
to their values
|
||||
validate_schema: True to run schema validation; False otherwise
|
||||
Returns:
|
||||
YAML containing the expanded configuration and its layout, in the following
|
||||
format:
|
||||
|
||||
config:
|
||||
...
|
||||
layout:
|
||||
...
|
||||
|
||||
Raises:
|
||||
ExpansionError: if there is any error occurred during expansion
|
||||
"""
|
||||
try:
|
||||
return _Expand(config, imports=imports, env=env,
|
||||
validate_schema=validate_schema)
|
||||
except Exception as e:
|
||||
# print traceback.format_exc()
|
||||
raise ExpansionError('config', str(e))
|
||||
|
||||
|
||||
def _Expand(config, imports=None, env=None, validate_schema=False):
|
||||
"""Expand the configuration with imports."""
|
||||
|
||||
FileAccessRedirector.redirect(imports)
|
||||
|
||||
yaml_config = None
|
||||
try:
|
||||
yaml_config = yaml.safe_load(config)
|
||||
except yaml.scanner.ScannerError as e:
|
||||
# Here we know that YAML parser could not parse the template we've given it.
|
||||
# YAML raises a ScannerError that specifies which file had the problem, as
|
||||
# well as line and column, but since we're giving it the template from
|
||||
# string, error message contains <string>, which is not very helpful on the
|
||||
# user end, so replace it with word "template" and make it obvious that YAML
|
||||
# contains a syntactic error.
|
||||
msg = str(e).replace('"<string>"', 'template')
|
||||
raise Exception('Error parsing YAML: %s' % msg)
|
||||
|
||||
# Handle empty file case
|
||||
if not yaml_config:
|
||||
return ''
|
||||
|
||||
# If the configuration does not have ':' in it, the yaml_config will be a
|
||||
# string. If this is the case just return the str. The code below it assumes
|
||||
# yaml_config is a map for common cases.
|
||||
if type(yaml_config) is str:
|
||||
return yaml_config
|
||||
|
||||
if not yaml_config.has_key('resources') or yaml_config['resources'] is None:
|
||||
yaml_config['resources'] = []
|
||||
|
||||
config = {'resources': []}
|
||||
layout = {'resources': []}
|
||||
|
||||
_ValidateUniqueNames(yaml_config['resources'])
|
||||
|
||||
# Iterate over all the resources to process.
|
||||
for resource in yaml_config['resources']:
|
||||
processed_resource = _ProcessResource(resource, imports, env,
|
||||
validate_schema)
|
||||
|
||||
config['resources'].extend(processed_resource['config']['resources'])
|
||||
layout['resources'].append(processed_resource['layout'])
|
||||
|
||||
result = {'config': config, 'layout': layout}
|
||||
return yaml.safe_dump(result, default_flow_style=False)
|
||||
|
||||
|
||||
def _ProcessResource(resource, imports, env, validate_schema=False):
|
||||
"""Processes a resource and expands if template.
|
||||
|
||||
Args:
|
||||
resource: the resource to be processed, as a map.
|
||||
imports: map from string to string, the map of imported files names
|
||||
and contents
|
||||
env: map from string to string, the map of environment variable names
|
||||
to their values
|
||||
validate_schema: True to run schema validation; False otherwise
|
||||
Returns:
|
||||
A map containing the layout and configuration of the expanded
|
||||
resource and any sub-resources, in the format:
|
||||
|
||||
{'config': ..., 'layout': ...}
|
||||
Raises:
|
||||
ExpansionError: if there is any error occurred during expansion
|
||||
"""
|
||||
# A resource has to have to a name.
|
||||
if not resource.has_key('name'):
|
||||
raise ExpansionError(resource, 'Resource does not have a name.')
|
||||
|
||||
# A resource has to have a type.
|
||||
if not resource.has_key('type'):
|
||||
raise ExpansionError(resource, 'Resource does not have type defined.')
|
||||
|
||||
config = {'resources': []}
|
||||
# Initialize layout with basic resource information.
|
||||
layout = {'name': resource['name'],
|
||||
'type': resource['type']}
|
||||
|
||||
if IsTemplate(resource['type']) and resource['type'] in imports:
|
||||
# A template resource, which contains sub-resources.
|
||||
expanded_template = ExpandTemplate(resource, imports, env, validate_schema)
|
||||
|
||||
if expanded_template['resources'] is not None:
|
||||
_ValidateUniqueNames(expanded_template['resources'], resource['type'])
|
||||
|
||||
# Process all sub-resources of this template.
|
||||
for resource_to_process in expanded_template['resources']:
|
||||
processed_resource = _ProcessResource(resource_to_process, imports, env)
|
||||
|
||||
# Append all sub-resources to the config resources, and the resulting
|
||||
# layout of sub-resources.
|
||||
config['resources'].extend(processed_resource['config']['resources'])
|
||||
|
||||
# Lazy-initialize resources key here because it is not set for
|
||||
# non-template layouts.
|
||||
if 'resources' not in layout:
|
||||
layout['resources'] = []
|
||||
layout['resources'].append(processed_resource['layout'])
|
||||
|
||||
if 'properties' in resource:
|
||||
layout['properties'] = resource['properties']
|
||||
else:
|
||||
# A normal resource has only itself for config.
|
||||
config['resources'] = [resource]
|
||||
|
||||
return {'config': config,
|
||||
'layout': layout}
|
||||
|
||||
|
||||
def _ValidateUniqueNames(template_resources, template_name='config'):
|
||||
"""Make sure that every resource name in the given template is unique."""
|
||||
names = set()
|
||||
# Validate that every resource name is unique
|
||||
for resource in template_resources:
|
||||
if 'name' in resource:
|
||||
if resource['name'] in names:
|
||||
raise ExpansionError(
|
||||
resource,
|
||||
'Resource name \'%s\' is not unique in %s.' % (resource['name'],
|
||||
template_name))
|
||||
names.add(resource['name'])
|
||||
# If this resource doesn't have a name, we will report that error later
|
||||
|
||||
|
||||
def IsTemplate(resource_type):
|
||||
"""Returns whether a given resource type is a Template."""
|
||||
return resource_type.endswith('.py') or resource_type.endswith('.jinja')
|
||||
|
||||
|
||||
def ExpandTemplate(resource, imports, env, validate_schema=False):
|
||||
"""Expands a template, calling expansion mechanism based on type.
|
||||
|
||||
Args:
|
||||
resource: resource object, the resource that contains parameters to the
|
||||
jinja file
|
||||
imports: map from string to string, the map of imported files names
|
||||
and contents
|
||||
env: map from string to string, the map of environment variable names
|
||||
to their values
|
||||
validate_schema: True to run schema validation; False otherwise
|
||||
Returns:
|
||||
The final expanded template
|
||||
|
||||
Raises:
|
||||
ExpansionError: if there is any error occurred during expansion
|
||||
"""
|
||||
source_file = resource['type']
|
||||
|
||||
# Look for Template in imports.
|
||||
if source_file not in imports:
|
||||
raise ExpansionError(
|
||||
source_file,
|
||||
'Unable to find source file %s in imports.' % (source_file))
|
||||
|
||||
resource['imports'] = imports
|
||||
|
||||
# Populate the additional environment variables.
|
||||
if env is None:
|
||||
env = {}
|
||||
env['name'] = resource['name']
|
||||
env['type'] = resource['type']
|
||||
resource['env'] = env
|
||||
|
||||
schema = source_file + '.schema'
|
||||
if validate_schema and schema in imports:
|
||||
properties = resource['properties'] if 'properties' in resource else {}
|
||||
try:
|
||||
resource['properties'] = schema_validation.Validate(
|
||||
properties, schema, source_file, imports)
|
||||
except schema_validation.ValidationErrors as e:
|
||||
raise ExpansionError(resource, e.message)
|
||||
|
||||
if source_file.endswith('jinja'):
|
||||
expanded_template = ExpandJinja(imports[source_file], resource, imports)
|
||||
elif source_file.endswith('py'):
|
||||
# This is a Python template.
|
||||
expanded_template = ExpandPython(
|
||||
imports[source_file], source_file, resource)
|
||||
else:
|
||||
# The source file is not a jinja file or a python file.
|
||||
# This in fact should never happen due to the IsTemplate check above.
|
||||
raise ExpansionError(
|
||||
resource['source'],
|
||||
'Unsupported source file: %s.' % (source_file))
|
||||
|
||||
parsed_template = yaml.safe_load(expanded_template)
|
||||
|
||||
if parsed_template is None or 'resources' not in parsed_template:
|
||||
raise ExpansionError(resource['type'],
|
||||
'Template did not return a \'resources:\' field.')
|
||||
|
||||
return parsed_template
|
||||
|
||||
|
||||
def ExpandJinja(source_template, resource, imports):
|
||||
"""Render the jinja template using jinja libraries.
|
||||
|
||||
Args:
|
||||
source_template: string, the content of jinja file to be render
|
||||
resource: resource object, the resource that contains parameters to the
|
||||
jinja file
|
||||
imports: map from string to string, the map of imported files names
|
||||
and contents
|
||||
Returns:
|
||||
The final expanded template
|
||||
"""
|
||||
|
||||
# The standard jinja loader doesn't work in the sandbox as it calls getmtime()
|
||||
# and this system call is not supported.
|
||||
env = jinja2.Environment(loader=jinja2.DictLoader(imports))
|
||||
|
||||
template = env.from_string(source_template)
|
||||
|
||||
if (resource.has_key('properties') or resource.has_key('env') or
|
||||
resource.has_key('imports')):
|
||||
return template.render(resource)
|
||||
else:
|
||||
return template.render()
|
||||
|
||||
|
||||
def ExpandPython(python_source, file_name, params):
|
||||
"""Run python script to get the expanded template.
|
||||
|
||||
Args:
|
||||
python_source: string, the python source file to run
|
||||
file_name: string, the name of the python source file
|
||||
params: object that contains 'imports' and 'params', the parameters to
|
||||
the python script
|
||||
Returns:
|
||||
The final expanded template.
|
||||
"""
|
||||
|
||||
try:
|
||||
# Compile the python code to be run.
|
||||
constructor = {}
|
||||
compiled_code = compile(python_source, '<string>', 'exec')
|
||||
exec compiled_code in constructor # pylint: disable=exec-used
|
||||
|
||||
# Construct the parameters to the python script.
|
||||
evaluation_context = PythonEvaluationContext(params)
|
||||
|
||||
return constructor['GenerateConfig'](evaluation_context)
|
||||
except Exception:
|
||||
st = 'Exception in %s\n%s' % (file_name, traceback.format_exc())
|
||||
raise ExpansionError(file_name, st)
|
||||
|
||||
|
||||
class PythonEvaluationContext(object):
|
||||
"""The python evaluation context.
|
||||
|
||||
Attributes:
|
||||
params -- the parameters to be used in the expansion
|
||||
"""
|
||||
|
||||
def __init__(self, params):
|
||||
if params.has_key('properties'):
|
||||
self.properties = params['properties']
|
||||
else:
|
||||
self.properties = None
|
||||
|
||||
if params.has_key('imports'):
|
||||
self.imports = params['imports']
|
||||
else:
|
||||
self.imports = None
|
||||
|
||||
if params.has_key('env'):
|
||||
self.env = params['env']
|
||||
else:
|
||||
self.env = None
|
||||
|
||||
|
||||
class ExpansionError(Exception):
|
||||
"""Exception raised for errors during expansion process.
|
||||
|
||||
Attributes:
|
||||
resource: the resource processed that results in the error
|
||||
message: the detailed message of the error
|
||||
"""
|
||||
|
||||
def __init__(self, resource, message):
|
||||
self.resource = resource
|
||||
self.message = message + ' Resource: ' + str(resource)
|
||||
super(ExpansionError, self).__init__(self.message)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print >> sys.stderr, 'No input specified.'
|
||||
sys.exit(1)
|
||||
template = sys.argv[1]
|
||||
idx = 2
|
||||
imports = {}
|
||||
while idx < len(sys.argv):
|
||||
if idx + 1 == len(sys.argv):
|
||||
print >>sys.stderr, 'Invalid import definition at argv pos %d' % idx
|
||||
sys.exit(1)
|
||||
name = sys.argv[idx]
|
||||
value = sys.argv[idx + 1]
|
||||
imports[name] = value
|
||||
idx += 2
|
||||
|
||||
env = {}
|
||||
env['deployment'] = os.environ['DEPLOYMENT_NAME']
|
||||
env['project'] = os.environ['PROJECT']
|
||||
|
||||
validate_schema = 'VALIDATE_SCHEMA' in os.environ
|
||||
|
||||
# Call the expansion logic to actually expand the template.
|
||||
print Expand(template, imports, env=env, validate_schema=validate_schema)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -0,0 +1,48 @@
|
||||
######################################################################
|
||||
# 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()
|
@ -0,0 +1,153 @@
|
||||
######################################################################
|
||||
# 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
|
||||
|
@ -0,0 +1,213 @@
|
||||
######################################################################
|
||||
# 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)
|
@ -0,0 +1,124 @@
|
||||
######################################################################
|
||||
# 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
|
@ -0,0 +1,51 @@
|
||||
/*
|
||||
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())
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
pyyaml
|
||||
Jinja2
|
||||
Jsonschema
|
@ -0,0 +1,91 @@
|
||||
/*
|
||||
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))
|
||||
}
|
@ -0,0 +1,222 @@
|
||||
/*
|
||||
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
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
######################################################################
|
||||
# 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
|
@ -0,0 +1,22 @@
|
||||
######################################################################
|
||||
# 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
|
@ -0,0 +1,22 @@
|
||||
######################################################################
|
||||
# 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
|
@ -0,0 +1,22 @@
|
||||
######################################################################
|
||||
# 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
|
@ -0,0 +1,20 @@
|
||||
######################################################################
|
||||
# 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
|
@ -0,0 +1,21 @@
|
||||
######################################################################
|
||||
# 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
|
@ -0,0 +1,21 @@
|
||||
######################################################################
|
||||
# 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
|
@ -0,0 +1,21 @@
|
||||
######################################################################
|
||||
# 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
|
@ -0,0 +1,27 @@
|
||||
######################################################################
|
||||
# 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
|
@ -0,0 +1,176 @@
|
||||
######################################################################
|
||||
# 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
|
@ -0,0 +1,34 @@
|
||||
# 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"]
|
@ -0,0 +1,22 @@
|
||||
# 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)
|
||||
|
@ -0,0 +1,310 @@
|
||||
/*
|
||||
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)
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
/*
|
||||
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))
|
||||
}
|
@ -0,0 +1,164 @@
|
||||
/*
|
||||
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
|
||||
}
|
@ -0,0 +1,300 @@
|
||||
/*
|
||||
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)
|
||||
}
|
@ -0,0 +1,235 @@
|
||||
/*
|
||||
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
|
||||
}
|
@ -0,0 +1,351 @@
|
||||
/*
|
||||
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 (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"util"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
)
|
||||
|
||||
type mockResolver struct {
|
||||
responses [][]*ImportFile
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
func (r *mockResolver) ResolveTypes(c *Configuration, i []*ImportFile) ([]*ImportFile, error) {
|
||||
if len(r.responses) < 1 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ret := r.responses[0]
|
||||
r.responses = r.responses[1:]
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
var validTemplateTestCaseData = Template{
|
||||
Name: "TestTemplate",
|
||||
Content: string(validContentTestCaseData),
|
||||
Imports: validImportFilesTestCaseData,
|
||||
}
|
||||
|
||||
var validContentTestCaseData = []byte(`
|
||||
imports:
|
||||
- path: test-type.py
|
||||
resources:
|
||||
- name: test
|
||||
type: test-type.py
|
||||
properties:
|
||||
test-property: test-value
|
||||
`)
|
||||
|
||||
var validImportFilesTestCaseData = []*ImportFile{
|
||||
&ImportFile{
|
||||
Name: "test-type.py",
|
||||
Content: "test-type.py validTemplateTestCaseData content",
|
||||
},
|
||||
}
|
||||
|
||||
var validConfigTestCaseData = []byte(`
|
||||
resources:
|
||||
- name: test-service
|
||||
properties:
|
||||
test-property: test-value
|
||||
type: Service
|
||||
- name: test-rc
|
||||
properties:
|
||||
test-property: test-value
|
||||
type: ReplicationController
|
||||
- name: test3-service
|
||||
properties:
|
||||
test-property: test-value
|
||||
type: Service
|
||||
- name: test3-rc
|
||||
properties:
|
||||
test-property: test-value
|
||||
type: ReplicationController
|
||||
- name: test4-service
|
||||
properties:
|
||||
test-property: test-value
|
||||
type: Service
|
||||
- name: test4-rc
|
||||
properties:
|
||||
test-property: test-value
|
||||
type: ReplicationController
|
||||
`)
|
||||
|
||||
var validLayoutTestCaseData = []byte(`
|
||||
resources:
|
||||
- name: test
|
||||
properties:
|
||||
test-property: test-value
|
||||
resources:
|
||||
- name: test-service
|
||||
type: Service
|
||||
- name: test-rc
|
||||
type: ReplicationController
|
||||
type: test-type.py
|
||||
- name: test2
|
||||
properties: null
|
||||
resources:
|
||||
- name: test3
|
||||
properties:
|
||||
test-property: test-value
|
||||
resources:
|
||||
- name: test3-service
|
||||
type: Service
|
||||
- name: test3-rc
|
||||
type: ReplicationController
|
||||
type: test-type.py
|
||||
- name: test4
|
||||
properties:
|
||||
test-property: test-value
|
||||
resources:
|
||||
- name: test4-service
|
||||
type: Service
|
||||
- name: test4-rc
|
||||
type: ReplicationController
|
||||
type: test-type.py
|
||||
type: test2.jinja
|
||||
`)
|
||||
|
||||
var validResponseTestCaseData = ExpansionResponse{
|
||||
Config: string(validConfigTestCaseData),
|
||||
Layout: string(validLayoutTestCaseData),
|
||||
}
|
||||
|
||||
var roundTripContent = `
|
||||
config:
|
||||
resources:
|
||||
- name: test
|
||||
type: test
|
||||
properties:
|
||||
test: test
|
||||
`
|
||||
|
||||
var roundTripExpanded = `
|
||||
resources:
|
||||
- name: test2
|
||||
type: test2
|
||||
properties:
|
||||
test: test
|
||||
`
|
||||
|
||||
var roundTripLayout = `
|
||||
resources:
|
||||
- name: test
|
||||
type: test
|
||||
properties:
|
||||
test: test
|
||||
resources:
|
||||
- name: test2
|
||||
type: test2
|
||||
properties:
|
||||
test: test
|
||||
`
|
||||
|
||||
var roundTripExpanded2 = `
|
||||
resources:
|
||||
- name: test3
|
||||
type: Service
|
||||
properties:
|
||||
test: test
|
||||
`
|
||||
|
||||
var roundTripLayout2 = `
|
||||
resources:
|
||||
- name: test2
|
||||
type: test2
|
||||
properties:
|
||||
test: test
|
||||
resources:
|
||||
- name: test3
|
||||
type: Service
|
||||
properties:
|
||||
test: test
|
||||
`
|
||||
|
||||
var finalExpanded = `
|
||||
config:
|
||||
resources:
|
||||
- name: test3
|
||||
type: Service
|
||||
properties:
|
||||
test: test
|
||||
layout:
|
||||
resources:
|
||||
- name: test
|
||||
type: test
|
||||
properties:
|
||||
test: test
|
||||
resources:
|
||||
- name: test2
|
||||
type: test2
|
||||
properties:
|
||||
test: test
|
||||
resources:
|
||||
- name: test3
|
||||
type: Service
|
||||
properties:
|
||||
test: test
|
||||
`
|
||||
|
||||
var roundTripTemplate = Template{
|
||||
Name: "TestTemplate",
|
||||
Content: roundTripContent,
|
||||
Imports: nil,
|
||||
}
|
||||
|
||||
type ExpanderTestCase struct {
|
||||
Description string
|
||||
Error string
|
||||
Handler func(w http.ResponseWriter, r *http.Request)
|
||||
Resolver TypeResolver
|
||||
ValidResponse *ExpandedTemplate
|
||||
}
|
||||
|
||||
func TestExpandTemplate(t *testing.T) {
|
||||
roundTripResponse := &ExpandedTemplate{}
|
||||
if err := yaml.Unmarshal([]byte(finalExpanded), roundTripResponse); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
tests := []ExpanderTestCase{
|
||||
{
|
||||
"expect success for ExpandTemplate",
|
||||
"",
|
||||
expanderSuccessHandler,
|
||||
&mockResolver{},
|
||||
getValidResponse(t, "expect success for ExpandTemplate"),
|
||||
},
|
||||
{
|
||||
"expect error for ExpandTemplate",
|
||||
"cannot expand template",
|
||||
expanderErrorHandler,
|
||||
&mockResolver{},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"expect success for ExpandTemplate with two expansions",
|
||||
"",
|
||||
roundTripHandler,
|
||||
&mockResolver{[][]*ImportFile{
|
||||
{},
|
||||
{&ImportFile{Name: "test"}},
|
||||
}, t},
|
||||
roundTripResponse,
|
||||
},
|
||||
}
|
||||
|
||||
for _, etc := range tests {
|
||||
ts := httptest.NewServer(http.HandlerFunc(etc.Handler))
|
||||
defer ts.Close()
|
||||
|
||||
expander := NewExpander(ts.URL, etc.Resolver)
|
||||
actualResponse, err := expander.ExpandTemplate(validTemplateTestCaseData)
|
||||
if err != nil {
|
||||
message := err.Error()
|
||||
if etc.Error == "" {
|
||||
t.Errorf("Error in test case %s when there should not be.", etc.Description)
|
||||
}
|
||||
if !strings.Contains(message, etc.Error) {
|
||||
t.Errorf("error in test case:%s:%s\n", etc.Description, message)
|
||||
}
|
||||
} else {
|
||||
if etc.Error != "" {
|
||||
t.Errorf("expected error:%s\ndid not occur in test case:%s\n",
|
||||
etc.Error, etc.Description)
|
||||
}
|
||||
|
||||
expectedResponse := etc.ValidResponse
|
||||
if !reflect.DeepEqual(expectedResponse, actualResponse) {
|
||||
t.Errorf("error in test case:%s:\nwant:%s\nhave:%s\n",
|
||||
etc.Description, util.ToYAMLOrError(expectedResponse), util.ToYAMLOrError(actualResponse))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getValidResponse(t *testing.T, description string) *ExpandedTemplate {
|
||||
response, err := validResponseTestCaseData.Unmarshal()
|
||||
if err != nil {
|
||||
t.Errorf("cannot unmarshal valid response for test case '%s': %s\n", description, err)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
func expanderErrorHandler(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
http.Error(w, "something failed", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
var roundTripResponse = ExpansionResponse{
|
||||
Config: roundTripExpanded,
|
||||
Layout: roundTripLayout,
|
||||
}
|
||||
|
||||
var roundTripResponse2 = ExpansionResponse{
|
||||
Config: roundTripExpanded2,
|
||||
Layout: roundTripLayout2,
|
||||
}
|
||||
|
||||
var roundTripResponses = []ExpansionResponse{
|
||||
roundTripResponse,
|
||||
roundTripResponse2,
|
||||
}
|
||||
|
||||
func roundTripHandler(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
handler := "expandybird: expand"
|
||||
util.LogHandlerEntry(handler, r)
|
||||
util.LogHandlerExitWithJSON(handler, w, roundTripResponses[0], http.StatusOK)
|
||||
roundTripResponses = roundTripResponses[1:]
|
||||
}
|
||||
|
||||
func expanderSuccessHandler(w http.ResponseWriter, r *http.Request) {
|
||||
handler := "expandybird: expand"
|
||||
util.LogHandlerEntry(handler, r)
|
||||
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
|
||||
}
|
||||
|
||||
template := &Template{}
|
||||
if err := json.Unmarshal(body, template); err != nil {
|
||||
status := fmt.Sprintf("cannot unmarshal request body:%s\n%s\n", err, body)
|
||||
http.Error(w, status, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(validTemplateTestCaseData, *template) {
|
||||
status := fmt.Sprintf("error in http handler:\nwant:%s\nhave:%s\n",
|
||||
util.ToJSONOrError(validTemplateTestCaseData), util.ToJSONOrError(template))
|
||||
http.Error(w, status, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
util.LogHandlerExitWithJSON(handler, w, validResponseTestCaseData, http.StatusOK)
|
||||
}
|
@ -0,0 +1,255 @@
|
||||
/*
|
||||
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"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Manager manages a persistent set of Deployments.
|
||||
type Manager interface {
|
||||
ListDeployments() ([]Deployment, error)
|
||||
GetDeployment(name string) (*Deployment, error)
|
||||
CreateDeployment(t *Template) (*Deployment, error)
|
||||
DeleteDeployment(name string, forget bool) (*Deployment, error)
|
||||
PutDeployment(name string, t *Template) (*Deployment, error)
|
||||
ListManifests(deploymentName string) (map[string]*Manifest, error)
|
||||
GetManifest(deploymentName string, manifest string) (*Manifest, error)
|
||||
ListTypes() []string
|
||||
ListInstances(typeName string) []*TypeInstance
|
||||
}
|
||||
|
||||
type manager struct {
|
||||
expander Expander
|
||||
deployer Deployer
|
||||
repository Repository
|
||||
}
|
||||
|
||||
// NewManager returns a new initialized Manager.
|
||||
func NewManager(expander Expander, deployer Deployer, repository Repository) Manager {
|
||||
return &manager{expander, deployer, repository}
|
||||
}
|
||||
|
||||
// ListDeployments returns the list of deployments
|
||||
func (m *manager) ListDeployments() ([]Deployment, error) {
|
||||
l, err := m.repository.ListDeployments()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// GetDeployment retrieves the configuration stored for a given deployment
|
||||
// as well as the current configuration from the cluster.
|
||||
func (m *manager) GetDeployment(name string) (*Deployment, error) {
|
||||
d, err := m.repository.GetDeployment(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
latest := getLatestManifest(d.Manifests)
|
||||
if latest != nil {
|
||||
d.Current = latest.ExpandedConfig
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// ListManifests retrieves the manifests for a given deployment
|
||||
// of each of the deployments in the repository and returns the deployments.
|
||||
func (m *manager) ListManifests(deploymentName string) (map[string]*Manifest, error) {
|
||||
l, err := m.repository.ListManifests(deploymentName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// GetManifest retrieves the specified manifest for a given deployment
|
||||
func (m *manager) GetManifest(deploymentName string, manifestName string) (*Manifest, error) {
|
||||
d, err := m.repository.GetManifest(deploymentName, manifestName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// CreateDeployment expands the supplied template, creates the resulting
|
||||
// configuration in the cluster, creates a new deployment that tracks it,
|
||||
// and stores the deployment in the repository. Returns the deployment.
|
||||
func (m *manager) CreateDeployment(t *Template) (*Deployment, error) {
|
||||
log.Printf("Creating deployment: %s", t.Name)
|
||||
et, err := m.expander.ExpandTemplate(*t)
|
||||
if err != nil {
|
||||
log.Printf("Expansion failed %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = m.repository.CreateDeployment(t.Name)
|
||||
if err != nil {
|
||||
log.Printf("CreateDeployment failed %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
manifest := NewManifest(t.Name, generateManifestName())
|
||||
manifest.InputConfig = t
|
||||
manifest.ExpandedConfig = et.Config
|
||||
manifest.Layout = et.Layout
|
||||
|
||||
err = m.repository.AddManifest(t.Name, manifest)
|
||||
if err != nil {
|
||||
log.Printf("AddManifest failed %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: Mark this as failed instead of deleting.
|
||||
if err := m.deployer.CreateConfiguration(et.Config); err != nil {
|
||||
m.repository.DeleteDeployment(t.Name, true)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Finally update the type instances for this deployment.
|
||||
m.addTypeInstances(t.Name, manifest.Name, manifest.Layout)
|
||||
|
||||
return m.repository.GetValidDeployment(t.Name)
|
||||
}
|
||||
|
||||
func (m *manager) addTypeInstances(deploymentName string, manifestName string, layout *Layout) {
|
||||
m.repository.ClearTypeInstances(deploymentName)
|
||||
|
||||
instances := make(map[string][]*TypeInstance)
|
||||
for i, r := range layout.Resources {
|
||||
addTypeInstances(&instances, r, deploymentName, manifestName, fmt.Sprintf("$.resources[%d]", i))
|
||||
}
|
||||
|
||||
m.repository.SetTypeInstances(deploymentName, instances)
|
||||
}
|
||||
|
||||
func addTypeInstances(instances *map[string][]*TypeInstance, r *LayoutResource, deploymentName string, manifestName string, jsonPath string) {
|
||||
// Add this resource.
|
||||
inst := &TypeInstance{
|
||||
Name: r.Name,
|
||||
Type: r.Type,
|
||||
Deployment: deploymentName,
|
||||
Manifest: manifestName,
|
||||
Path: jsonPath,
|
||||
}
|
||||
(*instances)[r.Type] = append((*instances)[r.Type], inst)
|
||||
|
||||
// Add all sub resources if they exist.
|
||||
for i, sr := range r.Resources {
|
||||
addTypeInstances(instances, sr, deploymentName, manifestName, fmt.Sprintf("%s.resources[%d]", jsonPath, i))
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteDeployment deletes the configuration for the deployment with
|
||||
// the supplied identifier from the cluster.repository. If forget is true, then
|
||||
// the deployment is removed from the repository. Otherwise, it is marked
|
||||
// as deleted and retained.
|
||||
func (m *manager) DeleteDeployment(name string, forget bool) (*Deployment, error) {
|
||||
log.Printf("Deleting deployment: %s", name)
|
||||
d, err := m.repository.GetValidDeployment(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If there's a latest manifest, delete the underlying resources.
|
||||
latest := getLatestManifest(d.Manifests)
|
||||
if latest != nil {
|
||||
log.Printf("Deleting resources from the latest manifest")
|
||||
if err := m.deployer.DeleteConfiguration(latest.ExpandedConfig); err != nil {
|
||||
log.Printf("Failed to delete resources from the latest manifest: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create an empty manifest since resources have been deleted.
|
||||
err = m.repository.AddManifest(name, NewManifest(name, generateManifestName()))
|
||||
if err != nil {
|
||||
log.Printf("Failed to add empty manifest")
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
d, err = m.repository.DeleteDeployment(name, forget)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Finally remove the type instances for this deployment.
|
||||
m.repository.ClearTypeInstances(name)
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// PutDeployment replaces the configuration of the deployment with
|
||||
// the supplied identifier in the cluster, and returns the deployment.
|
||||
func (m *manager) PutDeployment(name string, t *Template) (*Deployment, error) {
|
||||
_, err := m.repository.GetValidDeployment(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO(bmelville): This should just return a new manifest filled in.
|
||||
et, err := m.expander.ExpandTemplate(*t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := m.deployer.PutConfiguration(et.Config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
manifest := NewManifest(t.Name, generateManifestName())
|
||||
manifest.InputConfig = t
|
||||
manifest.ExpandedConfig = et.Config
|
||||
manifest.Layout = et.Layout
|
||||
|
||||
err = m.repository.AddManifest(t.Name, manifest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Finally update the type instances for this deployment.
|
||||
m.addTypeInstances(t.Name, manifest.Name, manifest.Layout)
|
||||
|
||||
return m.repository.GetValidDeployment(t.Name)
|
||||
}
|
||||
|
||||
func (m *manager) ListTypes() []string {
|
||||
return m.repository.ListTypes()
|
||||
}
|
||||
|
||||
func (m *manager) ListInstances(typeName string) []*TypeInstance {
|
||||
return m.repository.GetTypeInstances(typeName)
|
||||
}
|
||||
|
||||
func generateManifestName() string {
|
||||
return fmt.Sprintf("manifest-%d", time.Now().UTC().UnixNano())
|
||||
}
|
||||
|
||||
// Given a map of manifests, finds the largest time stamp, hence probably the latest manifest.
|
||||
// This is a hack until we get a real story for storage.
|
||||
func getLatestManifest(l map[string]*Manifest) *Manifest {
|
||||
var latest = 0
|
||||
var ret *Manifest
|
||||
for k, v := range l {
|
||||
var i = 0
|
||||
fmt.Sscanf(k, "manifest-%d", &i)
|
||||
if i > latest {
|
||||
latest = i
|
||||
ret = v
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
@ -0,0 +1,400 @@
|
||||
/*
|
||||
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 (
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var template = Template{Name: "test", Content: "test"}
|
||||
|
||||
var layout = Layout{
|
||||
Resources: []*LayoutResource{&LayoutResource{Resource: Resource{Name: "test", Type: "test"}}},
|
||||
}
|
||||
var configuration = Configuration{
|
||||
Resources: []*Resource{&Resource{Name: "test", Type: "test"}},
|
||||
}
|
||||
var expandedConfig = ExpandedTemplate{
|
||||
Config: &configuration,
|
||||
Layout: &layout,
|
||||
}
|
||||
|
||||
var deploymentName = "deployment"
|
||||
var deploymentNoManifestName = "deploymentNoManifest"
|
||||
|
||||
var manifestName = "manifest-2"
|
||||
var manifest = Manifest{Name: manifestName, ExpandedConfig: &configuration, Layout: &layout}
|
||||
var manifestMap = map[string]*Manifest{manifest.Name: &manifest}
|
||||
|
||||
var deploymentNoManifest = Deployment{
|
||||
Name: "test",
|
||||
}
|
||||
var deployment = Deployment{
|
||||
Name: "test",
|
||||
Manifests: manifestMap,
|
||||
}
|
||||
var deploymentWithConfiguration = Deployment{
|
||||
Name: "test",
|
||||
Manifests: manifestMap,
|
||||
Current: &configuration,
|
||||
}
|
||||
var deploymentList = []Deployment{deployment, {Name: "test2"}}
|
||||
|
||||
var typeInstMap = map[string][]string{"test": []string{"test"}}
|
||||
|
||||
var errTest = errors.New("test")
|
||||
|
||||
type expanderStub struct{}
|
||||
|
||||
func (expander *expanderStub) ExpandTemplate(t Template) (*ExpandedTemplate, error) {
|
||||
if reflect.DeepEqual(t, template) {
|
||||
return &expandedConfig, nil
|
||||
}
|
||||
|
||||
return nil, errTest
|
||||
}
|
||||
|
||||
type deployerStub struct {
|
||||
FailCreate bool
|
||||
Created []*Configuration
|
||||
FailDelete bool
|
||||
Deleted []*Configuration
|
||||
}
|
||||
|
||||
func (deployer *deployerStub) reset() {
|
||||
deployer.FailCreate = false
|
||||
deployer.Created = make([]*Configuration, 0)
|
||||
deployer.FailDelete = false
|
||||
deployer.Deleted = make([]*Configuration, 0)
|
||||
}
|
||||
|
||||
func newDeployerStub() *deployerStub {
|
||||
ret := &deployerStub{}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (deployer *deployerStub) GetConfiguration(cached *Configuration) (*Configuration, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (deployer *deployerStub) CreateConfiguration(configuration *Configuration) error {
|
||||
if deployer.FailCreate {
|
||||
return errTest
|
||||
}
|
||||
|
||||
deployer.Created = append(deployer.Created, configuration)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (deployer *deployerStub) DeleteConfiguration(configuration *Configuration) error {
|
||||
if deployer.FailDelete {
|
||||
return errTest
|
||||
}
|
||||
deployer.Deleted = append(deployer.Deleted, configuration)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (deployer *deployerStub) PutConfiguration(configuration *Configuration) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type repositoryStub struct {
|
||||
FailListDeployments bool
|
||||
Created []string
|
||||
ManifestAdd map[string]*Manifest
|
||||
Deleted []string
|
||||
GetValid []string
|
||||
TypeInstances map[string][]string
|
||||
TypeInstancesCleared bool
|
||||
GetTypeInstancesCalled bool
|
||||
ListTypesCalled bool
|
||||
}
|
||||
|
||||
func (repository *repositoryStub) reset() {
|
||||
repository.FailListDeployments = false
|
||||
repository.Created = make([]string, 0)
|
||||
repository.ManifestAdd = make(map[string]*Manifest)
|
||||
repository.Deleted = make([]string, 0)
|
||||
repository.GetValid = make([]string, 0)
|
||||
repository.TypeInstances = make(map[string][]string)
|
||||
repository.TypeInstancesCleared = false
|
||||
repository.GetTypeInstancesCalled = false
|
||||
repository.ListTypesCalled = false
|
||||
}
|
||||
|
||||
func newRepositoryStub() *repositoryStub {
|
||||
ret := &repositoryStub{}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (repository *repositoryStub) ListDeployments() ([]Deployment, error) {
|
||||
if repository.FailListDeployments {
|
||||
return deploymentList, errTest
|
||||
}
|
||||
return deploymentList, nil
|
||||
}
|
||||
|
||||
func (repository *repositoryStub) GetDeployment(d string) (*Deployment, error) {
|
||||
if d == deploymentName {
|
||||
return &deployment, nil
|
||||
}
|
||||
|
||||
if d == deploymentNoManifestName {
|
||||
return &deploymentNoManifest, nil
|
||||
}
|
||||
|
||||
return nil, errTest
|
||||
}
|
||||
|
||||
func (repository *repositoryStub) GetValidDeployment(d string) (*Deployment, error) {
|
||||
repository.GetValid = append(repository.GetValid, d)
|
||||
return &deploymentWithConfiguration, nil
|
||||
}
|
||||
|
||||
func (repository *repositoryStub) CreateDeployment(d string) (*Deployment, error) {
|
||||
repository.Created = append(repository.Created, d)
|
||||
return &deploymentWithConfiguration, nil
|
||||
}
|
||||
|
||||
func (repository *repositoryStub) DeleteDeployment(d string, forget bool) (*Deployment, error) {
|
||||
repository.Deleted = append(repository.Deleted, d)
|
||||
return &deploymentWithConfiguration, nil
|
||||
}
|
||||
|
||||
func (repository *repositoryStub) AddManifest(d string, manifest *Manifest) error {
|
||||
repository.ManifestAdd[d] = manifest
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repository *repositoryStub) ListManifests(d string) (map[string]*Manifest, error) {
|
||||
if d == deploymentName {
|
||||
return manifestMap, nil
|
||||
}
|
||||
|
||||
return nil, errTest
|
||||
}
|
||||
|
||||
func (repository *repositoryStub) GetManifest(d string, m string) (*Manifest, error) {
|
||||
if d == deploymentName && m == manifestName {
|
||||
return &manifest, nil
|
||||
}
|
||||
|
||||
return nil, errTest
|
||||
}
|
||||
|
||||
func (r *repositoryStub) ListTypes() []string {
|
||||
r.ListTypesCalled = true
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func (r *repositoryStub) GetTypeInstances(t string) []*TypeInstance {
|
||||
r.GetTypeInstancesCalled = true
|
||||
return []*TypeInstance{}
|
||||
}
|
||||
|
||||
func (r *repositoryStub) ClearTypeInstances(d string) {
|
||||
r.TypeInstancesCleared = true
|
||||
}
|
||||
|
||||
func (r *repositoryStub) SetTypeInstances(d string, is map[string][]*TypeInstance) {
|
||||
for k, _ := range is {
|
||||
r.TypeInstances[d] = append(r.TypeInstances[d], k)
|
||||
}
|
||||
}
|
||||
|
||||
var testExpander = &expanderStub{}
|
||||
var testRepository = newRepositoryStub()
|
||||
var testDeployer = newDeployerStub()
|
||||
var testManager = NewManager(testExpander, testDeployer, testRepository)
|
||||
|
||||
func TestListDeployments(t *testing.T) {
|
||||
testRepository.reset()
|
||||
d, err := testManager.ListDeployments()
|
||||
if !reflect.DeepEqual(d, deploymentList) || err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestListDeploymentsFail(t *testing.T) {
|
||||
testRepository.reset()
|
||||
testRepository.FailListDeployments = true
|
||||
d, err := testManager.ListDeployments()
|
||||
if d != nil || err != errTest {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDeployment(t *testing.T) {
|
||||
testRepository.reset()
|
||||
d, err := testManager.GetDeployment(deploymentName)
|
||||
if !reflect.DeepEqual(d, &deploymentWithConfiguration) || err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDeploymentNoManifest(t *testing.T) {
|
||||
testRepository.reset()
|
||||
d, err := testManager.GetDeployment(deploymentNoManifestName)
|
||||
if !reflect.DeepEqual(d, &deploymentNoManifest) || err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestListManifests(t *testing.T) {
|
||||
testRepository.reset()
|
||||
m, err := testManager.ListManifests(deploymentName)
|
||||
if !reflect.DeepEqual(m, manifestMap) || err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetManifest(t *testing.T) {
|
||||
testRepository.reset()
|
||||
m, err := testManager.GetManifest(deploymentName, manifestName)
|
||||
if !reflect.DeepEqual(m, &manifest) || err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateDeployment(t *testing.T) {
|
||||
testRepository.reset()
|
||||
testDeployer.reset()
|
||||
d, err := testManager.CreateDeployment(&template)
|
||||
if !reflect.DeepEqual(d, &deploymentWithConfiguration) || err != nil {
|
||||
t.Errorf("Expected a different set of response values from invoking CreateDeployment."+
|
||||
"Received: %s, %s. Expected: %s, %s.", d, err, &deploymentWithConfiguration, "nil")
|
||||
}
|
||||
|
||||
if testRepository.Created[0] != template.Name {
|
||||
t.Errorf("Repository CreateDeployment was called with %s but expected %s.",
|
||||
testRepository.Created[0], template.Name)
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(testRepository.ManifestAdd[template.Name].Name, "manifest-") {
|
||||
t.Errorf("Repository AddManifest was called with %s but expected manifest name"+
|
||||
"to begin with manifest-.", testRepository.ManifestAdd[template.Name].Name)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(*testDeployer.Created[0], configuration) || err != nil {
|
||||
t.Errorf("Deployer CreateConfiguration was called with %s but expected %s.",
|
||||
testDeployer.Created[0], configuration)
|
||||
}
|
||||
|
||||
if !testRepository.TypeInstancesCleared {
|
||||
t.Error("Repository did not clear type instances during creation")
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(testRepository.TypeInstances, typeInstMap) {
|
||||
t.Errorf("Unexpected type instances after CreateDeployment: %s", testRepository.TypeInstances)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateDeploymentCreationFailure(t *testing.T) {
|
||||
testRepository.reset()
|
||||
testDeployer.reset()
|
||||
testDeployer.FailCreate = true
|
||||
d, err := testManager.CreateDeployment(&template)
|
||||
|
||||
if testRepository.Created[0] != template.Name {
|
||||
t.Errorf("Repository CreateDeployment was called with %s but expected %s.",
|
||||
testRepository.Created[0], template.Name)
|
||||
}
|
||||
|
||||
if testRepository.Deleted[0] != template.Name {
|
||||
t.Errorf("Repository DeleteDeployment was called with %s but expected %s.",
|
||||
testRepository.Created[0], template.Name)
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(testRepository.ManifestAdd[template.Name].Name, "manifest-") {
|
||||
t.Errorf("Repository AddManifest was called with %s but expected manifest name"+
|
||||
"to begin with manifest-.", testRepository.ManifestAdd[template.Name].Name)
|
||||
}
|
||||
|
||||
if err != errTest || d != nil {
|
||||
t.Errorf("Expected a different set of response values from invoking CreateDeployment."+
|
||||
"Received: %s, %s. Expected: %s, %s.", d, err, "nil", errTest)
|
||||
}
|
||||
|
||||
if testRepository.TypeInstancesCleared {
|
||||
t.Error("Unexpected change to type instances during CreateDeployment failure.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteDeploymentForget(t *testing.T) {
|
||||
testRepository.reset()
|
||||
testDeployer.reset()
|
||||
d, err := testManager.CreateDeployment(&template)
|
||||
if !reflect.DeepEqual(d, &deploymentWithConfiguration) || err != nil {
|
||||
t.Errorf("Expected a different set of response values from invoking CreateDeployment."+
|
||||
"Received: %s, %s. Expected: %s, %s.", d, err, &deploymentWithConfiguration, "nil")
|
||||
}
|
||||
|
||||
if testRepository.Created[0] != template.Name {
|
||||
t.Errorf("Repository CreateDeployment was called with %s but expected %s.",
|
||||
testRepository.Created[0], template.Name)
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(testRepository.ManifestAdd[template.Name].Name, "manifest-") {
|
||||
t.Errorf("Repository AddManifest was called with %s but expected manifest name"+
|
||||
"to begin with manifest-.", testRepository.ManifestAdd[template.Name].Name)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(*testDeployer.Created[0], configuration) || err != nil {
|
||||
t.Errorf("Deployer CreateConfiguration was called with %s but expected %s.",
|
||||
testDeployer.Created[0], configuration)
|
||||
}
|
||||
oldManifestName := testRepository.ManifestAdd[template.Name].Name
|
||||
d, err = testManager.DeleteDeployment("test", true)
|
||||
if err != nil {
|
||||
t.Errorf("DeleteDeployment failed with %v", err)
|
||||
}
|
||||
if testRepository.ManifestAdd[template.Name].Name == oldManifestName {
|
||||
t.Errorf("New manifest was not created, is still: %s", oldManifestName)
|
||||
}
|
||||
if testRepository.ManifestAdd[template.Name].InputConfig != nil {
|
||||
t.Errorf("New manifest has non-nil config, is still: %v", testRepository.ManifestAdd[template.Name].InputConfig)
|
||||
}
|
||||
// Make sure the resources were deleted through deployer.
|
||||
if !reflect.DeepEqual(*testDeployer.Deleted[0], configuration) || err != nil {
|
||||
t.Errorf("Deployer DeleteConfiguration was called with %s but expected %s.",
|
||||
testDeployer.Created[0], configuration)
|
||||
}
|
||||
|
||||
if !testRepository.TypeInstancesCleared {
|
||||
t.Error("Expected type instances to be cleared during DeleteDeployment.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListTypes(t *testing.T) {
|
||||
testRepository.reset()
|
||||
|
||||
testManager.ListTypes()
|
||||
|
||||
if !testRepository.ListTypesCalled {
|
||||
t.Error("expected repository ListTypes() call.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListInstances(t *testing.T) {
|
||||
testRepository.reset()
|
||||
|
||||
testManager.ListInstances("all")
|
||||
|
||||
if !testRepository.GetTypeInstancesCalled {
|
||||
t.Error("expected repository GetTypeInstances() call.")
|
||||
}
|
||||
}
|
@ -0,0 +1,172 @@
|
||||
/*
|
||||
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"
|
||||
"net/http"
|
||||
"time"
|
||||
"util"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
)
|
||||
|
||||
const (
|
||||
maxURLImports = 100
|
||||
schemaSuffix = ".schema"
|
||||
)
|
||||
|
||||
// TypeResolver finds Types in a Configuration which aren't yet reduceable to an import file
|
||||
// or primitive, and attempts to replace them with a template from a URL.
|
||||
type TypeResolver interface {
|
||||
ResolveTypes(config *Configuration, imports []*ImportFile) ([]*ImportFile, error)
|
||||
}
|
||||
|
||||
type typeResolver struct {
|
||||
getter util.HTTPClient
|
||||
maxUrls int
|
||||
}
|
||||
|
||||
// NewTypeResolver returns a new initialized TypeResolver.
|
||||
func NewTypeResolver() TypeResolver {
|
||||
ret := &typeResolver{}
|
||||
client := http.DefaultClient
|
||||
//TODO (iantw): Make this a flag
|
||||
timeout, _ := time.ParseDuration("10s")
|
||||
client.Timeout = timeout
|
||||
ret.getter = util.NewHTTPClient(3, client, util.NewSleeper())
|
||||
ret.maxUrls = maxURLImports
|
||||
return ret
|
||||
}
|
||||
|
||||
func resolverError(c *Configuration, err error) error {
|
||||
return fmt.Errorf("cannot resolve types in configuration %s due to: \n%s\n",
|
||||
c, err)
|
||||
}
|
||||
|
||||
func performHTTPGet(g util.HTTPClient, u string, allowMissing bool) (content string, err error) {
|
||||
r, code, err := g.Get(u)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if allowMissing && code == http.StatusNotFound {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if code != http.StatusOK {
|
||||
return "", fmt.Errorf(
|
||||
"Received status code %d attempting to fetch Type at %s", code, u)
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// ResolveTypes resolves the types in the supplied configuration and returns
|
||||
// resolved type definitions in t.ImportFiles. Types can be either
|
||||
// primitive (i.e., built in), resolved (i.e., already t.ImportFiles), or remote
|
||||
// (i.e., described by a URL that must be fetched to resolve the type).
|
||||
func (tr *typeResolver) ResolveTypes(config *Configuration, imports []*ImportFile) ([]*ImportFile, error) {
|
||||
existing := map[string]bool{}
|
||||
for _, v := range imports {
|
||||
existing[v.Name] = true
|
||||
}
|
||||
|
||||
fetched := map[string][]*ImportFile{}
|
||||
toFetch := make([]string, 0, tr.maxUrls)
|
||||
for _, r := range config.Resources {
|
||||
if !Primitives[r.Type] && !existing[r.Type] {
|
||||
toFetch = append(toFetch, r.Type)
|
||||
fetched[r.Type] = append(fetched[r.Type], &ImportFile{Name: r.Type})
|
||||
}
|
||||
}
|
||||
count := 0
|
||||
for len(toFetch) > 0 {
|
||||
//1. Fetch import URL. Exit if no URLs left
|
||||
//2. Check/handle HTTP status
|
||||
//3. Store results in all ImportFiles from that URL
|
||||
//4. Check for the optional schema file at import URL + .schema
|
||||
//5. Repeat 2,3 for schema file
|
||||
//6. Add each schema import to fetch if not already done
|
||||
//7. Mark URL done. Return to 1.
|
||||
if count >= tr.maxUrls {
|
||||
return nil, resolverError(config,
|
||||
fmt.Errorf("Number of imports exceeds maximum of %d", tr.maxUrls))
|
||||
}
|
||||
|
||||
url := toFetch[0]
|
||||
template, err := performHTTPGet(tr.getter, url, false)
|
||||
if err != nil {
|
||||
return nil, resolverError(config, err)
|
||||
}
|
||||
|
||||
for _, i := range fetched[url] {
|
||||
i.Content = template
|
||||
}
|
||||
|
||||
schemaURL := url + schemaSuffix
|
||||
sch, err := performHTTPGet(tr.getter, schemaURL, true)
|
||||
if err != nil {
|
||||
return nil, resolverError(config, err)
|
||||
}
|
||||
|
||||
if sch != "" {
|
||||
var s Schema
|
||||
if err := yaml.Unmarshal([]byte(sch), &s); err != nil {
|
||||
return nil, resolverError(config, err)
|
||||
}
|
||||
// Here we handle any nested imports in the schema we've just fetched.
|
||||
for _, v := range s.Imports {
|
||||
i := &ImportFile{Name: v.Name}
|
||||
var existingSchema string
|
||||
if len(fetched[v.Path]) == 0 {
|
||||
// If this import URL is new to us, add it to the URLs to fetch.
|
||||
toFetch = append(toFetch, v.Path)
|
||||
} else {
|
||||
// If this is not a new import URL and we've already fetched its contents,
|
||||
// reuse them. Also, check if we also found a schema for that import URL and
|
||||
// record those contents for re-use as well.
|
||||
if fetched[v.Path][0].Content != "" {
|
||||
i.Content = fetched[v.Path][0].Content
|
||||
if len(fetched[v.Path+schemaSuffix]) > 0 {
|
||||
existingSchema = fetched[v.Path+schemaSuffix][0].Content
|
||||
}
|
||||
}
|
||||
}
|
||||
fetched[v.Path] = append(fetched[v.Path], i)
|
||||
if existingSchema != "" {
|
||||
fetched[v.Path+schemaSuffix] = append(fetched[v.Path+schemaSuffix],
|
||||
&ImportFile{Name: v.Name + schemaSuffix, Content: existingSchema})
|
||||
}
|
||||
}
|
||||
|
||||
// Add the schema we've fetched as the schema for any templates which used this URL.
|
||||
for _, i := range fetched[url] {
|
||||
schemaImportName := i.Name + schemaSuffix
|
||||
fetched[schemaURL] = append(fetched[schemaURL],
|
||||
&ImportFile{Name: schemaImportName, Content: sch})
|
||||
}
|
||||
}
|
||||
|
||||
count = count + 1
|
||||
toFetch = toFetch[1:]
|
||||
}
|
||||
|
||||
ret := []*ImportFile{}
|
||||
for _, v := range fetched {
|
||||
ret = append(ret, v...)
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
@ -0,0 +1,264 @@
|
||||
/*
|
||||
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 (
|
||||
"errors"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
)
|
||||
|
||||
type responseAndError struct {
|
||||
err error
|
||||
code int
|
||||
resp string
|
||||
}
|
||||
|
||||
type resolverTestCase struct {
|
||||
config string
|
||||
imports []*ImportFile
|
||||
responses map[string]responseAndError
|
||||
urlcount int
|
||||
expectedErr error
|
||||
importOut []*ImportFile
|
||||
}
|
||||
|
||||
type testGetter struct {
|
||||
responses map[string]responseAndError
|
||||
count int
|
||||
test *testing.T
|
||||
}
|
||||
|
||||
func (tg *testGetter) Get(url string) (body string, code int, err error) {
|
||||
tg.count = tg.count + 1
|
||||
ret := tg.responses[url]
|
||||
|
||||
return ret.resp, ret.code, ret.err
|
||||
}
|
||||
|
||||
func testDriver(c resolverTestCase, t *testing.T) {
|
||||
g := &testGetter{test: t, responses: c.responses}
|
||||
r := &typeResolver{getter: g, maxUrls: 5}
|
||||
|
||||
conf := &Configuration{}
|
||||
dataErr := yaml.Unmarshal([]byte(c.config), conf)
|
||||
if dataErr != nil {
|
||||
panic("bad test data")
|
||||
}
|
||||
|
||||
result, err := r.ResolveTypes(conf, c.imports)
|
||||
|
||||
if g.count != c.urlcount {
|
||||
t.Errorf("Expected %d url GETs but only %d found", c.urlcount, g.count)
|
||||
}
|
||||
|
||||
if (err != nil && c.expectedErr == nil) || (err == nil && c.expectedErr != nil) {
|
||||
t.Errorf("Expected error %s but found %s", c.expectedErr, err)
|
||||
} else if err != nil && !strings.Contains(err.Error(), c.expectedErr.Error()) {
|
||||
t.Errorf("Expected error %s but found %s", c.expectedErr, err)
|
||||
}
|
||||
|
||||
resultImport := map[ImportFile]bool{}
|
||||
expectedImport := map[ImportFile]bool{}
|
||||
for _, i := range result {
|
||||
resultImport[*i] = true
|
||||
}
|
||||
|
||||
for _, i := range c.importOut {
|
||||
expectedImport[*i] = true
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(resultImport, expectedImport) {
|
||||
t.Errorf("Expected imports %+v but found %+v", expectedImport, resultImport)
|
||||
}
|
||||
}
|
||||
|
||||
var simpleContent = `
|
||||
resources:
|
||||
- name: test
|
||||
type: ReplicationController
|
||||
`
|
||||
|
||||
func TestNoImports(t *testing.T) {
|
||||
test := resolverTestCase{config: simpleContent}
|
||||
testDriver(test, t)
|
||||
}
|
||||
|
||||
var includeImport = `
|
||||
resources:
|
||||
- name: foo
|
||||
type: foo.py
|
||||
`
|
||||
|
||||
func TestIncludedImport(t *testing.T) {
|
||||
imports := []*ImportFile{&ImportFile{Name: "foo.py"}}
|
||||
test := resolverTestCase{
|
||||
config: includeImport,
|
||||
imports: imports,
|
||||
}
|
||||
testDriver(test, t)
|
||||
}
|
||||
|
||||
var templateSingleURL = `
|
||||
resources:
|
||||
- name: foo
|
||||
type: my-fake-url
|
||||
`
|
||||
|
||||
func TestSingleUrl(t *testing.T) {
|
||||
finalImports := []*ImportFile{&ImportFile{Name: "my-fake-url", Content: "my-content"}}
|
||||
|
||||
responses := map[string]responseAndError{
|
||||
"my-fake-url": responseAndError{nil, http.StatusOK, "my-content"},
|
||||
"my-fake-url.schema": responseAndError{nil, http.StatusNotFound, ""},
|
||||
}
|
||||
|
||||
test := resolverTestCase{
|
||||
config: templateSingleURL,
|
||||
importOut: finalImports,
|
||||
urlcount: 2,
|
||||
responses: responses,
|
||||
}
|
||||
testDriver(test, t)
|
||||
}
|
||||
|
||||
func TestSingleUrlWith500(t *testing.T) {
|
||||
responses := map[string]responseAndError{
|
||||
"my-fake-url": responseAndError{nil, http.StatusInternalServerError, "my-content"},
|
||||
}
|
||||
|
||||
test := resolverTestCase{
|
||||
config: templateSingleURL,
|
||||
urlcount: 1,
|
||||
responses: responses,
|
||||
expectedErr: errors.New("Received status code 500"),
|
||||
}
|
||||
testDriver(test, t)
|
||||
}
|
||||
|
||||
var schema1 = `
|
||||
imports:
|
||||
- path: my-next-url
|
||||
name: schema-import
|
||||
`
|
||||
|
||||
func TestSingleUrlWithSchema(t *testing.T) {
|
||||
finalImports := []*ImportFile{
|
||||
&ImportFile{Name: "my-fake-url", Content: "my-content"},
|
||||
&ImportFile{Name: "schema-import", Content: "schema-import"},
|
||||
&ImportFile{Name: "my-fake-url.schema", Content: schema1},
|
||||
}
|
||||
|
||||
responses := map[string]responseAndError{
|
||||
"my-fake-url": responseAndError{nil, http.StatusOK, "my-content"},
|
||||
"my-fake-url.schema": responseAndError{nil, http.StatusOK, schema1},
|
||||
"my-next-url": responseAndError{nil, http.StatusOK, "schema-import"},
|
||||
"my-next-url.schema": responseAndError{nil, http.StatusNotFound, ""},
|
||||
}
|
||||
|
||||
test := resolverTestCase{
|
||||
config: templateSingleURL,
|
||||
importOut: finalImports,
|
||||
urlcount: 4,
|
||||
responses: responses,
|
||||
}
|
||||
testDriver(test, t)
|
||||
}
|
||||
|
||||
var templateExceedsMax = `
|
||||
resources:
|
||||
- name: foo
|
||||
type: my-fake-url
|
||||
- name: foo1
|
||||
type: my-fake-url1
|
||||
- name: foo2
|
||||
type: my-fake-url2
|
||||
- name: foo3
|
||||
type: my-fake-url3
|
||||
- name: foo4
|
||||
type: my-fake-url4
|
||||
- name: foo5
|
||||
type: my-fake-url5
|
||||
`
|
||||
|
||||
func TestTooManyImports(t *testing.T) {
|
||||
responses := map[string]responseAndError{
|
||||
"my-fake-url": responseAndError{nil, http.StatusOK, "my-content"},
|
||||
"my-fake-url.schema": responseAndError{nil, http.StatusNotFound, ""},
|
||||
"my-fake-url1": responseAndError{nil, http.StatusOK, "my-content"},
|
||||
"my-fake-url1.schema": responseAndError{nil, http.StatusNotFound, ""},
|
||||
"my-fake-url2": responseAndError{nil, http.StatusOK, "my-content"},
|
||||
"my-fake-url2.schema": responseAndError{nil, http.StatusNotFound, ""},
|
||||
"my-fake-url3": responseAndError{nil, http.StatusOK, "my-content"},
|
||||
"my-fake-url3.schema": responseAndError{nil, http.StatusNotFound, ""},
|
||||
"my-fake-url4": responseAndError{nil, http.StatusOK, "my-content"},
|
||||
"my-fake-url4.schema": responseAndError{nil, http.StatusNotFound, ""},
|
||||
"my-fake-url5": responseAndError{nil, http.StatusOK, "my-content"},
|
||||
"my-fake-url5.schema": responseAndError{nil, http.StatusNotFound, ""},
|
||||
}
|
||||
|
||||
test := resolverTestCase{
|
||||
config: templateExceedsMax,
|
||||
urlcount: 10,
|
||||
responses: responses,
|
||||
expectedErr: errors.New("Number of imports exceeds maximum of 5"),
|
||||
}
|
||||
testDriver(test, t)
|
||||
}
|
||||
|
||||
var templateSharesImport = `
|
||||
resources:
|
||||
- name: foo
|
||||
type: my-fake-url
|
||||
- name: foo1
|
||||
type: my-fake-url1
|
||||
`
|
||||
|
||||
var schema2 = `
|
||||
imports:
|
||||
- path: my-next-url
|
||||
name: schema-import-1
|
||||
`
|
||||
|
||||
func TestSharedImport(t *testing.T) {
|
||||
finalImports := []*ImportFile{
|
||||
&ImportFile{Name: "my-fake-url", Content: "my-content"},
|
||||
&ImportFile{Name: "my-fake-url1", Content: "my-content-1"},
|
||||
&ImportFile{Name: "schema-import", Content: "schema-import"},
|
||||
&ImportFile{Name: "schema-import-1", Content: "schema-import"},
|
||||
&ImportFile{Name: "my-fake-url.schema", Content: schema1},
|
||||
&ImportFile{Name: "my-fake-url1.schema", Content: schema2},
|
||||
}
|
||||
|
||||
responses := map[string]responseAndError{
|
||||
"my-fake-url": responseAndError{nil, http.StatusOK, "my-content"},
|
||||
"my-fake-url.schema": responseAndError{nil, http.StatusOK, schema1},
|
||||
"my-fake-url1": responseAndError{nil, http.StatusOK, "my-content-1"},
|
||||
"my-fake-url1.schema": responseAndError{nil, http.StatusOK, schema2},
|
||||
"my-next-url": responseAndError{nil, http.StatusOK, "schema-import"},
|
||||
"my-next-url.schema": responseAndError{nil, http.StatusNotFound, ""},
|
||||
}
|
||||
|
||||
test := resolverTestCase{
|
||||
config: templateSharesImport,
|
||||
urlcount: 6,
|
||||
responses: responses,
|
||||
importOut: finalImports,
|
||||
}
|
||||
testDriver(test, t)
|
||||
}
|
@ -0,0 +1,165 @@
|
||||
/*
|
||||
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 (
|
||||
"time"
|
||||
)
|
||||
|
||||
// This map defines the primitives that DM knows how to handle implicitly.
|
||||
// TODO (iantw): Make these come from the resourcifier(?). Add more as appropriate...
|
||||
var Primitives = map[string]bool{
|
||||
"Pod": true,
|
||||
"ReplicationController": true,
|
||||
"Service": true,
|
||||
"Namespace": true,
|
||||
"Volume": true,
|
||||
}
|
||||
|
||||
// SchemaImport represents an import as declared in a schema file.
|
||||
type SchemaImport struct {
|
||||
Path string `json:"path"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// Schema is a partial DM schema. We only need access to the imports object at this level.
|
||||
type Schema struct {
|
||||
Imports []SchemaImport `json:"imports"`
|
||||
}
|
||||
|
||||
// Repository manages storage for all Deployment Manager entities, as well as
|
||||
// the common operations to store, access and manage them.
|
||||
type Repository interface {
|
||||
// Deployments.
|
||||
ListDeployments() ([]Deployment, error)
|
||||
GetDeployment(name string) (*Deployment, error)
|
||||
GetValidDeployment(name string) (*Deployment, error)
|
||||
CreateDeployment(name string) (*Deployment, error)
|
||||
DeleteDeployment(name string, forget bool) (*Deployment, error)
|
||||
|
||||
// Manifests.
|
||||
AddManifest(deploymentName string, manifest *Manifest) error
|
||||
ListManifests(deploymentName string) (map[string]*Manifest, error)
|
||||
GetManifest(deploymentName string, manifestName string) (*Manifest, error)
|
||||
|
||||
// Types.
|
||||
ListTypes() []string
|
||||
GetTypeInstances(typeName string) []*TypeInstance
|
||||
ClearTypeInstances(deploymentName string)
|
||||
SetTypeInstances(deploymentName string, instances map[string][]*TypeInstance)
|
||||
}
|
||||
|
||||
// Deployment defines a deployment that describes
|
||||
// the creation, modification and/or deletion of a set of resources.
|
||||
type Deployment struct {
|
||||
Name string `json:"name"`
|
||||
ID int `json:"id"`
|
||||
CreatedAt time.Time `json:"createdAt,omitempty"`
|
||||
DeployedAt time.Time `json:"deployedAt,omitempty"`
|
||||
ModifiedAt time.Time `json:"modifiedAt,omitempty"`
|
||||
DeletedAt time.Time `json:"deletedAt,omitempty"`
|
||||
Status deploymentStatus `json:"status,omitempty"`
|
||||
Current *Configuration `json:"current,omitEmpty"`
|
||||
Manifests map[string]*Manifest `json:"manifests,omitempty"`
|
||||
}
|
||||
|
||||
// NewDeployment creates a new deployment.
|
||||
func NewDeployment(name string, id int) *Deployment {
|
||||
return &Deployment{Name: name, ID: id, CreatedAt: time.Now(), Status: CreatedStatus,
|
||||
Manifests: make(map[string]*Manifest, 0)}
|
||||
}
|
||||
|
||||
// NewManifest creates a new manifest.
|
||||
func NewManifest(deploymentName string, manifestName string) *Manifest {
|
||||
return &Manifest{Deployment: deploymentName, Name: manifestName}
|
||||
}
|
||||
|
||||
// deploymentStatus is an enumeration type for the status of a deployment.
|
||||
type deploymentStatus string
|
||||
|
||||
// These constants implement the deploymentStatus enumeration type.
|
||||
const (
|
||||
CreatedStatus deploymentStatus = "Created"
|
||||
DeletedStatus deploymentStatus = "Deleted"
|
||||
DeployedStatus deploymentStatus = "Deployed"
|
||||
FailedStatus deploymentStatus = "Failed"
|
||||
ModifiedStatus deploymentStatus = "Modified"
|
||||
)
|
||||
|
||||
func (s deploymentStatus) String() string {
|
||||
return string(s)
|
||||
}
|
||||
|
||||
// LayoutResource defines the structure of resources in the manifest layout.
|
||||
type LayoutResource struct {
|
||||
Resource
|
||||
Layout
|
||||
}
|
||||
|
||||
// Layout defines the structure of a layout as returned from expansion.
|
||||
type Layout struct {
|
||||
Resources []*LayoutResource `json:"resources,omitempty"`
|
||||
}
|
||||
|
||||
// Manifest contains the input configuration for a deployment, the fully
|
||||
// expanded configuration, and the layout structure of the manifest.
|
||||
//
|
||||
type Manifest struct {
|
||||
Deployment string `json:"deployment"`
|
||||
Name string `json:"name"`
|
||||
InputConfig *Template `json:"inputConfig"`
|
||||
ExpandedConfig *Configuration `json:"expandedConfig,omitempty"`
|
||||
Layout *Layout `json:"layout,omitempty"`
|
||||
}
|
||||
|
||||
// Template describes a set of resources to be deployed.
|
||||
// Manager expands a Template into a Configuration, which
|
||||
// describes the set in a form that can be instantiated.
|
||||
type Template struct {
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
Imports []*ImportFile `json:"imports"`
|
||||
}
|
||||
|
||||
// ImportFile describes a base64 encoded file imported by a Template.
|
||||
type ImportFile struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// Configuration describes a set of resources in a form
|
||||
// that can be instantiated.
|
||||
type Configuration struct {
|
||||
Resources []*Resource `json:"resources"`
|
||||
}
|
||||
|
||||
// Resource describes a resource in a configuration. A resource has
|
||||
// a name, a type and a set of properties. The name and type are used
|
||||
// to identify the resource in Kubernetes. The properties are passed
|
||||
// to Kubernetes as the resource configuration.
|
||||
type Resource struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Properties map[string]interface{} `json:"properties,omitempty"`
|
||||
}
|
||||
|
||||
// TypeInstance defines the metadata for an instantiation of a template type
|
||||
// in a deployment.
|
||||
type TypeInstance struct {
|
||||
Name string `json:"name"` // instance name
|
||||
Type string `json:"type"` // instance type
|
||||
Deployment string `json:"deployment"` // deployment name
|
||||
Manifest string `json:"manifest"` // manifest name
|
||||
Path string `json:"path"` // JSON path within manifest
|
||||
}
|
@ -0,0 +1,248 @@
|
||||
/*
|
||||
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 repository implements a deployment repository using a map.
|
||||
// It can be easily replaced by a deployment repository that uses some
|
||||
// form of persistent storage.
|
||||
package repository
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"manager/manager"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// deploymentTypeInstanceMap stores type instances mapped by deployment name.
|
||||
// This allows for simple updating and deleting of per-deployment instances
|
||||
// when deployments are created/updated/deleted.
|
||||
type deploymentTypeInstanceMap map[string][]*manager.TypeInstance
|
||||
type typeInstanceMap map[string]deploymentTypeInstanceMap
|
||||
|
||||
type mapBasedRepository struct {
|
||||
sync.RWMutex
|
||||
deployments map[string]manager.Deployment
|
||||
instances typeInstanceMap
|
||||
lastID int
|
||||
}
|
||||
|
||||
// NewMapBasedRepository returns a new map based repository.
|
||||
func NewMapBasedRepository() manager.Repository {
|
||||
return &mapBasedRepository{
|
||||
deployments: make(map[string]manager.Deployment, 0),
|
||||
instances: typeInstanceMap{},
|
||||
}
|
||||
}
|
||||
|
||||
// ListDeployments returns of all of the deployments in the repository.
|
||||
func (r *mapBasedRepository) ListDeployments() ([]manager.Deployment, error) {
|
||||
r.RLock()
|
||||
defer r.RUnlock()
|
||||
|
||||
l := []manager.Deployment{}
|
||||
for _, deployment := range r.deployments {
|
||||
l = append(l, deployment)
|
||||
}
|
||||
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// GetDeployment returns the deployment with the supplied name.
|
||||
// If the deployment is not found, it returns an error.
|
||||
func (r *mapBasedRepository) GetDeployment(name string) (*manager.Deployment, error) {
|
||||
d, ok := r.deployments[name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("deployment %s not found", name)
|
||||
}
|
||||
return &d, nil
|
||||
}
|
||||
|
||||
// GetValidDeployment returns the deployment with the supplied name.
|
||||
// If the deployment is not found or marked as deleted, it returns an error.
|
||||
func (r *mapBasedRepository) GetValidDeployment(name string) (*manager.Deployment, error) {
|
||||
d, err := r.GetDeployment(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if d.Status == manager.DeletedStatus {
|
||||
return nil, fmt.Errorf("deployment %s is deleted", name)
|
||||
}
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// CreateDeployment creates a new deployment and stores it in the repository.
|
||||
func (r *mapBasedRepository) CreateDeployment(name string) (*manager.Deployment, error) {
|
||||
d, err := func() (*manager.Deployment, error) {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
exists, _ := r.GetValidDeployment(name)
|
||||
if exists != nil {
|
||||
return nil, fmt.Errorf("Deployment %s already exists", name)
|
||||
}
|
||||
|
||||
r.lastID++
|
||||
|
||||
d := manager.NewDeployment(name, r.lastID)
|
||||
d.Status = manager.CreatedStatus
|
||||
d.DeployedAt = time.Now()
|
||||
r.deployments[name] = *d
|
||||
return d, nil
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Printf("created deployment: %v", d)
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (r *mapBasedRepository) AddManifest(deploymentName string, manifest *manager.Manifest) error {
|
||||
err := func() error {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
d, err := r.GetValidDeployment(deploymentName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Make sure the manifest doesn't already exist, and if not, add the manifest to
|
||||
// map of manifests this deployment has
|
||||
if _, ok := d.Manifests[manifest.Name]; ok {
|
||||
return fmt.Errorf("Manifest %s already exists in deployment %s", manifest.Name, deploymentName)
|
||||
}
|
||||
d.Manifests[manifest.Name] = manifest
|
||||
r.deployments[deploymentName] = *d
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Added manifest %s to deployment: %s", manifest.Name, deploymentName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteDeployment deletes the deployment with the supplied name.
|
||||
// If forget is true, then the deployment is removed from the repository.
|
||||
// Otherwise, it is marked as deleted and retained.
|
||||
func (r *mapBasedRepository) DeleteDeployment(name string, forget bool) (*manager.Deployment, error) {
|
||||
d, err := func() (*manager.Deployment, error) {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
d, err := r.GetValidDeployment(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !forget {
|
||||
d.DeletedAt = time.Now()
|
||||
d.Status = manager.DeletedStatus
|
||||
r.deployments[name] = *d
|
||||
} else {
|
||||
delete(r.deployments, name)
|
||||
}
|
||||
|
||||
return d, nil
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Printf("deleted deployment: %v", d)
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (r *mapBasedRepository) ListManifests(deploymentName string) (map[string]*manager.Manifest, error) {
|
||||
d, err := r.GetValidDeployment(deploymentName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.Manifests, nil
|
||||
}
|
||||
|
||||
func (r *mapBasedRepository) GetManifest(deploymentName string, manifestName string) (*manager.Manifest, error) {
|
||||
d, err := r.GetValidDeployment(deploymentName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if m, ok := d.Manifests[manifestName]; ok {
|
||||
return m, nil
|
||||
}
|
||||
return nil, fmt.Errorf("manifest %s not found in deployment %s", manifestName, deploymentName)
|
||||
}
|
||||
|
||||
// ListTypes returns all types known from existing instances.
|
||||
func (r *mapBasedRepository) ListTypes() []string {
|
||||
var keys []string
|
||||
for k := range r.instances {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
|
||||
return keys
|
||||
}
|
||||
|
||||
// GetTypeInstances returns all instances of a given type. If type is empty,
|
||||
// returns all instances for all types.
|
||||
func (r *mapBasedRepository) GetTypeInstances(typeName string) []*manager.TypeInstance {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
var instances []*manager.TypeInstance
|
||||
for t, dInstMap := range r.instances {
|
||||
if t == typeName || typeName == "all" {
|
||||
for _, i := range dInstMap {
|
||||
instances = append(instances, i...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return instances
|
||||
}
|
||||
|
||||
// ClearTypeInstances deletes all instances associated with the given
|
||||
// deployment name from the type instance repository.
|
||||
func (r *mapBasedRepository) ClearTypeInstances(deploymentName string) {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
for t, dMap := range r.instances {
|
||||
delete(dMap, deploymentName)
|
||||
if len(dMap) == 0 {
|
||||
delete(r.instances, t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetTypeInstances sets all type instances for a given deployment name.
|
||||
//
|
||||
// To clear the current set of instances first, caller should first use
|
||||
// ClearTypeInstances().
|
||||
func (r *mapBasedRepository) SetTypeInstances(deploymentName string, instances map[string][]*manager.TypeInstance) {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
// Add each instance list to the appropriate type map.
|
||||
for t, is := range instances {
|
||||
if r.instances[t] == nil {
|
||||
r.instances[t] = make(deploymentTypeInstanceMap)
|
||||
}
|
||||
|
||||
r.instances[t][deploymentName] = is
|
||||
}
|
||||
}
|
@ -0,0 +1,332 @@
|
||||
/*
|
||||
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 repository
|
||||
|
||||
import (
|
||||
"manager/manager"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRepositoryListEmpty(t *testing.T) {
|
||||
r := NewMapBasedRepository()
|
||||
d, err := r.ListDeployments()
|
||||
if err != nil {
|
||||
t.Fatal("List Deployments failed")
|
||||
}
|
||||
if len(d) != 0 {
|
||||
t.Fatal("Returned non zero list")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepositoryGetFailsWithNonExistentDeployment(t *testing.T) {
|
||||
r := NewMapBasedRepository()
|
||||
_, err := r.GetDeployment("nothere")
|
||||
if err == nil {
|
||||
t.Fatal("GetDeployment didn't fail with non-existent deployment")
|
||||
}
|
||||
if err.Error() != "deployment nothere not found" {
|
||||
t.Fatal("Error message doesn't match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepositoryCreateDeploymentWorks(t *testing.T) {
|
||||
var deploymentName = "mydeployment"
|
||||
var manifestName = "manifest-0"
|
||||
r := NewMapBasedRepository()
|
||||
manifest := manager.Manifest{Deployment: deploymentName, Name: manifestName}
|
||||
d, err := r.CreateDeployment(deploymentName)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateDeployment failed: %v", err)
|
||||
}
|
||||
l, err := r.ListDeployments()
|
||||
if err != nil {
|
||||
t.Fatalf("ListDeployments failed: %v", err)
|
||||
}
|
||||
if len(l) != 1 {
|
||||
t.Fatalf("List of deployments is not 1: %d", len(l))
|
||||
}
|
||||
dNew, err := r.GetDeployment(deploymentName)
|
||||
if err != nil {
|
||||
t.Fatalf("GetDeployment failed: %v", err)
|
||||
}
|
||||
if dNew.Name != d.Name {
|
||||
t.Fatalf("Deployment Names don't match, got: %v, expected %v", dNew, d)
|
||||
}
|
||||
if len(dNew.Manifests) != 0 {
|
||||
t.Fatalf("Deployment has non-zero manifest count: %d", len(dNew.Manifests))
|
||||
}
|
||||
err = r.AddManifest(deploymentName, &manifest)
|
||||
if err != nil {
|
||||
t.Fatalf("AddManifest failed: %v", err)
|
||||
}
|
||||
dNew, err = r.GetDeployment(deploymentName)
|
||||
if err != nil {
|
||||
t.Fatalf("GetDeployment failed: %v", err)
|
||||
}
|
||||
if len(dNew.Manifests) != 1 {
|
||||
t.Fatalf("Fetched deployment does not have manifest count of 1: %d", len(dNew.Manifests))
|
||||
}
|
||||
manifestList, err := r.ListManifests(deploymentName)
|
||||
if err != nil {
|
||||
t.Fatalf("ListManifests failed: %v", err)
|
||||
}
|
||||
if len(manifestList) != 1 {
|
||||
t.Fatalf("ManifestList does not have manifest count of 1: %d", len(manifestList))
|
||||
}
|
||||
m, err := r.GetManifest(deploymentName, manifestName)
|
||||
if err != nil {
|
||||
t.Fatalf("GetManifest failed: %v", err)
|
||||
}
|
||||
if m.Name != manifestName {
|
||||
t.Fatalf("manifest name doesn't match: %v", m)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepositoryMultipleManifestsWorks(t *testing.T) {
|
||||
var deploymentName = "mydeployment"
|
||||
var manifestName = "manifest-0"
|
||||
var manifestName2 = "manifest-1"
|
||||
r := NewMapBasedRepository()
|
||||
manifest := manager.Manifest{Deployment: deploymentName, Name: manifestName}
|
||||
manifest2 := manager.Manifest{Deployment: deploymentName, Name: manifestName2}
|
||||
d, err := r.CreateDeployment(deploymentName)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateDeployment failed: %v", err)
|
||||
}
|
||||
dNew, err := r.GetDeployment(deploymentName)
|
||||
if err != nil {
|
||||
t.Fatalf("GetDeployment failed: %v", err)
|
||||
}
|
||||
if dNew.Name != d.Name {
|
||||
t.Fatalf("Deployment Names don't match, got: %v, expected %v", dNew, d)
|
||||
}
|
||||
if len(dNew.Manifests) != 0 {
|
||||
t.Fatalf("Deployment has non-zero manifest count: %d", len(dNew.Manifests))
|
||||
}
|
||||
err = r.AddManifest(deploymentName, &manifest)
|
||||
if err != nil {
|
||||
t.Fatalf("AddManifest failed: %v", err)
|
||||
}
|
||||
dNew, err = r.GetDeployment(deploymentName)
|
||||
if err != nil {
|
||||
t.Fatalf("GetDeployment failed: %v", err)
|
||||
}
|
||||
if len(dNew.Manifests) != 1 {
|
||||
t.Fatalf("Fetched deployment does not have manifest count of 1: %d", len(dNew.Manifests))
|
||||
}
|
||||
manifestList, err := r.ListManifests(deploymentName)
|
||||
if err != nil {
|
||||
t.Fatalf("ListManifests failed: %v", err)
|
||||
}
|
||||
if len(manifestList) != 1 {
|
||||
t.Fatalf("ManifestList does not have manifest count of 1: %d", len(manifestList))
|
||||
}
|
||||
m, err := r.GetManifest(deploymentName, manifestName)
|
||||
if err != nil {
|
||||
t.Fatalf("GetManifest failed: %v", err)
|
||||
}
|
||||
if m.Name != manifestName {
|
||||
t.Fatalf("manifest name doesn't match: %v", m)
|
||||
}
|
||||
err = r.AddManifest(deploymentName, &manifest2)
|
||||
if err != nil {
|
||||
t.Fatalf("AddManifest failed: %v", err)
|
||||
}
|
||||
dNew, err = r.GetDeployment(deploymentName)
|
||||
if err != nil {
|
||||
t.Fatalf("GetDeployment failed: %v", err)
|
||||
}
|
||||
if len(dNew.Manifests) != 2 {
|
||||
t.Fatalf("Fetched deployment does not have manifest count of 2: %d", len(dNew.Manifests))
|
||||
}
|
||||
manifestList, err = r.ListManifests(deploymentName)
|
||||
if err != nil {
|
||||
t.Fatalf("ListManifests failed: %v", err)
|
||||
}
|
||||
if len(manifestList) != 2 {
|
||||
t.Fatalf("ManifestList does not have manifest count of 1: %d", len(manifestList))
|
||||
}
|
||||
m, err = r.GetManifest(deploymentName, manifestName)
|
||||
if err != nil {
|
||||
t.Fatalf("GetManifest failed: %v", err)
|
||||
}
|
||||
if m.Name != manifestName {
|
||||
t.Fatalf("manifest name doesn't match: %v", m)
|
||||
}
|
||||
m, err = r.GetManifest(deploymentName, manifestName2)
|
||||
if err != nil {
|
||||
t.Fatalf("GetManifest failed: %v", err)
|
||||
}
|
||||
if m.Name != manifestName2 {
|
||||
t.Fatalf("manifest name doesn't match: %v", m)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepositoryDeleteFailsWithNonExistentDeployment(t *testing.T) {
|
||||
var deploymentName = "mydeployment"
|
||||
r := NewMapBasedRepository()
|
||||
d, err := r.DeleteDeployment(deploymentName, false)
|
||||
if err == nil {
|
||||
t.Fatalf("DeleteDeployment didn't fail with non existent deployment")
|
||||
}
|
||||
if d != nil {
|
||||
t.Fatalf("DeleteDeployment returned non-nil for non existent deployment")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepositoryDeleteWorksWithNoLatestManifest(t *testing.T) {
|
||||
var deploymentName = "mydeployment"
|
||||
r := NewMapBasedRepository()
|
||||
_, err := r.CreateDeployment(deploymentName)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateDeployment failed: %v", err)
|
||||
}
|
||||
dDeleted, err := r.DeleteDeployment(deploymentName, false)
|
||||
if err != nil {
|
||||
t.Fatalf("DeleteDeployment failed: %v", err)
|
||||
}
|
||||
if dDeleted.Status != manager.DeletedStatus {
|
||||
t.Fatalf("Deployment Status is not deleted")
|
||||
}
|
||||
if len(dDeleted.Manifests) != 0 {
|
||||
t.Fatalf("manifests count is not 0, is: %d", len(dDeleted.Manifests))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepositoryDeleteDeploymentWorksNoForget(t *testing.T) {
|
||||
var deploymentName = "mydeployment"
|
||||
var manifestName = "manifest-0"
|
||||
r := NewMapBasedRepository()
|
||||
manifest := manager.Manifest{Deployment: deploymentName, Name: manifestName}
|
||||
_, err := r.CreateDeployment(deploymentName)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateDeployment failed: %v", err)
|
||||
}
|
||||
err = r.AddManifest(deploymentName, &manifest)
|
||||
if err != nil {
|
||||
t.Fatalf("AddManifest failed: %v", err)
|
||||
}
|
||||
dDeleted, err := r.DeleteDeployment(deploymentName, false)
|
||||
if err != nil {
|
||||
t.Fatalf("DeleteDeployment failed: %v", err)
|
||||
}
|
||||
if dDeleted.Status != manager.DeletedStatus {
|
||||
t.Fatalf("Deployment Status is not deleted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepositoryDeleteDeploymentWorksForget(t *testing.T) {
|
||||
var deploymentName = "mydeployment"
|
||||
var manifestName = "manifest-0"
|
||||
r := NewMapBasedRepository()
|
||||
manifest := manager.Manifest{Deployment: deploymentName, Name: manifestName}
|
||||
_, err := r.CreateDeployment(deploymentName)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateDeployment failed: %v", err)
|
||||
}
|
||||
err = r.AddManifest(deploymentName, &manifest)
|
||||
if err != nil {
|
||||
t.Fatalf("AddManifest failed: %v", err)
|
||||
}
|
||||
dDeleted, err := r.DeleteDeployment(deploymentName, true)
|
||||
if err != nil {
|
||||
t.Fatalf("DeleteDeployment failed: %v", err)
|
||||
}
|
||||
if dDeleted.Status != manager.CreatedStatus {
|
||||
t.Fatalf("Deployment Status is not created")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepositoryTypeInstances(t *testing.T) {
|
||||
r := NewMapBasedRepository()
|
||||
|
||||
d1Map := map[string][]*manager.TypeInstance{
|
||||
"t1": []*manager.TypeInstance{
|
||||
&manager.TypeInstance{
|
||||
Name: "i1",
|
||||
Type: "t1",
|
||||
Deployment: "d1",
|
||||
Manifest: "m1",
|
||||
Path: "p1",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
d2Map := map[string][]*manager.TypeInstance{
|
||||
"t2": []*manager.TypeInstance{
|
||||
&manager.TypeInstance{
|
||||
Name: "i2",
|
||||
Type: "t2",
|
||||
Deployment: "d2",
|
||||
Manifest: "m2",
|
||||
Path: "p2",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
d3Map := map[string][]*manager.TypeInstance{
|
||||
"t2": []*manager.TypeInstance{
|
||||
&manager.TypeInstance{
|
||||
Name: "i3",
|
||||
Type: "t2",
|
||||
Deployment: "d3",
|
||||
Manifest: "m3",
|
||||
Path: "p3",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if instances := r.GetTypeInstances("noinstances"); len(instances) != 0 {
|
||||
t.Fatalf("expected no instances: %v", instances)
|
||||
}
|
||||
|
||||
if types := r.ListTypes(); len(types) != 0 {
|
||||
t.Fatalf("expected no types: %v", types)
|
||||
}
|
||||
|
||||
r.SetTypeInstances("d1", d1Map)
|
||||
r.SetTypeInstances("d2", d2Map)
|
||||
r.SetTypeInstances("d3", d3Map)
|
||||
|
||||
if instances := r.GetTypeInstances("unknowntype"); len(instances) != 0 {
|
||||
t.Fatalf("expected no instances: %v", instances)
|
||||
}
|
||||
|
||||
if instances := r.GetTypeInstances("t1"); len(instances) != 1 {
|
||||
t.Fatalf("expected one instance: %v", instances)
|
||||
}
|
||||
|
||||
if instances := r.GetTypeInstances("t2"); len(instances) != 2 {
|
||||
t.Fatalf("expected two instances: %v", instances)
|
||||
}
|
||||
|
||||
if instances := r.GetTypeInstances("all"); len(instances) != 3 {
|
||||
t.Fatalf("expected three total instances: %v", instances)
|
||||
}
|
||||
|
||||
if types := r.ListTypes(); len(types) != 2 {
|
||||
t.Fatalf("expected two total types: %v", types)
|
||||
}
|
||||
|
||||
r.ClearTypeInstances("d1")
|
||||
if instances := r.GetTypeInstances("t1"); len(instances) != 0 {
|
||||
t.Fatalf("expected no instances after clear: %v", instances)
|
||||
}
|
||||
|
||||
if types := r.ListTypes(); len(types) != 1 {
|
||||
t.Fatalf("expected one total type: %v", types)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(vaikas): Add more tests
|
@ -0,0 +1,54 @@
|
||||
# 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 /usr/local/bin
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y wget
|
||||
|
||||
ENV KUBE_PACKAGE kubernetes.tar.gz
|
||||
ENV KUBE_VERSION v1.0.5
|
||||
ENV KUBE_BASE https://github.com/kubernetes/kubernetes
|
||||
ENV KUBE_RELEASE "$KUBE_BASE"/releases/download
|
||||
ENV KUBE_DOWLOAD "$KUBE_RELEASE"/"$KUBE_VERSION"/"$KUBE_PACKAGE"
|
||||
ENV KUBE_COMMAND kubernetes/platforms/linux/amd64/kubectl
|
||||
RUN wget -O - "$KUBE_DOWLOAD" 2> /dev/null | tar xzf - -C /tmp "$KUBE_COMMAND" \
|
||||
&& mv /tmp/"$KUBE_COMMAND" . \
|
||||
&& rm -rf /tmp/kubernetes
|
||||
|
||||
RUN apt-get purge -y --auto-remove wget
|
||||
|
||||
WORKDIR /go/src
|
||||
|
||||
RUN mkdir -p expandybird
|
||||
COPY expandybird expandybird
|
||||
|
||||
RUN mkdir -p resourcifier
|
||||
COPY resourcifier resourcifier
|
||||
|
||||
RUN mkdir -p util
|
||||
COPY util util
|
||||
|
||||
RUN mkdir -p version
|
||||
COPY version version
|
||||
|
||||
RUN go-wrapper download resourcifier/...
|
||||
RUN go-wrapper install resourcifier/...
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
ENTRYPOINT ["go-wrapper", "run", "--kubectl=/usr/local/bin/kubectl"]
|
@ -0,0 +1,21 @@
|
||||
# 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)
|
@ -0,0 +1,292 @@
|
||||
/*
|
||||
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 (
|
||||
"resourcifier/configurator"
|
||||
"util"
|
||||
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
var configurations = []Route{
|
||||
{"ListConfigurations", "/configurations/{type}", "GET", listConfigurationsHandlerFunc, ""},
|
||||
{"GetConfiguration", "/configurations/{type}/{name}", "GET", getConfigurationHandlerFunc, ""},
|
||||
{"CreateConfiguration", "/configurations", "POST", createConfigurationHandlerFunc, "JSON"},
|
||||
{"DeleteConfiguration", "/configurations", "DELETE", deleteConfigurationHandlerFunc, "JSON"},
|
||||
{"PutConfiguration", "/configurations", "PUT", putConfigurationHandlerFunc, "JSON"},
|
||||
}
|
||||
|
||||
var (
|
||||
maxLength = flag.Int64("maxLength", 1024*8, "The maximum length (KB) of a configuration.")
|
||||
kubePath = flag.String("kubectl", "./kubectl", "The path to the kubectl binary.")
|
||||
kubeService = flag.String("service", "", "The DNS name of the kubernetes service.")
|
||||
kubeServer = flag.String("server", "", "The IP address and optional port of the kubernetes master.")
|
||||
kubeInsecure = flag.Bool("insecure-skip-tls-verify", false, "Do not check the server's certificate for validity.")
|
||||
kubeConfig = flag.String("config", "", "Path to a kubeconfig file.")
|
||||
kubeCertAuth = flag.String("certificate-authority", "", "Path to a file for the certificate authority.")
|
||||
kubeClientCert = flag.String("client-certificate", "", "Path to a client certificate file.")
|
||||
kubeClientKey = flag.String("client-key", "", "Path to a client key file.")
|
||||
kubeToken = flag.String("token", "", "A service account token.")
|
||||
kubeUsername = flag.String("username", "", "The username to use for basic auth.")
|
||||
kubePassword = flag.String("password", "", "The password to use for basic auth.")
|
||||
)
|
||||
|
||||
var backend *configurator.Configurator
|
||||
|
||||
func init() {
|
||||
if !flag.Parsed() {
|
||||
flag.Parse()
|
||||
}
|
||||
|
||||
routes = append(routes, configurations...)
|
||||
backend = getConfigurator()
|
||||
}
|
||||
|
||||
func listConfigurationsHandlerFunc(w http.ResponseWriter, r *http.Request) {
|
||||
handler := "resourcifier: list configurations"
|
||||
util.LogHandlerEntry(handler, r)
|
||||
rtype, err := getPathVariable(w, r, "type", handler)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c := &configurator.Configuration{
|
||||
[]configurator.Resource{
|
||||
{Type: rtype},
|
||||
},
|
||||
}
|
||||
|
||||
output, err := backend.Configure(c, configurator.GetOperation)
|
||||
if err != nil {
|
||||
util.LogAndReturnError(handler, http.StatusBadRequest, err, w)
|
||||
return
|
||||
}
|
||||
|
||||
util.LogHandlerExit(handler, http.StatusOK, output, w)
|
||||
util.WriteYAML(handler, w, []byte(output), http.StatusOK)
|
||||
}
|
||||
|
||||
func getConfigurationHandlerFunc(w http.ResponseWriter, r *http.Request) {
|
||||
handler := "resourcifier: get configuration"
|
||||
util.LogHandlerEntry(handler, r)
|
||||
rtype, err := getPathVariable(w, r, "type", handler)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
rname, err := getPathVariable(w, r, "name", handler)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c := &configurator.Configuration{
|
||||
[]configurator.Resource{
|
||||
{Name: rname, Type: rtype},
|
||||
},
|
||||
}
|
||||
|
||||
output, err := backend.Configure(c, configurator.GetOperation)
|
||||
if err != nil {
|
||||
util.LogAndReturnError(handler, http.StatusBadRequest, err, w)
|
||||
return
|
||||
}
|
||||
|
||||
util.LogHandlerExit(handler, http.StatusOK, output, w)
|
||||
util.WriteYAML(handler, w, []byte(output), http.StatusOK)
|
||||
}
|
||||
|
||||
func createConfigurationHandlerFunc(w http.ResponseWriter, r *http.Request) {
|
||||
handler := "resourcifier: create configuration"
|
||||
util.LogHandlerEntry(handler, r)
|
||||
defer r.Body.Close()
|
||||
c := getConfiguration(w, r, handler)
|
||||
if c != nil {
|
||||
_, err := backend.Configure(c, configurator.CreateOperation)
|
||||
if err != nil {
|
||||
util.LogAndReturnError(handler, http.StatusBadRequest, err, w)
|
||||
return
|
||||
}
|
||||
|
||||
util.LogHandlerExitWithYAML(handler, w, c, http.StatusCreated)
|
||||
return
|
||||
}
|
||||
|
||||
util.LogHandlerExit(handler, http.StatusOK, "OK", w)
|
||||
}
|
||||
|
||||
func deleteConfigurationHandlerFunc(w http.ResponseWriter, r *http.Request) {
|
||||
handler := "resourcifier: delete configuration"
|
||||
util.LogHandlerEntry(handler, r)
|
||||
defer r.Body.Close()
|
||||
c := getConfiguration(w, r, handler)
|
||||
if c != nil {
|
||||
if _, err := backend.Configure(c, configurator.DeleteOperation); err != nil {
|
||||
e := errors.New("cannot delete configuration: " + err.Error() + "\n")
|
||||
util.LogAndReturnError(handler, http.StatusBadRequest, e, w)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
util.LogHandlerExit(handler, http.StatusNoContent, "No Content", w)
|
||||
return
|
||||
}
|
||||
|
||||
util.LogHandlerExit(handler, http.StatusOK, "OK", w)
|
||||
}
|
||||
|
||||
func putConfigurationHandlerFunc(w http.ResponseWriter, r *http.Request) {
|
||||
handler := "resourcifier: update configuration"
|
||||
util.LogHandlerEntry(handler, r)
|
||||
defer r.Body.Close()
|
||||
c := getConfiguration(w, r, handler)
|
||||
if c != nil {
|
||||
if _, err := backend.Configure(c, configurator.ReplaceOperation); err != nil {
|
||||
e := errors.New("cannot replace configuration: " + err.Error() + "\n")
|
||||
util.LogAndReturnError(handler, http.StatusBadRequest, e, w)
|
||||
return
|
||||
}
|
||||
|
||||
util.LogHandlerExitWithYAML(handler, w, c, http.StatusCreated)
|
||||
return
|
||||
}
|
||||
|
||||
util.LogHandlerExit(handler, http.StatusOK, "OK", w)
|
||||
}
|
||||
|
||||
func getConfigurator() *configurator.Configurator {
|
||||
if *kubePath == "" {
|
||||
log.Fatalf("kubectl path cannot be empty")
|
||||
}
|
||||
|
||||
// If a configuration file is specified, then it will provide the server
|
||||
// address and credentials. If not, then we check for the server address
|
||||
// and credentials as individual flags.
|
||||
var args []string
|
||||
if *kubeConfig != "" {
|
||||
*kubeConfig = os.ExpandEnv(*kubeConfig)
|
||||
args = append(args, fmt.Sprintf("--kubeconfig=%s", *kubeConfig))
|
||||
} else {
|
||||
if *kubeServer != "" {
|
||||
args = append(args, fmt.Sprintf("--server=https://%s", *kubeServer))
|
||||
} else if *kubeService != "" {
|
||||
addrs, err := net.LookupHost(*kubeService)
|
||||
if err != nil || len(addrs) < 1 {
|
||||
log.Fatalf("cannot resolve DNS name: %v", *kubeService)
|
||||
}
|
||||
|
||||
args = append(args, fmt.Sprintf("--server=https://%s", addrs[0]))
|
||||
}
|
||||
|
||||
if *kubeInsecure {
|
||||
args = append(args, "--insecure-skip-tls-verify")
|
||||
} else {
|
||||
if *kubeCertAuth != "" {
|
||||
args = append(args, fmt.Sprintf("--certificate-authority=%s", *kubeCertAuth))
|
||||
if *kubeClientCert == "" {
|
||||
args = append(args, fmt.Sprintf("--client-certificate=%s", *kubeClientCert))
|
||||
}
|
||||
|
||||
if *kubeClientKey == "" {
|
||||
args = append(args, fmt.Sprintf("--client-key=%s", *kubeClientKey))
|
||||
}
|
||||
}
|
||||
|
||||
if *kubeToken == "" {
|
||||
args = append(args, fmt.Sprintf("--token=%s", *kubeToken))
|
||||
} else {
|
||||
if *kubeUsername != "" {
|
||||
args = append(args, fmt.Sprintf("--username=%s", *kubeUsername))
|
||||
}
|
||||
|
||||
if *kubePassword != "" {
|
||||
args = append(args, fmt.Sprintf("--password=%s", *kubePassword))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return configurator.NewConfigurator(*kubePath, args)
|
||||
}
|
||||
|
||||
func getPathVariable(w http.ResponseWriter, r *http.Request, variable, handler string) (string, error) {
|
||||
vars := mux.Vars(r)
|
||||
variable, ok := vars[variable]
|
||||
if !ok {
|
||||
e := errors.New(fmt.Sprintf("%s name not found in URL", variable))
|
||||
util.LogAndReturnError(handler, http.StatusBadRequest, e, w)
|
||||
return "", e
|
||||
}
|
||||
|
||||
unescaped, err := url.QueryUnescape(variable)
|
||||
if err != nil {
|
||||
e := fmt.Errorf("cannot decode name (%v)", variable)
|
||||
util.LogAndReturnError(handler, http.StatusBadRequest, e, w)
|
||||
return "", e
|
||||
}
|
||||
|
||||
return unescaped, nil
|
||||
}
|
||||
|
||||
func getConfiguration(w http.ResponseWriter, r *http.Request, handler string) *configurator.Configuration {
|
||||
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("configuration exceeds maximum length of %d KB.", *maxLength)
|
||||
util.LogAndReturnError(handler, http.StatusBadRequest, e, w)
|
||||
return nil
|
||||
}
|
||||
|
||||
j, err := yaml.YAMLToJSON(y)
|
||||
if err != nil {
|
||||
e := errors.New(err.Error() + "\n" + string(y))
|
||||
util.LogAndReturnError(handler, http.StatusBadRequest, e, w)
|
||||
return nil
|
||||
}
|
||||
|
||||
c := &configurator.Configuration{}
|
||||
if err := json.Unmarshal(j, c); err != nil {
|
||||
e := errors.New(err.Error() + "\n" + string(j))
|
||||
util.LogAndReturnError(handler, http.StatusBadRequest, e, w)
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(c.Resources) < 1 {
|
||||
e := fmt.Errorf("configuration is empty")
|
||||
util.LogAndReturnError(handler, http.StatusBadRequest, e, w)
|
||||
return nil
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
@ -0,0 +1,153 @@
|
||||
/*
|
||||
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 configurator
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/ghodss/yaml"
|
||||
"log"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// TODO(jackgr): Define an interface and a struct type for Configurator and move initialization to the caller.
|
||||
|
||||
// Configuration describes a configuration deserialized from a YAML or JSON file.
|
||||
type Configuration struct {
|
||||
Resources []Resource `json:"resources"`
|
||||
}
|
||||
|
||||
// Resource describes a resource in a deserialized configuration. A resource has
|
||||
// a name, a type and a set of properties. The properties are passed directly to
|
||||
// kubectl as the definition of the resource on the server.
|
||||
type Resource struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Properties map[string]interface{} `json:"properties"`
|
||||
}
|
||||
|
||||
type Configurator struct {
|
||||
KubePath string
|
||||
Arguments []string
|
||||
}
|
||||
|
||||
func NewConfigurator(kubectlPath string, arguments []string) *Configurator {
|
||||
return &Configurator{kubectlPath, arguments}
|
||||
}
|
||||
|
||||
// operation is an enumeration type for kubectl operations.
|
||||
type operation string
|
||||
|
||||
// These constants implement the operation enumeration type.
|
||||
const (
|
||||
CreateOperation operation = "create"
|
||||
DeleteOperation operation = "delete"
|
||||
GetOperation operation = "get"
|
||||
ReplaceOperation operation = "replace"
|
||||
)
|
||||
|
||||
func (o operation) String() string {
|
||||
return string(o)
|
||||
}
|
||||
|
||||
// TODO(jackgr): Configure resources without dependencies in parallel.
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Configure passes the configuration in the given deployment to kubectl
|
||||
// and then updates the deployment with the completion status and completion time.
|
||||
func (a *Configurator) Configure(c *Configuration, o operation) (string, error) {
|
||||
errors := &Error{}
|
||||
var output []string
|
||||
for _, resource := range c.Resources {
|
||||
args := []string{o.String()}
|
||||
if o == GetOperation {
|
||||
args = append(args, "-o", "yaml")
|
||||
if resource.Type != "" {
|
||||
args = append(args, resource.Type)
|
||||
if resource.Name != "" {
|
||||
args = append(args, resource.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var y []byte
|
||||
if len(resource.Properties) > 0 {
|
||||
var err error
|
||||
y, err = yaml.Marshal(resource.Properties)
|
||||
if err != nil {
|
||||
e := fmt.Errorf("yaml marshal failed for resource: %v: %v", resource.Name, err)
|
||||
log.Println(errors.appendError(e))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if len(y) > 0 {
|
||||
args = append(args, "-f", "-")
|
||||
}
|
||||
|
||||
args = append(args, a.Arguments...)
|
||||
cmd := exec.Command(a.KubePath, args...)
|
||||
cmd.Stdin = bytes.NewBuffer(y)
|
||||
|
||||
// Combine stdout and stderr into a single dynamically resized buffer
|
||||
combined := &bytes.Buffer{}
|
||||
cmd.Stdout = combined
|
||||
cmd.Stderr = combined
|
||||
|
||||
// log.Printf("starting command:%s %s\nin directory: %s\nwith environment: %s\nwith stdin:\n%s\n",
|
||||
// cmd.Path, strings.Join(cmd.Args, " "), cmd.Dir, strings.Join(cmd.Env, "\n"), string(y))
|
||||
if err := cmd.Start(); err != nil {
|
||||
e := fmt.Errorf("cannot start kubetcl for resource: %v: %v", resource.Name, err)
|
||||
log.Println(errors.appendError(e))
|
||||
continue
|
||||
}
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
e := fmt.Errorf("kubetcl failed for resource: %v: %v: %v", resource.Name, err, combined.String())
|
||||
log.Println(errors.appendError(e))
|
||||
continue
|
||||
}
|
||||
|
||||
output = append(output, combined.String())
|
||||
log.Printf("kubectl succeeded for resource: %v: SysTime: %v UserTime: %v\n%v",
|
||||
resource.Name, cmd.ProcessState.SystemTime(), cmd.ProcessState.UserTime(), combined.String())
|
||||
}
|
||||
|
||||
if len(errors.errors) > 0 {
|
||||
return "", errors
|
||||
}
|
||||
|
||||
return strings.Join(output, "\n"), nil
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
/*
|
||||
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)
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
@ -0,0 +1,153 @@
|
||||
/*
|
||||
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 util
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
accHeader = "Accept-Encoding"
|
||||
typeHeader = "Content-Type"
|
||||
encHeader = "Content-Encoding"
|
||||
gzipHeader = "gzip"
|
||||
)
|
||||
|
||||
// TODO (iantw): Consider creating the Duration objects up front... May just need an all around
|
||||
// refactor if we want to support other types of backoff etc.
|
||||
var sleepIntervals = []int{1, 1, 2, 3, 5, 8, 10}
|
||||
|
||||
// Sleeper exposes a Sleep func which causes the current goroutine to sleep for the requested
|
||||
// duration.
|
||||
type Sleeper interface {
|
||||
Sleep(d time.Duration)
|
||||
}
|
||||
|
||||
type sleeper struct {
|
||||
}
|
||||
|
||||
func (s sleeper) Sleep(d time.Duration) {
|
||||
time.Sleep(d)
|
||||
}
|
||||
|
||||
// NewSleeper returns a new Sleeper.
|
||||
func NewSleeper() Sleeper {
|
||||
return sleeper{}
|
||||
}
|
||||
|
||||
// HTTPDoer is an interface for something that can 'Do' an http.Request and return an http.Response
|
||||
// and error.
|
||||
type HTTPDoer interface {
|
||||
Do(req *http.Request) (resp *http.Response, err error)
|
||||
}
|
||||
|
||||
// HTTPClient is a higher level HTTP client which takes a URL and returns the response body as a
|
||||
// string, along with the resulting status code and any errors.
|
||||
type HTTPClient interface {
|
||||
Get(url string) (body string, code int, err error)
|
||||
}
|
||||
|
||||
type httpClient struct {
|
||||
retries uint
|
||||
client HTTPDoer
|
||||
sleep Sleeper
|
||||
}
|
||||
|
||||
// NewHTTPClient returns a new HTTPClient.
|
||||
func NewHTTPClient(retries uint, c HTTPDoer, s Sleeper) HTTPClient {
|
||||
ret := httpClient{}
|
||||
ret.client = c
|
||||
ret.sleep = s
|
||||
ret.retries = retries
|
||||
return ret
|
||||
}
|
||||
|
||||
func readBody(b io.ReadCloser, ctype string, encoding string) (body string, err error) {
|
||||
defer b.Close()
|
||||
var r io.Reader
|
||||
if encoding == gzipHeader {
|
||||
gr, err := gzip.NewReader(b)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
r = gr
|
||||
defer gr.Close()
|
||||
} else if encoding == "" {
|
||||
r = b
|
||||
} else {
|
||||
return "", fmt.Errorf("Unknown %s: %s", encHeader, encoding)
|
||||
}
|
||||
|
||||
// TODO(iantw): If we find a need, allow character set conversions...
|
||||
// Unlikely to be an issue for now.
|
||||
// if ctype != "" {
|
||||
// r, err = charset.NewReader(r, ctype)
|
||||
//
|
||||
// if err != nil {
|
||||
// return "", err
|
||||
// }
|
||||
// }
|
||||
|
||||
bytes, err := ioutil.ReadAll(r)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
func (client httpClient) Get(url string) (body string, code int, err error) {
|
||||
retryCount := client.retries
|
||||
numRetries := uint(0)
|
||||
shouldRetry := true
|
||||
for shouldRetry {
|
||||
body = ""
|
||||
code = 0
|
||||
err = nil
|
||||
var req *http.Request
|
||||
req, err = http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
req.Header.Add(accHeader, gzipHeader)
|
||||
|
||||
var r *http.Response
|
||||
r, err = client.client.Do(req)
|
||||
if err == nil {
|
||||
body, err = readBody(r.Body, r.Header.Get(typeHeader), r.Header.Get(encHeader))
|
||||
if err == nil {
|
||||
code = r.StatusCode
|
||||
}
|
||||
}
|
||||
|
||||
if code != 200 {
|
||||
if numRetries < retryCount {
|
||||
numRetries = numRetries + 1
|
||||
sleepIndex := int(numRetries)
|
||||
if numRetries >= uint(len(sleepIntervals)) {
|
||||
sleepIndex = len(sleepIntervals) - 1
|
||||
}
|
||||
d, _ := time.ParseDuration(string(sleepIntervals[sleepIndex]) + "s")
|
||||
client.sleep.Sleep(d)
|
||||
} else {
|
||||
shouldRetry = false
|
||||
}
|
||||
} else {
|
||||
shouldRetry = false
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
@ -0,0 +1,160 @@
|
||||
/*
|
||||
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 util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type mockSleeper struct {
|
||||
args []time.Duration
|
||||
}
|
||||
|
||||
func (m *mockSleeper) Sleep(d time.Duration) {
|
||||
m.args = append(m.args, d)
|
||||
}
|
||||
|
||||
type responseAndError struct {
|
||||
err error
|
||||
resp *http.Response
|
||||
}
|
||||
|
||||
type testBody struct {
|
||||
closed bool
|
||||
body io.Reader
|
||||
}
|
||||
|
||||
func (tb *testBody) Read(p []byte) (n int, err error) {
|
||||
return tb.body.Read(p)
|
||||
}
|
||||
|
||||
func (tb *testBody) Close() error {
|
||||
tb.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func createResponse(err error, code int, body string, shouldClose bool,
|
||||
headers map[string]string) responseAndError {
|
||||
httpBody := testBody{body: strings.NewReader(body), closed: !shouldClose}
|
||||
header := http.Header{}
|
||||
for k, v := range headers {
|
||||
header.Add(k, v)
|
||||
}
|
||||
httpResponse := &http.Response{
|
||||
Body: &httpBody,
|
||||
ContentLength: int64(len(body)),
|
||||
StatusCode: code,
|
||||
Header: header,
|
||||
}
|
||||
return responseAndError{err: err, resp: httpResponse}
|
||||
}
|
||||
|
||||
type mockDoer struct {
|
||||
resp []responseAndError
|
||||
t *testing.T
|
||||
url string
|
||||
headers map[string]string
|
||||
}
|
||||
|
||||
func (doer *mockDoer) Do(req *http.Request) (res *http.Response, err error) {
|
||||
if req.URL.String() != doer.url {
|
||||
doer.t.Errorf("Expected url %s but got url %s", doer.url, req.URL.String())
|
||||
}
|
||||
|
||||
for k, v := range doer.headers {
|
||||
if req.Header.Get(k) != v {
|
||||
doer.t.Errorf("Expected header %s with value %s but found %s", k, v, req.Header.Get(k))
|
||||
}
|
||||
}
|
||||
|
||||
if len(doer.resp) == 0 {
|
||||
doer.t.Errorf("Do method was called more times than expected.")
|
||||
}
|
||||
|
||||
res = doer.resp[0].resp
|
||||
err = doer.resp[0].err
|
||||
doer.resp = doer.resp[1:]
|
||||
return
|
||||
}
|
||||
|
||||
func testClientDriver(md mockDoer, ms mockSleeper, expectedErr error, code int,
|
||||
result string, t *testing.T) {
|
||||
expectedCalls := len(md.resp)
|
||||
client := NewHTTPClient(uint(expectedCalls)-1, &md, &ms)
|
||||
|
||||
r, c, e := client.Get(md.url)
|
||||
|
||||
if expectedCalls-1 != len(ms.args) {
|
||||
t.Errorf("Expected %d calls to sleeper but found %d", expectedCalls-1, len(ms.args))
|
||||
}
|
||||
|
||||
if r != result {
|
||||
t.Errorf("Expected result %s but received %s", result, r)
|
||||
}
|
||||
|
||||
if c != code {
|
||||
t.Errorf("Expected status code %d but received %d", code, c)
|
||||
}
|
||||
|
||||
if e != expectedErr {
|
||||
t.Errorf("Expected error %s but received %s", expectedErr, e)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGzip(t *testing.T) {
|
||||
doer := mockDoer{}
|
||||
var b bytes.Buffer
|
||||
gz := gzip.NewWriter(&b)
|
||||
gz.Write([]byte("Test"))
|
||||
gz.Flush()
|
||||
gz.Close()
|
||||
result := b.String()
|
||||
|
||||
doer.resp = []responseAndError{
|
||||
createResponse(nil, 200, result, true, map[string]string{"Content-Encoding": "gzip"}),
|
||||
}
|
||||
|
||||
sleeper := mockSleeper{}
|
||||
testClientDriver(doer, sleeper, nil, 200, "Test", t)
|
||||
}
|
||||
|
||||
func TestRetry(t *testing.T) {
|
||||
doer := mockDoer{}
|
||||
doer.resp = []responseAndError{
|
||||
createResponse(nil, 404, "", true, map[string]string{}),
|
||||
createResponse(nil, 200, "Test", true, map[string]string{}),
|
||||
}
|
||||
|
||||
sleeper := mockSleeper{}
|
||||
testClientDriver(doer, sleeper, nil, 200, "Test", t)
|
||||
}
|
||||
|
||||
func TestFail(t *testing.T) {
|
||||
doer := mockDoer{}
|
||||
err := errors.New("Error")
|
||||
doer.resp = []responseAndError{
|
||||
createResponse(nil, 404, "", true, map[string]string{}),
|
||||
createResponse(err, 0, "", false, map[string]string{}),
|
||||
}
|
||||
|
||||
sleeper := mockSleeper{}
|
||||
testClientDriver(doer, sleeper, err, 0, "", t)
|
||||
}
|
@ -0,0 +1,202 @@
|
||||
/*
|
||||
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 util
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
)
|
||||
|
||||
// A HandlerTester is a function that takes an HTTP method, an URL path, and a
|
||||
// reader for a request body, creates a request from them, and serves it to the
|
||||
// handler to which it was bound and returns a response recorder describing the
|
||||
// outcome.
|
||||
type HandlerTester func(method, path, ctype string, reader io.Reader) (*httptest.ResponseRecorder, error)
|
||||
|
||||
// NewHandlerTester creates and returns a new HandlerTester for an http.Handler.
|
||||
func NewHandlerTester(handler http.Handler) HandlerTester {
|
||||
return func(method, path, ctype string, reader io.Reader) (*httptest.ResponseRecorder, error) {
|
||||
r, err := http.NewRequest(method, path, reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.Header.Set("Content-Type", ctype)
|
||||
w := httptest.NewRecorder()
|
||||
handler.ServeHTTP(w, r)
|
||||
return w, nil
|
||||
}
|
||||
}
|
||||
|
||||
// A ServerTester is a function that takes an HTTP method, an URL path, and a
|
||||
// reader for a request body, creates a request from them, and serves it to a
|
||||
// test server using the handler to which it was bound and returns the response.
|
||||
type ServerTester func(method, path, ctype string, reader io.Reader) (*http.Response, error)
|
||||
|
||||
// NewServerTester creates and returns a new NewServerTester for an http.Handler.
|
||||
func NewServerTester(handler http.Handler) ServerTester {
|
||||
return func(method, path, ctype string, reader io.Reader) (*http.Response, error) {
|
||||
server := httptest.NewServer(handler)
|
||||
defer server.Close()
|
||||
request := fmt.Sprintf("%s/%s", server.URL, path)
|
||||
r, err := http.NewRequest(method, request, reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.Header.Set("Content-Type", ctype)
|
||||
return http.DefaultClient.Do(r)
|
||||
}
|
||||
}
|
||||
|
||||
const formContentType = "application/x-www-form-urlencoded; param=value"
|
||||
|
||||
// TestWithURL invokes a HandlerTester with the given HTTP method, an URL path
|
||||
// parsed from the given URL string, and a string reader on the query parameters
|
||||
// parsed from the given URL string.
|
||||
func (h HandlerTester) TestWithURL(method, urlString string) (*httptest.ResponseRecorder, error) {
|
||||
request, err := url.Parse(urlString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reader := strings.NewReader(request.Query().Encode())
|
||||
return h(method, request.Path, formContentType, reader)
|
||||
}
|
||||
|
||||
// TestHandlerWithURL creates a HandlerTester with the given handler, and tests
|
||||
// it with the given HTTP method and URL string using HandlerTester.TestWithURL.
|
||||
func TestHandlerWithURL(handler http.Handler, method, urlString string) (*httptest.ResponseRecorder, error) {
|
||||
return NewHandlerTester(handler).TestWithURL(method, urlString)
|
||||
}
|
||||
|
||||
// LogHandlerEntry logs the start of the given handler handling the given request.
|
||||
func LogHandlerEntry(handler string, r *http.Request) {
|
||||
log.Printf("%s: handling request:%s %s\n", handler, r.Method, r.URL.RequestURI())
|
||||
}
|
||||
|
||||
// LogHandlerExit logs the response from the given handler with the given results.
|
||||
func LogHandlerExit(handler string, statusCode int, status string, w http.ResponseWriter) {
|
||||
log.Printf("%s: returning response: status code:%d, status:%s\n", handler, statusCode, status)
|
||||
}
|
||||
|
||||
// LogAndReturnError logs the given error and status to stderr,
|
||||
// and then returns them as the HTTP response.
|
||||
func LogAndReturnError(handler string, statusCode int, err error, w http.ResponseWriter) {
|
||||
LogHandlerExit(handler, statusCode, err.Error(), w)
|
||||
http.Error(w, err.Error(), statusCode)
|
||||
}
|
||||
|
||||
// LogHandlerExitWithJSON marshals the given object as JSON,
|
||||
// writes it to the response body, returns the given status, and then logs the
|
||||
// response.
|
||||
func LogHandlerExitWithJSON(handler string, w http.ResponseWriter, v interface{}, statusCode int) {
|
||||
j := MarshalAndWriteJSON(handler, w, v, statusCode)
|
||||
LogHandlerExit(handler, statusCode, string(j), w)
|
||||
}
|
||||
|
||||
// MarshalAndWriteJSON marshals the given object as JSON, writes it
|
||||
// to the response body, and then returns the given status.
|
||||
func MarshalAndWriteJSON(handler string, w http.ResponseWriter, v interface{}, statusCode int) []byte {
|
||||
j, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
LogAndReturnError(handler, http.StatusInternalServerError, err, w)
|
||||
return nil
|
||||
}
|
||||
|
||||
WriteJSON(handler, w, j, statusCode)
|
||||
return j
|
||||
}
|
||||
|
||||
// WriteJSON writes the given bytes to the response body, sets the content type
|
||||
// to "application/json; charset=UTF-8", and then returns the given status.
|
||||
func WriteJSON(handler string, w http.ResponseWriter, j []byte, status int) {
|
||||
WriteResponse(handler, w, j, "application/json; charset=UTF-8", status)
|
||||
}
|
||||
|
||||
// LogHandlerExitWithYAML marshals the given object as YAML,
|
||||
// writes it to the response body, returns the given status, and then logs the
|
||||
// response.
|
||||
func LogHandlerExitWithYAML(handler string, w http.ResponseWriter, v interface{}, statusCode int) {
|
||||
y := MarshalAndWriteYAML(handler, w, v, statusCode)
|
||||
LogHandlerExit(handler, statusCode, string(y), w)
|
||||
}
|
||||
|
||||
// MarshalAndWriteYAML marshals the given object as YAML, writes it
|
||||
// to the response body, and then returns the given status.
|
||||
func MarshalAndWriteYAML(handler string, w http.ResponseWriter, v interface{}, statusCode int) []byte {
|
||||
y, err := yaml.Marshal(v)
|
||||
if err != nil {
|
||||
LogAndReturnError(handler, http.StatusInternalServerError, err, w)
|
||||
return nil
|
||||
}
|
||||
|
||||
WriteYAML(handler, w, y, statusCode)
|
||||
return y
|
||||
}
|
||||
|
||||
// WriteYAML writes the given bytes to the response body, sets the content type
|
||||
// to "application/x-yaml; charset=UTF-8", and then returns the given status.
|
||||
func WriteYAML(handler string, w http.ResponseWriter, y []byte, status int) {
|
||||
WriteResponse(handler, w, y, "application/x-yaml; charset=UTF-8", status)
|
||||
}
|
||||
|
||||
// WriteResponse writes the given bytes to the response body, sets the content
|
||||
// type to the given value, and then returns the given status.
|
||||
func WriteResponse(handler string, w http.ResponseWriter, v []byte, ct string, status int) {
|
||||
// Header must be set before status is written
|
||||
if len(v) > 0 {
|
||||
w.Header().Set("Content-Type", ct)
|
||||
}
|
||||
|
||||
// Header and status must be written before content is written
|
||||
w.WriteHeader(status)
|
||||
if len(v) > 0 {
|
||||
if _, err := w.Write(v); err != nil {
|
||||
LogAndReturnError(handler, http.StatusInternalServerError, err, w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ToYAMLOrError marshals the given object to YAML and returns either the
|
||||
// resulting YAML or an error message. Useful when marshaling an object for
|
||||
// a log entry.
|
||||
func ToYAMLOrError(v interface{}) string {
|
||||
y, err := yaml.Marshal(v)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("yaml marshal failed:%s\n%v\n", err, v)
|
||||
}
|
||||
|
||||
return string(y)
|
||||
}
|
||||
|
||||
// ToJSONOrError marshals the given object to JSON and returns either the
|
||||
// resulting YAML or an error message. Useful when marshaling an object for
|
||||
// a log entry.
|
||||
func ToJSONOrError(v interface{}) string {
|
||||
j, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("json marshal failed:%s\n%v\n", err, v)
|
||||
}
|
||||
|
||||
return string(j)
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
// 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.
|
||||
|
||||
package version
|
||||
|
||||
// DeploymentManagerVersion is the current version of the Deployment Manager.
|
||||
// Update this whenever making a new release.
|
||||
// The version is of the format Major.Minor.Patch
|
||||
// Increment major number for new feature additions and behavioral changes.
|
||||
// Increment minor number for bug fixes and performance enhancements.
|
||||
// Increment patch number for critical fixes to existing releases.
|
||||
const DeploymentManagerVersion = "0.0.1"
|
Loading…
Reference in new issue