diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..c61a717ac --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.coverage/ +bin/ +rootfs/helm +rootfs/tiller +vendor/ +_proto/*.pb.go diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..00336c131 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,37 @@ +# Contributing Guidelines + +The Kubernetes Helm project accepts contributions via GitHub pull requests. This document outlines the process to help get your contribution accepted. + +## 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. + +## How to Contribute A Patch + +1. If 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. + +### Merge Approval + +Helm collaborators may add "LGTM" (Looks Good To Me) or an equivalent comment to indicate that a PR is acceptable. Any change requires at least one LGTM. No pull requests can be merged until at least one Helm collaborator signs off with an LGTM. + +If the PR is from a Helm collaborator, then he or she should be the one to merge and close it. This keeps the commit stream clean and gives the collaborator the benefit of revisiting the PR before deciding whether or not to merge the changes. + +## Support Channels + +Whether you are a user or contributor, official support channels include: + +- GitHub issues: https://github.com/kubenetes/helm/issues/new +- Slack: #Helm room in the [Kubernetes Slack](http://slack.kubernetes.io/) + +Before opening a new issue or submitting a new pull request, it's helpful to search the project - it's likely that another user has already reported the issue you're facing, or it's a known issue that we're already aware of. diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..fd5e64151 --- /dev/null +++ b/Makefile @@ -0,0 +1,73 @@ +DOCKER_REGISTRY ?= gcr.io +IMAGE_PREFIX ?= deis-sandbox +SHORT_NAME ?= tiller + +# go option +GO ?= go +GOARCH ?= $(shell go env GOARCH) +GOOS ?= $(shell go env GOOS) +PKG := $(shell glide novendor) +TAGS := +TESTS := . +TESTFLAGS := +LDFLAGS := +GOFLAGS := +BINDIR := ./bin +BINARIES := helm tiller + +include versioning.mk + +.PHONY: all +all: build + +.PHONY: build +build: GOFLAGS += -a -installsuffix cgo +build: + @for i in $(BINARIES); do \ + CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) $(GO) build -o $(BINDIR)/$$i $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)' ./cmd/$$i || exit 1; \ + done + +.PHONY: check-docker +check-docker: + @if [ -z $$(which docker) ]; then \ + echo "Missing \`docker\` client which is required for development"; \ + exit 2; \ + fi + +.PHONY: docker-binary +docker-binary: GOOS = linux +docker-binary: GOARCH = amd64 +docker-binary: BINDIR = ./rootfs +docker-binary: build + +.PHONY: docker-build +docker-build: check-docker docker-binary + docker build --rm -t ${IMAGE} rootfs + docker tag -f ${IMAGE} ${MUTABLE_IMAGE} + +.PHONY: test +test: build +test: TESTFLAGS += -race -v +test: test-style +test: test-unit + +.PHONY: test-unit +test-unit: + $(GO) test $(GOFLAGS) -run $(TESTS) $(PKG) $(TESTFLAGS) + +.PHONY: test-style +test-style: + @scripts/validate-go.sh + +.PHONY: clean +clean: + @rm -rf $(BINDIR) + +.PHONY: coverage +coverage: + @scripts/coverage.sh + +.PHONY: bootstrap +bootstrap: + glide install + diff --git a/README.md b/README.md new file mode 100644 index 000000000..81872159e --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +# Kubernetes Helm + +Helm is a tool for managing Kubernetes charts. Charts are packages of +pre-configured Kubernetes resources. + +## Install + +Helm is in its early stages of development. At this time there are no +releases. + +To install Helm from source, follow this process: + +Make sure you have the prerequisites: +- Go 1.6 +- A running Kubernetes cluster +- `kubectl` properly configured to talk to your cluster +- Glide 0.10 or greater + +1. Clone (or otherwise download) this repository +2. Run `make boostrap build` + +You will now have two binaries built: + +- `bin/helm` is the client +- `bin/tiller` is the server + +You can locally run Tiller, or you build a Docker image (`make +docker-build`) and then deploy it (`helm init -i IMAGE_NAME`). + +The [documentation](docs) folder contains more information about the +architecture and usage of Helm/Tiller. + +## The History of the Project + +Kubernetes Helm is the merged result of [Helm +Classic](https://github.com/helm/helm) and the Kubernetes port of GCS Deployment +Manager. The project was jointly started by Google and Deis, though it +is now part of the CNCF. diff --git a/_proto/Makefile b/_proto/Makefile new file mode 100644 index 000000000..baf437cc5 --- /dev/null +++ b/_proto/Makefile @@ -0,0 +1,37 @@ +space := $(empty) $(empty) +comma := , +empty := + +import_path = github.com/deis/tiller/pkg/proto/hapi + +dst = ../pkg/proto +target = go +plugins = grpc + +chart_ias = $(subst $(space),$(comma),$(addsuffix =$(import_path)/$(chart_pkg),$(addprefix M,$(chart_pbs)))) +chart_pbs = $(wildcard hapi/chart/*.proto) +chart_pkg = chart + +release_ias = $(subst $(space),$(comma),$(addsuffix =$(import_path)/$(release_pkg),$(addprefix M,$(release_pbs)))) +release_pbs = $(wildcard hapi/release/*.proto) +release_pkg = release + +services_ias = $(subst $(space),$(comma),$(addsuffix =$(import_path)/$(services_pkg),$(addprefix M,$(services_pbs)))) +services_pbs = $(wildcard hapi/services/*.proto) +services_pkg = services + +google_deps = Mgoogle/protobuf/timestamp.proto=github.com/golang/protobuf/ptypes/timestamp,Mgoogle/protobuf/any.proto=github.com/golang/protobuf/ptypes/any + +all: chart release services + +chart: + protoc --$(target)_out=plugins=$(plugins),$(google_deps),$(chart_ias):$(dst) $(chart_pbs) + +release: + protoc --$(target)_out=plugins=$(plugins),$(google_deps),$(chart_ias):$(dst) $(release_pbs) + +services: + protoc --$(target)_out=plugins=$(plugins),$(google_deps),$(chart_ias),$(release_ias):$(dst) $(services_pbs) + +clean: + @rm -rf $(dst)/hapi 2>/dev/null diff --git a/_proto/hapi/chart/chart.proto b/_proto/hapi/chart/chart.proto new file mode 100644 index 000000000..90e4938cc --- /dev/null +++ b/_proto/hapi/chart/chart.proto @@ -0,0 +1,29 @@ +syntax = "proto3"; + +package hapi.chart; + +import "hapi/chart/config.proto"; +import "hapi/chart/metadata.proto"; +import "hapi/chart/template.proto"; + +option go_package = "chart"; + +// +// Chart: +// A chart is a helm package that contains metadata, a default config, zero or more +// optionally parameterizable templates, and zero or more charts (dependencies). +// +message Chart { + // Contents of the Chartfile. + hapi.chart.Metadata metadata = 1; + + // Templates for this chart. + repeated hapi.chart.Template templates = 2; + + // Charts that this chart depends on. + repeated Chart dependencies = 3; + + // Default config for this template. + hapi.chart.Config values = 4; + +} diff --git a/_proto/hapi/chart/config.proto b/_proto/hapi/chart/config.proto new file mode 100644 index 000000000..0829632ac --- /dev/null +++ b/_proto/hapi/chart/config.proto @@ -0,0 +1,25 @@ +syntax = "proto3"; + +package hapi.chart; + +option go_package = "chart"; + +// +// Config: +// +// A config supplies values to the parametrizable templates of a chart. +// +message Config { + string raw = 1; + + map values = 2; +} + +// +// Value: +// +// TODO +// +message Value { + string value = 1; +} diff --git a/_proto/hapi/chart/metadata.proto b/_proto/hapi/chart/metadata.proto new file mode 100644 index 000000000..08e03985d --- /dev/null +++ b/_proto/hapi/chart/metadata.proto @@ -0,0 +1,49 @@ +syntax = "proto3"; + +package hapi.chart; + +option go_package = "chart"; + +// +// Maintainer: +// +// A descriptor of the Chart maintainer(s). +// +message Maintainer { + // Name is a user name or organization name + string name = 1; + + // Email is an optional email address to contact the named maintainer + string email = 2; +} + +// +// Metadata: +// +// Metadata for a Chart file. This models the structure +// of a Chart.yaml file. +// +// Spec: https://github.com/kubernetes/helm/blob/master/docs/design/chart_format.md#the-chart-file +// +message Metadata { + // The name of the chart + string name = 1; + + // The URL to a relecant project page, git repo, or contact person + string home = 2; + + // Source is the URL to the source code of this chart + repeated string sources = 3; + + // A SemVer 2 conformant version string of the chart + string version = 4; + + // A one-sentence description of the chart + string description = 5; + + // A list of string keywords + repeated string keywords = 6; + + // A list of name and URL/email address combinations for the maintainer(s) + repeated Maintainer maintainers = 7; +} diff --git a/_proto/hapi/chart/template.proto b/_proto/hapi/chart/template.proto new file mode 100644 index 000000000..3e68113c2 --- /dev/null +++ b/_proto/hapi/chart/template.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +package hapi.chart; + +option go_package = "chart"; + +// Template represents a template as a name/value pair. +// +// By convention, name is a relative path within the scope of the chart's +// base directory. +message Template { + // Name is the path-like name of the template. + string name = 1; + + // Data is the template as byte data. + bytes data = 2; +} diff --git a/_proto/hapi/release/info.proto b/_proto/hapi/release/info.proto new file mode 100644 index 000000000..382f4acfc --- /dev/null +++ b/_proto/hapi/release/info.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; + +package hapi.release; + +import "google/protobuf/timestamp.proto"; +import "hapi/release/status.proto"; + +option go_package = "release"; + +// +// Info: +// +// +message Info { + Status status = 1; + + google.protobuf.Timestamp first_deployed = 2; + + google.protobuf.Timestamp last_deployed = 3; + + // Deleted tracks when this object was deleted. + google.protobuf.Timestamp deleted = 4; +} diff --git a/_proto/hapi/release/release.proto b/_proto/hapi/release/release.proto new file mode 100644 index 000000000..52ba2cd44 --- /dev/null +++ b/_proto/hapi/release/release.proto @@ -0,0 +1,33 @@ +syntax = "proto3"; + +package hapi.release; + +import "hapi/release/info.proto"; +import "hapi/chart/config.proto"; +import "hapi/chart/chart.proto"; + +option go_package = "release"; + +// +// Release: +// +// A release describes a deployment of a chart, together with the chart +// and the variables used to deploy that chart. +// +message Release { + // Name is the name of the release + string name = 1; + + // Info provides information about a release + hapi.release.Info info = 2; + + // Chart is the chart that was released. + hapi.chart.Chart chart = 3; + + // Config is the set of extra Values added to the chart. + // These values override the default values inside of the chart. + hapi.chart.Config config = 4; + + // Manifest is the string representation of the rendered template. + string manifest = 5; +} diff --git a/_proto/hapi/release/status.proto b/_proto/hapi/release/status.proto new file mode 100644 index 000000000..9ec021005 --- /dev/null +++ b/_proto/hapi/release/status.proto @@ -0,0 +1,27 @@ +syntax = "proto3"; + +package hapi.release; + +import "google/protobuf/any.proto"; + +option go_package = "release"; + +// +// Status: +// +// +message Status { + enum Code { + UNKNOWN = 0; + + DEPLOYED = 1; + + DELETED = 2; + + SUPERSEDED = 3; + } + + Code code = 1; + + google.protobuf.Any details = 2; +} diff --git a/_proto/hapi/services/probe.proto b/_proto/hapi/services/probe.proto new file mode 100644 index 000000000..062b37bdb --- /dev/null +++ b/_proto/hapi/services/probe.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package hapi.services.probe; + +option go_package = "services"; + +service ProbeService { + rpc Ready(ReadyRequest) returns (ReadyResponse) { + } +} + +message ReadyRequest { +} + +message ReadyResponse { +} diff --git a/_proto/hapi/services/tiller.proto b/_proto/hapi/services/tiller.proto new file mode 100644 index 000000000..cd5adb958 --- /dev/null +++ b/_proto/hapi/services/tiller.proto @@ -0,0 +1,181 @@ +syntax = "proto3"; + +package hapi.services.tiller; + +import "hapi/chart/chart.proto"; +import "hapi/chart/config.proto"; +import "hapi/release/release.proto"; +import "hapi/release/info.proto"; + +option go_package = "services"; + +// +// ReleaseService: +// +// The service that a helm application uses to mutate, +// query, and manage releases. +// +// Release: A named installation composed of a chart and +// config. At any given time a release has one +// chart and one config. +// +// Config: A config is a TOML file that supplies values +// to the parametrizable templates of a chart. +// +// Chart: A chart is a helm package that contains +// metadata, a default config, zero or more +// optionally parameterizable templates, and +// zero or more charts (dependencies). +// +// +service ReleaseService { + // + // Retrieve release history. TODO: Allow filtering the set of releases by + // release status. By default, ListAllReleases returns the releases who + // current status is "Active". + // + rpc ListReleases(ListReleasesRequest) returns (stream ListReleasesResponse) { + } + + // + // Retrieve status information for the specified release. + // + rpc GetReleaseStatus(GetReleaseStatusRequest) returns (GetReleaseStatusResponse) { + } + + // + // Retrieve the release content (chart + value) for the specifed release. + // + rpc GetReleaseContent(GetReleaseContentRequest) returns (GetReleaseContentResponse) { + } + + // + // Update release content. + // + rpc UpdateRelease(UpdateReleaseRequest) returns (UpdateReleaseResponse) { + } + + // + // Request release install. + // + rpc InstallRelease(InstallReleaseRequest) returns (InstallReleaseResponse) { + } + + // + // Request release deletion. + // + rpc UninstallRelease(UninstallReleaseRequest) returns (UninstallReleaseResponse) { + } +} + +// +// ListReleasesRequest: +// +// TODO +// +message ListReleasesRequest { + // The maximum number of releases to be returned + int64 limit = 1; + + // The zero-based offset at which the returned release list begins + int64 offset = 2; +} + +// +// ListReleasesResponse: +// +// TODO +// +message ListReleasesResponse { + // The expected total number of releases to be returned + int64 count = 1; + + // The zero-based offset at which the list is positioned + int64 offset = 2; + + // The total number of queryable releases + int64 total = 3; + + // The resulting releases + repeated hapi.release.Release releases = 4; +} + +// GetReleaseStatusRequest is a request to get the status of a release. +message GetReleaseStatusRequest { + // Name is the name of the release + string name = 1; +} + +// GetReleaseStatusResponse is the response indicating the status of the named release. +message GetReleaseStatusResponse { + // Name is the name of the release. + string name = 1; + + // Info contains information about the release. + hapi.release.Info info = 2; +} + +// GetReleaseContentRequest is a request to get the contents of a release. +message GetReleaseContentRequest { + // The name of the release + string name = 1; +} + +// GetReleaseContentResponse is a response containing the contents of a release. +message GetReleaseContentResponse { + // The release content + hapi.release.Release release = 1; +} + +// +// UpdateReleaseRequest: +// +// TODO +// +message UpdateReleaseRequest { +} + +// +// UpdateReleaseResponse: +// +// TODO +// +message UpdateReleaseResponse { +} + +// +// InstallReleaseRequest: +// +// TODO +// +message InstallReleaseRequest { + // Chart is the protobuf representation of a chart. + hapi.chart.Chart chart = 1; + // Values is a string containing (unparsed) TOML values. + hapi.chart.Config values = 2; + // DryRun, if true, will run through the release logic, but neither create + // a release object nor deploy to Kubernetes. The release object returned + // in the response will be fake. + bool dry_run = 3; +} + +// +// InstallReleaseResponse: +// +// TODO +// +message InstallReleaseResponse { + hapi.release.Release release = 1; +} + +// UninstallReleaseRequest represents a request to uninstall a named release. +message UninstallReleaseRequest { + // Name is the name of the release to delete. + string name = 1; +} + +// UninstallReleaseResponse represents a successful response to an uninstall request. +message UninstallReleaseResponse { + // Release is the release that was marked deleted. + hapi.release.Release release = 1; +} diff --git a/circle.yml b/circle.yml new file mode 100644 index 000000000..2d7c7a450 --- /dev/null +++ b/circle.yml @@ -0,0 +1,31 @@ +machine: + environment: + GLIDE_VERSION: "0.10.1" + GO15VENDOREXPERIMENT: 1 + GOPATH: /usr/local/go_workspace + HOME: /home/ubuntu + IMPORT_PATH: "github.com/deis/tiller" + PATH: $HOME/go/bin:$PATH + GOROOT: $HOME/go + +dependencies: + override: + - mkdir -p $HOME/go + - wget "https://storage.googleapis.com/golang/go1.6.linux-amd64.tar.gz" + - tar -C $HOME -xzf go1.6.linux-amd64.tar.gz + - go version + - go env + - sudo chown -R $(whoami):staff /usr/local + - cd $GOPATH + - mkdir -p $GOPATH/src/$IMPORT_PATH + - cd $HOME/tiller + - rsync -az --delete ./ "$GOPATH/src/$IMPORT_PATH/" + - wget "https://github.com/Masterminds/glide/releases/download/$GLIDE_VERSION/glide-$GLIDE_VERSION-linux-amd64.tar.gz" + - mkdir -p $HOME/bin + - tar -vxz -C $HOME/bin --strip=1 -f glide-$GLIDE_VERSION-linux-amd64.tar.gz + - export PATH="$HOME/bin:$PATH" GLIDE_HOME="$HOME/.glide" + - cd $GOPATH/src/$IMPORT_PATH + +test: + override: + - cd $GOPATH/src/$IMPORT_PATH && make bootstrap test diff --git a/cmd/helm/create.go b/cmd/helm/create.go new file mode 100644 index 000000000..7f52d04b2 --- /dev/null +++ b/cmd/helm/create.go @@ -0,0 +1,63 @@ +package main + +import ( + "errors" + "path/filepath" + + "github.com/deis/tiller/pkg/chart" + "github.com/spf13/cobra" +) + +const createDesc = ` +This command creates a chart directory along with the common files and +directories used in a chart. + +For example, 'helm create foo' will create a directory structure that looks +something like this: + + foo/ + |- Chart.yaml # Information about your chart + | + |- values.toml # The default values for your templates + | + |- charts/ # Charts that this chart depends on + | + |- templates/ # The template files + +'helm create' takes a path for an argument. If directories in the given path +do not exist, Helm will attempt to create them as it goes. If the given +destination exists and there are files in that directory, conflicting files +will be overwritten, but other files will be left alone. +` + +func init() { + RootCommand.AddCommand(createCmd) +} + +var createCmd = &cobra.Command{ + Use: "create [PATH]", + Short: "Create a new chart at the location specified.", + Long: createDesc, + RunE: runCreate, +} + +func runCreate(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("the name of the new chart is required") + } + cname := args[0] + cmd.Printf("Creating %s\n", cname) + + chartname := filepath.Base(cname) + cfile := chart.Chartfile{ + Name: chartname, + Description: "A Helm chart for Kubernetes", + Version: "0.1.0", + } + + if _, err := chart.Create(&cfile, filepath.Dir(cname)); err != nil { + return err + } + + return nil +} diff --git a/cmd/helm/fetch.go b/cmd/helm/fetch.go new file mode 100644 index 000000000..c14c4f103 --- /dev/null +++ b/cmd/helm/fetch.go @@ -0,0 +1,46 @@ +package main + +import ( + "fmt" + "io" + "net/http" + "os" + + "github.com/spf13/cobra" +) + +func init() { + RootCommand.AddCommand(fetchCmd) +} + +var fetchCmd = &cobra.Command{ + Use: "fetch", + Short: "Download a chart from a repository and unpack it in local directory.", + Long: "", + RunE: fetch, +} + +func fetch(cmd *cobra.Command, args []string) error { + // parse args + // get download url + // call download url + out, err := os.Create("nginx-2.0.0.tgz") + if err != nil { + return err + } + defer out.Close() + resp, err := http.Get("http://localhost:8879/charts/nginx-2.0.0.tgz") + fmt.Println("after req") + // unpack file + if err != nil { + return err + } + + defer resp.Body.Close() + + _, err = io.Copy(out, resp.Body) + if err != nil { + return err + } + return nil +} diff --git a/cmd/helm/get.go b/cmd/helm/get.go new file mode 100644 index 000000000..9ba031435 --- /dev/null +++ b/cmd/helm/get.go @@ -0,0 +1,53 @@ +package main + +import ( + "errors" + "fmt" + + "github.com/deis/tiller/pkg/helm" + "github.com/spf13/cobra" +) + +var getHelp = ` +This command shows the details of a named release. + +It can be used to get extended information about the release, including: + + - The values used to generate the release + - The chart used to generate the release + - The generated manifest file + +By default, this prints a human readable collection of information about the +chart, the supplied values, and the generated manifest file. +` + +var errReleaseRequired = errors.New("release name is required") + +var getCommand = &cobra.Command{ + Use: "get [flags] RELEASE_NAME", + Short: "Download a named release", + Long: getHelp, + RunE: getCmd, +} + +func init() { + RootCommand.AddCommand(getCommand) +} + +func getCmd(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errReleaseRequired + } + + res, err := helm.GetReleaseContent(args[0]) + if err != nil { + return err + } + + fmt.Printf("Chart/Version: %s %s\n", res.Release.Chart.Metadata.Name, res.Release.Chart.Metadata.Version) + fmt.Println("Config:") + fmt.Println(res.Release.Config) + fmt.Println("\nManifest:") + fmt.Println(res.Release.Manifest) + return nil +} diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go new file mode 100644 index 000000000..43a032c00 --- /dev/null +++ b/cmd/helm/helm.go @@ -0,0 +1,46 @@ +package main + +import ( + "os" + + "github.com/spf13/cobra" +) + +var stdout = os.Stdout +var helmHome string + +var globalUsage = `The Kubernetes package manager + +To begin working with Helm, run the 'helm init' command: + +$ helm init + +This will install Tiller to your running Kubernetes cluster. +It will also set up any necessary local configuration. + +Commond actions from this point on include: + +- helm search: search for charts +- helm fetch: download a chart to your local directory to view +- helm install: upload the chart to Kubernetes +- helm list: list releases of charts + +ENVIRONMENT: +$HELM_HOME: Set an alternative location for Helm files. + By default, these are stored in ~/.helm +` + +// RootCommand is the top-level command for Helm. +var RootCommand = &cobra.Command{ + Use: "helm", + Short: "The Helm package manager for Kubernetes.", + Long: globalUsage, +} + +func init() { + RootCommand.PersistentFlags().StringVar(&helmHome, "home", "$HOME/.helm", "location of you Helm files [$HELM_HOME]") +} + +func main() { + RootCommand.Execute() +} diff --git a/cmd/helm/home.go b/cmd/helm/home.go new file mode 100644 index 000000000..0f3d4480a --- /dev/null +++ b/cmd/helm/home.go @@ -0,0 +1,25 @@ +package main + +import ( + "github.com/spf13/cobra" +) + +var longHomeHelp = ` +This command displays the location of HELM_HOME. This is where +any helm configuration files live. +` + +var homeCommand = &cobra.Command{ + Use: "home", + Short: "Displays the location of HELM_HOME", + Long: longHomeHelp, + Run: home, +} + +func init() { + RootCommand.AddCommand(homeCommand) +} + +func home(cmd *cobra.Command, args []string) { + cmd.Printf(homePath() + "\n") +} diff --git a/cmd/helm/init.go b/cmd/helm/init.go new file mode 100644 index 000000000..24dbde8c2 --- /dev/null +++ b/cmd/helm/init.go @@ -0,0 +1,120 @@ +package main + +import ( + "errors" + "fmt" + "io/ioutil" + "os" + + "github.com/deis/tiller/pkg/client" + "github.com/deis/tiller/pkg/kubectl" + "github.com/spf13/cobra" +) + +const initDesc = ` +This command installs Tiller (the helm server side component) onto your +Kubernetes Cluster and sets up local configuration in $HELM_HOME (default: ~/.helm/) +` + +var ( + tillerImg string + defaultRepo = map[string]string{"default-name": "default-url"} +) + +func init() { + initCmd.Flags().StringVarP(&tillerImg, "tiller-image", "i", "", "override tiller image") + RootCommand.AddCommand(initCmd) +} + +var initCmd = &cobra.Command{ + Use: "init", + Short: "Initialize Helm on both client and server.", + Long: installDesc, + RunE: runInit, +} + +// runInit initializes local config and installs tiller to Kubernetes Cluster +func runInit(cmd *cobra.Command, args []string) error { + if len(args) != 0 { + return errors.New("This command does not accept arguments. \n") + } + + if err := ensureHome(); err != nil { + return err + } + + if err := installTiller(); err != nil { + return err + } + + fmt.Println("Happy Helming!") + return nil +} + +func installTiller() error { + // TODO: take value of global flag kubectl and pass that in + runner := buildKubectlRunner("") + + i := client.NewInstaller() + i.Tiller["Image"] = tillerImg + out, err := i.Install(runner) + + if err != nil { + return fmt.Errorf("error installing %s %s", string(out), err) + } + fmt.Println("\nTiller (the helm server side component) has been installed into your Kubernetes Cluster.") + + return nil +} + +func buildKubectlRunner(kubectlPath string) kubectl.Runner { + if kubectlPath != "" { + kubectl.Path = kubectlPath + } + return &kubectl.RealRunner{} +} + +// ensureHome checks to see if $HELM_HOME exists +// +// If $HELM_HOME does not exist, this function will create it. +func ensureHome() error { + configDirectories := []string{homePath(), cacheDirectory(), localRepoDirectory()} + + for _, p := range configDirectories { + if fi, err := os.Stat(p); err != nil { + fmt.Printf("Creating %s \n", p) + if err := os.MkdirAll(p, 0755); err != nil { + return fmt.Errorf("Could not create %s: %s", p, err) + } + } else if !fi.IsDir() { + return fmt.Errorf("%s must be a directory", p) + } + } + + repoFile := repositoriesFile() + if fi, err := os.Stat(repoFile); err != nil { + fmt.Printf("Creating %s \n", repoFile) + if err := ioutil.WriteFile(repoFile, []byte("local: localhost:8879/charts\n"), 0644); err != nil { + return err + } + } else if fi.IsDir() { + return fmt.Errorf("%s must be a file, not a directory", repoFile) + } + + localRepoCacheFile := localRepoDirectory(localRepoCacheFilePath) + if fi, err := os.Stat(localRepoCacheFile); err != nil { + fmt.Printf("Creating %s \n", localRepoCacheFile) + _, err := os.Create(localRepoCacheFile) + if err != nil { + return err + } + + //TODO: take this out and replace with helm update functionality + os.Symlink(localRepoCacheFile, cacheDirectory("local-cache.yaml")) + } else if fi.IsDir() { + return fmt.Errorf("%s must be a file, not a directory", localRepoCacheFile) + } + + fmt.Printf("$HELM_HOME has been configured at %s.\n", helmHome) + return nil +} diff --git a/cmd/helm/init_test.go b/cmd/helm/init_test.go new file mode 100644 index 000000000..6c5bebbe9 --- /dev/null +++ b/cmd/helm/init_test.go @@ -0,0 +1,42 @@ +package main + +import ( + "io/ioutil" + "os" + "testing" +) + +func TestEnsureHome(t *testing.T) { + home := createTmpHome() + helmHome = home + if err := ensureHome(); err != nil { + t.Errorf("%s", err) + } + + expectedDirs := []string{homePath(), cacheDirectory(), localRepoDirectory()} + for _, dir := range expectedDirs { + if fi, err := os.Stat(dir); err != nil { + t.Errorf("%s", err) + } else if !fi.IsDir() { + t.Errorf("%s is not a directory", fi) + } + } + + if fi, err := os.Stat(repositoriesFile()); err != nil { + t.Errorf("%s", err) + } else if fi.IsDir() { + t.Errorf("%s should not be a directory", fi) + } + + if fi, err := os.Stat(localRepoDirectory(localRepoCacheFilePath)); err != nil { + t.Errorf("%s", err) + } else if fi.IsDir() { + t.Errorf("%s should not be a directory", fi) + } +} + +func createTmpHome() string { + tmpHome, _ := ioutil.TempDir("", "helm_home") + defer os.Remove(tmpHome) + return tmpHome +} diff --git a/cmd/helm/install.go b/cmd/helm/install.go new file mode 100644 index 000000000..073edfd97 --- /dev/null +++ b/cmd/helm/install.go @@ -0,0 +1,65 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/spf13/cobra" + + "github.com/deis/tiller/pkg/chart" + "github.com/deis/tiller/pkg/helm" +) + +const installDesc = ` +This command installs a chart archive. +` + +func init() { + RootCommand.Flags() + RootCommand.AddCommand(installCmd) +} + +var installCmd = &cobra.Command{ + Use: "install [CHART]", + Short: "install a chart archive.", + Long: installDesc, + RunE: runInstall, +} + +func runInstall(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return fmt.Errorf("This command needs at least one argument, the name of the chart.") + } + + ch, err := loadChart(args[0]) + if err != nil { + return err + } + + res, err := helm.InstallRelease(ch) + if err != nil { + return err + } + + fmt.Printf("release.name: %s\n", res.Release.Name) + fmt.Printf("release.chart: %s\n", res.Release.Chart.Metadata.Name) + fmt.Printf("release.status: %s\n", res.Release.Info.Status.Code) + + return nil +} + +func loadChart(path string) (*chart.Chart, error) { + path, err := filepath.Abs(path) + if err != nil { + return nil, err + } + + if fi, err := os.Stat(path); err != nil { + return nil, err + } else if fi.IsDir() { + return chart.LoadDir(path) + } + + return chart.Load(path) +} diff --git a/cmd/helm/lint.go b/cmd/helm/lint.go new file mode 100644 index 000000000..1210d15d0 --- /dev/null +++ b/cmd/helm/lint.go @@ -0,0 +1,39 @@ +package main + +import ( + "fmt" + + "github.com/deis/tiller/pkg/lint" + "github.com/spf13/cobra" +) + +var longLintHelp = ` +This command takes a path to a chart and runs a series of tests to verify that +the chart is well-formed. + +If the linter encounters things that will cause the chart to fail installation, +it will emit [ERROR] messages. If it encounters issues that break with convention +or recommendation, it will emit [WARNING] messages. +` + +var lintCommand = &cobra.Command{ + Use: "lint [flags] PATH", + Short: "Examines a chart for possible issues", + Long: longLintHelp, + Run: lintCmd, +} + +func init() { + RootCommand.AddCommand(lintCommand) +} + +func lintCmd(cmd *cobra.Command, args []string) { + path := "." + if len(args) > 0 { + path = args[0] + } + issues := lint.All(path) + for _, i := range issues { + fmt.Printf("%s\n", i) + } +} diff --git a/cmd/helm/package.go b/cmd/helm/package.go new file mode 100644 index 000000000..98c12ce05 --- /dev/null +++ b/cmd/helm/package.go @@ -0,0 +1,74 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/deis/tiller/pkg/chart" + "github.com/deis/tiller/pkg/repo" + "github.com/spf13/cobra" +) + +const packageDesc = ` +This command packages a chart into a versioned chart archive file. If a path +is given, this will look at that path for a chart (which must contain a +Chart.yaml file) and then package that directory. + +If no path is given, this will look in the present working directory for a +Chart.yaml file, and (if found) build the current directory into a chart. + +Versioned chart archives are used by Helm package repositories. +` + +var save bool + +func init() { + packageCmd.Flags().BoolVar(&save, "save", true, "save packaged chart to local chart repository") + RootCommand.AddCommand(packageCmd) +} + +var packageCmd = &cobra.Command{ + Use: "package [CHART_PATH]", + Short: "Package a chart directory into a chart archive.", + Long: packageDesc, + RunE: runPackage, +} + +func runPackage(cmd *cobra.Command, args []string) error { + path := "." + + if len(args) > 0 { + path = args[0] + } else { + return fmt.Errorf("This command needs at least one argument, the path to the chart.") + } + + path, err := filepath.Abs(path) + if err != nil { + return err + } + + ch, err := chart.LoadDir(path) + if err != nil { + return err + } + + // Save to $HELM_HOME/local directory. + if save { + if err := repo.AddChartToLocalRepo(ch, localRepoDirectory()); err != nil { + return err + } + } + + // Save to the current working directory. + cwd, err := os.Getwd() + if err != nil { + return err + } + name, err := chart.Save(ch, cwd) + if err == nil { + cmd.Printf("Saved %s to current directory\n", name) + } + return err +} diff --git a/cmd/helm/remove.go b/cmd/helm/remove.go new file mode 100644 index 000000000..cdaf93b13 --- /dev/null +++ b/cmd/helm/remove.go @@ -0,0 +1,52 @@ +package main + +import ( + "errors" + "fmt" + + "github.com/deis/tiller/pkg/helm" + "github.com/spf13/cobra" +) + +const removeDesc = ` +This command takes a release name, and then deletes the release from Kubernetes. +It removes all of the resources associated with the last release of the chart. + +Use the '--dry-run' flag to see which releases will be deleted without actually +deleting them. +` + +var removeDryRun bool + +var removeCommand = &cobra.Command{ + Use: "remove [flags] RELEASE_NAME", + Aliases: []string{"rm"}, + SuggestFor: []string{"delete", "del"}, + Short: "Given a release name, remove the release from Kubernetes", + Long: removeDesc, + RunE: rmRelease, +} + +func init() { + RootCommand.AddCommand(removeCommand) + removeCommand.Flags().BoolVar(&removeDryRun, "dry-run", false, "Simulate action, but don't actually do it.") +} + +func rmRelease(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("Command 'remove' requires a release name.") + } + + // TODO: Handle dry run use case. + if removeDryRun { + fmt.Printf("Deleting %s\n", args[0]) + return nil + } + + _, err := helm.UninstallRelease(args[0]) + if err != nil { + return err + } + + return nil +} diff --git a/cmd/helm/repo.go b/cmd/helm/repo.go new file mode 100644 index 000000000..43f32f15a --- /dev/null +++ b/cmd/helm/repo.go @@ -0,0 +1,99 @@ +package main + +import ( + "fmt" + "os" + + "github.com/deis/tiller/pkg/repo" + "github.com/gosuri/uitable" + "github.com/spf13/cobra" + "gopkg.in/yaml.v2" +) + +func init() { + repoCmd.AddCommand(repoAddCmd) + repoCmd.AddCommand(repoListCmd) + RootCommand.AddCommand(repoCmd) +} + +var repoCmd = &cobra.Command{ + Use: "repo add|remove|list [ARG]", + Short: "add, list, or remove chart repositories", +} + +var repoAddCmd = &cobra.Command{ + Use: "add [flags] [NAME] [URL]", + Short: "add a chart repository", + RunE: runRepoAdd, +} + +var repoListCmd = &cobra.Command{ + Use: "list [flags]", + Short: "list chart repositories", + RunE: runRepoList, +} + +func runRepoAdd(cmd *cobra.Command, args []string) error { + if len(args) != 2 { + return fmt.Errorf("This command needs two argument, a name for the chart repository and the url of the chart repository") + } + + err := insertRepoLine(args[0], args[1]) + if err != nil { + return err + } + fmt.Println(args[0] + " has been added to your repositories") + return nil +} + +func runRepoList(cmd *cobra.Command, args []string) error { + f, err := repo.LoadRepositoriesFile(repositoriesFile()) + if err != nil { + return err + } + if len(f.Repositories) == 0 { + fmt.Println("No repositories to show") + return nil + } + table := uitable.New() + table.MaxColWidth = 50 + table.AddRow("NAME", "URL") + for k, v := range f.Repositories { + table.AddRow(k, v) + } + fmt.Println(table) + return nil +} + +func insertRepoLine(name, url string) error { + err := checkUniqueName(name) + if err != nil { + return err + } + + b, _ := yaml.Marshal(map[string]string{name: url}) + f, err := os.OpenFile(repositoriesFile(), os.O_APPEND|os.O_WRONLY, 0666) + if err != nil { + return err + } + defer f.Close() + _, err = f.Write(b) + if err != nil { + return err + } + + return nil +} + +func checkUniqueName(name string) error { + file, err := repo.LoadRepositoriesFile(repositoriesFile()) + if err != nil { + return err + } + + _, ok := file.Repositories[name] + if ok { + return fmt.Errorf("The repository name you provided (%s) already exists. Please specifiy a different name.", name) + } + return nil +} diff --git a/cmd/helm/repo_test.go b/cmd/helm/repo_test.go new file mode 100644 index 000000000..f4c234ebf --- /dev/null +++ b/cmd/helm/repo_test.go @@ -0,0 +1,35 @@ +package main + +import ( + "testing" + + "github.com/deis/tiller/pkg/repo" +) + +func TestRepoAdd(t *testing.T) { + home := createTmpHome() + helmHome = home + if err := ensureHome(); err != nil { + t.Errorf("%s", err) + } + + testName := "test-name" + testURL := "test-url" + if err := insertRepoLine(testName, testURL); err != nil { + t.Errorf("%s", err) + } + + f, err := repo.LoadRepositoriesFile(repositoriesFile()) + if err != nil { + t.Errorf("%s", err) + } + _, ok := f.Repositories[testName] + if !ok { + t.Errorf("%s was not successfully inserted into %s", testName, repositoriesFile()) + } + + if err := insertRepoLine(testName, testURL); err == nil { + t.Errorf("Duplicate repository name was added") + } + +} diff --git a/cmd/helm/search.go b/cmd/helm/search.go new file mode 100644 index 000000000..b4175b99c --- /dev/null +++ b/cmd/helm/search.go @@ -0,0 +1,60 @@ +package main + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/deis/tiller/pkg/repo" + "github.com/spf13/cobra" +) + +func init() { + RootCommand.AddCommand(searchCmd) +} + +var searchCmd = &cobra.Command{ + Use: "search [CHART]", + Short: "Search for charts", + Long: "", //TODO: add search command description + RunE: search, +} + +func search(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("This command needs at least one argument") + } + + results, err := searchCacheForPattern(args[0]) + if err != nil { + return err + } + cmd.Println("Charts:") + for _, result := range results { + fmt.Println(result) + } + return nil +} + +func searchCacheForPattern(name string) ([]string, error) { + fileList := []string{} + filepath.Walk(cacheDirectory(), func(path string, f os.FileInfo, err error) error { + if !f.IsDir() { + fileList = append(fileList, path) + } + return nil + }) + matches := []string{} + for _, f := range fileList { + cache, _ := repo.LoadCacheFile(f) + repoName := filepath.Base(strings.TrimRight(f, "-cache.txt")) + for k := range cache.Entries { + if strings.Contains(k, name) { + matches = append(matches, repoName+"/"+k) + } + } + } + return matches, nil +} diff --git a/cmd/helm/serve.go b/cmd/helm/serve.go new file mode 100644 index 000000000..fd713dafd --- /dev/null +++ b/cmd/helm/serve.go @@ -0,0 +1,26 @@ +package main + +import ( + "github.com/deis/tiller/pkg/repo" + "github.com/spf13/cobra" +) + +var serveDesc = `This command starts a local chart repository server that serves the charts saved in your $HELM_HOME/local/ directory.` + +//TODO: add repoPath flag to be passed in in case you want +// to serve charts from a different local dir + +func init() { + RootCommand.AddCommand(serveCmd) +} + +var serveCmd = &cobra.Command{ + Use: "serve", + Short: "Start a local http web server", + Long: serveDesc, + Run: serve, +} + +func serve(cmd *cobra.Command, args []string) { + repo.StartLocalRepo(localRepoDirectory()) +} diff --git a/cmd/helm/status.go b/cmd/helm/status.go new file mode 100644 index 000000000..2a1ec08ed --- /dev/null +++ b/cmd/helm/status.go @@ -0,0 +1,44 @@ +package main + +import ( + "fmt" + "time" + + "github.com/deis/tiller/pkg/helm" + "github.com/deis/tiller/pkg/timeconv" + "github.com/spf13/cobra" +) + +var statusHelp = ` +This command shows the status of a named release. +` + +var statusCommand = &cobra.Command{ + Use: "status [flags] RELEASE_NAME", + Short: "Displays the status of the named release", + Long: statusHelp, + RunE: status, +} + +func init() { + RootCommand.AddCommand(statusCommand) +} + +func status(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errReleaseRequired + } + + res, err := helm.GetReleaseStatus(args[0]) + if err != nil { + return err + } + + fmt.Printf("Last Deployed: %s\n", timeconv.Format(res.Info.LastDeployed, time.ANSIC)) + fmt.Printf("Status: %s\n", res.Info.Status.Code) + if res.Info.Status.Details != nil { + fmt.Printf("Details: %s\n", res.Info.Status.Details) + } + + return nil +} diff --git a/cmd/helm/structure.go b/cmd/helm/structure.go new file mode 100644 index 000000000..056be291d --- /dev/null +++ b/cmd/helm/structure.go @@ -0,0 +1,31 @@ +package main + +import ( + "os" + "path/filepath" +) + +const ( + repositoriesFilePath string = "repositories.yaml" + cachePath string = "cache" + localRepoPath string = "local" + localRepoCacheFilePath string = "cache.yaml" +) + +func homePath() string { + return os.ExpandEnv(helmHome) +} + +func cacheDirectory(paths ...string) string { + fragments := append([]string{homePath(), cachePath}, paths...) + return filepath.Join(fragments...) +} + +func localRepoDirectory(paths ...string) string { + fragments := append([]string{homePath(), localRepoPath}, paths...) + return filepath.Join(fragments...) +} + +func repositoriesFile() string { + return filepath.Join(homePath(), repositoriesFilePath) +} diff --git a/cmd/tiller/environment/environment.go b/cmd/tiller/environment/environment.go new file mode 100644 index 000000000..e8949b1cd --- /dev/null +++ b/cmd/tiller/environment/environment.go @@ -0,0 +1,142 @@ +package environment + +import ( + "github.com/deis/tiller/pkg/engine" + "github.com/deis/tiller/pkg/proto/hapi/chart" + "github.com/deis/tiller/pkg/proto/hapi/release" + "github.com/deis/tiller/pkg/storage" +) + +// GoTplEngine is the name of the Go template engine, as registered in the EngineYard. +const GoTplEngine = "gotpl" + +// DefaultEngine points to the engine that the EngineYard should treat as the +// default. A chart that does not specify an engine may be run through the +// default engine. +var DefaultEngine = GoTplEngine + +// EngineYard maps engine names to engine implementations. +type EngineYard map[string]Engine + +// Get retrieves a template engine by name. +// +// If no matching template engine is found, the second return value will +// be false. +func (y EngineYard) Get(k string) (Engine, bool) { + e, ok := y[k] + return e, ok +} + +// Default returns the default template engine. +// +// The default is specified by DefaultEngine. +// +// If the default template engine cannot be found, this panics. +func (y EngineYard) Default() Engine { + d, ok := y[DefaultEngine] + if !ok { + // This is a developer error! + panic("Default template engine does not exist") + } + return d +} + +// Engine represents a template engine that can render templates. +// +// For some engines, "rendering" includes both compiling and executing. (Other +// engines do not distinguish between phases.) +// +// The engine returns a map where the key is the named output entity (usually +// a file name) and the value is the rendered content of the template. +// +// An Engine must be capable of executing multiple concurrent requests, but +// without tainting one request's environment with data from another request. +type Engine interface { + Render(*chart.Chart, *chart.Config) (map[string]string, error) +} + +// ReleaseStorage represents a storage engine for a Release. +// +// Release storage must be concurrency safe. +type ReleaseStorage interface { + + // Create stores a release in the storage. + // + // If a release with the same name exists, this returns an error. + // + // It may return other errors in cases where it cannot write to storage. + Create(*release.Release) error + // Read takes a name and returns a release that has that name. + // + // It will only return releases that are not deleted and not superseded. + // + // It will return an error if no relevant release can be found, or if storage + // is not properly functioning. + Read(name string) (*release.Release, error) + + // Update looks for a release with the same name and updates it with the + // present release contents. + // + // For immutable storage backends, this may result in a new release record + // being created, and the previous release being marked as superseded. + // + // It will return an error if a previous release is not found. It may also + // return an error if the storage backend encounters an error. + Update(*release.Release) error + + // Delete marks a Release as deleted. + // + // It returns the deleted record. If the record is not found or if the + // underlying storage encounters an error, this will return an error. + Delete(name string) (*release.Release, error) + + // List lists all active (non-deleted, non-superseded) releases. + // + // To get deleted or superseded releases, use Query. + List() ([]*release.Release, error) + + // Query takes a map of labels and returns any releases that match. + // + // Query will search all releases, including deleted and superseded ones. + // The provided map will be used to filter results. + Query(map[string]string) ([]*release.Release, error) +} + +// KubeClient represents a client capable of communicating with the Kubernetes API. +// +// A KubeClient must be concurrency safe. +type KubeClient interface { + // Install takes a map where the key is a "file name" (read: unique relational + // id) and the value is a Kubernetes manifest containing one or more resource + // definitions. + // + // TODO: Can these be in YAML or JSON, or must they be in one particular + // format? + Install(manifests map[string]string) error +} + +// Environment provides the context for executing a client request. +// +// All services in a context are concurrency safe. +type Environment struct { + // EngineYard provides access to the known template engines. + EngineYard EngineYard + // Releases stores records of releases. + Releases ReleaseStorage + // KubeClient is a Kubernetes API client. + KubeClient KubeClient +} + +// New returns an environment initialized with the defaults. +func New() *Environment { + e := engine.New() + var ey EngineYard = map[string]Engine{ + // Currently, the only template engine we support is the GoTpl one. But + // we can easily add some here. + GoTplEngine: e, + } + return &Environment{ + EngineYard: ey, + Releases: storage.NewMemory(), + } +} diff --git a/cmd/tiller/environment/environment_test.go b/cmd/tiller/environment/environment_test.go new file mode 100644 index 000000000..5cfbf22ae --- /dev/null +++ b/cmd/tiller/environment/environment_test.go @@ -0,0 +1,110 @@ +package environment + +import ( + "testing" + + "github.com/deis/tiller/pkg/proto/hapi/chart" + "github.com/deis/tiller/pkg/proto/hapi/release" +) + +type mockEngine struct { + out map[string]string +} + +func (e *mockEngine) Render(chrt *chart.Chart, v *chart.Config) (map[string]string, error) { + return e.out, nil +} + +type mockReleaseStorage struct { + rel *release.Release +} + +func (r *mockReleaseStorage) Create(v *release.Release) error { + r.rel = v + return nil +} + +func (r *mockReleaseStorage) Read(k string) (*release.Release, error) { + return r.rel, nil +} + +func (r *mockReleaseStorage) Update(v *release.Release) error { + r.rel = v + return nil +} + +func (r *mockReleaseStorage) Delete(k string) (*release.Release, error) { + return r.rel, nil +} + +func (r *mockReleaseStorage) List() ([]*release.Release, error) { + return []*release.Release{}, nil +} + +func (r *mockReleaseStorage) Query(labels map[string]string) ([]*release.Release, error) { + return []*release.Release{}, nil +} + +type mockKubeClient struct { +} + +func (k *mockKubeClient) Install(manifests map[string]string) error { + return nil +} + +var _ Engine = &mockEngine{} +var _ ReleaseStorage = &mockReleaseStorage{} +var _ KubeClient = &mockKubeClient{} + +func TestEngine(t *testing.T) { + eng := &mockEngine{out: map[string]string{"albatross": "test"}} + + env := New() + env.EngineYard = EngineYard(map[string]Engine{"test": eng}) + + if engine, ok := env.EngineYard.Get("test"); !ok { + t.Errorf("failed to get engine from EngineYard") + } else if out, err := engine.Render(&chart.Chart{}, &chart.Config{}); err != nil { + t.Errorf("unexpected template error: %s", err) + } else if out["albatross"] != "test" { + t.Errorf("expected 'test', got %q", out["albatross"]) + } +} + +func TestReleaseStorage(t *testing.T) { + rs := &mockReleaseStorage{} + env := New() + env.Releases = rs + + release := &release.Release{Name: "mariner"} + + if err := env.Releases.Create(release); err != nil { + t.Fatalf("failed to store release: %s", err) + } + + if err := env.Releases.Update(release); err != nil { + t.Fatalf("failed to update release: %s", err) + } + + if v, err := env.Releases.Read("albatross"); err != nil { + t.Errorf("Error fetching release: %s", err) + } else if v.Name != "mariner" { + t.Errorf("Expected mariner, got %q", v.Name) + } + + if _, err := env.Releases.Delete("albatross"); err != nil { + t.Fatalf("failed to delete release: %s", err) + } +} + +func TestKubeClient(t *testing.T) { + kc := &mockKubeClient{} + env := New() + env.KubeClient = kc + + manifests := map[string]string{} + + if err := env.KubeClient.Install(manifests); err != nil { + t.Errorf("Kubeclient failed: %s", err) + } +} diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go new file mode 100644 index 000000000..887968803 --- /dev/null +++ b/cmd/tiller/release_server.go @@ -0,0 +1,143 @@ +package main + +import ( + "bytes" + "errors" + "log" + + "github.com/deis/tiller/cmd/tiller/environment" + "github.com/deis/tiller/pkg/proto/hapi/release" + "github.com/deis/tiller/pkg/proto/hapi/services" + "github.com/deis/tiller/pkg/timeconv" + "github.com/technosophos/moniker" + ctx "golang.org/x/net/context" +) + +func init() { + srv := &releaseServer{ + env: env, + } + services.RegisterReleaseServiceServer(rootServer, srv) +} + +type releaseServer struct { + env *environment.Environment +} + +var ( + // errNotImplemented is a temporary error for uninmplemented callbacks. + errNotImplemented = errors.New("not implemented") + // errMissingChart indicates that a chart was not provided. + errMissingChart = errors.New("no chart provided") + // errMissingRelease indicates that a release (name) was not provided. + errMissingRelease = errors.New("no release provided") +) + +func (s *releaseServer) ListReleases(req *services.ListReleasesRequest, stream services.ReleaseService_ListReleasesServer) error { + return errNotImplemented +} + +func (s *releaseServer) GetReleaseStatus(c ctx.Context, req *services.GetReleaseStatusRequest) (*services.GetReleaseStatusResponse, error) { + if req.Name == "" { + return nil, errMissingRelease + } + rel, err := s.env.Releases.Read(req.Name) + if err != nil { + return nil, err + } + if rel.Info == nil { + return nil, errors.New("release info is missing") + } + return &services.GetReleaseStatusResponse{Info: rel.Info}, nil +} + +func (s *releaseServer) GetReleaseContent(c ctx.Context, req *services.GetReleaseContentRequest) (*services.GetReleaseContentResponse, error) { + if req.Name == "" { + return nil, errMissingRelease + } + rel, err := s.env.Releases.Read(req.Name) + return &services.GetReleaseContentResponse{Release: rel}, err +} + +func (s *releaseServer) UpdateRelease(c ctx.Context, req *services.UpdateReleaseRequest) (*services.UpdateReleaseResponse, error) { + return nil, errNotImplemented +} + +func (s *releaseServer) InstallRelease(c ctx.Context, req *services.InstallReleaseRequest) (*services.InstallReleaseResponse, error) { + if req.Chart == nil { + return nil, errMissingChart + } + + // We should probably make a name generator part of the Environment. + namer := moniker.New() + // TODO: Make sure this is unique. + name := namer.NameSep("-") + ts := timeconv.Now() + + // Render the templates + files, err := s.env.EngineYard.Default().Render(req.Chart, req.Values) + if err != nil { + return nil, err + } + + b := bytes.NewBuffer(nil) + for name, file := range files { + // Ignore empty documents because the Kubernetes library can't handle + // them. + if len(file) > 0 { + b.WriteString("\n---\n# Source: " + name + "\n") + b.WriteString(file) + } + } + + // Store a release. + r := &release.Release{ + Name: name, + Chart: req.Chart, + Config: req.Values, + Info: &release.Info{ + FirstDeployed: ts, + LastDeployed: ts, + Status: &release.Status{Code: release.Status_UNKNOWN}, + }, + Manifest: b.String(), + } + + if req.DryRun { + log.Printf("Dry run for %s", name) + return &services.InstallReleaseResponse{Release: r}, nil + } + + if err := s.env.Releases.Create(r); err != nil { + return nil, err + } + + return &services.InstallReleaseResponse{Release: r}, nil +} + +func (s *releaseServer) UninstallRelease(c ctx.Context, req *services.UninstallReleaseRequest) (*services.UninstallReleaseResponse, error) { + if req.Name == "" { + log.Printf("uninstall: Release not found: %s", req.Name) + return nil, errMissingRelease + } + + rel, err := s.env.Releases.Read(req.Name) + if err != nil { + log.Printf("uninstall: Release not loaded: %s", req.Name) + return nil, err + } + + log.Printf("uninstall: Deleting %s", req.Name) + rel.Info.Status.Code = release.Status_DELETED + rel.Info.Deleted = timeconv.Now() + + // TODO: Once KubeClient is ready, delete the resources. + log.Println("WARNING: Currently not deleting resources from k8s") + + if err := s.env.Releases.Update(rel); err != nil { + log.Printf("uninstall: Failed to store updated release: %s", err) + } + + res := services.UninstallReleaseResponse{Release: rel} + return &res, nil +} diff --git a/cmd/tiller/release_server_test.go b/cmd/tiller/release_server_test.go new file mode 100644 index 000000000..cf1369a38 --- /dev/null +++ b/cmd/tiller/release_server_test.go @@ -0,0 +1,200 @@ +package main + +import ( + "strings" + "testing" + + "github.com/deis/tiller/cmd/tiller/environment" + "github.com/deis/tiller/pkg/proto/hapi/chart" + "github.com/deis/tiller/pkg/proto/hapi/release" + "github.com/deis/tiller/pkg/proto/hapi/services" + "github.com/deis/tiller/pkg/storage" + "github.com/deis/tiller/pkg/timeconv" + "github.com/golang/protobuf/ptypes/timestamp" + "golang.org/x/net/context" +) + +func rsFixture() *releaseServer { + return &releaseServer{ + env: mockEnvironment(), + } +} + +func releaseMock() *release.Release { + date := timestamp.Timestamp{242085845, 0} + return &release.Release{ + Name: "angry-panda", + Info: &release.Info{ + FirstDeployed: &date, + LastDeployed: &date, + Status: &release.Status{Code: release.Status_DEPLOYED}, + }, + Chart: &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "foo", + Version: "0.1.0-beta.1", + }, + Templates: []*chart.Template{ + {Name: "foo.tpl", Data: []byte("Hello")}, + }, + }, + Config: &chart.Config{Raw: `name = "value"`}, + } +} + +func TestInstallRelease(t *testing.T) { + c := context.Background() + rs := rsFixture() + + req := &services.InstallReleaseRequest{ + Chart: &chart.Chart{ + Metadata: &chart.Metadata{Name: "hello"}, + Templates: []*chart.Template{ + {Name: "hello", Data: []byte("hello: world")}, + }, + }, + } + res, err := rs.InstallRelease(c, req) + if err != nil { + t.Errorf("Failed install: %s", err) + } + if res.Release.Name == "" { + t.Errorf("Expected release name.") + } + + rel, err := rs.env.Releases.Read(res.Release.Name) + if err != nil { + t.Errorf("Expected release for %s (%v).", res.Release.Name, rs.env.Releases) + } + + t.Logf("rel: %v", rel) + + if len(res.Release.Manifest) == 0 { + t.Errorf("No manifest returned: %v", res.Release) + } + + if len(rel.Manifest) == 0 { + t.Errorf("Expected manifest in %v", res) + } + + if !strings.Contains(rel.Manifest, "---\n# Source: hello\nhello: world") { + t.Errorf("unexpected output: %s", rel.Manifest) + } +} + +func TestInstallReleaseDryRun(t *testing.T) { + c := context.Background() + rs := rsFixture() + + req := &services.InstallReleaseRequest{ + Chart: &chart.Chart{ + Metadata: &chart.Metadata{Name: "hello"}, + Templates: []*chart.Template{ + {Name: "hello", Data: []byte("hello: world")}, + {Name: "goodbye", Data: []byte("goodbye: world")}, + {Name: "empty", Data: []byte("")}, + }, + }, + DryRun: true, + } + res, err := rs.InstallRelease(c, req) + if err != nil { + t.Errorf("Failed install: %s", err) + } + if res.Release.Name == "" { + t.Errorf("Expected release name.") + } + + if !strings.Contains(res.Release.Manifest, "---\n# Source: hello\nhello: world") { + t.Errorf("unexpected output: %s", res.Release.Manifest) + } + + if !strings.Contains(res.Release.Manifest, "---\n# Source: goodbye\ngoodbye: world") { + t.Errorf("unexpected output: %s", res.Release.Manifest) + } + + if strings.Contains(res.Release.Manifest, "empty") { + t.Errorf("Should not contain template data for an empty file. %s", res.Release.Manifest) + } + + if _, err := rs.env.Releases.Read(res.Release.Name); err == nil { + t.Errorf("Expected no stored release.") + } +} + +func TestUninstallRelease(t *testing.T) { + c := context.Background() + rs := rsFixture() + rs.env.Releases.Create(&release.Release{ + Name: "angry-panda", + Info: &release.Info{ + FirstDeployed: timeconv.Now(), + Status: &release.Status{ + Code: release.Status_DEPLOYED, + }, + }, + }) + + req := &services.UninstallReleaseRequest{ + Name: "angry-panda", + } + + res, err := rs.UninstallRelease(c, req) + if err != nil { + t.Errorf("Failed uninstall: %s", err) + } + + if res.Release.Name != "angry-panda" { + t.Errorf("Expected angry-panda, got %q", res.Release.Name) + } + + if res.Release.Info.Status.Code != release.Status_DELETED { + t.Errorf("Expected status code to be DELETED, got %d", res.Release.Info.Status.Code) + } + + if res.Release.Info.Deleted.Seconds <= 0 { + t.Errorf("Expected valid UNIX date, got %d", res.Release.Info.Deleted.Seconds) + } +} + +func TestGetReleaseContent(t *testing.T) { + c := context.Background() + rs := rsFixture() + rel := releaseMock() + if err := rs.env.Releases.Create(rel); err != nil { + t.Fatalf("Could not store mock release: %s", err) + } + + res, err := rs.GetReleaseContent(c, &services.GetReleaseContentRequest{Name: rel.Name}) + if err != nil { + t.Errorf("Error getting release content: %s", err) + } + + if res.Release.Chart.Metadata.Name != rel.Chart.Metadata.Name { + t.Errorf("Expected %q, got %q", rel.Chart.Metadata.Name, res.Release.Chart.Metadata.Name) + } +} + +func TestGetReleaseStatus(t *testing.T) { + c := context.Background() + rs := rsFixture() + rel := releaseMock() + if err := rs.env.Releases.Create(rel); err != nil { + t.Fatalf("Could not store mock release: %s", err) + } + + res, err := rs.GetReleaseStatus(c, &services.GetReleaseStatusRequest{Name: rel.Name}) + if err != nil { + t.Errorf("Error getting release content: %s", err) + } + + if res.Info.Status.Code != release.Status_DEPLOYED { + t.Errorf("Expected %d, got %d", release.Status_DEPLOYED, res.Info.Status.Code) + } +} + +func mockEnvironment() *environment.Environment { + e := environment.New() + e.Releases = storage.NewMemory() + return e +} diff --git a/cmd/tiller/tiller.go b/cmd/tiller/tiller.go new file mode 100644 index 000000000..d53d220ba --- /dev/null +++ b/cmd/tiller/tiller.go @@ -0,0 +1,51 @@ +package main + +import ( + "fmt" + "net" + "os" + + "github.com/deis/tiller/cmd/tiller/environment" + "github.com/spf13/cobra" + "google.golang.org/grpc" +) + +// rootServer is the root gRPC server. +// +// Each gRPC service registers itself to this server during init(). +var rootServer = grpc.NewServer() +var env = environment.New() + +const globalUsage = `The Kubernetes Helm server. + +Tiller is the server for Helm. It provides in-cluster resource management. + +By default, Tiller listens for gRPC connections on port 44134. +` + +var rootCommand = &cobra.Command{ + Use: "tiller", + Short: "The Kubernetes Helm server.", + Long: globalUsage, + Run: start, +} + +func main() { + rootCommand.Execute() +} + +func start(c *cobra.Command, args []string) { + addr := ":44134" + lstn, err := net.Listen("tcp", addr) + if err != nil { + fmt.Fprintf(os.Stderr, "Server died: %s\n", err) + os.Exit(1) + } + + fmt.Printf("Tiller is running on %s\n", addr) + + if err := rootServer.Serve(lstn); err != nil { + fmt.Fprintf(os.Stderr, "Server died: %s\n", err) + os.Exit(1) + } +} diff --git a/cmd/tiller/tiller_test.go b/cmd/tiller/tiller_test.go new file mode 100644 index 000000000..58a79744e --- /dev/null +++ b/cmd/tiller/tiller_test.go @@ -0,0 +1,33 @@ +package main + +import ( + "testing" + + "github.com/deis/tiller/cmd/tiller/environment" + "github.com/deis/tiller/pkg/engine" + "github.com/deis/tiller/pkg/storage" +) + +// These are canary tests to make sure that the default server actually +// fulfills its requirements. +var _ environment.Engine = &engine.Engine{} +var _ environment.ReleaseStorage = storage.NewMemory() + +func TestInit(t *testing.T) { + defer func() { + if recover() != nil { + t.Fatalf("Panic trapped. Check EngineYard.Default()") + } + }() + + // This will panic if it is not correct. + env.EngineYard.Default() + + e, ok := env.EngineYard.Get(environment.GoTplEngine) + if !ok { + t.Fatalf("Could not find GoTplEngine") + } + if e == nil { + t.Fatalf("Template engine GoTplEngine returned nil.") + } +} diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 000000000..3cf6c186b --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,84 @@ +# The Kubernetes Helm Architecture + +This document describes the Helm architecture at a high level. + +## The Purpose of Helm + +Helm is a tool for managing Kubernetes packages called _charts_. Helm +can do the following: + +- Create new charts from scratch +- Package charts into chart archive (tgz) files +- Interact with chart repositories where charts are stored +- Install and uninstall charts into an existing Kubernetes cluster +- Manage the releases of charts that have been installed with Helm + +For Helm, there are three important concepts: + +1. The _chart_ is a bundle of information necessary to create an + instance of a Kubernetes application. +2. The _config_ contains configuration information that can be merged + into a packaged chart to create a releasable object. +3. A _release_ is a running instance of a _chart_, combined with a + specific _config_. + +Following the formula made famous by the 12 Factor App, _chart + config += release_. + +## Components + +Helm has two major components: + +**The Helm Client** is a command-line client for end users. The client +is responsible for the following domains: + +- Local chart development +- Managing repositories +- Interacting with the Tiller server + - Sending charts to be installed + - Asking for information about releases + - Requesting upgrading or uninstalling of existing releases + +**The Tiller Server** is an in-cluster server that interacts with the +Helm client, and interfaces with the Kubernetes API server. The server +is responsible for the following: + +- Listing for incomming requests from the Helm client +- Combining a chart and configuration to build a release +- Installing charts into Kubernetes, and then tracking the subsequent + release +- Upgrading and uninstalling charts by interacting with Kubernetes + +In a nutshell, the client is responsible for managing charts, and the +server is responsible for managing releases. + +## Implementation + +The Helm client is written in the Go programming language, and uses the +gRPC protocol suite to interact with the Tiller server. + +The Tiller server is also written in Go. It provides a gRPC server to +connect with the client, and it uses the Kubernetes client library to +communicate with Kubernetes. Currently, that library uses REST+JSON. + +The Tiller server stores information in ConfigMaps located inside of +Kubernetes. It does not need its own database. + +### Structure of the Code + +The individual programs are located in `cmd/`. Shared libraries are +stored in `pkg/`. The raw ProtoBuf files are stored in `_proto/hapi` +(where `hapi` stands for the Helm Application Programming Interface). +The Go files generated from the `proto` definitions are stored in +`pkg/proto`. + +Docker images are built by cross-compiling Linux binaries and then +building a Docker image from the files in `rootfs`. + +The `scripts/` directory contains a number of utility scripts, including +`local-cluster.sh`, which can start a full Kubernetes instance inside of +a Docker container. + +Go dependencies are managed with +[Glide](https://github.com/Masterminds/glide) and stored in the +`vendor/` directory. diff --git a/docs/charts.md b/docs/charts.md new file mode 100644 index 000000000..baa779238 --- /dev/null +++ b/docs/charts.md @@ -0,0 +1,196 @@ +# Charts + +Helm uses a packaging format called _charts_. A chart is a collection of files +that collectively describe a set of Kubernetes resources. + +## The Chart File Structure + +A chart is organized as a collection of files inside of a directory. The +directory name is the name of the chart (without versioning information). Thus, +a chart describing Wordpress would be stored in the `wordpress/` directory. + +Inside of this directory, Helm will expect a structure that matches this: + +``` +wordpress/ + Chart.yaml # A YAML file containing information about the chart + LICENSE # A plain text file containing the license for the chart + README.md # A human-readable README file + values.toml # The default configuration values for this chart + charts/ # A directory containing any charts upon which this chart depends. + templates/ # A directory of templates that, when combined with values, + # will generate valid Kubernetes manifest files. +``` + +## The Chart.yaml File + +The Chart.yaml file is required for a chart. It contains the following fields: + +```yaml +name: The name of the chart (required) +version: A SemVer 2 version (required) +description: A single-sentence description of this project (optional) +keywords: + - A list of keywords about this project (optional) +home: The URL of this project's home page (optional) +sources: + - A list of URLs to source code for this project (optional) +maintainers: # (optional) + - name: The maintainer's name (required for each maintainer) + email: The maintainer's email (optional for each maintainer) +``` + +If you are familiar with the Chart.yaml file format for Helm Classic, you will +notice that fields specifying dependencies have been removed. That is because +the new Chart format expresses dependencies using the `charts/` directory. + +## Chart Dependencies + +In Helm, one chart may depend on any number of other charts. These +dependencies are expressed explicitly by copying the dependency charts +into the `charts/` directory. + +For example, if the Wordpress chart depends on the Apache chart, the +Apache chart (of the correct version) is supplied in the Wordpress +chart's `charts/` directory: + +``` +wordpress: + Chart.yaml + # ... + charts/ + apache/ + Chart.yaml + # ... + mysql/ + Chart.yaml + # ... +``` + +The example above shows how the Wordpress chart expresses its dependency +on Apache and MySQL by including those charts inside of its `charts/` +directory. + +## Templates and Values + +In Helm Charts, templates are written in the Go template language, with the +addition of 50 or so add-on template functions. + +All template files are stored in a chart's `templates/` folder. When +Helm renders the charts, it will pass every file in that directory +through the template engine. + +Values for the templates are supplied two ways: + - Chart developers may supply a file called `values.toml` inside of a + chart. This file can contain default values. + - Chart users may supply a TOML file that contains values. This can be + provided on the command line with `helm install`. + +When a user supplies custom values, these values will override the +values in the chart's `values.toml` file. + +### Template Files + +Template files follow the standard conventions for writing Go templates. +An example template file might look something like this: + +```yaml +apiVersion: v1 +kind: ReplicationController +metadata: + name: deis-database + namespace: deis + labels: + heritage: deis +spec: + replicas: 1 + selector: + app: deis-database + template: + metadata: + labels: + app: deis-database + spec: + serviceAccount: deis-database + containers: + - name: deis-database + image: {{.imageRegistry}}/postgres:{{.dockerTag}} + imagePullPolicy: {{.pullPolicy}} + ports: + - containerPort: 5432 + env: + - name: DATABASE_STORAGE + value: {{default "minio" .storage}} +``` + +The above example, based loosely on [https://github.com/deis/charts](the +chart for Deis), is a template for a Kubernetes replication controller. +It can use the following four template values: + +- `imageRegistry`: The source registry for the Docker image. +- `dockerTag`: The tag for the docker image. +- `pullPolicy`: The Kubernetes pull policy. +- `storage`: The storage backend, whose default is set to `"minio"` + +All of these values are defined by the template author. Helm does not +require or dictate parameters. + +### Values files + +Considering the template in the previous section, a `values.toml` file +that supplies the necessary values would look like this: + +```toml +imageRegistry = "quay.io/deis" +dockerTag = "latest" +pullPolicy = "alwaysPull" +storage = "s3" +``` + +When a chart includes dependency charts, values can be supplied to those +charts using TOML tables: + +```toml +imageRegistry = "quay.io/deis" +dockerTag = "latest" +pullPolicy = "alwaysPull" +storage = "s3" + +[router] +hostname = "example.com" +``` + +In the above example, the value of `hostname` will be passed to a chart +named `router` (if it exists) in the `charts/` directory. + +### References +- [Go templates](https://godoc.org/text/template) +- [Extra template functions](https://godoc.org/github.com/Masterminds/sprig) +- [The TOML format](https://github.com/toml-lang/toml) + +## Using Helm to Manage Charts + +The `helm` tool has several commands for working with charts. + +It can create a new chart for you: + +```console +$ helm create mychart +Created mychart/ +``` + +Once you have edited a chart, `helm` can package it into a chart archive +for you: + +```console +$ helm package mychart +Archived mychart-0.1.-.tgz +``` + +You can also use `helm` to help you find issues with your chart's +formatting or information: + +```console +$ helm lint mychart +No issues found +``` diff --git a/docs/developers.md b/docs/developers.md new file mode 100644 index 000000000..ddea7e9ef --- /dev/null +++ b/docs/developers.md @@ -0,0 +1,77 @@ +# Developers Guide + +This guide explains how to set up your environment for developing on +Helm and Tiller. + +## Prerequisites + +- Go 1.6.0 or later +- Glide 0.10.2 or later +- kubectl 1.2 or later +- A Kubernetes cluster (optional) +- The gRPC toolchain + +## Building Helm/Tiller + +We use Make to build our programs. The simplest way to get started is: + +```console +$ make boostrap build +``` + +This will build both Helm and Tiller. + +To run all of the tests (without running the tests for `vendor/`), run +`make test`. + +To run Helm and Tiller locally, you can run `bin/helm` or `bin/tiller`. + +- Helm and Tiller are known to run on Mac OSX and most Linuxes, including + Alpine. +- Tiller must have access to a Kubernets cluster. It learns about the + cluster by examining the Kube config files that `kubectl` uese. + +## gRPC and Protobuf + +Tiller uses gRPC. To get started with gRPC, you will need to... + +- Install `protoc` for compiling protobuf files. Releases are + [here](https://github.com/google/protobuf/releases) +- Install the protoc Go plugin: `go get -u github.com/golang/protobuf/protoc-gen-go` + +Note that you need to be on protobuf 3.x (`protoc --version`) and use the latest Go plugin. + +### The Helm API (HAPI) + +We use gRPC as an API layer. See `pkg/proto/hapi` for the generated Go code, +and `_proto` for the protocol buffer definitions. + +To regenerate the Go files from the protobuf source, `cd _proto && +make`. + +## Docker Images + +To build Docker images, use `make docker-build` + +## Running a Local Cluster + +You can run tests locally using the `scripts/local-cluster.sh` script to +start Kubernetes inside of a Docker container. For OS X, you will need +to be running `docker-machine`. + +## Contribution Guidelines + +We welcome contributions. This project has set up some guidelines in +order to ensure that (a) code quality remains high, (b) the project +remains consistent, and (c) contributions follow the open source legal +requirements. Our intent is not to burden contributors, but to build +elegant and high-quality open source code so that our users will benefit. + +We follow the coding standards and guidelines outlined by the Deis +project: + +https://github.com/deis/workflow/blob/master/CONTRIBUTING.md +https://github.com/deis/workflow/blob/master/src/contributing/submitting-a-pull-request.md + +Adidtionally, contributors must have a CLA with CNCF/Google before we can +accept contributions. diff --git a/docs/examples/README.md b/docs/examples/README.md new file mode 100644 index 000000000..aa42ee075 --- /dev/null +++ b/docs/examples/README.md @@ -0,0 +1,4 @@ +# Helm Examples + +This directory contains example charts to help you get started with +chart development. diff --git a/docs/examples/alpine/Chart.yaml b/docs/examples/alpine/Chart.yaml new file mode 100644 index 000000000..6e9f807e0 --- /dev/null +++ b/docs/examples/alpine/Chart.yaml @@ -0,0 +1,4 @@ +name: alpine +description: Deploy a basic Alpine Linux pod +version: 0.1.0 +home: "https://github.com/deis/tiller" diff --git a/docs/examples/alpine/README.md b/docs/examples/alpine/README.md new file mode 100644 index 000000000..a7c84fc41 --- /dev/null +++ b/docs/examples/alpine/README.md @@ -0,0 +1,9 @@ +This example was generated using the command `helm create alpine`. + +The `templates/` directory contains a very simple pod resource with a +couple of parameters. + +The `values.toml` file contains the default values for the +`alpine-pod.yaml` template. + +You can install this example using `helm install docs/examples/alpine`. diff --git a/docs/examples/alpine/templates/alpine-pod.yaml b/docs/examples/alpine/templates/alpine-pod.yaml new file mode 100644 index 000000000..000f106e3 --- /dev/null +++ b/docs/examples/alpine/templates/alpine-pod.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{default "alpine" .name}} + labels: + heritage: helm +spec: + restartPolicy: {{default "Never" .restart_policy}} + containers: + - name: waiter + image: "alpine:3.3" + command: ["/bin/sleep","9000"] diff --git a/docs/examples/alpine/values.toml b/docs/examples/alpine/values.toml new file mode 100644 index 000000000..504e6e1be --- /dev/null +++ b/docs/examples/alpine/values.toml @@ -0,0 +1,2 @@ +# The pod name +name = "my-alpine" diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 000000000..04b431f5b --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,75 @@ +# Quickstart Guide + +This guide covers how you can quickly get started using Helm. + +## Prerequisites + +- You must have Kubernetes installed, and have a local configured copy + of `kubectl`. + +## Install Helm + +Download a binary release of the Helm client from the official project +page. + +Alternately, you can clone the GitHub project and build your own +client from source. The quickest route to installing from source is to +run `make boostrap build`, and then use `bin/helm`. + +## Initialize Helm and Install Tiller + +Once you have Helm ready, you can initialize the local CLI and also +install Tiller into your Kubernetes cluster in one step: + +```console +$ helm init +``` + +## Install an Existing Chart + +To install an existing chart, you can run the `helm install` command: + +_TODO:_ Update this to the correct URL. + +```console +$ helm install https://helm.sh/charts/nginx-0.1.0.tgz +Released smiling-penguin +``` + +In the example above, the `nginx` chart was released, and the name of +our new release is `smiling-penguin` + +## Learn About The Release + +To find out about our release, run `helm status`: + +```console +$ helm status smiling-penguin +Status: DEPLOYED +``` + +## Uninstall a Release + +To remove a release, use the `helm remove` command: + +```console +$ helm remove smiling-penguin +Removed smiling-penguin +``` + +This will uninstall `smiling-penguin` from Kubernetes, but you will +still be able to request information about that release: + +```console +$ helm status smiling-penguin +Status: DELETED +``` + +## Reading the Help Text + +To learn more about the available Helm commands, use `helm help` or type +a command followed by the `-h` flag: + +```console +$ helm get -h +``` diff --git a/glide.lock b/glide.lock new file mode 100644 index 000000000..52380e0f9 --- /dev/null +++ b/glide.lock @@ -0,0 +1,355 @@ +hash: 998d87445fec0bd715fa5ccbcc227cb4997e56ceff58dc8eb53ea2e0cc84abfd +updated: 2016-04-27T16:11:47.531200165-06:00 +imports: +- name: bitbucket.org/ww/goautoneg + version: 75cd24fc2f2c +- name: github.com/aokoli/goutils + version: 9c37978a95bd5c709a15883b6242714ea6709e64 +- name: github.com/beorn7/perks + version: b965b613227fddccbfffe13eae360ed3fa822f8d + subpackages: + - quantile +- name: github.com/blang/semver + version: 31b736133b98f26d5e078ec9eb591666edfd091f +- name: github.com/BurntSushi/toml + version: bbd5bb678321a0d6e58f1099321dfa73391c1b6f +- name: github.com/davecgh/go-spew + version: 5215b55f46b2b919f50a1df0eaa5886afe4e3b3d + subpackages: + - spew +- name: github.com/docker/distribution + version: 55f1b7651f6242617133312ff8af5c2e4e3628ea + subpackages: + - digest + - reference +- name: github.com/docker/docker + version: 0f5c9d301b9b1cca66b3ea0f9dec3b5317d3686d + subpackages: + - pkg/jsonmessage + - pkg/mount + - pkg/stdcopy + - pkg/symlink + - pkg/term + - pkg/timeutils + - pkg/units +- name: github.com/docker/engine-api + version: 26cdffeca716ae4df98070051a852b3198d7d153 + subpackages: + - client + - types + - types/container + - types/filters + - types/network + - types/registry + - types/blkiodev + - types/strslice +- name: github.com/docker/go-connections + version: f549a9393d05688dff0992ef3efd8bbe6c628aeb + subpackages: + - nat + - sockets + - tlsconfig +- name: github.com/docker/go-units + version: 0bbddae09c5a5419a8c6dcdd7ff90da3d450393b +- name: github.com/emicklei/go-restful + version: 496d495156da218b9912f03dfa7df7f80fbd8cc3 + subpackages: + - swagger + - log +- name: github.com/evanphx/json-patch + version: 7dd4489c2eb6073e5a9d7746c3274c5b5f0387df +- name: github.com/ghodss/yaml + version: 73d445a93680fa1a78ae23a5839bad48f32ba1ee +- name: github.com/gogo/protobuf + version: 82d16f734d6d871204a3feb1a73cb220cc92574c + subpackages: + - gogoproto + - plugin/defaultcheck + - plugin/description + - plugin/embedcheck + - plugin/enumstringer + - plugin/equal + - plugin/face + - plugin/gostring + - plugin/grpc + - plugin/marshalto + - plugin/oneofcheck + - plugin/populate + - plugin/size + - plugin/stringer + - plugin/testgen + - plugin/union + - plugin/unmarshal + - proto + - protoc-gen-gogo/descriptor + - protoc-gen-gogo/generator + - protoc-gen-gogo/plugin + - sortkeys + - vanity +- name: github.com/golang/glog + version: 44145f04b68cf362d9c4df2182967c2275eaefed +- name: github.com/golang/groupcache + version: 604ed5785183e59ae2789449d89e73f3a2a77987 + subpackages: + - lru +- name: github.com/golang/protobuf + version: f0a097ddac24fb00e07d2ac17f8671423f3ea47c + subpackages: + - proto + - ptypes/any + - ptypes/timestamp +- name: github.com/google/cadvisor + version: 546a3771589bdb356777c646c6eca24914fdd48b + subpackages: + - api + - cache/memory + - collector + - container + - events + - fs + - healthz + - http + - info/v1 + - info/v2 + - manager + - metrics + - pages + - storage + - summary + - utils + - validate + - version +- name: github.com/google/gofuzz + version: bbcb9da2d746f8bdbd6a936686a0a6067ada0ec5 +- name: github.com/gosuri/uitable + version: 36ee7e946282a3fb1cfecd476ddc9b35d8847e42 + subpackages: + - util/strutil + - util/wordwrap +- name: github.com/imdario/mergo + version: 6633656539c1639d9d78127b7d47c622b5d7b6dc +- name: github.com/inconshreveable/mousetrap + version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 +- name: github.com/juju/ratelimit + version: 77ed1c8a01217656d2080ad51981f6e99adaa177 +- name: github.com/Masterminds/semver + version: 808ed7761c233af2de3f9729a041d68c62527f3a +- name: github.com/Masterminds/sprig + version: e6494bc7e81206ba6db404d2fd96500ffc453407 +- name: github.com/mattn/go-runewidth + version: d6bea18f789704b5f83375793155289da36a3c7f +- name: github.com/matttproud/golang_protobuf_extensions + version: fc2b8d3a73c4867e51861bbdd5ae3c1f0869dd6a + subpackages: + - pbutil +- name: github.com/opencontainers/runc + version: 7ca2aa4873aea7cb4265b1726acb24b90d8726c6 + subpackages: + - libcontainer + - libcontainer/cgroups/fs + - libcontainer/configs + - libcontainer/cgroups + - libcontainer/system +- name: github.com/pborman/uuid + version: ca53cad383cad2479bbba7f7a1a05797ec1386e4 +- name: github.com/prometheus/client_golang + version: 3b78d7a77f51ccbc364d4bc170920153022cfd08 + subpackages: + - prometheus +- name: github.com/prometheus/client_model + version: fa8ad6fec33561be4280a8f0514318c79d7f6cb6 + subpackages: + - go +- name: github.com/prometheus/common + version: ef7a9a5fb138aa5d3a19988537606226869a0390 + subpackages: + - expfmt + - model +- name: github.com/prometheus/procfs + version: 490cc6eb5fa45bf8a8b7b73c8bc82a8160e8531d +- name: github.com/spf13/cobra + version: e14e47b7a916ed178f4559ebd7e625cf16410181 + subpackages: + - cobra +- name: github.com/spf13/pflag + version: cb88ea77998c3f024757528e3305022ab50b43be +- name: github.com/technosophos/moniker + version: 9f956786b91d9786ca11aa5be6104542fa911546 +- name: github.com/ugorji/go + version: f4485b318aadd133842532f841dc205a8e339d74 + subpackages: + - codec +- name: golang.org/x/net + version: fb93926129b8ec0056f2f458b1f519654814edf0 + subpackages: + - context + - http2 + - trace + - http2/hpack + - internal/timeseries + - context/ctxhttp +- name: golang.org/x/oauth2 + version: b5adcc2dcdf009d0391547edc6ecbaff889f5bb9 + subpackages: + - google + - internal + - jwt + - jws +- name: google.golang.org/appengine + version: 12d5545dc1cfa6047a286d5e853841b6471f4c19 + subpackages: + - internal + - internal/app_identity + - internal/base + - internal/datastore + - internal/log + - internal/modules + - internal/remote_api + - urlfetch + - internal/urlfetch +- name: google.golang.org/cloud + version: eb47ba841d53d93506cfbfbc03927daf9cc48f88 + subpackages: + - compute/metadata + - internal +- name: google.golang.org/grpc + version: dec33edc378cf4971a2741cfd86ed70a644d6ba3 + subpackages: + - codes + - credentials + - grpclog + - internal + - metadata + - naming + - transport + - peer +- name: gopkg.in/yaml.v2 + version: a83829b6f1293c91addabc89d0571c246397bbf4 +- name: k8s.io/heapster + version: 0991ac528ea24aae194e45d6dcf01896cb42cbea + subpackages: + - api/v1/types +- name: k8s.io/kubernetes + version: 95f2ca2ff65a03342746a2a49b8f360428dd94a2 + subpackages: + - pkg/client/unversioned/clientcmd + - pkg/kubectl/cmd/util + - pkg/kubectl/resource + - pkg/api + - pkg/api/unversioned + - pkg/client/restclient + - pkg/client/unversioned/auth + - pkg/client/unversioned/clientcmd/api + - pkg/client/unversioned/clientcmd/api/latest + - pkg/runtime + - pkg/util/errors + - pkg/util/homedir + - pkg/util/validation + - pkg/api/errors + - pkg/api/meta + - pkg/api/validation + - pkg/apimachinery + - pkg/apimachinery/registered + - pkg/apis/apps + - pkg/apis/autoscaling + - pkg/apis/batch + - pkg/apis/extensions + - pkg/apis/metrics + - pkg/client/typed/discovery + - pkg/client/unversioned + - pkg/client/unversioned/adapters/internalclientset + - pkg/kubectl + - pkg/labels + - pkg/registry/thirdpartyresourcedata + - pkg/runtime/serializer/json + - pkg/util/flag + - pkg/util/strategicpatch + - pkg/util/sets + - pkg/util/yaml + - pkg/watch + - pkg/api/resource + - pkg/auth/user + - pkg/conversion + - pkg/fields + - pkg/runtime/serializer + - pkg/types + - pkg/util + - pkg/util/intstr + - pkg/util/rand + - pkg/api/v1 + - pkg/client/metrics + - pkg/client/transport + - pkg/util/crypto + - pkg/util/flowcontrol + - pkg/util/net + - pkg/version + - pkg/watch/json + - pkg/client/unversioned/clientcmd/api/v1 + - pkg/runtime/serializer/versioning + - pkg/conversion/queryparams + - pkg/util/json + - pkg/util/validation/field + - pkg/api/endpoints + - pkg/api/pod + - pkg/api/service + - pkg/api/util + - pkg/capabilities + - pkg/api/install + - pkg/apis/apps/install + - pkg/apis/authorization/install + - pkg/apis/autoscaling/install + - pkg/apis/batch/install + - pkg/apis/componentconfig/install + - pkg/apis/extensions/install + - pkg/apis/metrics/install + - pkg/util/wait + - plugin/pkg/client/auth + - pkg/client/clientset_generated/internalclientset + - pkg/client/clientset_generated/internalclientset/typed/core/unversioned + - pkg/client/clientset_generated/internalclientset/typed/extensions/unversioned + - pkg/apis/batch/v1 + - pkg/credentialprovider + - pkg/fieldpath + - pkg/kubelet/qos/util + - pkg/util/deployment + - pkg/util/integer + - pkg/util/jsonpath + - pkg/api/rest + - pkg/apis/extensions/v1beta1 + - pkg/apis/extensions/validation + - pkg/registry/generic + - pkg/util/framer + - third_party/forked/json + - pkg/util/runtime + - third_party/forked/reflect + - pkg/runtime/serializer/protobuf + - pkg/runtime/serializer/recognizer + - pkg/util/parsers + - pkg/watch/versioned + - pkg/util/hash + - pkg/util/net/sets + - pkg/apis/apps/v1alpha1 + - pkg/apis/authorization + - pkg/apis/authorization/v1beta1 + - pkg/apis/autoscaling/v1 + - pkg/apis/componentconfig + - pkg/apis/componentconfig/v1alpha1 + - pkg/apis/metrics/v1alpha1 + - plugin/pkg/client/auth/gcp + - pkg/client/clientset_generated/internalclientset/typed/batch/unversioned + - pkg/controller + - pkg/util/labels + - pkg/util/pod + - third_party/golang/template + - pkg/api/unversioned/validation + - pkg/controller/podautoscaler + - pkg/storage + - pkg/kubelet/qos + - pkg/master/ports + - pkg/client/cache + - pkg/client/record + - pkg/controller/framework + - pkg/controller/podautoscaler/metrics +- name: speter.net/go/exp/math/dec/inf + version: 42ca6cd68aa922bc3f32f1e056e61b65945d9ad7 +devImports: [] diff --git a/glide.yaml b/glide.yaml new file mode 100644 index 000000000..5cc7b0744 --- /dev/null +++ b/glide.yaml @@ -0,0 +1,32 @@ +package: github.com/deis/tiller +import: +- package: golang.org/x/net + version: fb93926129b8ec0056f2f458b1f519654814edf0 + subpackages: + - context +- package: github.com/spf13/cobra + subpackages: + - cobra +- package: github.com/Masterminds/sprig + version: ^2.1 +- package: gopkg.in/yaml.v2 +- package: github.com/Masterminds/semver + version: 1.1.0 +- package: github.com/BurntSushi/toml + version: bbd5bb678321a0d6e58f1099321dfa73391c1b6f +- package: github.com/technosophos/moniker +- package: github.com/golang/protobuf + version: f0a097ddac24fb00e07d2ac17f8671423f3ea47c + subpackages: + - proto + - ptypes/any + - ptypes/timestamp +- package: google.golang.org/grpc + version: dec33edc378cf4971a2741cfd86ed70a644d6ba3 +- package: k8s.io/kubernetes + version: ^1.2 + subpackages: + - pkg/client/unversioned/clientcmd + - pkg/kubectl/cmd/util + - pkg/kubectl/resource +- package: github.com/gosuri/uitable diff --git a/pkg/chart/chart.go b/pkg/chart/chart.go new file mode 100644 index 000000000..73b948e26 --- /dev/null +++ b/pkg/chart/chart.go @@ -0,0 +1,438 @@ +/* +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 chart + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" +) + +// ChartfileName is the default Chart file name. +const ChartfileName string = "Chart.yaml" + +const ( + preTemplates string = "templates/" + preValues string = "values.toml" + preCharts string = "charts/" +) + +const defaultValues = `# Default values for %s. +# This is a TOML-formatted file. https://github.com/toml-lang/toml +# Declare name/value pairs to be passed into your templates. +# name = "value" +` + +var headerBytes = []byte("+aHR0cHM6Ly95b3V0dS5iZS96OVV6MWljandyTQo=") + +// Chart represents a complete chart. +// +// A chart consists of the following parts: +// +// - Chart.yaml: In code, we refer to this as the Chartfile +// - templates/*: The template directory +// - README.md: Optional README file +// - LICENSE: Optional license file +// - hooks/: Optional hooks registry +// - docs/: Optional docs directory +// +// Packed charts are stored in gzipped tar archives (.tgz). Unpackaged charts +// are directories where the directory name is the Chartfile.Name. +// +// Optionally, a chart might also locate a provenance (.prov) file that it +// can use for cryptographic signing. +type Chart struct { + loader chartLoader +} + +// Close the chart. +// +// Charts should always be closed when no longer needed. +func (c *Chart) Close() error { + return c.loader.close() +} + +// Chartfile gets the Chartfile (Chart.yaml) for this chart. +func (c *Chart) Chartfile() *Chartfile { + return c.loader.chartfile() +} + +// Dir returns the directory where the charts are located. +func (c *Chart) Dir() string { + return c.loader.dir() +} + +// TemplatesDir returns the directory where the templates are stored. +func (c *Chart) TemplatesDir() string { + return filepath.Join(c.loader.dir(), preTemplates) +} + +// ChartsDir returns teh directory where dependency charts are stored. +func (c *Chart) ChartsDir() string { + return filepath.Join(c.loader.dir(), preCharts) +} + +// LoadValues loads the contents of values.toml into a map +func (c *Chart) LoadValues() (Values, error) { + return ReadValuesFile(filepath.Join(c.loader.dir(), preValues)) +} + +// chartLoader provides load, close, and save implementations for a chart. +type chartLoader interface { + // Chartfile resturns a *Chartfile for this chart. + chartfile() *Chartfile + // Dir returns a directory where the chart can be accessed. + dir() string + + // Close cleans up a chart. + close() error +} + +type dirChart struct { + chartyaml *Chartfile + chartdir string +} + +func (d *dirChart) chartfile() *Chartfile { + return d.chartyaml +} + +func (d *dirChart) dir() string { + return d.chartdir +} + +func (d *dirChart) close() error { + return nil +} + +type tarChart struct { + chartyaml *Chartfile + tmpDir string +} + +func (t *tarChart) chartfile() *Chartfile { + return t.chartyaml +} + +func (t *tarChart) dir() string { + return t.tmpDir +} + +func (t *tarChart) close() error { + // Remove the temp directory. + return os.RemoveAll(t.tmpDir) +} + +// Create creates a new chart in a directory. +// +// Inside of dir, this will create a directory based on the name of +// chartfile.Name. It will then write the Chart.yaml into this directory and +// create the (empty) appropriate directories. +// +// The returned *Chart will point to the newly created directory. +// +// If dir does not exist, this will return an error. +// If Chart.yaml or any directories cannot be created, this will return an +// error. In such a case, this will attempt to clean up by removing the +// new chart directory. +func Create(chartfile *Chartfile, dir string) (*Chart, error) { + path, err := filepath.Abs(dir) + if err != nil { + return nil, err + } + + if fi, err := os.Stat(path); err != nil { + return nil, err + } else if !fi.IsDir() { + return nil, fmt.Errorf("no such directory %s", path) + } + + n := fname(chartfile.Name) + cdir := filepath.Join(path, n) + if fi, err := os.Stat(cdir); err == nil && !fi.IsDir() { + return nil, fmt.Errorf("file %s already exists and is not a directory", cdir) + } + if err := os.MkdirAll(cdir, 0755); err != nil { + return nil, err + } + + if err := chartfile.Save(filepath.Join(cdir, ChartfileName)); err != nil { + return nil, err + } + + val := []byte(fmt.Sprintf(defaultValues, chartfile.Name)) + if err := ioutil.WriteFile(filepath.Join(cdir, preValues), val, 0644); err != nil { + return nil, err + } + + for _, d := range []string{preTemplates, preCharts} { + if err := os.MkdirAll(filepath.Join(cdir, d), 0755); err != nil { + return nil, err + } + } + + return &Chart{ + loader: &dirChart{chartyaml: chartfile, chartdir: cdir}, + }, nil +} + +// fname prepares names for the filesystem +func fname(name string) string { + // Right now, we don't do anything. Do we need to encode any particular + // characters? What characters are legal in a chart name, but not in file + // names on Windows, Linux, or OSX. + return name +} + +// LoadDir loads an entire chart from a directory. +// +// This includes the Chart.yaml (*Chartfile) and all of the manifests. +// +// If you are just reading the Chart.yaml file, it is substantially more +// performant to use LoadChartfile. +func LoadDir(chart string) (*Chart, error) { + dir, err := filepath.Abs(chart) + if err != nil { + return nil, fmt.Errorf("%s is not a valid path", chart) + } + + if fi, err := os.Stat(dir); err != nil { + return nil, err + } else if !fi.IsDir() { + return nil, fmt.Errorf("%s is not a directory", chart) + } + + cf, err := LoadChartfile(filepath.Join(dir, "Chart.yaml")) + if err != nil { + return nil, err + } + + cl := &dirChart{ + chartyaml: cf, + chartdir: dir, + } + + return &Chart{ + loader: cl, + }, nil +} + +// LoadData loads a chart from data, where data is a []byte containing a gzipped tar file. +func LoadData(data []byte) (*Chart, error) { + return LoadDataFromReader(bytes.NewBuffer(data)) +} + +// Load loads a chart from a chart archive. +// +// A chart archive is a gzipped tar archive that follows the Chart format +// specification. +func Load(archive string) (*Chart, error) { + if fi, err := os.Stat(archive); err != nil { + return nil, err + } else if fi.IsDir() { + return nil, errors.New("cannot load a directory with chart.Load()") + } + + raw, err := os.Open(archive) + if err != nil { + return nil, err + } + defer raw.Close() + + return LoadDataFromReader(raw) +} + +// LoadDataFromReader loads a chart from a reader +func LoadDataFromReader(r io.Reader) (*Chart, error) { + unzipped, err := gzip.NewReader(r) + if err != nil { + return nil, err + } + defer unzipped.Close() + + untarred := tar.NewReader(unzipped) + c, err := loadTar(untarred) + if err != nil { + return nil, err + } + + cf, err := LoadChartfile(filepath.Join(c.tmpDir, ChartfileName)) + if err != nil { + return nil, err + } + c.chartyaml = cf + return &Chart{loader: c}, nil +} + +func loadTar(r *tar.Reader) (*tarChart, error) { + td, err := ioutil.TempDir("", "chart-") + if err != nil { + return nil, err + } + + // ioutil.TempDir uses Getenv("TMPDIR"), so there are no guarantees + dir, err := filepath.Abs(td) + if err != nil { + return nil, fmt.Errorf("%s is not a valid path", td) + } + + c := &tarChart{ + chartyaml: &Chartfile{}, + tmpDir: dir, + } + + firstDir := "" + + hdr, err := r.Next() + for err == nil { + // This is to prevent malformed tar attacks. + hdr.Name = filepath.Clean(hdr.Name) + + if firstDir == "" { + fi := hdr.FileInfo() + if fi.IsDir() { + firstDir = hdr.Name + } + } else if strings.HasPrefix(hdr.Name, firstDir) { + // We know this has the prefix, so we know there won't be an error. + rel, _ := filepath.Rel(firstDir, hdr.Name) + + // If tar record is a directory, create one in the tmpdir and return. + if hdr.FileInfo().IsDir() { + os.MkdirAll(filepath.Join(c.tmpDir, rel), 0755) + hdr, err = r.Next() + continue + } + + //dest := filepath.Join(c.tmpDir, rel) + f, err := os.Create(filepath.Join(c.tmpDir, rel)) + if err != nil { + hdr, err = r.Next() + continue + } + if _, err := io.Copy(f, r); err != nil { + } + f.Close() + } + hdr, err = r.Next() + } + + if err != nil && err != io.EOF { + c.close() + return c, err + } + return c, nil +} + +// Member is a file in a chart. +type Member struct { + Path string `json:"path"` // Path from the root of the chart. + Content []byte `json:"content"` // Base64 encoded content. +} + +// LoadTemplates loads the members of TemplatesDir(). +func (c *Chart) LoadTemplates() ([]*Member, error) { + dir := c.TemplatesDir() + return c.loadDirectory(dir) +} + +// loadDirectory loads the members of a directory. +func (c *Chart) loadDirectory(dir string) ([]*Member, error) { + files, err := ioutil.ReadDir(dir) + if err != nil { + return nil, err + } + + members := []*Member{} + for _, file := range files { + filename := filepath.Join(dir, file.Name()) + if !file.IsDir() { + addition, err := c.loadMember(filename) + if err != nil { + return nil, err + } + + members = append(members, addition) + } else { + additions, err := c.loadDirectory(filename) + if err != nil { + return nil, err + } + + members = append(members, additions...) + } + } + + return members, nil +} + +// LoadMember loads a chart member from a given path where path is the root of the chart. +func (c *Chart) LoadMember(path string) (*Member, error) { + filename := filepath.Join(c.loader.dir(), path) + return c.loadMember(filename) +} + +// loadMember loads and base 64 encodes a file. +func (c *Chart) loadMember(filename string) (*Member, error) { + dir := c.Dir() + if !strings.HasPrefix(filename, dir) { + err := fmt.Errorf("File %s is outside chart directory %s", filename, dir) + return nil, err + } + + content, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + + path := strings.TrimPrefix(filename, dir) + path = strings.TrimLeft(path, "/") + result := &Member{ + Path: path, + Content: content, + } + + return result, nil +} + +// Content is abstraction for the contents of a chart. +type Content struct { + Chartfile *Chartfile `json:"chartfile"` + Members []*Member `json:"members"` +} + +// LoadContent loads contents of a chart directory into Content +func (c *Chart) LoadContent() (*Content, error) { + ms, err := c.loadDirectory(c.Dir()) + if err != nil { + return nil, err + } + + cc := &Content{ + Chartfile: c.Chartfile(), + Members: ms, + } + + return cc, nil +} diff --git a/pkg/chart/chart_test.go b/pkg/chart/chart_test.go new file mode 100644 index 000000000..e14ffe396 --- /dev/null +++ b/pkg/chart/chart_test.go @@ -0,0 +1,272 @@ +/* +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 chart + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "reflect" + "testing" +) + +const ( + testfile = "testdata/frobnitz/Chart.yaml" + testdir = "testdata/frobnitz/" + testarchive = "testdata/frobnitz-0.0.1.tgz" + testmember = "templates/template.tpl" +) + +// Type canaries. If these fail, they will fail at compile time. +var _ chartLoader = &dirChart{} +var _ chartLoader = &tarChart{} + +func TestLoadDir(t *testing.T) { + + c, err := LoadDir(testdir) + if err != nil { + t.Errorf("Failed to load chart: %s", err) + } + + if c.Chartfile().Name != "frobnitz" { + t.Errorf("Expected chart name to be 'frobnitz'. Got '%s'.", c.Chartfile().Name) + } +} + +func TestCreate(t *testing.T) { + tdir, err := ioutil.TempDir("", "helm-") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tdir) + + cf := &Chartfile{Name: "foo"} + + c, err := Create(cf, tdir) + if err != nil { + t.Fatal(err) + } + + dir := filepath.Join(tdir, "foo") + + if c.Chartfile().Name != "foo" { + t.Errorf("Expected name to be 'foo', got %q", c.Chartfile().Name) + } + + for _, d := range []string{preTemplates, preCharts} { + if fi, err := os.Stat(filepath.Join(dir, d)); err != nil { + t.Errorf("Expected %s dir: %s", d, err) + } else if !fi.IsDir() { + t.Errorf("Expected %s to be a directory.", d) + } + } + + for _, f := range []string{ChartfileName, preValues} { + if fi, err := os.Stat(filepath.Join(dir, f)); err != nil { + t.Errorf("Expected %s file: %s", f, err) + } else if fi.IsDir() { + t.Errorf("Expected %s to be a fle.", f) + } + } + +} + +func TestLoad(t *testing.T) { + c, err := Load(testarchive) + if err != nil { + t.Errorf("Failed to load chart: %s", err) + return + } + defer c.Close() + + if c.Chartfile() == nil { + t.Error("No chartfile was loaded.") + return + } + + if c.Chartfile().Name != "frobnitz" { + t.Errorf("Expected name to be frobnitz, got %q", c.Chartfile().Name) + } +} + +func TestLoadData(t *testing.T) { + data, err := ioutil.ReadFile(testarchive) + if err != nil { + t.Errorf("Failed to read testarchive file: %s", err) + return + } + c, err := LoadData(data) + if err != nil { + t.Errorf("Failed to load chart: %s", err) + return + } + if c.Chartfile() == nil { + t.Error("No chartfile was loaded.") + return + } + + if c.Chartfile().Name != "frobnitz" { + t.Errorf("Expected name to be frobnitz, got %q", c.Chartfile().Name) + } +} + +func TestChart(t *testing.T) { + c, err := LoadDir(testdir) + if err != nil { + t.Errorf("Failed to load chart: %s", err) + } + defer c.Close() + + if c.Dir() != c.loader.dir() { + t.Errorf("Unexpected location for directory: %s", c.Dir()) + } + + if c.Chartfile().Name != c.loader.chartfile().Name { + t.Errorf("Unexpected chart file name: %s", c.Chartfile().Name) + } + + dir := c.Dir() + d := c.ChartsDir() + if d != filepath.Join(dir, preCharts) { + t.Errorf("Unexpectedly, charts are in %s", d) + } + + d = c.TemplatesDir() + if d != filepath.Join(dir, preTemplates) { + t.Errorf("Unexpectedly, templates are in %s", d) + } +} + +func TestLoadTemplates(t *testing.T) { + c, err := LoadDir(testdir) + if err != nil { + t.Errorf("Failed to load chart: %s", err) + } + + members, err := c.LoadTemplates() + if members == nil { + t.Fatalf("Cannot load templates: unknown error") + } + + if err != nil { + t.Fatalf("Cannot load templates: %s", err) + } + + dir := c.TemplatesDir() + files, err := ioutil.ReadDir(dir) + if err != nil { + t.Fatalf("Cannot read template directory: %s", err) + } + + if len(members) != len(files) { + t.Fatalf("Expected %d templates, got %d", len(files), len(members)) + } + + root := c.loader.dir() + for _, file := range files { + path := filepath.Join(preTemplates, file.Name()) + if err := findMember(root, path, members); err != nil { + t.Fatal(err) + } + } +} + +func findMember(root, path string, members []*Member) error { + for _, member := range members { + if member.Path == path { + filename := filepath.Join(root, path) + if err := compareContent(filename, member.Content); err != nil { + return err + } + + return nil + } + } + + return fmt.Errorf("Template not found: %s", path) +} + +func TestLoadMember(t *testing.T) { + c, err := LoadDir(testdir) + if err != nil { + t.Errorf("Failed to load chart: %s", err) + } + + member, err := c.LoadMember(testmember) + if member == nil { + t.Fatalf("Cannot load member %s: unknown error", testmember) + } + + if err != nil { + t.Fatalf("Cannot load member %s: %s", testmember, err) + } + + if member.Path != testmember { + t.Errorf("Expected member path %s, got %s", testmember, member.Path) + } + + filename := filepath.Join(c.loader.dir(), testmember) + if err := compareContent(filename, member.Content); err != nil { + t.Fatal(err) + } +} + +func TestLoadContent(t *testing.T) { + c, err := LoadDir(testdir) + if err != nil { + t.Errorf("Failed to load chart: %s", err) + } + + content, err := c.LoadContent() + if err != nil { + t.Errorf("Failed to load chart content: %s", err) + } + + want := c.Chartfile() + have := content.Chartfile + if !reflect.DeepEqual(want, have) { + t.Errorf("Unexpected chart file\nwant:\n%v\nhave:\n%v\n", want, have) + } + + for _, member := range content.Members { + have := member.Content + wantMember, err := c.LoadMember(member.Path) + if err != nil { + t.Errorf("Failed to load chart member: %s", err) + } + + t.Logf("%s:\n%s\n\n", member.Path, member.Content) + want := wantMember.Content + if !reflect.DeepEqual(want, have) { + t.Errorf("Unexpected chart member %s\nwant:\n%v\nhave:\n%v\n", member.Path, want, have) + } + } +} + +func compareContent(filename string, content []byte) error { + compare, err := ioutil.ReadFile(filename) + if err != nil { + return fmt.Errorf("Cannot read test file %s: %s", filename, err) + } + + if !reflect.DeepEqual(compare, content) { + return fmt.Errorf("Expected member content\n%v\ngot\n%v", compare, content) + } + + return nil +} diff --git a/pkg/chart/chartfile.go b/pkg/chart/chartfile.go new file mode 100644 index 000000000..23d74c035 --- /dev/null +++ b/pkg/chart/chartfile.go @@ -0,0 +1,65 @@ +/* +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 chart + +import ( + "io/ioutil" + + "gopkg.in/yaml.v2" +) + +// Chartfile describes a Helm Chart (e.g. Chart.yaml) +type Chartfile struct { + Name string `yaml:"name"` + Description string `yaml:"description"` + Version string `yaml:"version"` + Keywords []string `yaml:"keywords,omitempty"` + Maintainers []*Maintainer `yaml:"maintainers,omitempty"` + Source []string `yaml:"source,omitempty"` + Home string `yaml:"home"` +} + +// Maintainer describes a chart maintainer. +type Maintainer struct { + Name string `yaml:"name"` + Email string `yaml:"email,omitempty"` +} + +// LoadChartfile loads a Chart.yaml file into a *Chart. +func LoadChartfile(filename string) (*Chartfile, error) { + b, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + var y Chartfile + return &y, yaml.Unmarshal(b, &y) +} + +// Save saves a Chart.yaml file +func (c *Chartfile) Save(filename string) error { + b, err := c.Marshal() + if err != nil { + return err + } + + return ioutil.WriteFile(filename, b, 0644) +} + +// Marshal encodes the chart file into YAML. +func (c *Chartfile) Marshal() ([]byte, error) { + return yaml.Marshal(c) +} diff --git a/pkg/chart/chartfile_test.go b/pkg/chart/chartfile_test.go new file mode 100644 index 000000000..975871d03 --- /dev/null +++ b/pkg/chart/chartfile_test.go @@ -0,0 +1,41 @@ +/* +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 chart + +import ( + "testing" +) + +func TestLoadChartfile(t *testing.T) { + f, err := LoadChartfile(testfile) + if err != nil { + t.Errorf("Failed to open %s: %s", testfile, err) + return + } + + if f.Name != "frobnitz" { + t.Errorf("Expected frobnitz, got %s", f.Name) + } + + if len(f.Maintainers) != 2 { + t.Errorf("Expected 2 maintainers, got %d", len(f.Maintainers)) + } + + if f.Source[0] != "https://example.com/foo/bar" { + t.Errorf("Expected https://example.com/foo/bar, got %s", f.Source) + } +} diff --git a/pkg/chart/doc.go b/pkg/chart/doc.go new file mode 100644 index 000000000..ec0627506 --- /dev/null +++ b/pkg/chart/doc.go @@ -0,0 +1,23 @@ +/* +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 chart implements the Chart format. + +This package provides tools for working with the Chart format, including the +Chartfile (chart.yaml) and compressed chart archives. +*/ +package chart diff --git a/pkg/chart/save.go b/pkg/chart/save.go new file mode 100644 index 000000000..d6fc5600e --- /dev/null +++ b/pkg/chart/save.go @@ -0,0 +1,117 @@ +/* +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 chart + +import ( + "archive/tar" + "compress/gzip" + "fmt" + "io" + "os" + "path/filepath" +) + +// Save creates an archived chart to the given directory. +// +// This takes an existing chart and a destination directory. +// +// If the directory is /foo, and the chart is named bar, with version 1.0.0, this +// will generate /foo/bar-1.0.0.tgz. +// +// This returns the absolute path to the chart archive file. +func Save(c *Chart, outDir string) (string, error) { + // Create archive + if fi, err := os.Stat(outDir); err != nil { + return "", err + } else if !fi.IsDir() { + return "", fmt.Errorf("location %s is not a directory", outDir) + } + + cfile := c.Chartfile() + dir := c.Dir() + pdir := filepath.Dir(dir) + filename := fmt.Sprintf("%s-%s.tgz", fname(cfile.Name), cfile.Version) + filename = filepath.Join(outDir, filename) + + // Fail early if the YAML is borked. + if err := cfile.Save(filepath.Join(dir, ChartfileName)); err != nil { + return "", err + } + + // Create file. + f, err := os.Create(filename) + if err != nil { + return "", err + } + + // Wrap in gzip writer + zipper := gzip.NewWriter(f) + zipper.Header.Extra = headerBytes + zipper.Header.Comment = "Helm" + + // Wrap in tar writer + twriter := tar.NewWriter(zipper) + rollback := false + defer func() { + twriter.Close() + zipper.Close() + f.Close() + if rollback { + os.Remove(filename) + } + }() + + err = filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error { + if err != nil { + return err + } + hdr, err := tar.FileInfoHeader(fi, ".") + if err != nil { + return err + } + + relpath, err := filepath.Rel(pdir, path) + if err != nil { + return err + } + hdr.Name = relpath + + twriter.WriteHeader(hdr) + + // Skip directories. + if fi.IsDir() { + return nil + } + + in, err := os.Open(path) + if err != nil { + return err + } + _, err = io.Copy(twriter, in) + in.Close() + if err != nil { + return err + } + + return nil + }) + if err != nil { + rollback = true + return filename, err + } + return filename, nil +} diff --git a/pkg/chart/save_test.go b/pkg/chart/save_test.go new file mode 100644 index 000000000..5722e987f --- /dev/null +++ b/pkg/chart/save_test.go @@ -0,0 +1,120 @@ +/* +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 chart + +import ( + "archive/tar" + "compress/gzip" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "sort" + "testing" +) + +const sprocketdir = "testdata/sprocket" + +func TestSave(t *testing.T) { + + tmpdir, err := ioutil.TempDir("", "helm-") + if err != nil { + t.Fatal("Could not create temp directory") + } + t.Logf("Temp: %s", tmpdir) + // Because of the defer, don't call t.Fatal in the remainder of this + // function. + defer os.RemoveAll(tmpdir) + + c, err := LoadDir(sprocketdir) + if err != nil { + t.Errorf("Failed to load %s: %s", sprocketdir, err) + return + } + + tfile, err := Save(c, tmpdir) + if err != nil { + t.Errorf("Failed to save %s to %s: %s", c.Chartfile().Name, tmpdir, err) + return + } + + b := filepath.Base(tfile) + expectname := "sprocket-1.2.3-alpha.1+12345.tgz" + if b != expectname { + t.Errorf("Expected %q, got %q", expectname, b) + } + + files, err := getAllFiles(tfile) + if err != nil { + t.Errorf("Could not extract files: %s", err) + } + + // Files should come back in order. + expect := []string{ + "sprocket", + "sprocket/Chart.yaml", + "sprocket/values.toml", + "sprocket/templates", + "sprocket/templates/template.tpl", + } + if len(expect) != len(files) { + t.Errorf("Expected %d files, found %d", len(expect), len(files)) + return + } + sort.Strings(files) + sort.Strings(expect) + for i := 0; i < len(expect); i++ { + if expect[i] != files[i] { + t.Errorf("Expected file %q, got %q", expect[i], files[i]) + } + } +} + +func getAllFiles(tfile string) ([]string, error) { + f1, err := os.Open(tfile) + if err != nil { + return []string{}, err + } + f2, err := gzip.NewReader(f1) + if err != nil { + f1.Close() + return []string{}, err + } + + if f2.Header.Comment != "Helm" { + return []string{}, fmt.Errorf("Expected header Helm. Got %s", f2.Header.Comment) + } + if string(f2.Header.Extra) != string(headerBytes) { + return []string{}, fmt.Errorf("Expected header signature. Got %v", f2.Header.Extra) + } + + f3 := tar.NewReader(f2) + + files := []string{} + var e error + var hdr *tar.Header + for e == nil { + hdr, e = f3.Next() + if e == nil { + files = append(files, hdr.Name) + } + } + + f2.Close() + f1.Close() + return files, nil +} diff --git a/pkg/chart/testdata/README.md b/pkg/chart/testdata/README.md new file mode 100644 index 000000000..a3aa71f14 --- /dev/null +++ b/pkg/chart/testdata/README.md @@ -0,0 +1 @@ +This directory houses charts used in testing. diff --git a/pkg/chart/testdata/coleridge.toml b/pkg/chart/testdata/coleridge.toml new file mode 100644 index 000000000..bd16a8c84 --- /dev/null +++ b/pkg/chart/testdata/coleridge.toml @@ -0,0 +1,11 @@ +poet = "Coleridge" +title = "Rime of the Ancient Mariner" +stanza = ["at", "length", "did", "cross", "an", "Albatross"] + +[mariner] +with = "crossbow" +shot = "ALBATROSS" + +[water.water] +where = "everywhere" +nor = "any drop to drink" diff --git a/pkg/chart/testdata/frobnitz-0.0.1.tgz b/pkg/chart/testdata/frobnitz-0.0.1.tgz new file mode 100644 index 000000000..41f3197a5 Binary files /dev/null and b/pkg/chart/testdata/frobnitz-0.0.1.tgz differ diff --git a/pkg/chart/testdata/frobnitz/Chart.toml b/pkg/chart/testdata/frobnitz/Chart.toml new file mode 100644 index 000000000..0f4c51d57 --- /dev/null +++ b/pkg/chart/testdata/frobnitz/Chart.toml @@ -0,0 +1,15 @@ +name = "frobnitz" +description = "This is a frobniz." +version = "1.2.3-alpha.1+12345" +keywords = ["frobnitz", "sprocket", "dodad"] +home = "http://example.com" +source = [ + "https://example.com/foo/bar", + "https://github.com/example/foo" +] +[[maintainer]] + name = "The Helm Team" + email = "helm@example.com" +[[maintainer]] + name = "Someone Else" + email = "nobody@example.com" diff --git a/pkg/chart/testdata/frobnitz/Chart.yaml b/pkg/chart/testdata/frobnitz/Chart.yaml new file mode 100644 index 000000000..2fa9ac025 --- /dev/null +++ b/pkg/chart/testdata/frobnitz/Chart.yaml @@ -0,0 +1,15 @@ +name: frobnitz +description: This is a frobniz. +version: "1.2.3-alpha.1+12345" +keywords: + - frobnitz + - sprocket + - dodad +maintainers: + - name: The Helm Team + email: helm@example.com + - name: Someone Else + email: nobody@example.com +source: + - https://example.com/foo/bar +home: http://example.com diff --git a/pkg/chart/testdata/frobnitz/INSTALL.txt b/pkg/chart/testdata/frobnitz/INSTALL.txt new file mode 100644 index 000000000..2010438c2 --- /dev/null +++ b/pkg/chart/testdata/frobnitz/INSTALL.txt @@ -0,0 +1 @@ +This is an install document. The client may display this. diff --git a/pkg/chart/testdata/frobnitz/LICENSE b/pkg/chart/testdata/frobnitz/LICENSE new file mode 100644 index 000000000..6121943b1 --- /dev/null +++ b/pkg/chart/testdata/frobnitz/LICENSE @@ -0,0 +1 @@ +LICENSE placeholder. diff --git a/pkg/chart/testdata/frobnitz/README.md b/pkg/chart/testdata/frobnitz/README.md new file mode 100644 index 000000000..8cf4cc3d7 --- /dev/null +++ b/pkg/chart/testdata/frobnitz/README.md @@ -0,0 +1,11 @@ +# Frobnitz + +This is an example chart. + +## Usage + +This is an example. It has no usage. + +## Development + +For developer info, see the top-level repository. diff --git a/pkg/chart/testdata/frobnitz/docs/README.md b/pkg/chart/testdata/frobnitz/docs/README.md new file mode 100644 index 000000000..d40747caf --- /dev/null +++ b/pkg/chart/testdata/frobnitz/docs/README.md @@ -0,0 +1 @@ +This is a placeholder for documentation. diff --git a/pkg/chart/testdata/frobnitz/icon.svg b/pkg/chart/testdata/frobnitz/icon.svg new file mode 100644 index 000000000..892130606 --- /dev/null +++ b/pkg/chart/testdata/frobnitz/icon.svg @@ -0,0 +1,8 @@ + + + Example icon + + + diff --git a/pkg/chart/testdata/frobnitz/templates/template.tpl b/pkg/chart/testdata/frobnitz/templates/template.tpl new file mode 100644 index 000000000..c651ee6a0 --- /dev/null +++ b/pkg/chart/testdata/frobnitz/templates/template.tpl @@ -0,0 +1 @@ +Hello {{.Name | default "world"}} diff --git a/pkg/chart/testdata/frobnitz/values.toml b/pkg/chart/testdata/frobnitz/values.toml new file mode 100644 index 000000000..6fc24051f --- /dev/null +++ b/pkg/chart/testdata/frobnitz/values.toml @@ -0,0 +1,6 @@ +# A values file contains configuration. + +name = "Some Name" + +[section] +name = "Name in a section" diff --git a/pkg/chart/testdata/sprocket/Chart.yaml b/pkg/chart/testdata/sprocket/Chart.yaml new file mode 100644 index 000000000..1a0b759b5 --- /dev/null +++ b/pkg/chart/testdata/sprocket/Chart.yaml @@ -0,0 +1,15 @@ +name: sprocket +description: This is a sprocket" +version: 1.2.3-alpha.1+12345 +keywords: +- frobnitz +- sprocket +- dodad +maintainers: +- name: The Helm Team + email: helm@example.com +- name: Someone Else + email: nobody@example.com +source: +- https://example.com/foo/bar +home: http://example.com diff --git a/pkg/chart/testdata/sprocket/templates/template.tpl b/pkg/chart/testdata/sprocket/templates/template.tpl new file mode 100644 index 000000000..c651ee6a0 --- /dev/null +++ b/pkg/chart/testdata/sprocket/templates/template.tpl @@ -0,0 +1 @@ +Hello {{.Name | default "world"}} diff --git a/pkg/chart/testdata/sprocket/values.toml b/pkg/chart/testdata/sprocket/values.toml new file mode 100644 index 000000000..6fc24051f --- /dev/null +++ b/pkg/chart/testdata/sprocket/values.toml @@ -0,0 +1,6 @@ +# A values file contains configuration. + +name = "Some Name" + +[section] +name = "Name in a section" diff --git a/pkg/chart/values.go b/pkg/chart/values.go new file mode 100644 index 000000000..20a712dc3 --- /dev/null +++ b/pkg/chart/values.go @@ -0,0 +1,69 @@ +package chart + +import ( + "errors" + "io/ioutil" + "strings" + + "github.com/BurntSushi/toml" +) + +// ErrNoTable indicates that a chart does not have a matching table. +var ErrNoTable = errors.New("no table") + +// Values represents a collection of chart values. +type Values map[string]interface{} + +// Table gets a table (TOML subsection) from a Values object. +// +// The table is returned as a Values. +// +// Compound table names may be specified with dots: +// +// foo.bar +// +// The above will be evaluated as "The table bar inside the table +// foo". +// +// An ErrNoTable is returned if the table does not exist. +func (v Values) Table(name string) (Values, error) { + names := strings.Split(name, ".") + table := v + var err error + + for _, n := range names { + table, err = tableLookup(table, n) + if err != nil { + return table, err + } + } + return table, err +} + +func tableLookup(v Values, simple string) (Values, error) { + v2, ok := v[simple] + if !ok { + return v, ErrNoTable + } + vv, ok := v2.(map[string]interface{}) + if !ok { + return vv, ErrNoTable + } + return vv, nil +} + +// ReadValues will parse TOML byte data into a Values. +func ReadValues(data []byte) (Values, error) { + out := map[string]interface{}{} + err := toml.Unmarshal(data, out) + return out, err +} + +// ReadValuesFile will parse a TOML file into a Values. +func ReadValuesFile(filename string) (Values, error) { + data, err := ioutil.ReadFile(filename) + if err != nil { + return map[string]interface{}{}, err + } + return ReadValues(data) +} diff --git a/pkg/chart/values_test.go b/pkg/chart/values_test.go new file mode 100644 index 000000000..ecb6b3df9 --- /dev/null +++ b/pkg/chart/values_test.go @@ -0,0 +1,134 @@ +package chart + +import ( + "bytes" + "fmt" + "testing" + "text/template" +) + +func TestReadValues(t *testing.T) { + doc := `# Test TOML parse +poet = "Coleridge" +title = "Rime of the Ancient Mariner" +stanza = ["at", "length", "did", "cross", "an", "Albatross"] + +[mariner] +with = "crossbow" +shot = "ALBATROSS" + +[water.water] +where = "everywhere" +nor = "any drop to drink" +` + + data, err := ReadValues([]byte(doc)) + if err != nil { + t.Fatalf("Error parsing bytes: %s", err) + } + matchValues(t, data) +} + +func TestReadValuesFile(t *testing.T) { + data, err := ReadValuesFile("./testdata/coleridge.toml") + if err != nil { + t.Fatalf("Error reading TOML file: %s", err) + } + matchValues(t, data) +} + +func ExampleValues() { + doc := `title="Moby Dick" +[chapter.one] +title = "Loomings" + +[chapter.two] +title = "The Carpet-Bag" + +[chapter.three] +title = "The Spouter Inn" +` + d, err := ReadValues([]byte(doc)) + if err != nil { + panic(err) + } + ch1, err := d.Table("chapter.one") + if err != nil { + panic("could not find chapter one") + } + fmt.Print(ch1["title"]) + // Output: + // Loomings +} + +func TestTable(t *testing.T) { + doc := `title="Moby Dick" +[chapter.one] +title = "Loomings" + +[chapter.two] +title = "The Carpet-Bag" + +[chapter.three] +title = "The Spouter Inn" +` + d, err := ReadValues([]byte(doc)) + if err != nil { + t.Fatalf("Failed to parse the White Whale: %s", err) + } + + if _, err := d.Table("title"); err == nil { + t.Fatalf("Title is not a table.") + } + + if _, err := d.Table("chapter"); err != nil { + t.Fatalf("Failed to get the chapter table: %s\n%v", err, d) + } + + if v, err := d.Table("chapter.one"); err != nil { + t.Errorf("Failed to get chapter.one: %s", err) + } else if v["title"] != "Loomings" { + t.Errorf("Unexpected title: %s", v["title"]) + } + + if _, err := d.Table("chapter.three"); err != nil { + t.Errorf("Chapter three is missing: %s\n%v", err, d) + } + + if _, err := d.Table("chapter.OneHundredThirtySix"); err == nil { + t.Errorf("I think you mean 'Epilogue'") + } +} + +func matchValues(t *testing.T, data map[string]interface{}) { + if data["poet"] != "Coleridge" { + t.Errorf("Unexpected poet: %s", data["poet"]) + } + + if o, err := ttpl("{{len .stanza}}", data); err != nil { + t.Errorf("len stanza: %s", err) + } else if o != "6" { + t.Errorf("Expected 6, got %s", o) + } + + if o, err := ttpl("{{.mariner.shot}}", data); err != nil { + t.Errorf(".mariner.shot: %s", err) + } else if o != "ALBATROSS" { + t.Errorf("Expected that mariner shot ALBATROSS") + } + + if o, err := ttpl("{{.water.water.where}}", data); err != nil { + t.Errorf(".water.water.where: %s", err) + } else if o != "everywhere" { + t.Errorf("Expected water water everywhere") + } +} + +func ttpl(tpl string, v map[string]interface{}) (string, error) { + var b bytes.Buffer + tt := template.Must(template.New("t").Parse(tpl)) + if err := tt.Execute(&b, v); err != nil { + return "", err + } + return b.String(), nil +} diff --git a/pkg/client/install.go b/pkg/client/install.go new file mode 100644 index 000000000..adf34f8d0 --- /dev/null +++ b/pkg/client/install.go @@ -0,0 +1,91 @@ +package client + +import ( + "bytes" + "text/template" + + "github.com/Masterminds/sprig" + "github.com/deis/tiller/pkg/kubectl" +) + +// Installer installs tiller into Kubernetes +// +// See InstallYAML. +type Installer struct { + + // Metadata holds any global metadata attributes for the resources + Metadata map[string]interface{} + + // Tiller specific metadata + Tiller map[string]interface{} +} + +// NewInstaller creates a new Installer +func NewInstaller() *Installer { + return &Installer{ + Metadata: map[string]interface{}{}, + Tiller: map[string]interface{}{}, + } +} + +// Install uses kubectl to install tiller +// +// Returns the string output received from the operation, and an error if the +// command failed. +func (i *Installer) Install(runner kubectl.Runner) (string, error) { + b, err := i.expand() + if err != nil { + return "", err + } + + o, err := runner.Create(b) + return string(o), err +} + +func (i *Installer) expand() ([]byte, error) { + var b bytes.Buffer + t := template.Must(template.New("manifest").Funcs(sprig.TxtFuncMap()).Parse(InstallYAML)) + err := t.Execute(&b, i) + return b.Bytes(), err +} + +// InstallYAML is the installation YAML for DM. +const InstallYAML = ` +--- +apiVersion: v1 +kind: Namespace +metadata: + labels: + app: helm + name: helm-namespace + name: helm +--- +apiVersion: v1 +kind: ReplicationController +metadata: + labels: + app: helm + name: tiller + name: tiller-rc + namespace: helm +spec: + replicas: 1 + selector: + app: helm + name: tiller + template: + metadata: + labels: + app: helm + name: tiller + spec: + containers: + - env: [] + image: {{default "gcr.io/deis-sandbox/tiller:canary" .Tiller.Image}} + name: tiller + ports: + - containerPort: 8080 + name: tiller + imagePullPolicy: Always +--- +` diff --git a/pkg/engine/doc.go b/pkg/engine/doc.go new file mode 100644 index 000000000..e657b1734 --- /dev/null +++ b/pkg/engine/doc.go @@ -0,0 +1,7 @@ +/*Package engine implements the Go template engine as a Tiller Engine. + +Tiller provides a simple interface for taking a Chart and rendering its templates. +The 'engine' package implements this interface using Go's built-in 'text/template' +package. +*/ +package engine diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go new file mode 100644 index 000000000..76af245a9 --- /dev/null +++ b/pkg/engine/engine.go @@ -0,0 +1,226 @@ +package engine + +import ( + "bytes" + "fmt" + "log" + "text/template" + + "github.com/Masterminds/sprig" + chartutil "github.com/deis/tiller/pkg/chart" + "github.com/deis/tiller/pkg/proto/hapi/chart" +) + +// Engine is an implementation of 'cmd/tiller/environment'.Engine that uses Go templates. +type Engine struct { + // FuncMap contains the template functions that will be passed to each + // render call. This may only be modified before the first call to Render. + FuncMap template.FuncMap +} + +// New creates a new Go template Engine instance. +// +// The FuncMap is initialized here. You may modify the FuncMap _prior to_ the +// first invocation of Render. +// +// The FuncMap sets all of the Sprig functions except for those that provide +// access to the underlying OS (env, expandenv). +func New() *Engine { + f := sprig.TxtFuncMap() + delete(f, "env") + delete(f, "expandenv") + return &Engine{ + FuncMap: f, + } +} + +// Render takes a chart, optional values, and attempts to render the Go templates. +// +// Render can be called repeatedly on the same engine. +// +// This will look in the chart's 'templates' data (e.g. the 'templates/' directory) +// and attempt to render the templates there using the values passed in. +// +// Values are scoped to their templates. A dependency template will not have +// access to the values set for its parent. If chart "foo" includes chart "bar", +// "bar" will not have access to the values for "foo". +// +// Values are passed through the templates according to scope. If the top layer +// chart includes the chart foo, which includes the chart bar, the values map +// will be examined for a table called "foo". If "foo" is found in vals, +// that section of the values will be passed into the "foo" chart. And if that +// section contains a value named "bar", that value will be passed on to the +// bar chart during render time. +// +// Values are coalesced together using the fillowing rules: +// +// - Values in a higher level chart always override values in a lower-level +// dependency chart +// - Scalar values and arrays are replaced, maps are merged +// - A chart has access to all of the variables for it, as well as all of +// the values destined for its dependencies. +func (e *Engine) Render(chrt *chart.Chart, vals *chart.Config) (map[string]string, error) { + var cvals chartutil.Values + + // Parse values if not nil. We merge these at the top level because + // the passed-in values are in the same namespace as the parent chart. + if vals != nil { + evals, err := chartutil.ReadValues([]byte(vals.Raw)) + if err != nil { + return map[string]string{}, err + } + cvals = coalesceValues(chrt, evals) + } + + // Render the charts + tmap := allTemplates(chrt, cvals) + return e.render(tmap) +} + +// renderable is an object that can be rendered. +type renderable struct { + // tpl is the current template. + tpl string + // vals are the values to be supplied to the template. + vals chartutil.Values +} + +// render takes a map of templates/values and renders them. +func (e *Engine) render(tpls map[string]renderable) (map[string]string, error) { + // Basically, what we do here is start with an empty parent template and then + // build up a list of templates -- one for each file. Once all of the templates + // have been parsed, we loop through again and execute every template. + // + // The idea with this process is to make it possible for more complex templates + // to share common blocks, but to make the entire thing feel like a file-based + // template engine. + t := template.New("gotpl") + files := []string{} + for fname, r := range tpls { + t = t.New(fname).Funcs(e.FuncMap) + if _, err := t.Parse(r.tpl); err != nil { + return map[string]string{}, fmt.Errorf("parse error in %q: %s", fname, err) + } + files = append(files, fname) + } + + rendered := make(map[string]string, len(files)) + var buf bytes.Buffer + for _, file := range files { + // log.Printf("Exec %s with %v (%s)", file, tpls[file].vals, tpls[file].tpl) + if err := t.ExecuteTemplate(&buf, file, tpls[file].vals); err != nil { + return map[string]string{}, fmt.Errorf("render error in %q: %s", file, err) + } + rendered[file] = buf.String() + buf.Reset() + } + + return rendered, nil +} + +// allTemplates returns all templates for a chart and its dependencies. +// +// As it goes, it also prepares the values in a scope-sensitive manner. +func allTemplates(c *chart.Chart, vals chartutil.Values) map[string]renderable { + templates := map[string]renderable{} + recAllTpls(c, templates, vals, true) + return templates +} + +// recAllTpls recurses through the templates in a chart. +// +// As it recurses, it also sets the values to be appropriate for the template +// scope. +func recAllTpls(c *chart.Chart, templates map[string]renderable, parentVals chartutil.Values, top bool) { + var pvals chartutil.Values + if top { + // If this is the top of the rendering tree, assume that parentVals + // is already resolved to the authoritative values. + pvals = parentVals + } else if c.Metadata != nil && c.Metadata.Name != "" { + // An error indicates that the table doesn't exist. So we leave it as + // an empty map. + tmp, err := parentVals.Table(c.Metadata.Name) + if err == nil { + pvals = tmp + } + } + cvals := coalesceValues(c, pvals) + //log.Printf("racAllTpls values: %v", cvals) + for _, child := range c.Dependencies { + recAllTpls(child, templates, cvals, false) + } + for _, t := range c.Templates { + templates[t.Name] = renderable{ + tpl: string(t.Data), + vals: cvals, + } + } +} + +// coalesceValues builds up a values map for a particular chart. +// +// Values in v will override the values in the chart. +func coalesceValues(c *chart.Chart, v chartutil.Values) chartutil.Values { + // If there are no values in the chart, we just return the given values + if c.Values == nil { + return v + } + + nv, err := chartutil.ReadValues([]byte(c.Values.Raw)) + if err != nil { + // On error, we return just the overridden values. + // FIXME: We should log this error. It indicates that the TOML data + // did not parse. + log.Printf("error reading default values: %s", err) + return v + } + + for k, val := range v { + // NOTE: We could block coalesce on cases where nv does not explicitly + // declare a value. But that forces the chart author to explicitly + // set a default for every template param. We want to preserve the + // possibility of "hidden" parameters. + if istable(val) { + if inmap, ok := nv[k]; ok && istable(inmap) { + coalesceTables(inmap.(map[string]interface{}), val.(map[string]interface{})) + } else if ok { + log.Printf("Cannot copy table into non-table value for %s (%v)", k, inmap) + } else { + // The parent table does not have a key entry for this item, + // so we can safely set it. This is necessary for nested charts. + log.Printf("Copying %s into map %v", k, nv) + nv[k] = val + } + } else { + nv[k] = val + } + } + return nv +} + +// coalesceTables merges a source map into a destination map. +func coalesceTables(dst, src map[string]interface{}) { + for key, val := range src { + if istable(val) { + if innerdst, ok := dst[key]; !ok { + dst[key] = val + } else if istable(innerdst) { + coalesceTables(innerdst.(map[string]interface{}), val.(map[string]interface{})) + } else { + log.Printf("Cannot overwrite table with non table for %s (%v)", key, val) + } + continue + } else if dv, ok := dst[key]; ok && istable(dv) { + log.Printf("Destination for %s is a table. Ignoring non-table value %v", key, val) + continue + } + dst[key] = val + } +} + +// istable is a special-purpose function to see if the present thing matches the definition of a TOML table. +func istable(v interface{}) bool { + _, ok := v.(map[string]interface{}) + return ok +} diff --git a/pkg/engine/engine_test.go b/pkg/engine/engine_test.go new file mode 100644 index 000000000..6a88fe2bd --- /dev/null +++ b/pkg/engine/engine_test.go @@ -0,0 +1,269 @@ +package engine + +import ( + "fmt" + "sync" + "testing" + + chartutil "github.com/deis/tiller/pkg/chart" + "github.com/deis/tiller/pkg/proto/hapi/chart" +) + +func TestEngine(t *testing.T) { + e := New() + + // Forbidden because they allow access to the host OS. + forbidden := []string{"env", "expandenv"} + for _, f := range forbidden { + if _, ok := e.FuncMap[f]; ok { + t.Errorf("Forbidden function %s exists in FuncMap.", f) + } + } +} + +func TestRender(t *testing.T) { + t.Skip() +} + +func TestRenderInternals(t *testing.T) { + // Test the internals of the rendering tool. + e := New() + + vals := chartutil.Values{"Name": "one", "Value": "two"} + tpls := map[string]renderable{ + "one": {tpl: `Hello {{title .Name}}`, vals: vals}, + "two": {tpl: `Goodbye {{upper .Value}}`, vals: vals}, + // Test whether a template can reliably reference another template + // without regard for ordering. + "three": {tpl: `{{template "two" dict "Value" "three"}}`, vals: vals}, + } + + out, err := e.render(tpls) + if err != nil { + t.Fatalf("Failed template rendering: %s", err) + } + + if len(out) != 3 { + t.Fatalf("Expected 3 templates, got %d", len(out)) + } + + if out["one"] != "Hello One" { + t.Errorf("Expected 'Hello One', got %q", out["one"]) + } + + if out["two"] != "Goodbye TWO" { + t.Errorf("Expected 'Goodbye TWO'. got %q", out["two"]) + } + + if out["three"] != "Goodbye THREE" { + t.Errorf("Expected 'Goodbye THREE'. got %q", out["two"]) + } +} + +func TestParallelRenderInternals(t *testing.T) { + // Make sure that we can use one Engine to run parallel template renders. + e := New() + var wg sync.WaitGroup + for i := 0; i < 20; i++ { + wg.Add(1) + go func(i int) { + fname := "my/file/name" + tt := fmt.Sprintf("expect-%d", i) + v := chartutil.Values{"val": tt} + tpls := map[string]renderable{fname: {tpl: `{{.val}}`, vals: v}} + out, err := e.render(tpls) + if err != nil { + t.Errorf("Failed to render %s: %s", tt, err) + } + if out[fname] != tt { + t.Errorf("Expected %q, got %q", tt, out[fname]) + } + wg.Done() + }(i) + } + wg.Wait() +} + +func TestAllTemplates(t *testing.T) { + ch1 := &chart.Chart{ + Templates: []*chart.Template{ + {Name: "foo", Data: []byte("foo")}, + {Name: "bar", Data: []byte("bar")}, + }, + Dependencies: []*chart.Chart{ + { + Templates: []*chart.Template{ + {Name: "pinky", Data: []byte("pinky")}, + {Name: "brain", Data: []byte("brain")}, + }, + Dependencies: []*chart.Chart{ + {Templates: []*chart.Template{ + {Name: "innermost", Data: []byte("innermost")}, + }}, + }, + }, + }, + } + + var v chartutil.Values + tpls := allTemplates(ch1, v) + if len(tpls) != 5 { + t.Errorf("Expected 5 charts, got %d", len(tpls)) + } +} + +func TestRenderDependency(t *testing.T) { + e := New() + deptpl := `{{define "myblock"}}World{{end}}` + toptpl := `Hello {{template "myblock"}}` + ch := &chart.Chart{ + Templates: []*chart.Template{ + {Name: "outer", Data: []byte(toptpl)}, + }, + Dependencies: []*chart.Chart{ + { + Templates: []*chart.Template{ + {Name: "inner", Data: []byte(deptpl)}, + }, + }, + }, + } + + out, err := e.Render(ch, nil) + + if err != nil { + t.Fatalf("failed to render chart: %s", err) + } + + if len(out) != 2 { + t.Errorf("Expected 2, got %d", len(out)) + } + + expect := "Hello World" + if out["outer"] != expect { + t.Errorf("Expected %q, got %q", expect, out["outer"]) + } + +} + +func TestRenderNestedValues(t *testing.T) { + e := New() + + innerpath := "charts/inner/templates/inner.tpl" + outerpath := "templates/outer.tpl" + deepestpath := "charts/inner/charts/deepest/templates/deepest.tpl" + + deepest := &chart.Chart{ + Metadata: &chart.Metadata{Name: "deepest"}, + Templates: []*chart.Template{ + {Name: deepestpath, Data: []byte(`And this same {{.what}} that smiles to-day`)}, + }, + Values: &chart.Config{Raw: `what = "milkshake"`}, + } + + inner := &chart.Chart{ + Metadata: &chart.Metadata{Name: "herrick"}, + Templates: []*chart.Template{ + {Name: innerpath, Data: []byte(`Old {{.who}} is still a-flyin'`)}, + }, + Values: &chart.Config{Raw: `who = "Robert"`}, + Dependencies: []*chart.Chart{deepest}, + } + + outer := &chart.Chart{ + Metadata: &chart.Metadata{Name: "top"}, + Templates: []*chart.Template{ + {Name: outerpath, Data: []byte(`Gather ye {{.what}} while ye may`)}, + }, + Values: &chart.Config{ + Raw: `what = "stinkweed" + [herrick] + who = "time" + `}, + Dependencies: []*chart.Chart{inner}, + } + + inject := chart.Config{ + Raw: ` + what = "rosebuds" + [herrick.deepest] + what = "flower"`, + } + + out, err := e.Render(outer, &inject) + if err != nil { + t.Fatalf("failed to render templates: %s", err) + } + + if out[outerpath] != "Gather ye rosebuds while ye may" { + t.Errorf("Unexpected outer: %q", out[outerpath]) + } + + if out[innerpath] != "Old time is still a-flyin'" { + t.Errorf("Unexpected inner: %q", out[innerpath]) + } + + if out[deepestpath] != "And this same flower that smiles to-day" { + t.Errorf("Unexpected deepest: %q", out[deepestpath]) + } +} + +func TestCoalesceTables(t *testing.T) { + dst := map[string]interface{}{ + "name": "Ishmael", + "address": map[string]interface{}{ + "street": "123 Spouter Inn Ct.", + "city": "Nantucket", + }, + "details": map[string]interface{}{ + "friends": []string{"Tashtego"}, + }, + "boat": "pequod", + } + src := map[string]interface{}{ + "occupation": "whaler", + "address": map[string]interface{}{ + "state": "MA", + "street": "234 Spouter Inn Ct.", + }, + "details": "empty", + "boat": map[string]interface{}{ + "mast": true, + }, + } + coalesceTables(dst, src) + + if dst["name"] != "Ishmael" { + t.Errorf("Unexpected name: %s", dst["name"]) + } + if dst["occupation"] != "whaler" { + t.Errorf("Unexpected occupation: %s", dst["occupation"]) + } + + addr, ok := dst["address"].(map[string]interface{}) + if !ok { + t.Fatal("Address went away.") + } + + if addr["street"].(string) != "234 Spouter Inn Ct." { + t.Errorf("Unexpected address: %v", addr["street"]) + } + + if addr["city"].(string) != "Nantucket" { + t.Errorf("Unexpected city: %v", addr["city"]) + } + + if addr["state"].(string) != "MA" { + t.Errorf("Unexpected state: %v", addr["state"]) + } + + if det, ok := dst["details"].(map[string]interface{}); !ok { + t.Fatalf("Details is the wrong type: %v", dst["details"]) + } else if _, ok := det["friends"]; !ok { + t.Error("Could not find your friends. Maybe you don't have any. :-(") + } + + if dst["boat"].(string) != "pequod" { + t.Errorf("Expected boat string, got %v", dst["boat"]) + } +} diff --git a/pkg/helm/client.go b/pkg/helm/client.go new file mode 100644 index 000000000..69c6b4535 --- /dev/null +++ b/pkg/helm/client.go @@ -0,0 +1,43 @@ +package helm + +import ( + "golang.org/x/net/context" + "google.golang.org/grpc" + + "github.com/deis/tiller/pkg/proto/hapi/services" +) + +type client struct { + cfg *config + conn *grpc.ClientConn + impl services.ReleaseServiceClient +} + +func (c *client) dial() (err error) { + c.conn, err = grpc.Dial(c.cfg.ServAddr, c.cfg.DialOpts()...) + c.impl = services.NewReleaseServiceClient(c.conn) + return +} + +func (c *client) install(req *services.InstallReleaseRequest) (res *services.InstallReleaseResponse, err error) { + if err = c.dial(); err != nil { + return + } + + defer c.Close() + + return c.impl.InstallRelease(context.TODO(), req, c.cfg.CallOpts()...) +} + +func (c *client) uninstall(req *services.UninstallReleaseRequest) (*services.UninstallReleaseResponse, error) { + if err := c.dial(); err != nil { + return nil, err + } + defer c.Close() + + return c.impl.UninstallRelease(context.TODO(), req, c.cfg.CallOpts()...) +} + +func (c *client) Close() error { + return c.conn.Close() +} diff --git a/pkg/helm/config.go b/pkg/helm/config.go new file mode 100644 index 000000000..0e515a112 --- /dev/null +++ b/pkg/helm/config.go @@ -0,0 +1,28 @@ +package helm + +import ( + "google.golang.org/grpc" +) + +type config struct { + ServAddr string + Insecure bool +} + +func (cfg *config) DialOpts() (opts []grpc.DialOption) { + if cfg.Insecure { + opts = append(opts, grpc.WithInsecure()) + } else { + // TODO: handle transport credentials + } + + return +} + +func (cfg *config) CallOpts() (opts []grpc.CallOption) { + return +} + +func (cfg *config) client() *client { + return &client{cfg: cfg} +} diff --git a/pkg/helm/error.go b/pkg/helm/error.go new file mode 100644 index 000000000..b8d72a2c4 --- /dev/null +++ b/pkg/helm/error.go @@ -0,0 +1,16 @@ +package helm + +const ( + errNotImplemented = Error("helm api not implemented") + errMissingSrvAddr = Error("missing tiller address") + errMissingTpls = Error("missing chart templates") + errMissingChart = Error("missing chart metadata") + errMissingValues = Error("missing chart values") +) + +// Error represents a Helm client error. +type Error string + +func (e Error) Error() string { + return string(e) +} diff --git a/pkg/helm/helm.go b/pkg/helm/helm.go new file mode 100644 index 000000000..5224d251f --- /dev/null +++ b/pkg/helm/helm.go @@ -0,0 +1,146 @@ +package helm + +import ( + "github.com/deis/tiller/pkg/chart" + chartpb "github.com/deis/tiller/pkg/proto/hapi/chart" + "github.com/deis/tiller/pkg/proto/hapi/services" + "golang.org/x/net/context" +) + +// Config defines a gRPC client's configuration. +var Config = &config{ + ServAddr: ":44134", + Insecure: true, +} + +// ListReleases lists the current releases. +func ListReleases(limit, offset int) (<-chan *services.ListReleasesResponse, error) { + return nil, errNotImplemented +} + +// GetReleaseStatus returns the given release's status. +func GetReleaseStatus(name string) (*services.GetReleaseStatusResponse, error) { + c := Config.client() + if err := c.dial(); err != nil { + return nil, err + } + defer c.Close() + + req := &services.GetReleaseStatusRequest{Name: name} + return c.impl.GetReleaseStatus(context.TODO(), req, c.cfg.CallOpts()...) +} + +// GetReleaseContent returns the configuration for a given release. +func GetReleaseContent(name string) (*services.GetReleaseContentResponse, error) { + c := Config.client() + if err := c.dial(); err != nil { + return nil, err + } + defer c.Close() + + req := &services.GetReleaseContentRequest{Name: name} + return c.impl.GetReleaseContent(context.TODO(), req, c.cfg.CallOpts()...) +} + +// UpdateRelease updates a release to a new/different chart. +// TODO: This must take more than just name for an arg. +func UpdateRelease(name string) (*services.UpdateReleaseResponse, error) { + return nil, errNotImplemented +} + +// UninstallRelease uninstalls a named release and returns the response. +func UninstallRelease(name string) (*services.UninstallReleaseResponse, error) { + u := &services.UninstallReleaseRequest{ + Name: name, + } + return Config.client().uninstall(u) +} + +// InstallRelease installs a new chart and returns the release response. +func InstallRelease(ch *chart.Chart) (res *services.InstallReleaseResponse, err error) { + chpb := new(chartpb.Chart) + + chpb.Metadata, err = mkProtoMetadata(ch.Chartfile()) + if err != nil { + return + } + + chpb.Templates, err = mkProtoTemplates(ch) + if err != nil { + return + } + + chpb.Dependencies, err = mkProtoChartDeps(ch) + if err != nil { + return + } + + var vals *chartpb.Config + + vals, err = mkProtoConfigValues(ch) + if err != nil { + return + } + + res, err = Config.client().install(&services.InstallReleaseRequest{ + Chart: chpb, + Values: vals, + }) + + return +} + +// pkg/chart to proto/hapi/chart helpers. temporary. +func mkProtoMetadata(ch *chart.Chartfile) (*chartpb.Metadata, error) { + if ch == nil { + return nil, errMissingChart + } + + md := &chartpb.Metadata{ + Name: ch.Name, + Home: ch.Home, + Version: ch.Version, + Description: ch.Description, + } + + md.Sources = make([]string, len(ch.Source)) + copy(md.Sources, ch.Source) + + md.Keywords = make([]string, len(ch.Keywords)) + copy(md.Keywords, ch.Keywords) + + for _, maintainer := range ch.Maintainers { + md.Maintainers = append(md.Maintainers, &chartpb.Maintainer{ + Name: maintainer.Name, + Email: maintainer.Email, + }) + } + + return md, nil +} + +func mkProtoTemplates(ch *chart.Chart) ([]*chartpb.Template, error) { + tpls, err := ch.LoadTemplates() + if err != nil { + return nil, err + } + + _ = tpls + + return nil, nil +} + +func mkProtoChartDeps(ch *chart.Chart) ([]*chartpb.Chart, error) { + return nil, nil +} + +func mkProtoConfigValues(ch *chart.Chart) (*chartpb.Config, error) { + vals, err := ch.LoadValues() + if err != nil { + return nil, errMissingValues + } + + _ = vals + + return nil, nil +} diff --git a/pkg/kube/client.go b/pkg/kube/client.go new file mode 100644 index 000000000..7f6acf029 --- /dev/null +++ b/pkg/kube/client.go @@ -0,0 +1,64 @@ +package kube + +import ( + "fmt" + "io" + + "k8s.io/kubernetes/pkg/client/unversioned/clientcmd" + cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" + "k8s.io/kubernetes/pkg/kubectl/resource" +) + +const includeThirdPartyAPIs = false + +// ResourceActorFunc performs an action on a signle resource. +type ResourceActorFunc func(*resource.Info) error + +// Create creates kubernetes resources from an io.reader +// +// Namespace will set the namespace +// Config allows for overiding values from kubectl +func Create(namespace string, reader io.Reader, config clientcmd.ClientConfig) error { + f := cmdutil.NewFactory(config) + return perform(f, namespace, reader, createResource) +} + +func perform(f *cmdutil.Factory, namespace string, reader io.Reader, fn ResourceActorFunc) error { + r := f.NewBuilder(includeThirdPartyAPIs). + ContinueOnError(). + NamespaceParam(namespace). + RequireNamespace(). + Stream(reader, ""). + Flatten(). + Do() + + if r.Err() != nil { + return r.Err() + } + + count := 0 + err := r.Visit(func(info *resource.Info, err error) error { + if err != nil { + return err + } + err = fn(info) + + if err == nil { + count++ + } + return err + }) + + if err != nil { + return err + } + if count == 0 { + return fmt.Errorf("no objects passed to create") + } + return nil +} + +func createResource(info *resource.Info) error { + _, err := resource.NewHelper(info.Client, info.Mapping).Create(info.Namespace, true, info.Object) + return err +} diff --git a/pkg/kube/client_test.go b/pkg/kube/client_test.go new file mode 100644 index 000000000..daeb3dee4 --- /dev/null +++ b/pkg/kube/client_test.go @@ -0,0 +1,44 @@ +package kube + +import ( + "os" + "testing" + + "k8s.io/kubernetes/pkg/api/meta" + "k8s.io/kubernetes/pkg/client/unversioned/fake" + cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" + "k8s.io/kubernetes/pkg/kubectl/resource" +) + +func TestPerform(t *testing.T) { + input, err := os.Open("./testdata/guestbook-all-in-one.yaml") + if err != nil { + t.Fatal(err) + } + defer input.Close() + + results := []*resource.Info{} + + fn := func(info *resource.Info) error { + results = append(results, info) + + if info.Namespace != "test" { + t.Errorf("expected namespace to be 'test', got %s", info.Namespace) + } + + return nil + } + + f := cmdutil.NewFactory(nil) + f.ClientForMapping = func(mapping *meta.RESTMapping) (resource.RESTClient, error) { + return &fake.RESTClient{}, nil + } + + if err := perform(f, "test", input, fn); err != nil { + t.Fatalf("Unexpected error: %s", err) + } + + if len(results) != 6 { + t.Errorf("expected 6 result objects, got %d", len(results)) + } +} diff --git a/pkg/kube/testdata/guestbook-all-in-one.yaml b/pkg/kube/testdata/guestbook-all-in-one.yaml new file mode 100644 index 000000000..c6675e0eb --- /dev/null +++ b/pkg/kube/testdata/guestbook-all-in-one.yaml @@ -0,0 +1,179 @@ +apiVersion: v1 +kind: Service +metadata: + name: redis-master + labels: + app: redis + tier: backend + role: master +spec: + ports: + # the port that this service should serve on + - port: 6379 + targetPort: 6379 + selector: + app: redis + tier: backend + role: master +--- +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: redis-master + # these labels can be applied automatically + # from the labels in the pod template if not set + # labels: + # app: redis + # role: master + # tier: backend +spec: + # this replicas value is default + # modify it according to your case + replicas: 1 + # selector can be applied automatically + # from the labels in the pod template if not set + # selector: + # matchLabels: + # app: guestbook + # role: master + # tier: backend + template: + metadata: + labels: + app: redis + role: master + tier: backend + spec: + containers: + - name: master + image: gcr.io/google_containers/redis:e2e # or just image: redis + resources: + requests: + cpu: 100m + memory: 100Mi + ports: + - containerPort: 6379 +--- +apiVersion: v1 +kind: Service +metadata: + name: redis-slave + labels: + app: redis + tier: backend + role: slave +spec: + ports: + # the port that this service should serve on + - port: 6379 + selector: + app: redis + tier: backend + role: slave +--- +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: redis-slave + # these labels can be applied automatically + # from the labels in the pod template if not set + # labels: + # app: redis + # role: slave + # tier: backend +spec: + # this replicas value is default + # modify it according to your case + replicas: 2 + # selector can be applied automatically + # from the labels in the pod template if not set + # selector: + # matchLabels: + # app: guestbook + # role: slave + # tier: backend + template: + metadata: + labels: + app: redis + role: slave + tier: backend + spec: + containers: + - name: slave + image: gcr.io/google_samples/gb-redisslave:v1 + resources: + requests: + cpu: 100m + memory: 100Mi + env: + - name: GET_HOSTS_FROM + value: dns + # If your cluster config does not include a dns service, then to + # instead access an environment variable to find the master + # service's host, comment out the 'value: dns' line above, and + # uncomment the line below. + # value: env + ports: + - containerPort: 6379 +--- +apiVersion: v1 +kind: Service +metadata: + name: frontend + labels: + app: guestbook + tier: frontend +spec: + # if your cluster supports it, uncomment the following to automatically create + # an external load-balanced IP for the frontend service. + # type: LoadBalancer + ports: + # the port that this service should serve on + - port: 80 + selector: + app: guestbook + tier: frontend +--- +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: frontend + # these labels can be applied automatically + # from the labels in the pod template if not set + # labels: + # app: guestbook + # tier: frontend +spec: + # this replicas value is default + # modify it according to your case + replicas: 3 + # selector can be applied automatically + # from the labels in the pod template if not set + # selector: + # matchLabels: + # app: guestbook + # tier: frontend + template: + metadata: + labels: + app: guestbook + tier: frontend + spec: + containers: + - name: php-redis + image: gcr.io/google-samples/gb-frontend:v4 + resources: + requests: + cpu: 100m + memory: 100Mi + env: + - name: GET_HOSTS_FROM + value: dns + # If your cluster config does not include a dns service, then to + # instead access environment variables to find service host + # info, comment out the 'value: dns' line above, and uncomment the + # line below. + # value: env + ports: + - containerPort: 80 diff --git a/pkg/kubectl/command.go b/pkg/kubectl/command.go new file mode 100644 index 000000000..b36e0ad33 --- /dev/null +++ b/pkg/kubectl/command.go @@ -0,0 +1,32 @@ +package kubectl + +import ( + "bytes" + "fmt" + "io/ioutil" + "os/exec" + "strings" +) + +type cmd struct { + *exec.Cmd +} + +func command(args ...string) *cmd { + return &cmd{exec.Command(Path, args...)} +} + +func assignStdin(cmd *cmd, in []byte) { + cmd.Stdin = bytes.NewBuffer(in) +} + +func (c *cmd) String() string { + var stdin string + + if c.Stdin != nil { + b, _ := ioutil.ReadAll(c.Stdin) + stdin = fmt.Sprintf("< %s", string(b)) + } + + return fmt.Sprintf("[CMD] %s %s", strings.Join(c.Args, " "), stdin) +} diff --git a/pkg/kubectl/create.go b/pkg/kubectl/create.go new file mode 100644 index 000000000..af9297aa9 --- /dev/null +++ b/pkg/kubectl/create.go @@ -0,0 +1,21 @@ +package kubectl + +// Create uploads a chart to Kubernetes +func (r RealRunner) Create(stdin []byte) ([]byte, error) { + args := []string{"create", "-f", "-"} + + cmd := command(args...) + assignStdin(cmd, stdin) + + return cmd.CombinedOutput() +} + +// Create returns the commands to kubectl +func (r PrintRunner) Create(stdin []byte) ([]byte, error) { + args := []string{"create", "-f", "-"} + + cmd := command(args...) + assignStdin(cmd, stdin) + + return []byte(cmd.String()), nil +} diff --git a/pkg/kubectl/create_test.go b/pkg/kubectl/create_test.go new file mode 100644 index 000000000..bca94f6e5 --- /dev/null +++ b/pkg/kubectl/create_test.go @@ -0,0 +1,22 @@ +package kubectl + +import ( + "testing" +) + +func TestPrintCreate(t *testing.T) { + var client Runner = PrintRunner{} + + expected := `[CMD] kubectl create -f - < some stdin data` + + out, err := client.Create([]byte("some stdin data")) + if err != nil { + t.Error(err) + } + + actual := string(out) + + if expected != actual { + t.Fatalf("actual %s != expected %s", actual, expected) + } +} diff --git a/pkg/kubectl/kubectl.go b/pkg/kubectl/kubectl.go new file mode 100644 index 000000000..e527b71fe --- /dev/null +++ b/pkg/kubectl/kubectl.go @@ -0,0 +1,19 @@ +package kubectl + +// Path is the path of the kubectl binary +var Path = "kubectl" + +// Runner is an interface to wrap kubectl convenience methods +type Runner interface { + // Create uploads a chart to Kubernetes + Create(stdin []byte) ([]byte, error) +} + +// RealRunner implements Runner to execute kubectl commands +type RealRunner struct{} + +// PrintRunner implements Runner to return a []byte of the command to be executed +type PrintRunner struct{} + +// Client stores the instance of Runner +var Client Runner = RealRunner{} diff --git a/pkg/kubectl/kubectl_test.go b/pkg/kubectl/kubectl_test.go new file mode 100644 index 000000000..c3348bba5 --- /dev/null +++ b/pkg/kubectl/kubectl_test.go @@ -0,0 +1,12 @@ +package kubectl + +type TestRunner struct { + Runner + + out []byte + err error +} + +func (r TestRunner) Create(stdin []byte, ns string) ([]byte, error) { + return r.out, r.err +} diff --git a/pkg/lint/chartfile.go b/pkg/lint/chartfile.go new file mode 100644 index 000000000..b8a33a30b --- /dev/null +++ b/pkg/lint/chartfile.go @@ -0,0 +1,45 @@ +package lint + +import ( + "os" + "path/filepath" + + chartutil "github.com/deis/tiller/pkg/chart" +) + +func Chartfile(basepath string) (m []Message) { + m = []Message{} + + path := filepath.Join(basepath, "Chart.yaml") + if fi, err := os.Stat(path); err != nil { + m = append(m, Message{Severity: ErrorSev, Text: "No Chart.yaml file"}) + return + } else if fi.IsDir() { + m = append(m, Message{Severity: ErrorSev, Text: "Chart.yaml is a directory."}) + return + } + + cf, err := chartutil.LoadChartfile(path) + if err != nil { + m = append(m, Message{ + Severity: ErrorSev, + Text: err.Error(), + }) + return + } + + if cf.Name == "" { + m = append(m, Message{ + Severity: ErrorSev, + Text: "Chart.yaml: 'name' is required", + }) + } + + if cf.Version == "" || cf.Version == "0.0.0" { + m = append(m, Message{ + Severity: ErrorSev, + Text: "Chart.yaml: 'version' is required, and must be greater than 0.0.0", + }) + } + return +} diff --git a/pkg/lint/chartfile_test.go b/pkg/lint/chartfile_test.go new file mode 100644 index 000000000..c8c3be911 --- /dev/null +++ b/pkg/lint/chartfile_test.go @@ -0,0 +1,22 @@ +package lint + +import ( + "testing" +) + +const badchartfile = "testdata/badchartfile" + +func TestChartfile(t *testing.T) { + msgs := Chartfile(badchartfile) + if len(msgs) != 2 { + t.Errorf("Expected 2 errors, got %d", len(msgs)) + } + + if msgs[0].Text != "Chart.yaml: 'name' is required" { + t.Errorf("Unexpected message 0: %s", msgs[0].Text) + } + + if msgs[1].Text != "Chart.yaml: 'version' is required, and must be greater than 0.0.0" { + t.Errorf("Unexpected message 1: %s", msgs[1].Text) + } +} diff --git a/pkg/lint/doc.go b/pkg/lint/doc.go new file mode 100644 index 000000000..f2cc19670 --- /dev/null +++ b/pkg/lint/doc.go @@ -0,0 +1,6 @@ +/*Package lint contains tools for linting charts. + +Linting is the process of testing charts for errors or warnings regarding +formatting, compilation, or standards compliance. +*/ +package lint diff --git a/pkg/lint/lint.go b/pkg/lint/lint.go new file mode 100644 index 000000000..07a31acea --- /dev/null +++ b/pkg/lint/lint.go @@ -0,0 +1,7 @@ +package lint + +func All(basedir string) []Message { + out := Chartfile(basedir) + out = append(out, Templates(basedir)...) + return out +} diff --git a/pkg/lint/message.go b/pkg/lint/message.go new file mode 100644 index 000000000..a0acda0ca --- /dev/null +++ b/pkg/lint/message.go @@ -0,0 +1,27 @@ +package lint + +import "fmt" + +type Severity int + +const ( + UnknownSev = iota + WarningSev + ErrorSev +) + +var sev = []string{"INFO", "WARNING", "ERROR"} + +type Message struct { + // Severity is one of the *Sev constants + Severity int + // Text contains the message text + Text string +} + +// String prints a string representation of this Message. +// +// Implements fmt.Stringer. +func (m Message) String() string { + return fmt.Sprintf("[%s] %s", sev[m.Severity], m.Text) +} diff --git a/pkg/lint/message_test.go b/pkg/lint/message_test.go new file mode 100644 index 000000000..1d083b369 --- /dev/null +++ b/pkg/lint/message_test.go @@ -0,0 +1,20 @@ +package lint + +import ( + "fmt" + "testing" +) + +var _ fmt.Stringer = Message{} + +func TestMessage(t *testing.T) { + m := Message{ErrorSev, "Foo"} + if m.String() != "[ERROR] Foo" { + t.Errorf("Unexpected output: %s", m.String()) + } + + m = Message{WarningSev, "Bar"} + if m.String() != "[WARNING] Bar" { + t.Errorf("Unexpected output: %s", m.String()) + } +} diff --git a/pkg/lint/template.go b/pkg/lint/template.go new file mode 100644 index 000000000..f3bb559d0 --- /dev/null +++ b/pkg/lint/template.go @@ -0,0 +1,63 @@ +package lint + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "text/template" + + "github.com/Masterminds/sprig" +) + +func Templates(basepath string) (messages []Message) { + messages = []Message{} + path := filepath.Join(basepath, "templates") + if fi, err := os.Stat(path); err != nil { + messages = append(messages, Message{Severity: WarningSev, Text: "No templates"}) + return + } else if !fi.IsDir() { + messages = append(messages, Message{Severity: ErrorSev, Text: "'templates' is not a directory"}) + return + } + + tpl := template.New("tpl").Funcs(sprig.TxtFuncMap()) + + err := filepath.Walk(basepath, func(name string, fi os.FileInfo, e error) error { + // If an error is returned, we fail. Non-fatal errors should just be + // added directly to messages. + if e != nil { + return e + } + if fi.IsDir() { + return nil + } + + data, err := ioutil.ReadFile(name) + if err != nil { + messages = append(messages, Message{ + Severity: ErrorSev, + Text: fmt.Sprintf("cannot read %s: %s", name, err), + }) + return nil + } + + // An error rendering a file should emit a warning. + newtpl, err := tpl.Parse(string(data)) + if err != nil { + messages = append(messages, Message{ + Severity: ErrorSev, + Text: fmt.Sprintf("error processing %s: %s", name, err), + }) + return nil + } + tpl = newtpl + return nil + }) + + if err != nil { + messages = append(messages, Message{Severity: ErrorSev, Text: err.Error()}) + } + + return +} diff --git a/pkg/lint/template_test.go b/pkg/lint/template_test.go new file mode 100644 index 000000000..b8c7eadd4 --- /dev/null +++ b/pkg/lint/template_test.go @@ -0,0 +1,20 @@ +package lint + +import ( + "strings" + "testing" +) + +const templateTestBasedir = "./testdata/albatross" + +func TestTemplate(t *testing.T) { + res := Templates(templateTestBasedir) + + if len(res) != 1 { + t.Fatalf("Expected one error, got %d", len(res)) + } + + if !strings.Contains(res[0].Text, "deliberateSyntaxError") { + t.Errorf("Unexpected error: %s", res[0]) + } +} diff --git a/pkg/lint/testdata/albatross/Chart.yaml b/pkg/lint/testdata/albatross/Chart.yaml new file mode 100644 index 000000000..4aa22d376 --- /dev/null +++ b/pkg/lint/testdata/albatross/Chart.yaml @@ -0,0 +1,3 @@ +name: albatross +description: testing chart +version: 199.44.12345-Alpha.1+cafe009 diff --git a/pkg/lint/testdata/albatross/templates/albatross.yaml b/pkg/lint/testdata/albatross/templates/albatross.yaml new file mode 100644 index 000000000..6c2ceb8db --- /dev/null +++ b/pkg/lint/testdata/albatross/templates/albatross.yaml @@ -0,0 +1,2 @@ +metadata: + name: {{.name | default "foo" | title}} diff --git a/pkg/lint/testdata/albatross/templates/fail.yaml b/pkg/lint/testdata/albatross/templates/fail.yaml new file mode 100644 index 000000000..a11e0e90e --- /dev/null +++ b/pkg/lint/testdata/albatross/templates/fail.yaml @@ -0,0 +1 @@ +{{ deliberateSyntaxError }} diff --git a/pkg/lint/testdata/albatross/values.toml b/pkg/lint/testdata/albatross/values.toml new file mode 100644 index 000000000..388764d49 --- /dev/null +++ b/pkg/lint/testdata/albatross/values.toml @@ -0,0 +1 @@ +name = "mariner" diff --git a/pkg/lint/testdata/badchartfile/Chart.yaml b/pkg/lint/testdata/badchartfile/Chart.yaml new file mode 100644 index 000000000..dbb4a1501 --- /dev/null +++ b/pkg/lint/testdata/badchartfile/Chart.yaml @@ -0,0 +1,3 @@ +description: A Helm chart for Kubernetes +version: 0.0.0 +home: "" diff --git a/pkg/lint/testdata/badchartfile/values.toml b/pkg/lint/testdata/badchartfile/values.toml new file mode 100644 index 000000000..d6bba222c --- /dev/null +++ b/pkg/lint/testdata/badchartfile/values.toml @@ -0,0 +1,4 @@ +# Default values for badchartfile. +# This is a TOML-formatted file. https://github.com/toml-lang/toml +# Declare name/value pairs to be passed into your templates. +# name = "value" diff --git a/pkg/proto/hapi/chart/chart.pb.go b/pkg/proto/hapi/chart/chart.pb.go new file mode 100644 index 000000000..ee27d39f5 --- /dev/null +++ b/pkg/proto/hapi/chart/chart.pb.go @@ -0,0 +1,105 @@ +// Code generated by protoc-gen-go. +// source: hapi/chart/chart.proto +// DO NOT EDIT! + +/* +Package chart is a generated protocol buffer package. + +It is generated from these files: + hapi/chart/chart.proto + hapi/chart/config.proto + hapi/chart/metadata.proto + hapi/chart/template.proto + +It has these top-level messages: + Chart + Config + Value + Maintainer + Metadata + Template +*/ +package chart + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +const _ = proto.ProtoPackageIsVersion1 + +// +// Chart: +// A chart is a helm package that contains metadata, a default config, zero or more +// optionally parameterizable templates, and zero or more charts (dependencies). +// +type Chart struct { + // Contents of the Chartfile. + Metadata *Metadata `protobuf:"bytes,1,opt,name=metadata" json:"metadata,omitempty"` + // Templates for this chart. + Templates []*Template `protobuf:"bytes,2,rep,name=templates" json:"templates,omitempty"` + // Charts that this chart depends on. + Dependencies []*Chart `protobuf:"bytes,3,rep,name=dependencies" json:"dependencies,omitempty"` + // Default config for this template. + Values *Config `protobuf:"bytes,4,opt,name=values" json:"values,omitempty"` +} + +func (m *Chart) Reset() { *m = Chart{} } +func (m *Chart) String() string { return proto.CompactTextString(m) } +func (*Chart) ProtoMessage() {} +func (*Chart) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } + +func (m *Chart) GetMetadata() *Metadata { + if m != nil { + return m.Metadata + } + return nil +} + +func (m *Chart) GetTemplates() []*Template { + if m != nil { + return m.Templates + } + return nil +} + +func (m *Chart) GetDependencies() []*Chart { + if m != nil { + return m.Dependencies + } + return nil +} + +func (m *Chart) GetValues() *Config { + if m != nil { + return m.Values + } + return nil +} + +func init() { + proto.RegisterType((*Chart)(nil), "hapi.chart.Chart") +} + +var fileDescriptor0 = []byte{ + // 197 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x12, 0xcb, 0x48, 0x2c, 0xc8, + 0xd4, 0x4f, 0xce, 0x48, 0x2c, 0x2a, 0x81, 0x90, 0x7a, 0x05, 0x45, 0xf9, 0x25, 0xf9, 0x42, 0x5c, + 0x20, 0x71, 0x3d, 0xb0, 0x88, 0x94, 0x38, 0xb2, 0x9a, 0xfc, 0xbc, 0xb4, 0xcc, 0x74, 0x88, 0x22, + 0x29, 0x49, 0x24, 0x89, 0xdc, 0xd4, 0x92, 0xc4, 0x94, 0xc4, 0x92, 0x44, 0x2c, 0x52, 0x25, 0xa9, + 0xb9, 0x05, 0x39, 0x89, 0x25, 0xa9, 0x10, 0x29, 0xa5, 0x0b, 0x8c, 0x5c, 0xac, 0xce, 0x20, 0x09, + 0x21, 0x03, 0x2e, 0x0e, 0x98, 0x36, 0x09, 0x46, 0x05, 0x46, 0x0d, 0x6e, 0x23, 0x11, 0x3d, 0x84, + 0xbd, 0x7a, 0xbe, 0x50, 0xb9, 0x20, 0xb8, 0x2a, 0x21, 0x23, 0x2e, 0x4e, 0x98, 0x69, 0xc5, 0x12, + 0x4c, 0x0a, 0xcc, 0xe8, 0x5a, 0x42, 0xa0, 0x92, 0x41, 0x08, 0x65, 0x42, 0xa6, 0x5c, 0x3c, 0x29, + 0xa9, 0x05, 0xa9, 0x79, 0x29, 0xa9, 0x79, 0xc9, 0x99, 0x40, 0x6d, 0xcc, 0x60, 0x6d, 0x82, 0xc8, + 0xda, 0xc0, 0xce, 0x09, 0x42, 0x51, 0x26, 0xa4, 0xc5, 0xc5, 0x56, 0x96, 0x98, 0x53, 0x0a, 0xd4, + 0xc0, 0x02, 0x76, 0x9a, 0x10, 0x8a, 0x06, 0x70, 0x30, 0x04, 0x41, 0x55, 0x38, 0xb1, 0x47, 0xb1, + 0x82, 0xc5, 0x93, 0xd8, 0xc0, 0x5e, 0x34, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0xb5, 0xff, 0x0f, + 0xec, 0x57, 0x01, 0x00, 0x00, +} diff --git a/pkg/proto/hapi/chart/config.pb.go b/pkg/proto/hapi/chart/config.pb.go new file mode 100644 index 000000000..e4cd27998 --- /dev/null +++ b/pkg/proto/hapi/chart/config.pb.go @@ -0,0 +1,71 @@ +// Code generated by protoc-gen-go. +// source: hapi/chart/config.proto +// DO NOT EDIT! + +package chart + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// +// Config: +// +// A config supplies values to the parametrizable templates of a chart. +// +type Config struct { + Raw string `protobuf:"bytes,1,opt,name=raw" json:"raw,omitempty"` + Values map[string]*Value `protobuf:"bytes,2,rep,name=values" json:"values,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` +} + +func (m *Config) Reset() { *m = Config{} } +func (m *Config) String() string { return proto.CompactTextString(m) } +func (*Config) ProtoMessage() {} +func (*Config) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{0} } + +func (m *Config) GetValues() map[string]*Value { + if m != nil { + return m.Values + } + return nil +} + +// +// Value: +// +// TODO +// +type Value struct { + Value string `protobuf:"bytes,1,opt,name=value" json:"value,omitempty"` +} + +func (m *Value) Reset() { *m = Value{} } +func (m *Value) String() string { return proto.CompactTextString(m) } +func (*Value) ProtoMessage() {} +func (*Value) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{1} } + +func init() { + proto.RegisterType((*Config)(nil), "hapi.chart.Config") + proto.RegisterType((*Value)(nil), "hapi.chart.Value") +} + +var fileDescriptor1 = []byte{ + // 179 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x12, 0xcf, 0x48, 0x2c, 0xc8, + 0xd4, 0x4f, 0xce, 0x48, 0x2c, 0x2a, 0xd1, 0x4f, 0xce, 0xcf, 0x4b, 0xcb, 0x4c, 0xd7, 0x2b, 0x28, + 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x02, 0x49, 0xe8, 0x81, 0x25, 0x94, 0x16, 0x30, 0x72, 0xb1, 0x39, + 0x83, 0x25, 0x85, 0x04, 0xb8, 0x98, 0x8b, 0x12, 0xcb, 0x25, 0x18, 0x15, 0x18, 0x35, 0x38, 0x83, + 0x40, 0x4c, 0x21, 0x33, 0x2e, 0xb6, 0xb2, 0xc4, 0x9c, 0xd2, 0xd4, 0x62, 0x09, 0x26, 0x05, 0x66, + 0x0d, 0x6e, 0x23, 0x39, 0x3d, 0x84, 0x4e, 0x3d, 0x88, 0x2e, 0xbd, 0x30, 0xb0, 0x02, 0xd7, 0xbc, + 0x92, 0xa2, 0xca, 0x20, 0xa8, 0x6a, 0x29, 0x1f, 0x2e, 0x6e, 0x24, 0x61, 0x90, 0xc1, 0xd9, 0xa9, + 0x95, 0x30, 0x83, 0x81, 0x4c, 0x21, 0x75, 0x2e, 0x56, 0xb0, 0x52, 0xa0, 0xb9, 0x8c, 0x40, 0x73, + 0x05, 0x91, 0xcd, 0x05, 0xeb, 0x0c, 0x82, 0xc8, 0x5b, 0x31, 0x59, 0x30, 0x2a, 0xc9, 0x72, 0xb1, + 0x82, 0xc5, 0x84, 0x44, 0x60, 0xba, 0x20, 0x26, 0x41, 0x38, 0x4e, 0xec, 0x51, 0xac, 0x60, 0x8d, + 0x49, 0x6c, 0x60, 0xdf, 0x19, 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0xe1, 0x12, 0x60, 0xda, 0xf8, + 0x00, 0x00, 0x00, +} diff --git a/pkg/proto/hapi/chart/metadata.pb.go b/pkg/proto/hapi/chart/metadata.pb.go new file mode 100644 index 000000000..42f16e125 --- /dev/null +++ b/pkg/proto/hapi/chart/metadata.pb.go @@ -0,0 +1,91 @@ +// Code generated by protoc-gen-go. +// source: hapi/chart/metadata.proto +// DO NOT EDIT! + +package chart + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// +// Maintainer: +// +// A descriptor of the Chart maintainer(s). +// +type Maintainer struct { + // Name is a user name or organization name + Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` + // Email is an optional email address to contact the named maintainer + Email string `protobuf:"bytes,2,opt,name=email" json:"email,omitempty"` +} + +func (m *Maintainer) Reset() { *m = Maintainer{} } +func (m *Maintainer) String() string { return proto.CompactTextString(m) } +func (*Maintainer) ProtoMessage() {} +func (*Maintainer) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{0} } + +// +// Metadata: +// +// Metadata for a Chart file. This models the structure +// of a Chart.yaml file. +// +// Spec: https://github.com/kubernetes/helm/blob/master/docs/design/chart_format.md#the-chart-file +// +type Metadata struct { + // The name of the chart + Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` + // The URL to a relecant project page, git repo, or contact person + Home string `protobuf:"bytes,2,opt,name=home" json:"home,omitempty"` + // Source is the URL to the source code of this chart + Sources []string `protobuf:"bytes,3,rep,name=sources" json:"sources,omitempty"` + // A SemVer 2 conformant version string of the chart + Version string `protobuf:"bytes,4,opt,name=version" json:"version,omitempty"` + // A one-sentence description of the chart + Description string `protobuf:"bytes,5,opt,name=description" json:"description,omitempty"` + // A list of string keywords + Keywords []string `protobuf:"bytes,6,rep,name=keywords" json:"keywords,omitempty"` + // A list of name and URL/email address combinations for the maintainer(s) + Maintainers []*Maintainer `protobuf:"bytes,7,rep,name=maintainers" json:"maintainers,omitempty"` +} + +func (m *Metadata) Reset() { *m = Metadata{} } +func (m *Metadata) String() string { return proto.CompactTextString(m) } +func (*Metadata) ProtoMessage() {} +func (*Metadata) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{1} } + +func (m *Metadata) GetMaintainers() []*Maintainer { + if m != nil { + return m.Maintainers + } + return nil +} + +func init() { + proto.RegisterType((*Maintainer)(nil), "hapi.chart.Maintainer") + proto.RegisterType((*Metadata)(nil), "hapi.chart.Metadata") +} + +var fileDescriptor2 = []byte{ + // 224 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x6c, 0x90, 0x3f, 0x4f, 0xc4, 0x30, + 0x0c, 0xc5, 0x55, 0xee, 0x7a, 0x3d, 0xdc, 0xcd, 0x42, 0x28, 0x30, 0x55, 0x37, 0x31, 0xe5, 0x24, + 0x90, 0x10, 0x33, 0xfb, 0x2d, 0x37, 0xb2, 0x99, 0xd6, 0x52, 0x23, 0x48, 0x53, 0x25, 0x01, 0xc4, + 0x97, 0xe5, 0xb3, 0x90, 0xba, 0xf4, 0xcf, 0xc0, 0x60, 0xc9, 0xef, 0xfd, 0xfc, 0x2c, 0xd9, 0x70, + 0xd3, 0x52, 0x6f, 0x8e, 0x75, 0x4b, 0x3e, 0x1e, 0x2d, 0x47, 0x6a, 0x28, 0x92, 0xee, 0xbd, 0x8b, + 0x0e, 0x61, 0x40, 0x5a, 0xd0, 0xe1, 0x11, 0xe0, 0x44, 0xa6, 0x8b, 0xa9, 0xd8, 0x23, 0xc2, 0xb6, + 0x23, 0xcb, 0x2a, 0xab, 0xb2, 0xbb, 0xcb, 0xb3, 0xf4, 0x78, 0x05, 0x39, 0x5b, 0x32, 0xef, 0xea, + 0x42, 0xcc, 0x51, 0x1c, 0x7e, 0x32, 0xd8, 0x9f, 0xfe, 0xd6, 0xfe, 0x1b, 0x4b, 0x5e, 0xeb, 0x92, + 0x37, 0xa6, 0xa4, 0x47, 0x05, 0x45, 0x70, 0x1f, 0xbe, 0xe6, 0xa0, 0x36, 0xd5, 0x26, 0xd9, 0x93, + 0x1c, 0xc8, 0x27, 0xfb, 0x60, 0x5c, 0xa7, 0xb6, 0x12, 0x98, 0x24, 0x56, 0x50, 0x36, 0x1c, 0x6a, + 0x6f, 0xfa, 0x38, 0xd0, 0x5c, 0xe8, 0xda, 0xc2, 0x5b, 0xd8, 0xbf, 0xf1, 0xf7, 0x97, 0xf3, 0x4d, + 0x50, 0x3b, 0x59, 0x3b, 0x6b, 0x7c, 0x82, 0xd2, 0xce, 0xe7, 0x05, 0x55, 0x24, 0x5c, 0xde, 0x5f, + 0xeb, 0xe5, 0x01, 0x7a, 0xb9, 0xfe, 0xbc, 0x1e, 0x7d, 0x2e, 0x5e, 0x72, 0x19, 0x78, 0xdd, 0xc9, + 0xd3, 0x1e, 0x7e, 0x03, 0x00, 0x00, 0xff, 0xff, 0xb9, 0xaf, 0x44, 0xa7, 0x51, 0x01, 0x00, 0x00, +} diff --git a/pkg/proto/hapi/chart/template.pb.go b/pkg/proto/hapi/chart/template.pb.go new file mode 100644 index 000000000..115bc945e --- /dev/null +++ b/pkg/proto/hapi/chart/template.pb.go @@ -0,0 +1,45 @@ +// Code generated by protoc-gen-go. +// source: hapi/chart/template.proto +// DO NOT EDIT! + +package chart + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// Template represents a template as a name/value pair. +// +// By convention, name is a relative path within the scope of the chart's +// base directory. +type Template struct { + // Name is the path-like name of the template. + Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` + // Data is the template as byte data. + Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` +} + +func (m *Template) Reset() { *m = Template{} } +func (m *Template) String() string { return proto.CompactTextString(m) } +func (*Template) ProtoMessage() {} +func (*Template) Descriptor() ([]byte, []int) { return fileDescriptor3, []int{0} } + +func init() { + proto.RegisterType((*Template)(nil), "hapi.chart.Template") +} + +var fileDescriptor3 = []byte{ + // 106 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x92, 0xcc, 0x48, 0x2c, 0xc8, + 0xd4, 0x4f, 0xce, 0x48, 0x2c, 0x2a, 0xd1, 0x2f, 0x49, 0xcd, 0x2d, 0xc8, 0x49, 0x2c, 0x49, 0xd5, + 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x02, 0x49, 0xe9, 0x81, 0xa5, 0x94, 0x8c, 0xb8, 0x38, + 0x42, 0xa0, 0xb2, 0x42, 0x42, 0x5c, 0x2c, 0x79, 0x89, 0xb9, 0xa9, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, + 0x9c, 0x41, 0x60, 0x36, 0x48, 0x2c, 0x25, 0xb1, 0x24, 0x51, 0x82, 0x09, 0x28, 0xc6, 0x13, 0x04, + 0x66, 0x3b, 0xb1, 0x47, 0xb1, 0x82, 0x35, 0x27, 0xb1, 0x81, 0xcd, 0x33, 0x06, 0x04, 0x00, 0x00, + 0xff, 0xff, 0x53, 0xee, 0x0e, 0x67, 0x6c, 0x00, 0x00, 0x00, +} diff --git a/pkg/proto/hapi/release/info.pb.go b/pkg/proto/hapi/release/info.pb.go new file mode 100644 index 000000000..4c3d8ed7c --- /dev/null +++ b/pkg/proto/hapi/release/info.pb.go @@ -0,0 +1,98 @@ +// Code generated by protoc-gen-go. +// source: hapi/release/info.proto +// DO NOT EDIT! + +/* +Package release is a generated protocol buffer package. + +It is generated from these files: + hapi/release/info.proto + hapi/release/release.proto + hapi/release/status.proto + +It has these top-level messages: + Info + Release + Status +*/ +package release + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" +import google_protobuf "github.com/golang/protobuf/ptypes/timestamp" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +const _ = proto.ProtoPackageIsVersion1 + +// +// Info: +// +// +type Info struct { + Status *Status `protobuf:"bytes,1,opt,name=status" json:"status,omitempty"` + FirstDeployed *google_protobuf.Timestamp `protobuf:"bytes,2,opt,name=first_deployed,json=firstDeployed" json:"first_deployed,omitempty"` + LastDeployed *google_protobuf.Timestamp `protobuf:"bytes,3,opt,name=last_deployed,json=lastDeployed" json:"last_deployed,omitempty"` + // Deleted tracks when this object was deleted. + Deleted *google_protobuf.Timestamp `protobuf:"bytes,4,opt,name=deleted" json:"deleted,omitempty"` +} + +func (m *Info) Reset() { *m = Info{} } +func (m *Info) String() string { return proto.CompactTextString(m) } +func (*Info) ProtoMessage() {} +func (*Info) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } + +func (m *Info) GetStatus() *Status { + if m != nil { + return m.Status + } + return nil +} + +func (m *Info) GetFirstDeployed() *google_protobuf.Timestamp { + if m != nil { + return m.FirstDeployed + } + return nil +} + +func (m *Info) GetLastDeployed() *google_protobuf.Timestamp { + if m != nil { + return m.LastDeployed + } + return nil +} + +func (m *Info) GetDeleted() *google_protobuf.Timestamp { + if m != nil { + return m.Deleted + } + return nil +} + +func init() { + proto.RegisterType((*Info)(nil), "hapi.release.Info") +} + +var fileDescriptor0 = []byte{ + // 208 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x12, 0xcf, 0x48, 0x2c, 0xc8, + 0xd4, 0x2f, 0x4a, 0xcd, 0x49, 0x4d, 0x2c, 0x4e, 0xd5, 0xcf, 0xcc, 0x4b, 0xcb, 0xd7, 0x2b, 0x28, + 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x01, 0x49, 0xe8, 0x41, 0x25, 0xa4, 0xe4, 0xd3, 0xf3, 0xf3, 0xd3, + 0x73, 0x52, 0xf5, 0xc1, 0x72, 0x49, 0xa5, 0x69, 0xfa, 0x25, 0x99, 0xb9, 0xa9, 0xc5, 0x25, 0x89, + 0xb9, 0x05, 0x10, 0xe5, 0x52, 0x92, 0x28, 0xe6, 0x00, 0x65, 0x4a, 0x4a, 0x8b, 0x21, 0x52, 0x4a, + 0xef, 0x18, 0xb9, 0x58, 0x3c, 0x81, 0x06, 0x0b, 0xe9, 0x70, 0xb1, 0x41, 0x24, 0x24, 0x18, 0x15, + 0x18, 0x35, 0xb8, 0x8d, 0x44, 0xf4, 0x90, 0xed, 0xd0, 0x0b, 0x06, 0xcb, 0x05, 0x41, 0xd5, 0x08, + 0x39, 0x72, 0xf1, 0xa5, 0x65, 0x16, 0x15, 0x97, 0xc4, 0xa7, 0xa4, 0x16, 0xe4, 0xe4, 0x57, 0xa6, + 0xa6, 0x48, 0x30, 0x81, 0x75, 0x49, 0xe9, 0x41, 0xdc, 0xa2, 0x07, 0x73, 0x8b, 0x5e, 0x08, 0xcc, + 0x2d, 0x41, 0xbc, 0x60, 0x1d, 0x2e, 0x50, 0x0d, 0x42, 0xf6, 0x5c, 0xbc, 0x39, 0x89, 0xc8, 0x26, + 0x30, 0x13, 0x34, 0x81, 0x07, 0xa4, 0x01, 0x6e, 0x80, 0x09, 0x17, 0x7b, 0x0a, 0xd0, 0x75, 0x25, + 0x40, 0xad, 0x2c, 0x04, 0xb5, 0xc2, 0x94, 0x3a, 0x71, 0x46, 0xb1, 0x43, 0xfd, 0x94, 0xc4, 0x06, + 0x56, 0x67, 0x0c, 0x08, 0x00, 0x00, 0xff, 0xff, 0xeb, 0x9d, 0xa1, 0xf8, 0x67, 0x01, 0x00, 0x00, +} diff --git a/pkg/proto/hapi/release/release.pb.go b/pkg/proto/hapi/release/release.pb.go new file mode 100644 index 000000000..b97f38f15 --- /dev/null +++ b/pkg/proto/hapi/release/release.pb.go @@ -0,0 +1,83 @@ +// Code generated by protoc-gen-go. +// source: hapi/release/release.proto +// DO NOT EDIT! + +package release + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" +import hapi_chart "github.com/deis/tiller/pkg/proto/hapi/chart" +import hapi_chart3 "github.com/deis/tiller/pkg/proto/hapi/chart" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// +// Release: +// +// A release describes a deployment of a chart, together with the chart +// and the variables used to deploy that chart. +// +type Release struct { + // Name is the name of the release + Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` + // Info provides information about a release + Info *Info `protobuf:"bytes,2,opt,name=info" json:"info,omitempty"` + // Chart is the chart that was released. + Chart *hapi_chart3.Chart `protobuf:"bytes,3,opt,name=chart" json:"chart,omitempty"` + // Config is the set of extra Values added to the chart. + // These values override the default values inside of the chart. + Config *hapi_chart.Config `protobuf:"bytes,4,opt,name=config" json:"config,omitempty"` + // Manifest is the string representation of the rendered template. + Manifest string `protobuf:"bytes,5,opt,name=manifest" json:"manifest,omitempty"` +} + +func (m *Release) Reset() { *m = Release{} } +func (m *Release) String() string { return proto.CompactTextString(m) } +func (*Release) ProtoMessage() {} +func (*Release) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{0} } + +func (m *Release) GetInfo() *Info { + if m != nil { + return m.Info + } + return nil +} + +func (m *Release) GetChart() *hapi_chart3.Chart { + if m != nil { + return m.Chart + } + return nil +} + +func (m *Release) GetConfig() *hapi_chart.Config { + if m != nil { + return m.Config + } + return nil +} + +func init() { + proto.RegisterType((*Release)(nil), "hapi.release.Release") +} + +var fileDescriptor1 = []byte{ + // 200 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x54, 0x8f, 0xbd, 0x0e, 0x82, 0x30, + 0x14, 0x85, 0x83, 0xf2, 0x23, 0xd5, 0xc5, 0x3b, 0x68, 0xc3, 0x64, 0x1c, 0xd4, 0x38, 0x94, 0x44, + 0xdf, 0x40, 0x27, 0xd7, 0x8e, 0x6e, 0x95, 0x14, 0x69, 0x22, 0x2d, 0x01, 0x9e, 0xcd, 0xe7, 0x93, + 0xf6, 0x56, 0x83, 0xcb, 0x85, 0xde, 0xef, 0xcb, 0xe9, 0x29, 0xc9, 0x2a, 0xd1, 0xa8, 0xbc, 0x95, + 0x2f, 0x29, 0x3a, 0xf9, 0xfd, 0xb2, 0xa6, 0x35, 0xbd, 0x81, 0x85, 0x65, 0xcc, 0xef, 0xb2, 0xf5, + 0x9f, 0xa9, 0x74, 0x69, 0x50, 0xf3, 0xa0, 0xa8, 0x44, 0xdb, 0xe7, 0x85, 0xd1, 0xa5, 0x7a, 0x7a, + 0xb0, 0x1a, 0x03, 0x3b, 0x71, 0xbf, 0x7d, 0x07, 0x24, 0xe1, 0x98, 0x03, 0x40, 0x42, 0x2d, 0x6a, + 0x49, 0x83, 0x4d, 0x70, 0x48, 0xb9, 0xfb, 0x87, 0x1d, 0x09, 0x6d, 0x3c, 0x9d, 0x0c, 0xbb, 0xf9, + 0x09, 0xd8, 0xb8, 0x06, 0xbb, 0x0d, 0x84, 0x3b, 0x0e, 0x7b, 0x12, 0xb9, 0x58, 0x3a, 0x75, 0xe2, + 0x12, 0x45, 0xbc, 0xe9, 0x6a, 0x27, 0x47, 0x0e, 0x47, 0x12, 0x63, 0x31, 0x1a, 0x8e, 0x23, 0xbd, + 0xe9, 0x08, 0xf7, 0x06, 0x64, 0x64, 0x56, 0x0b, 0xad, 0x4a, 0xd9, 0xf5, 0x34, 0x72, 0xa5, 0x7e, + 0xe7, 0x4b, 0x7a, 0x4f, 0x7c, 0x8d, 0x47, 0xec, 0x9e, 0x72, 0xfe, 0x04, 0x00, 0x00, 0xff, 0xff, + 0xd4, 0xf3, 0x60, 0x0b, 0x40, 0x01, 0x00, 0x00, +} diff --git a/pkg/proto/hapi/release/status.pb.go b/pkg/proto/hapi/release/status.pb.go new file mode 100644 index 000000000..178402c8e --- /dev/null +++ b/pkg/proto/hapi/release/status.pb.go @@ -0,0 +1,86 @@ +// Code generated by protoc-gen-go. +// source: hapi/release/status.proto +// DO NOT EDIT! + +package release + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" +import google_protobuf1 "github.com/golang/protobuf/ptypes/any" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +type Status_Code int32 + +const ( + Status_UNKNOWN Status_Code = 0 + Status_DEPLOYED Status_Code = 1 + Status_DELETED Status_Code = 2 + Status_SUPERSEDED Status_Code = 3 +) + +var Status_Code_name = map[int32]string{ + 0: "UNKNOWN", + 1: "DEPLOYED", + 2: "DELETED", + 3: "SUPERSEDED", +} +var Status_Code_value = map[string]int32{ + "UNKNOWN": 0, + "DEPLOYED": 1, + "DELETED": 2, + "SUPERSEDED": 3, +} + +func (x Status_Code) String() string { + return proto.EnumName(Status_Code_name, int32(x)) +} +func (Status_Code) EnumDescriptor() ([]byte, []int) { return fileDescriptor2, []int{0, 0} } + +// +// Status: +// +// +type Status struct { + Code Status_Code `protobuf:"varint,1,opt,name=code,enum=hapi.release.Status_Code" json:"code,omitempty"` + Details *google_protobuf1.Any `protobuf:"bytes,2,opt,name=details" json:"details,omitempty"` +} + +func (m *Status) Reset() { *m = Status{} } +func (m *Status) String() string { return proto.CompactTextString(m) } +func (*Status) ProtoMessage() {} +func (*Status) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{0} } + +func (m *Status) GetDetails() *google_protobuf1.Any { + if m != nil { + return m.Details + } + return nil +} + +func init() { + proto.RegisterType((*Status)(nil), "hapi.release.Status") + proto.RegisterEnum("hapi.release.Status_Code", Status_Code_name, Status_Code_value) +} + +var fileDescriptor2 = []byte{ + // 215 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x92, 0xcc, 0x48, 0x2c, 0xc8, + 0xd4, 0x2f, 0x4a, 0xcd, 0x49, 0x4d, 0x2c, 0x4e, 0xd5, 0x2f, 0x2e, 0x49, 0x2c, 0x29, 0x2d, 0xd6, + 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x01, 0x49, 0xe9, 0x41, 0xa5, 0xa4, 0x24, 0xd3, 0xf3, + 0xf3, 0xd3, 0x73, 0x52, 0xf5, 0xc1, 0x72, 0x49, 0xa5, 0x69, 0xfa, 0x89, 0x79, 0x95, 0x10, 0x85, + 0x4a, 0xcb, 0x19, 0xb9, 0xd8, 0x82, 0xc1, 0x3a, 0x85, 0x74, 0xb9, 0x58, 0x92, 0xf3, 0x53, 0x52, + 0x25, 0x18, 0x15, 0x18, 0x35, 0xf8, 0x8c, 0x24, 0xf5, 0x90, 0x8d, 0xd0, 0x83, 0xa8, 0xd1, 0x73, + 0x06, 0x2a, 0x08, 0x02, 0x2b, 0x13, 0xd2, 0xe3, 0x62, 0x4f, 0x49, 0x2d, 0x49, 0xcc, 0xcc, 0x29, + 0x96, 0x60, 0x02, 0xea, 0xe0, 0x36, 0x12, 0xd1, 0x83, 0x58, 0xa3, 0x07, 0xb3, 0x46, 0xcf, 0x31, + 0xaf, 0x32, 0x08, 0xa6, 0x48, 0xc9, 0x8e, 0x8b, 0x05, 0xa4, 0x5b, 0x88, 0x9b, 0x8b, 0x3d, 0xd4, + 0xcf, 0xdb, 0xcf, 0x3f, 0xdc, 0x4f, 0x80, 0x41, 0x88, 0x87, 0x8b, 0xc3, 0xc5, 0x35, 0xc0, 0xc7, + 0x3f, 0xd2, 0xd5, 0x45, 0x80, 0x11, 0x24, 0xe5, 0xe2, 0xea, 0xe3, 0x1a, 0x02, 0xe4, 0x30, 0x09, + 0xf1, 0x71, 0x71, 0x05, 0x87, 0x06, 0xb8, 0x06, 0x05, 0xbb, 0xba, 0x00, 0xf9, 0xcc, 0x4e, 0x9c, + 0x51, 0xec, 0x50, 0xc7, 0x24, 0xb1, 0x81, 0x6d, 0x30, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0x0d, + 0xcd, 0xe7, 0x6f, 0x01, 0x01, 0x00, 0x00, +} diff --git a/pkg/proto/hapi/services/probe.pb.go b/pkg/proto/hapi/services/probe.pb.go new file mode 100644 index 000000000..60b432820 --- /dev/null +++ b/pkg/proto/hapi/services/probe.pb.go @@ -0,0 +1,145 @@ +// Code generated by protoc-gen-go. +// source: hapi/services/probe.proto +// DO NOT EDIT! + +/* +Package services is a generated protocol buffer package. + +It is generated from these files: + hapi/services/probe.proto + hapi/services/tiller.proto + +It has these top-level messages: + ReadyRequest + ReadyResponse + ListReleasesRequest + ListReleasesResponse + GetReleaseStatusRequest + GetReleaseStatusResponse + GetReleaseContentRequest + GetReleaseContentResponse + UpdateReleaseRequest + UpdateReleaseResponse + InstallReleaseRequest + InstallReleaseResponse + UninstallReleaseRequest + UninstallReleaseResponse +*/ +package services + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" + +import ( + context "golang.org/x/net/context" + grpc "google.golang.org/grpc" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +const _ = proto.ProtoPackageIsVersion1 + +type ReadyRequest struct { +} + +func (m *ReadyRequest) Reset() { *m = ReadyRequest{} } +func (m *ReadyRequest) String() string { return proto.CompactTextString(m) } +func (*ReadyRequest) ProtoMessage() {} +func (*ReadyRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } + +type ReadyResponse struct { +} + +func (m *ReadyResponse) Reset() { *m = ReadyResponse{} } +func (m *ReadyResponse) String() string { return proto.CompactTextString(m) } +func (*ReadyResponse) ProtoMessage() {} +func (*ReadyResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} } + +func init() { + proto.RegisterType((*ReadyRequest)(nil), "hapi.services.probe.ReadyRequest") + proto.RegisterType((*ReadyResponse)(nil), "hapi.services.probe.ReadyResponse") +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion1 + +// Client API for ProbeService service + +type ProbeServiceClient interface { + Ready(ctx context.Context, in *ReadyRequest, opts ...grpc.CallOption) (*ReadyResponse, error) +} + +type probeServiceClient struct { + cc *grpc.ClientConn +} + +func NewProbeServiceClient(cc *grpc.ClientConn) ProbeServiceClient { + return &probeServiceClient{cc} +} + +func (c *probeServiceClient) Ready(ctx context.Context, in *ReadyRequest, opts ...grpc.CallOption) (*ReadyResponse, error) { + out := new(ReadyResponse) + err := grpc.Invoke(ctx, "/hapi.services.probe.ProbeService/Ready", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// Server API for ProbeService service + +type ProbeServiceServer interface { + Ready(context.Context, *ReadyRequest) (*ReadyResponse, error) +} + +func RegisterProbeServiceServer(s *grpc.Server, srv ProbeServiceServer) { + s.RegisterService(&_ProbeService_serviceDesc, srv) +} + +func _ProbeService_Ready_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { + in := new(ReadyRequest) + if err := dec(in); err != nil { + return nil, err + } + out, err := srv.(ProbeServiceServer).Ready(ctx, in) + if err != nil { + return nil, err + } + return out, nil +} + +var _ProbeService_serviceDesc = grpc.ServiceDesc{ + ServiceName: "hapi.services.probe.ProbeService", + HandlerType: (*ProbeServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Ready", + Handler: _ProbeService_Ready_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, +} + +var fileDescriptor0 = []byte{ + // 131 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x92, 0xcc, 0x48, 0x2c, 0xc8, + 0xd4, 0x2f, 0x4e, 0x2d, 0x2a, 0xcb, 0x4c, 0x4e, 0x2d, 0xd6, 0x2f, 0x28, 0xca, 0x4f, 0x4a, 0xd5, + 0x03, 0x92, 0x25, 0xf9, 0x42, 0xc2, 0x20, 0x29, 0x3d, 0x98, 0x94, 0x1e, 0x58, 0x4a, 0x89, 0x8f, + 0x8b, 0x27, 0x28, 0x35, 0x31, 0xa5, 0x32, 0x28, 0xb5, 0xb0, 0x34, 0xb5, 0xb8, 0x44, 0x89, 0x9f, + 0x8b, 0x17, 0xca, 0x2f, 0x2e, 0xc8, 0xcf, 0x2b, 0x4e, 0x35, 0x4a, 0xe0, 0xe2, 0x09, 0x00, 0xa9, + 0x0c, 0x86, 0xe8, 0x13, 0x0a, 0xe0, 0x62, 0x05, 0x2b, 0x10, 0x52, 0xd4, 0xc3, 0x62, 0x9e, 0x1e, + 0xb2, 0x61, 0x52, 0x4a, 0xf8, 0x94, 0x40, 0xcc, 0x57, 0x62, 0x70, 0xe2, 0x8a, 0xe2, 0x80, 0xa9, + 0x48, 0x62, 0x03, 0x3b, 0xd5, 0x18, 0x10, 0x00, 0x00, 0xff, 0xff, 0x65, 0x03, 0x07, 0x01, 0xc7, + 0x00, 0x00, 0x00, +} diff --git a/pkg/proto/hapi/services/tiller.pb.go b/pkg/proto/hapi/services/tiller.pb.go new file mode 100644 index 000000000..265544d6f --- /dev/null +++ b/pkg/proto/hapi/services/tiller.pb.go @@ -0,0 +1,566 @@ +// Code generated by protoc-gen-go. +// source: hapi/services/tiller.proto +// DO NOT EDIT! + +package services + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" +import hapi_chart3 "github.com/deis/tiller/pkg/proto/hapi/chart" +import hapi_chart "github.com/deis/tiller/pkg/proto/hapi/chart" +import hapi_release2 "github.com/deis/tiller/pkg/proto/hapi/release" +import hapi_release1 "github.com/deis/tiller/pkg/proto/hapi/release" + +import ( + context "golang.org/x/net/context" + grpc "google.golang.org/grpc" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// +// ListReleasesRequest: +// +// TODO +// +type ListReleasesRequest struct { + // The maximum number of releases to be returned + Limit int64 `protobuf:"varint,1,opt,name=limit" json:"limit,omitempty"` + // The zero-based offset at which the returned release list begins + Offset int64 `protobuf:"varint,2,opt,name=offset" json:"offset,omitempty"` +} + +func (m *ListReleasesRequest) Reset() { *m = ListReleasesRequest{} } +func (m *ListReleasesRequest) String() string { return proto.CompactTextString(m) } +func (*ListReleasesRequest) ProtoMessage() {} +func (*ListReleasesRequest) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{0} } + +// +// ListReleasesResponse: +// +// TODO +// +type ListReleasesResponse struct { + // The expected total number of releases to be returned + Count int64 `protobuf:"varint,1,opt,name=count" json:"count,omitempty"` + // The zero-based offset at which the list is positioned + Offset int64 `protobuf:"varint,2,opt,name=offset" json:"offset,omitempty"` + // The total number of queryable releases + Total int64 `protobuf:"varint,3,opt,name=total" json:"total,omitempty"` + // The resulting releases + Releases []*hapi_release2.Release `protobuf:"bytes,4,rep,name=releases" json:"releases,omitempty"` +} + +func (m *ListReleasesResponse) Reset() { *m = ListReleasesResponse{} } +func (m *ListReleasesResponse) String() string { return proto.CompactTextString(m) } +func (*ListReleasesResponse) ProtoMessage() {} +func (*ListReleasesResponse) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{1} } + +func (m *ListReleasesResponse) GetReleases() []*hapi_release2.Release { + if m != nil { + return m.Releases + } + return nil +} + +// GetReleaseStatusRequest is a request to get the status of a release. +type GetReleaseStatusRequest struct { + // Name is the name of the release + Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` +} + +func (m *GetReleaseStatusRequest) Reset() { *m = GetReleaseStatusRequest{} } +func (m *GetReleaseStatusRequest) String() string { return proto.CompactTextString(m) } +func (*GetReleaseStatusRequest) ProtoMessage() {} +func (*GetReleaseStatusRequest) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{2} } + +// GetReleaseStatusResponse is the response indicating the status of the named release. +type GetReleaseStatusResponse struct { + // Name is the name of the release. + Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` + // Info contains information about the release. + Info *hapi_release1.Info `protobuf:"bytes,2,opt,name=info" json:"info,omitempty"` +} + +func (m *GetReleaseStatusResponse) Reset() { *m = GetReleaseStatusResponse{} } +func (m *GetReleaseStatusResponse) String() string { return proto.CompactTextString(m) } +func (*GetReleaseStatusResponse) ProtoMessage() {} +func (*GetReleaseStatusResponse) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{3} } + +func (m *GetReleaseStatusResponse) GetInfo() *hapi_release1.Info { + if m != nil { + return m.Info + } + return nil +} + +// GetReleaseContentRequest is a request to get the contents of a release. +type GetReleaseContentRequest struct { + // The name of the release + Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` +} + +func (m *GetReleaseContentRequest) Reset() { *m = GetReleaseContentRequest{} } +func (m *GetReleaseContentRequest) String() string { return proto.CompactTextString(m) } +func (*GetReleaseContentRequest) ProtoMessage() {} +func (*GetReleaseContentRequest) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{4} } + +// GetReleaseContentResponse is a response containing the contents of a release. +type GetReleaseContentResponse struct { + // The release content + Release *hapi_release2.Release `protobuf:"bytes,1,opt,name=release" json:"release,omitempty"` +} + +func (m *GetReleaseContentResponse) Reset() { *m = GetReleaseContentResponse{} } +func (m *GetReleaseContentResponse) String() string { return proto.CompactTextString(m) } +func (*GetReleaseContentResponse) ProtoMessage() {} +func (*GetReleaseContentResponse) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{5} } + +func (m *GetReleaseContentResponse) GetRelease() *hapi_release2.Release { + if m != nil { + return m.Release + } + return nil +} + +// +// UpdateReleaseRequest: +// +// TODO +// +type UpdateReleaseRequest struct { +} + +func (m *UpdateReleaseRequest) Reset() { *m = UpdateReleaseRequest{} } +func (m *UpdateReleaseRequest) String() string { return proto.CompactTextString(m) } +func (*UpdateReleaseRequest) ProtoMessage() {} +func (*UpdateReleaseRequest) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{6} } + +// +// UpdateReleaseResponse: +// +// TODO +// +type UpdateReleaseResponse struct { +} + +func (m *UpdateReleaseResponse) Reset() { *m = UpdateReleaseResponse{} } +func (m *UpdateReleaseResponse) String() string { return proto.CompactTextString(m) } +func (*UpdateReleaseResponse) ProtoMessage() {} +func (*UpdateReleaseResponse) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{7} } + +// +// InstallReleaseRequest: +// +// TODO +// +type InstallReleaseRequest struct { + // Chart is the protobuf representation of a chart. + Chart *hapi_chart3.Chart `protobuf:"bytes,1,opt,name=chart" json:"chart,omitempty"` + // Values is a string containing (unparsed) TOML values. + Values *hapi_chart.Config `protobuf:"bytes,2,opt,name=values" json:"values,omitempty"` + // DryRun, if true, will run through the release logic, but neither create + // a release object nor deploy to Kubernetes. The release object returned + // in the response will be fake. + DryRun bool `protobuf:"varint,3,opt,name=dry_run,json=dryRun" json:"dry_run,omitempty"` +} + +func (m *InstallReleaseRequest) Reset() { *m = InstallReleaseRequest{} } +func (m *InstallReleaseRequest) String() string { return proto.CompactTextString(m) } +func (*InstallReleaseRequest) ProtoMessage() {} +func (*InstallReleaseRequest) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{8} } + +func (m *InstallReleaseRequest) GetChart() *hapi_chart3.Chart { + if m != nil { + return m.Chart + } + return nil +} + +func (m *InstallReleaseRequest) GetValues() *hapi_chart.Config { + if m != nil { + return m.Values + } + return nil +} + +// +// InstallReleaseResponse: +// +// TODO +// +type InstallReleaseResponse struct { + Release *hapi_release2.Release `protobuf:"bytes,1,opt,name=release" json:"release,omitempty"` +} + +func (m *InstallReleaseResponse) Reset() { *m = InstallReleaseResponse{} } +func (m *InstallReleaseResponse) String() string { return proto.CompactTextString(m) } +func (*InstallReleaseResponse) ProtoMessage() {} +func (*InstallReleaseResponse) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{9} } + +func (m *InstallReleaseResponse) GetRelease() *hapi_release2.Release { + if m != nil { + return m.Release + } + return nil +} + +// UninstallReleaseRequest represents a request to uninstall a named release. +type UninstallReleaseRequest struct { + // Name is the name of the release to delete. + Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` +} + +func (m *UninstallReleaseRequest) Reset() { *m = UninstallReleaseRequest{} } +func (m *UninstallReleaseRequest) String() string { return proto.CompactTextString(m) } +func (*UninstallReleaseRequest) ProtoMessage() {} +func (*UninstallReleaseRequest) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{10} } + +// UninstallReleaseResponse represents a successful response to an uninstall request. +type UninstallReleaseResponse struct { + // Release is the release that was marked deleted. + Release *hapi_release2.Release `protobuf:"bytes,1,opt,name=release" json:"release,omitempty"` +} + +func (m *UninstallReleaseResponse) Reset() { *m = UninstallReleaseResponse{} } +func (m *UninstallReleaseResponse) String() string { return proto.CompactTextString(m) } +func (*UninstallReleaseResponse) ProtoMessage() {} +func (*UninstallReleaseResponse) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{11} } + +func (m *UninstallReleaseResponse) GetRelease() *hapi_release2.Release { + if m != nil { + return m.Release + } + return nil +} + +func init() { + proto.RegisterType((*ListReleasesRequest)(nil), "hapi.services.tiller.ListReleasesRequest") + proto.RegisterType((*ListReleasesResponse)(nil), "hapi.services.tiller.ListReleasesResponse") + proto.RegisterType((*GetReleaseStatusRequest)(nil), "hapi.services.tiller.GetReleaseStatusRequest") + proto.RegisterType((*GetReleaseStatusResponse)(nil), "hapi.services.tiller.GetReleaseStatusResponse") + proto.RegisterType((*GetReleaseContentRequest)(nil), "hapi.services.tiller.GetReleaseContentRequest") + proto.RegisterType((*GetReleaseContentResponse)(nil), "hapi.services.tiller.GetReleaseContentResponse") + proto.RegisterType((*UpdateReleaseRequest)(nil), "hapi.services.tiller.UpdateReleaseRequest") + proto.RegisterType((*UpdateReleaseResponse)(nil), "hapi.services.tiller.UpdateReleaseResponse") + proto.RegisterType((*InstallReleaseRequest)(nil), "hapi.services.tiller.InstallReleaseRequest") + proto.RegisterType((*InstallReleaseResponse)(nil), "hapi.services.tiller.InstallReleaseResponse") + proto.RegisterType((*UninstallReleaseRequest)(nil), "hapi.services.tiller.UninstallReleaseRequest") + proto.RegisterType((*UninstallReleaseResponse)(nil), "hapi.services.tiller.UninstallReleaseResponse") +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion1 + +// Client API for ReleaseService service + +type ReleaseServiceClient interface { + // + // Retrieve release history. TODO: Allow filtering the set of releases by + // release status. By default, ListAllReleases returns the releases who + // current status is "Active". + // + ListReleases(ctx context.Context, in *ListReleasesRequest, opts ...grpc.CallOption) (ReleaseService_ListReleasesClient, error) + // + // Retrieve status information for the specified release. + // + GetReleaseStatus(ctx context.Context, in *GetReleaseStatusRequest, opts ...grpc.CallOption) (*GetReleaseStatusResponse, error) + // + // Retrieve the release content (chart + value) for the specifed release. + // + GetReleaseContent(ctx context.Context, in *GetReleaseContentRequest, opts ...grpc.CallOption) (*GetReleaseContentResponse, error) + // + // Update release content. + // + UpdateRelease(ctx context.Context, in *UpdateReleaseRequest, opts ...grpc.CallOption) (*UpdateReleaseResponse, error) + // + // Request release install. + // + InstallRelease(ctx context.Context, in *InstallReleaseRequest, opts ...grpc.CallOption) (*InstallReleaseResponse, error) + // + // Request release deletion. + // + UninstallRelease(ctx context.Context, in *UninstallReleaseRequest, opts ...grpc.CallOption) (*UninstallReleaseResponse, error) +} + +type releaseServiceClient struct { + cc *grpc.ClientConn +} + +func NewReleaseServiceClient(cc *grpc.ClientConn) ReleaseServiceClient { + return &releaseServiceClient{cc} +} + +func (c *releaseServiceClient) ListReleases(ctx context.Context, in *ListReleasesRequest, opts ...grpc.CallOption) (ReleaseService_ListReleasesClient, error) { + stream, err := grpc.NewClientStream(ctx, &_ReleaseService_serviceDesc.Streams[0], c.cc, "/hapi.services.tiller.ReleaseService/ListReleases", opts...) + if err != nil { + return nil, err + } + x := &releaseServiceListReleasesClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type ReleaseService_ListReleasesClient interface { + Recv() (*ListReleasesResponse, error) + grpc.ClientStream +} + +type releaseServiceListReleasesClient struct { + grpc.ClientStream +} + +func (x *releaseServiceListReleasesClient) Recv() (*ListReleasesResponse, error) { + m := new(ListReleasesResponse) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func (c *releaseServiceClient) GetReleaseStatus(ctx context.Context, in *GetReleaseStatusRequest, opts ...grpc.CallOption) (*GetReleaseStatusResponse, error) { + out := new(GetReleaseStatusResponse) + err := grpc.Invoke(ctx, "/hapi.services.tiller.ReleaseService/GetReleaseStatus", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *releaseServiceClient) GetReleaseContent(ctx context.Context, in *GetReleaseContentRequest, opts ...grpc.CallOption) (*GetReleaseContentResponse, error) { + out := new(GetReleaseContentResponse) + err := grpc.Invoke(ctx, "/hapi.services.tiller.ReleaseService/GetReleaseContent", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *releaseServiceClient) UpdateRelease(ctx context.Context, in *UpdateReleaseRequest, opts ...grpc.CallOption) (*UpdateReleaseResponse, error) { + out := new(UpdateReleaseResponse) + err := grpc.Invoke(ctx, "/hapi.services.tiller.ReleaseService/UpdateRelease", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *releaseServiceClient) InstallRelease(ctx context.Context, in *InstallReleaseRequest, opts ...grpc.CallOption) (*InstallReleaseResponse, error) { + out := new(InstallReleaseResponse) + err := grpc.Invoke(ctx, "/hapi.services.tiller.ReleaseService/InstallRelease", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *releaseServiceClient) UninstallRelease(ctx context.Context, in *UninstallReleaseRequest, opts ...grpc.CallOption) (*UninstallReleaseResponse, error) { + out := new(UninstallReleaseResponse) + err := grpc.Invoke(ctx, "/hapi.services.tiller.ReleaseService/UninstallRelease", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// Server API for ReleaseService service + +type ReleaseServiceServer interface { + // + // Retrieve release history. TODO: Allow filtering the set of releases by + // release status. By default, ListAllReleases returns the releases who + // current status is "Active". + // + ListReleases(*ListReleasesRequest, ReleaseService_ListReleasesServer) error + // + // Retrieve status information for the specified release. + // + GetReleaseStatus(context.Context, *GetReleaseStatusRequest) (*GetReleaseStatusResponse, error) + // + // Retrieve the release content (chart + value) for the specifed release. + // + GetReleaseContent(context.Context, *GetReleaseContentRequest) (*GetReleaseContentResponse, error) + // + // Update release content. + // + UpdateRelease(context.Context, *UpdateReleaseRequest) (*UpdateReleaseResponse, error) + // + // Request release install. + // + InstallRelease(context.Context, *InstallReleaseRequest) (*InstallReleaseResponse, error) + // + // Request release deletion. + // + UninstallRelease(context.Context, *UninstallReleaseRequest) (*UninstallReleaseResponse, error) +} + +func RegisterReleaseServiceServer(s *grpc.Server, srv ReleaseServiceServer) { + s.RegisterService(&_ReleaseService_serviceDesc, srv) +} + +func _ReleaseService_ListReleases_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(ListReleasesRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(ReleaseServiceServer).ListReleases(m, &releaseServiceListReleasesServer{stream}) +} + +type ReleaseService_ListReleasesServer interface { + Send(*ListReleasesResponse) error + grpc.ServerStream +} + +type releaseServiceListReleasesServer struct { + grpc.ServerStream +} + +func (x *releaseServiceListReleasesServer) Send(m *ListReleasesResponse) error { + return x.ServerStream.SendMsg(m) +} + +func _ReleaseService_GetReleaseStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { + in := new(GetReleaseStatusRequest) + if err := dec(in); err != nil { + return nil, err + } + out, err := srv.(ReleaseServiceServer).GetReleaseStatus(ctx, in) + if err != nil { + return nil, err + } + return out, nil +} + +func _ReleaseService_GetReleaseContent_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { + in := new(GetReleaseContentRequest) + if err := dec(in); err != nil { + return nil, err + } + out, err := srv.(ReleaseServiceServer).GetReleaseContent(ctx, in) + if err != nil { + return nil, err + } + return out, nil +} + +func _ReleaseService_UpdateRelease_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { + in := new(UpdateReleaseRequest) + if err := dec(in); err != nil { + return nil, err + } + out, err := srv.(ReleaseServiceServer).UpdateRelease(ctx, in) + if err != nil { + return nil, err + } + return out, nil +} + +func _ReleaseService_InstallRelease_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { + in := new(InstallReleaseRequest) + if err := dec(in); err != nil { + return nil, err + } + out, err := srv.(ReleaseServiceServer).InstallRelease(ctx, in) + if err != nil { + return nil, err + } + return out, nil +} + +func _ReleaseService_UninstallRelease_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { + in := new(UninstallReleaseRequest) + if err := dec(in); err != nil { + return nil, err + } + out, err := srv.(ReleaseServiceServer).UninstallRelease(ctx, in) + if err != nil { + return nil, err + } + return out, nil +} + +var _ReleaseService_serviceDesc = grpc.ServiceDesc{ + ServiceName: "hapi.services.tiller.ReleaseService", + HandlerType: (*ReleaseServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GetReleaseStatus", + Handler: _ReleaseService_GetReleaseStatus_Handler, + }, + { + MethodName: "GetReleaseContent", + Handler: _ReleaseService_GetReleaseContent_Handler, + }, + { + MethodName: "UpdateRelease", + Handler: _ReleaseService_UpdateRelease_Handler, + }, + { + MethodName: "InstallRelease", + Handler: _ReleaseService_InstallRelease_Handler, + }, + { + MethodName: "UninstallRelease", + Handler: _ReleaseService_UninstallRelease_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "ListReleases", + Handler: _ReleaseService_ListReleases_Handler, + ServerStreams: true, + }, + }, +} + +var fileDescriptor1 = []byte{ + // 540 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x9c, 0x55, 0xcd, 0x6e, 0xd3, 0x40, + 0x10, 0xae, 0x49, 0x9a, 0x86, 0x29, 0x54, 0x74, 0xc8, 0x8f, 0xf1, 0xa9, 0xda, 0x03, 0x94, 0x42, + 0x1d, 0x08, 0x6f, 0x40, 0x0e, 0x28, 0xa2, 0xa7, 0x45, 0xe5, 0xc0, 0x05, 0x99, 0x74, 0x43, 0x17, + 0x39, 0xeb, 0xe0, 0x5d, 0x47, 0xe2, 0x01, 0x38, 0xf2, 0x3e, 0x3c, 0x1e, 0xf6, 0xfe, 0x58, 0x71, + 0x62, 0x53, 0xab, 0x17, 0xb7, 0xbb, 0xdf, 0x37, 0xf3, 0xcd, 0xce, 0x7c, 0xa3, 0x40, 0x70, 0x1b, + 0xad, 0xf9, 0x44, 0xb2, 0x74, 0xc3, 0x17, 0x4c, 0x4e, 0x14, 0x8f, 0x63, 0x96, 0x86, 0xeb, 0x34, + 0x51, 0x09, 0x0e, 0x0a, 0x2c, 0x74, 0x58, 0x68, 0xb0, 0x60, 0xa4, 0x23, 0x16, 0xb7, 0x51, 0xaa, + 0xcc, 0xd7, 0xb0, 0x83, 0xf1, 0xf6, 0x7d, 0x22, 0x96, 0xfc, 0xbb, 0x05, 0x8c, 0x44, 0xca, 0x62, + 0x16, 0x49, 0xe6, 0xfe, 0x56, 0x82, 0x1c, 0xc6, 0xc5, 0x32, 0x31, 0x00, 0x99, 0xc1, 0xd3, 0x2b, + 0x2e, 0x15, 0x35, 0x88, 0xa4, 0xec, 0x67, 0xc6, 0xa4, 0xc2, 0x01, 0x1c, 0xc6, 0x7c, 0xc5, 0x95, + 0xef, 0x9d, 0x79, 0xe7, 0x1d, 0x6a, 0x0e, 0x38, 0x82, 0x5e, 0xb2, 0x5c, 0x4a, 0xa6, 0xfc, 0x07, + 0xfa, 0xda, 0x9e, 0xc8, 0x1f, 0x0f, 0x06, 0xd5, 0x2c, 0x72, 0x9d, 0x08, 0xc9, 0x8a, 0x34, 0x8b, + 0x24, 0x13, 0x65, 0x1a, 0x7d, 0x68, 0x4a, 0x53, 0xb0, 0x55, 0xa2, 0xa2, 0xd8, 0xef, 0x18, 0xb6, + 0x3e, 0xe0, 0x5b, 0xe8, 0xdb, 0xba, 0xa5, 0xdf, 0x3d, 0xeb, 0x9c, 0x1f, 0x4f, 0x87, 0xa1, 0x6e, + 0x98, 0x7b, 0xa1, 0x55, 0xa5, 0x25, 0x8d, 0x5c, 0xc2, 0xf8, 0x03, 0x73, 0xd5, 0x7c, 0x52, 0x91, + 0xca, 0xca, 0x87, 0x21, 0x74, 0x45, 0xb4, 0x62, 0xba, 0xa0, 0x87, 0x54, 0xff, 0x4f, 0x3e, 0x83, + 0xbf, 0x4f, 0xb7, 0x2f, 0xa8, 0xe1, 0xe3, 0x73, 0xe8, 0x16, 0x1d, 0xd4, 0xd5, 0x1f, 0x4f, 0xb1, + 0x5a, 0xcd, 0x3c, 0x47, 0xa8, 0xc6, 0x49, 0xb8, 0x9d, 0x77, 0x96, 0x08, 0xc5, 0x84, 0xfa, 0x5f, + 0x1d, 0x57, 0xf0, 0xac, 0x86, 0x6f, 0x0b, 0x99, 0xc0, 0x91, 0x95, 0xd0, 0x31, 0x8d, 0x5d, 0x70, + 0x2c, 0x32, 0x82, 0xc1, 0xf5, 0xfa, 0x26, 0x52, 0xcc, 0x21, 0x46, 0x99, 0x8c, 0x61, 0xb8, 0x73, + 0x6f, 0x14, 0xc8, 0x6f, 0x0f, 0x86, 0x73, 0x21, 0xf3, 0x9e, 0xc7, 0xd5, 0x10, 0x7c, 0x91, 0x8f, + 0xb1, 0xf0, 0x9b, 0x55, 0x3e, 0x35, 0xca, 0xc6, 0x94, 0xb3, 0xe2, 0x4b, 0x0d, 0x8e, 0x17, 0xd0, + 0xdb, 0x44, 0x71, 0x1e, 0x53, 0xed, 0x8d, 0x65, 0x6a, 0xb3, 0x52, 0xcb, 0xc0, 0x31, 0x1c, 0xdd, + 0xa4, 0xbf, 0xbe, 0xa6, 0x99, 0xd0, 0xf3, 0xee, 0xd3, 0x5e, 0x7e, 0xa4, 0x99, 0x20, 0x73, 0x18, + 0xed, 0x96, 0x71, 0xdf, 0x1e, 0xe4, 0x46, 0xb8, 0x16, 0xbc, 0xf6, 0x4d, 0x75, 0x03, 0xf8, 0x08, + 0xfe, 0x3e, 0xfd, 0x9e, 0xda, 0xd3, 0xbf, 0x87, 0x70, 0xe2, 0x3c, 0x65, 0x56, 0x1b, 0x39, 0x3c, + 0xda, 0x5e, 0x13, 0x7c, 0x19, 0xd6, 0x6d, 0x7e, 0x58, 0xb3, 0x90, 0xc1, 0x45, 0x1b, 0xaa, 0x1d, + 0xe4, 0xc1, 0x1b, 0x0f, 0x25, 0x3c, 0xd9, 0xf5, 0x34, 0x5e, 0xd6, 0xe7, 0x68, 0x58, 0x95, 0x20, + 0x6c, 0x4b, 0x77, 0xb2, 0xb8, 0x81, 0xd3, 0x3d, 0x03, 0xe3, 0x9d, 0x69, 0xaa, 0x9b, 0x11, 0x4c, + 0x5a, 0xf3, 0x4b, 0xdd, 0x1f, 0xf0, 0xb8, 0x62, 0x69, 0x6c, 0xe8, 0x56, 0xdd, 0x3e, 0x04, 0xaf, + 0x5a, 0x71, 0x4b, 0xad, 0x15, 0x9c, 0x54, 0xdd, 0x89, 0x0d, 0x09, 0x6a, 0x57, 0x29, 0x78, 0xdd, + 0x8e, 0x5c, 0xca, 0xe5, 0x73, 0xdc, 0xb5, 0x64, 0xd3, 0x1c, 0x1b, 0x9c, 0xde, 0x34, 0xc7, 0x26, + 0xa7, 0x93, 0x83, 0xf7, 0xf0, 0xa5, 0xef, 0xd8, 0xdf, 0x7a, 0xfa, 0x77, 0xe2, 0xdd, 0xbf, 0x00, + 0x00, 0x00, 0xff, 0xff, 0x8c, 0xd6, 0xc7, 0x2c, 0xc1, 0x06, 0x00, 0x00, +} diff --git a/pkg/repo/local.go b/pkg/repo/local.go new file mode 100644 index 000000000..c85e1e618 --- /dev/null +++ b/pkg/repo/local.go @@ -0,0 +1,136 @@ +package repo + +import ( + "fmt" + "io/ioutil" + "net/http" + "path/filepath" + "strings" + + "github.com/deis/tiller/pkg/chart" + "gopkg.in/yaml.v2" +) + +var localRepoPath string + +// CacheFile represents the cache file in a chart repository +type CacheFile struct { + Entries map[string]*ChartRef +} + +// ChartRef represents a chart entry in the CacheFile +type ChartRef struct { + Name string + URL string +} + +// StartLocalRepo starts a web server and serves files from the given path +func StartLocalRepo(path string) { + fmt.Println("Now serving you on localhost:8879...") + localRepoPath = path + http.HandleFunc("/", rootHandler) + http.HandleFunc("/charts/", indexHandler) + http.ListenAndServe(":8879", nil) +} +func rootHandler(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Welcome to the Kubernetes Package manager!\nBrowse charts on localhost:8879/charts!") +} +func indexHandler(w http.ResponseWriter, r *http.Request) { + file := r.URL.Path[len("/charts/"):] + if len(strings.Split(file, ".")) > 1 { + serveFile(w, r, file) + } else if file == "" { + fmt.Fprintf(w, "list of charts should be here at some point") + } else if file == "cache" { + fmt.Fprintf(w, "cache file data should be here at some point") + } else { + fmt.Fprintf(w, "Ummm... Nothing to see here folks") + } +} + +func serveFile(w http.ResponseWriter, r *http.Request, file string) { + http.ServeFile(w, r, filepath.Join(localRepoPath, file)) +} + +// AddChartToLocalRepo saves a chart in the given path and then reindexes the cache file +func AddChartToLocalRepo(ch *chart.Chart, path string) error { + name, err := chart.Save(ch, path) + if err != nil { + return err + } + err = ReindexCacheFile(ch, path+"/cache.yaml") + if err != nil { + return err + } + fmt.Printf("Saved %s to $HELM_HOME/local", name) + return nil +} + +// LoadCacheFile takes a file at the given path and returns a CacheFile object +func LoadCacheFile(path string) (*CacheFile, error) { + b, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + + //TODO: change variable name - y is not helpful :P + var y CacheFile + err = yaml.Unmarshal(b, &y) + if err != nil { + return nil, err + } + return &y, nil +} + +// ReindexCacheFile adds an entry to the cache file at the given path +func ReindexCacheFile(ch *chart.Chart, path string) error { + name := ch.Chartfile().Name + "-" + ch.Chartfile().Version + y, err := LoadCacheFile(path) + if err != nil { + return err + } + found := false + for k := range y.Entries { + if k == name { + found = true + break + } + } + if !found { + url := "localhost:8879/charts/" + name + ".tgz" + + out, err := y.insertChartEntry(name, url) + if err != nil { + return err + } + + ioutil.WriteFile(path, out, 0644) + } + return nil +} + +// UnmarshalYAML unmarshals the cache file +func (c *CacheFile) UnmarshalYAML(unmarshal func(interface{}) error) error { + var refs map[string]*ChartRef + if err := unmarshal(&refs); err != nil { + if _, ok := err.(*yaml.TypeError); !ok { + return err + } + } + c.Entries = refs + return nil +} + +func (c *CacheFile) insertChartEntry(name string, url string) ([]byte, error) { + if c.Entries == nil { + c.Entries = make(map[string]*ChartRef) + } + entry := ChartRef{Name: name, URL: url} + c.Entries[name] = &entry + out, err := yaml.Marshal(&c.Entries) + if err != nil { + return nil, err + } + + return out, nil +} diff --git a/pkg/repo/repo.go b/pkg/repo/repo.go new file mode 100644 index 000000000..13138c10d --- /dev/null +++ b/pkg/repo/repo.go @@ -0,0 +1,40 @@ +package repo + +import ( + "io/ioutil" + + "gopkg.in/yaml.v2" +) + +// RepoFile represents the .repositories file in $HELM_HOME +type RepoFile struct { + Repositories map[string]string +} + +// LoadRepositoriesFile takes a file at the given path and returns a RepoFile object +func LoadRepositoriesFile(path string) (*RepoFile, error) { + b, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + + var r RepoFile + err = yaml.Unmarshal(b, &r) + if err != nil { + return nil, err + } + + return &r, nil +} + +// UnmarshalYAML unmarshals the repo file +func (rf *RepoFile) UnmarshalYAML(unmarshal func(interface{}) error) error { + var repos map[string]string + if err := unmarshal(&repos); err != nil { + if _, ok := err.(*yaml.TypeError); !ok { + return err + } + } + rf.Repositories = repos + return nil +} diff --git a/pkg/storage/doc.go b/pkg/storage/doc.go new file mode 100644 index 000000000..21e11ade4 --- /dev/null +++ b/pkg/storage/doc.go @@ -0,0 +1,7 @@ +/*Package storage implements storage for Tiller objects. + +Tiller stores releases (see 'cmd/tiller/environment'.Environment). The backend +storage mechanism may be implemented with different backends. This package +and its subpackages provide storage layers for Tiller objects. +*/ +package storage diff --git a/pkg/storage/memory.go b/pkg/storage/memory.go new file mode 100644 index 000000000..bc57d7c4d --- /dev/null +++ b/pkg/storage/memory.go @@ -0,0 +1,91 @@ +package storage + +import ( + "errors" + "sync" + + "github.com/deis/tiller/pkg/proto/hapi/release" +) + +// Memory is an in-memory ReleaseStorage implementation. +type Memory struct { + sync.RWMutex + releases map[string]*release.Release +} + +// NewMemory creates a new in-memory storage. +func NewMemory() *Memory { + return &Memory{ + releases: map[string]*release.Release{}, + } +} + +// ErrNotFound indicates that a release is not found. +var ErrNotFound = errors.New("release not found") + +// Read returns the named Release. +// +// If the release is not found, an ErrNotFound error is returned. +func (m *Memory) Read(k string) (*release.Release, error) { + m.RLock() + defer m.RUnlock() + v, ok := m.releases[k] + if !ok { + return v, ErrNotFound + } + return v, nil +} + +// Create sets a release. +func (m *Memory) Create(rel *release.Release) error { + m.Lock() + defer m.Unlock() + m.releases[rel.Name] = rel + return nil +} + +// Update sets a release. +func (m *Memory) Update(rel *release.Release) error { + m.Lock() + defer m.Unlock() + if _, ok := m.releases[rel.Name]; !ok { + return ErrNotFound + } + + // FIXME: When Release is done, we need to do this right by marking the old + // release as superseded, and creating a new release. + m.releases[rel.Name] = rel + return nil +} + +// Delete removes a release. +func (m *Memory) Delete(name string) (*release.Release, error) { + m.Lock() + defer m.Unlock() + rel, ok := m.releases[name] + if !ok { + return nil, ErrNotFound + } + delete(m.releases, name) + return rel, nil +} + +// List returns all releases. +func (m *Memory) List() ([]*release.Release, error) { + m.RLock() + defer m.RUnlock() + buf := make([]*release.Release, len(m.releases)) + i := 0 + for _, v := range m.releases { + buf[i] = v + i++ + } + return buf, nil +} + +// Query searches all releases for matches. +func (m *Memory) Query(labels map[string]string) ([]*release.Release, error) { + m.RLock() + defer m.RUnlock() + return []*release.Release{}, errors.New("Cannot implement until release.Release is defined.") +} diff --git a/pkg/storage/memory_test.go b/pkg/storage/memory_test.go new file mode 100644 index 000000000..d2105b928 --- /dev/null +++ b/pkg/storage/memory_test.go @@ -0,0 +1,87 @@ +package storage + +import ( + "testing" + + "github.com/deis/tiller/pkg/proto/hapi/release" +) + +func TestCreate(t *testing.T) { + k := "test-1" + r := &release.Release{Name: k} + + ms := NewMemory() + if err := ms.Create(r); err != nil { + t.Fatalf("Failed create: %s", err) + } + + if ms.releases[k].Name != k { + t.Errorf("Unexpected release name: %s", ms.releases[k].Name) + } +} + +func TestRead(t *testing.T) { + k := "test-1" + r := &release.Release{Name: k} + + ms := NewMemory() + ms.Create(r) + + if out, err := ms.Read(k); err != nil { + t.Errorf("Could not get %s: %s", k, err) + } else if out.Name != k { + t.Errorf("Expected %s, got %s", k, out.Name) + } +} + +func TestUpdate(t *testing.T) { + k := "test-1" + r := &release.Release{Name: k} + + ms := NewMemory() + if err := ms.Create(r); err != nil { + t.Fatalf("Failed create: %s", err) + } + if err := ms.Update(r); err != nil { + t.Fatalf("Failed update: %s", err) + } + + if ms.releases[k].Name != k { + t.Errorf("Unexpected release name: %s", ms.releases[k].Name) + } +} + +func TestList(t *testing.T) { + ms := NewMemory() + rels := []string{"a", "b", "c"} + + for _, k := range rels { + ms.Create(&release.Release{Name: k}) + } + + l, err := ms.List() + if err != nil { + t.Error(err) + } + + if len(l) != 3 { + t.Errorf("Expected 3, got %d", len(l)) + } + + for _, n := range rels { + foundN := false + for _, rr := range l { + if rr.Name == n { + foundN = true + break + } + } + if !foundN { + t.Errorf("Did not find %s in list.", n) + } + } +} + +func TestQuery(t *testing.T) { + t.Skip("Not Implemented") +} diff --git a/pkg/timeconv/doc.go b/pkg/timeconv/doc.go new file mode 100644 index 000000000..42140ff35 --- /dev/null +++ b/pkg/timeconv/doc.go @@ -0,0 +1,7 @@ +/*Package timeconv contains utilities for converting time. + +The gRPC/Protobuf libraries contain time implementations that require conversion +to and from Go times. This library provides utilities and convenience functions +for performing conversions. +*/ +package timeconv diff --git a/pkg/timeconv/timeconv.go b/pkg/timeconv/timeconv.go new file mode 100644 index 000000000..8a3fb31c4 --- /dev/null +++ b/pkg/timeconv/timeconv.go @@ -0,0 +1,32 @@ +package timeconv + +import ( + "time" + + "github.com/golang/protobuf/ptypes/timestamp" +) + +// Now creates a timestamp.Timestamp representing the current time. +func Now() *timestamp.Timestamp { + return Timestamp(time.Now()) +} + +// Timestamp converts a time.Time to a protobuf *timestamp.Timestamp. +func Timestamp(t time.Time) *timestamp.Timestamp { + return ×tamp.Timestamp{ + Seconds: t.Unix(), + Nanos: int32(t.Nanosecond()), + } +} + +// Time converts a protobuf *timestamp.Timestamp to a time.Time. +func Time(ts *timestamp.Timestamp) time.Time { + return time.Unix(ts.Seconds, int64(ts.Nanos)) +} + +// Format formats a *timestamp.Timestamp into a string. +// +// This follows the rules for time.Time.Format(). +func Format(ts *timestamp.Timestamp, layout string) string { + return Time(ts).Format(layout) +} diff --git a/pkg/timeconv/timeconv_test.go b/pkg/timeconv/timeconv_test.go new file mode 100644 index 000000000..1090155b8 --- /dev/null +++ b/pkg/timeconv/timeconv_test.go @@ -0,0 +1,46 @@ +package timeconv + +import ( + "testing" + "time" +) + +func TestNow(t *testing.T) { + now := time.Now() + ts := Now() + var drift int64 = 5 + if ts.Seconds < int64(now.Second())-drift { + t.Errorf("Unexpected time drift: %d", ts.Seconds) + } +} + +func TestTimestamp(t *testing.T) { + now := time.Now() + ts := Timestamp(now) + + if now.Unix() != ts.Seconds { + t.Errorf("Unexpected time drift: %d to %d", now.Second(), ts.Seconds) + } + + if now.Nanosecond() != int(ts.Nanos) { + t.Errorf("Unexpected nano drift: %d to %d", now.Nanosecond(), ts.Nanos) + } +} + +func TestTime(t *testing.T) { + nowts := Now() + now := Time(nowts) + + if now.Unix() != nowts.Seconds { + t.Errorf("Unexpected time drift %d", now.Unix()) + } +} + +func TestFormat(t *testing.T) { + now := time.Now() + nowts := Timestamp(now) + + if now.Format(time.ANSIC) != Format(nowts, time.ANSIC) { + t.Error("Format mismatch") + } +} diff --git a/rootfs/Dockerfile b/rootfs/Dockerfile new file mode 100644 index 000000000..54760d87e --- /dev/null +++ b/rootfs/Dockerfile @@ -0,0 +1,8 @@ +FROM alpine:3.3 + +COPY . / + +EXPOSE 44134 + +CMD ["/tiller"] + diff --git a/rootfs/README.md b/rootfs/README.md new file mode 100644 index 000000000..cf8b2e61e --- /dev/null +++ b/rootfs/README.md @@ -0,0 +1,27 @@ +# RootFS + +This directory stores all files that should be copied to the rootfs of a +Docker container. The files should be stored according to the correct +directory structure of the destination container. For example: + +``` +rootfs/bin -> /bin +rootfs/usr/local/share -> /usr/local/share +``` + +## Dockerfile + +A Dockerfile in the rootfs is used to build the image. Where possible, +compilation should not be done in this Dockerfile, since we are +interested in deploying the smallest possible images. + +Example: + +```Dockerfile +FROM alpine:3.2 + +COPY . / + +ENTRYPOINT ["/usr/local/bin/boot"] +``` + diff --git a/scripts/cluster/kube-system.yaml b/scripts/cluster/kube-system.yaml new file mode 100644 index 000000000..986f4b482 --- /dev/null +++ b/scripts/cluster/kube-system.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: kube-system diff --git a/scripts/cluster/skydns.yaml b/scripts/cluster/skydns.yaml new file mode 100644 index 000000000..720877d5d --- /dev/null +++ b/scripts/cluster/skydns.yaml @@ -0,0 +1,137 @@ +apiVersion: v1 +kind: ReplicationController +metadata: + name: kube-dns-v10 + namespace: kube-system + labels: + k8s-app: kube-dns + version: v10 + kubernetes.io/cluster-service: "true" +spec: + replicas: 1 + selector: + k8s-app: kube-dns + version: v10 + template: + metadata: + labels: + k8s-app: kube-dns + version: v10 + kubernetes.io/cluster-service: "true" + spec: + containers: + - name: etcd + image: gcr.io/google_containers/etcd-amd64:2.2.1 + resources: + # keep request = limit to keep this container in guaranteed class + limits: + cpu: 100m + memory: 50Mi + requests: + cpu: 100m + memory: 50Mi + command: + - /usr/local/bin/etcd + - -data-dir + - /var/etcd/data + - -listen-client-urls + - http://127.0.0.1:2379,http://127.0.0.1:4001 + - -advertise-client-urls + - http://127.0.0.1:2379,http://127.0.0.1:4001 + - -initial-cluster-token + - skydns-etcd + volumeMounts: + - name: etcd-storage + mountPath: /var/etcd/data + - name: kube2sky + image: gcr.io/google_containers/kube2sky:1.12 + resources: + # keep request = limit to keep this container in guaranteed class + limits: + cpu: 100m + memory: 50Mi + requests: + cpu: 100m + memory: 50Mi + args: + # command = "/kube2sky" + - --domain=cluster.local + - name: skydns + image: gcr.io/google_containers/skydns:2015-10-13-8c72f8c + resources: + # keep request = limit to keep this container in guaranteed class + limits: + cpu: 100m + memory: 50Mi + requests: + cpu: 100m + memory: 50Mi + args: + # command = "/skydns" + - -machines=http://127.0.0.1:4001 + - -addr=0.0.0.0:53 + - -ns-rotate=false + - -domain=cluster.local. + ports: + - containerPort: 53 + name: dns + protocol: UDP + - containerPort: 53 + name: dns-tcp + protocol: TCP + livenessProbe: + httpGet: + path: /healthz + port: 8080 + scheme: HTTP + initialDelaySeconds: 30 + timeoutSeconds: 5 + readinessProbe: + httpGet: + path: /healthz + port: 8080 + scheme: HTTP + initialDelaySeconds: 1 + timeoutSeconds: 5 + - name: healthz + image: gcr.io/google_containers/exechealthz:1.0 + resources: + # keep request = limit to keep this container in guaranteed class + limits: + cpu: 10m + memory: 20Mi + requests: + cpu: 10m + memory: 20Mi + args: + - -cmd=nslookup kubernetes.default.svc.cluster.local 127.0.0.1 >/dev/null + - -port=8080 + ports: + - containerPort: 8080 + protocol: TCP + volumes: + - name: etcd-storage + emptyDir: {} + dnsPolicy: Default # Don't use cluster DNS. +--- +apiVersion: v1 +kind: Service +metadata: + name: kube-dns + namespace: kube-system + labels: + k8s-app: kube-dns + kubernetes.io/cluster-service: "true" + kubernetes.io/name: "KubeDNS" +spec: + selector: + k8s-app: kube-dns + clusterIP: 10.0.0.10 + ports: + - name: dns + port: 53 + protocol: UDP + - name: dns-tcp + port: 53 + protocol: TCP + diff --git a/scripts/coverage.sh b/scripts/coverage.sh new file mode 100755 index 000000000..9d6763a1e --- /dev/null +++ b/scripts/coverage.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -euo pipefail + +COVERDIR=${COVERDIR:-.coverage} +COVERMODE=${COVERMODE:-atomic} +PACKAGES=($(go list $(glide novendor))) + +if [[ ! -d "$COVERDIR" ]]; then + mkdir -p "$COVERDIR" +fi + +echo "mode: ${COVERMODE}" > "${COVERDIR}/coverage.out" + +for d in "${PACKAGES[@]}"; do + go test -coverprofile=profile.out -covermode="$COVERMODE" "$d" + if [ -f profile.out ]; then + sed "/mode: $COVERMODE/d" profile.out >> "${COVERDIR}/coverage.out" + rm profile.out + fi +done + +go tool cover -html "${COVERDIR}/coverage.out" -o "${COVERDIR}/coverage.html" diff --git a/scripts/local-cluster.sh b/scripts/local-cluster.sh new file mode 100755 index 000000000..7cc04b71b --- /dev/null +++ b/scripts/local-cluster.sh @@ -0,0 +1,331 @@ +#!/usr/bin/env bash + +# Bash 'Strict Mode' +# http://redsymbol.net/articles/unofficial-bash-strict-mode +set -euo pipefail +IFS=$'\n\t' + +HELM_ROOT="${BASH_SOURCE[0]%/*}/.." +cd "$HELM_ROOT" + +# Globals ---------------------------------------------------------------------- + +KUBE_VERSION=${KUBE_VERSION:-} +KUBE_PORT=${KUBE_PORT:-8080} +KUBE_CONTEXT=${KUBE_CONTEXT:-docker} +KUBECTL=${KUBECTL:-kubectl} +ENABLE_CLUSTER_DNS=${KUBE_ENABLE_CLUSTER_DNS:-true} +LOG_LEVEL=${LOG_LEVEL:-2} + +# Helper Functions ------------------------------------------------------------- + +# Display error message and exit +error_exit() { + echo "error: ${1:-"unknown error"}" 1>&2 + exit 1 +} + +# Checks if a command exists. Returns 1 or 0 +command_exists() { + hash "${1}" 2>/dev/null +} + +# Program Functions ------------------------------------------------------------ + +# Check host platform and docker host +verify_prereqs() { + echo "Verifying Prerequisites...." + + case "$(uname -s)" in + Darwin) + host_os=darwin + ;; + Linux) + host_os=linux + ;; + *) + error_exit "Unsupported host OS. Must be Linux or Mac OS X." + ;; + esac + + case "$(uname -m)" in + x86_64*) + host_arch=amd64 + ;; + i?86_64*) + host_arch=amd64 + ;; + amd64*) + host_arch=amd64 + ;; + arm*) + host_arch=arm + ;; + i?86*) + host_arch=x86 + ;; + s390x*) + host_arch=s390x + ;; + ppc64le*) + host_arch=ppc64le + ;; + *) + error_exit "Unsupported host arch. Must be x86_64, 386, arm, s390x or ppc64le." + ;; + esac + + + command_exists docker || error_exit "You need docker" + + if ! docker info > /dev/null 2>&1 ; then + error_exit "Can't connect to 'docker' daemon." + fi + + $KUBECTL version --client >/dev/null || download_kubectl +} + +# Get the latest stable release tag +get_latest_version_number() { + local -r latest_url="https://storage.googleapis.com/kubernetes-release/release/stable.txt" + if command_exists wget ; then + wget -qO- ${latest_url} + elif command_exists curl ; then + curl -Ss ${latest_url} + else + error_exit "Couldn't find curl or wget. Bailing out." + fi +} + +# Detect ip address od docker host +detect_docker_host_ip() { + if [ -n "${DOCKER_HOST:-}" ]; then + awk -F'[/:]' '{print $4}' <<< "$DOCKER_HOST" + else + ifconfig docker0 \ + | grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' \ + | grep -Eo '([0-9]*\.){3}[0-9]*' >/dev/null 2>&1 || : + fi +} + +# Set KUBE_MASTER_IP from docker host ip. Defaults to localhost +set_master_ip() { + local docker_ip + + if [[ -z "${KUBE_MASTER_IP:-}" ]]; then + docker_ip=$(detect_docker_host_ip) + if [[ -n "${docker_ip}" ]]; then + KUBE_MASTER_IP="${docker_ip}" + else + KUBE_MASTER_IP=localhost + fi + fi +} + +# Start dockerized kubelet +start_kubernetes() { + echo "Starting kubelet" + + # Enable dns + if [[ "${ENABLE_CLUSTER_DNS}" = true ]]; then + dns_args="--cluster-dns=10.0.0.1 --cluster-domain=cluster.local" + else + # DNS server for real world hostnames. + dns_args="--cluster-dns=8.8.8.8" + fi + + local start_time=$(date +%s) + + docker run \ + --name=kubelet \ + --volume=/:/rootfs:ro \ + --volume=/sys:/sys:ro \ + --volume=/var/lib/docker/:/var/lib/docker:rw \ + --volume=/var/lib/kubelet/:/var/lib/kubelet:rw \ + --volume=/var/run:/var/run:rw \ + --net=host \ + --pid=host \ + --privileged=true \ + -d \ + gcr.io/google_containers/hyperkube-amd64:${KUBE_VERSION} \ + /hyperkube kubelet \ + --containerized \ + --hostname-override="127.0.0.1" \ + --api-servers=http://localhost:8080 \ + --config=/etc/kubernetes/manifests \ + --allow-privileged=true \ + ${dns_args} \ + --v=${LOG_LEVEL} >/dev/null + + # We expect to have at least 3 running pods - etcd, master and kube-proxy. + local attempt=1 + while (($($KUBECTL get pods --no-headers 2>/dev/null | grep -c "Running") < 3)); do + echo -n "." + sleep $(( attempt++ )) + done + echo + + local end_time=$(date +%s) + echo "Started master components in $((end_time - start_time)) seconds." +} + +# Open kubernetes master api port. +setup_firewall() { + [[ -n "${DOCKER_MACHINE_NAME}" ]] || return + + echo "Adding iptables hackery for docker-machine..." + + local machine_ip + machine_ip=$(docker-machine ip "$DOCKER_MACHINE_NAME") + local iptables_rule="PREROUTING -p tcp -d ${machine_ip} --dport ${KUBE_PORT} -j DNAT --to-destination 127.0.0.1:${KUBE_PORT}" + + if ! docker-machine ssh "${DOCKER_MACHINE_NAME}" "sudo /usr/local/sbin/iptables -t nat -C ${iptables_rule}" &> /dev/null; then + docker-machine ssh "${DOCKER_MACHINE_NAME}" "sudo /usr/local/sbin/iptables -t nat -I ${iptables_rule}" + fi +} + +# Create kube-system namespace in kubernetes +create_kube_system_namespace() { + echo "Creating kube-system namespace..." + + $KUBECTL create -f ./scripts/cluster/kube-system.yaml >/dev/null +} + +# Activate skydns in kubernetes and wait for pods to be ready. +create_kube_dns() { + [[ "${ENABLE_CLUSTER_DNS}" = true ]] || return + + local start_time=$(date +%s) + + echo "Setting up cluster dns..." + + $KUBECTL create -f ./scripts/cluster/skydns.yaml >/dev/null + + echo "Waiting for cluster DNS to become available..." + + local attempt=1 + until $KUBECTL get pods --no-headers --namespace kube-system --selector=k8s-app=kube-dns 2>/dev/null | grep "Running" &>/dev/null; do + echo -n "." + sleep $(( attempt++ )) + done + echo + local end_time=$(date +%s) + echo "Started DNS in $((end_time - start_time)) seconds." +} + +# Generate kubeconfig data for the created cluster. +generate_kubeconfig() { + local cluster_args=( + "--server=http://${KUBE_MASTER_IP}:${KUBE_PORT}" + "--insecure-skip-tls-verify=true" + ) + + $KUBECTL config set-cluster "${KUBE_CONTEXT}" "${cluster_args[@]}" >/dev/null + $KUBECTL config set-context "${KUBE_CONTEXT}" --cluster="${KUBE_CONTEXT}" >/dev/null + $KUBECTL config use-context "${KUBE_CONTEXT}" >/dev/null + + echo "Wrote config for kubeconfig using context: '${KUBE_CONTEXT}'" +} + +# Download kubectl +download_kubectl() { + echo "Downloading kubectl binary..." + + kubectl_url="https://storage.googleapis.com/kubernetes-release/release/${KUBE_VERSION}/bin/${host_os}/${host_arch}/kubectl" + if command_exists wget; then + wget -O ./bin/kubectl "${kubectl_url}" + elif command_exists curl; then + curl -sSOL ./bin/kubectl "${kubectl_url}" + else + error_exit "Couldn't find curl or wget. Bailing out." + fi + chmod a+x ./bin/kubectl + + KUBECTL=./bin/kubectl +} + +# Clean volumes that are left by kubelet +# +# https://github.com/kubernetes/kubernetes/issues/23197 +# code stolen from https://github.com/huggsboson/docker-compose-kubernetes/blob/SwitchToSharedMount/kube-up.sh +clean_volumes() { + if [[ -n "${DOCKER_MACHINE_NAME}" ]]; then + docker-machine ssh "${DOCKER_MACHINE_NAME}" "mount | grep -o 'on /var/lib/kubelet.* type' | cut -c 4- | rev | cut -c 6- | rev | sort -r | xargs --no-run-if-empty sudo umount" + docker-machine ssh "${DOCKER_MACHINE_NAME}" "sudo rm -Rf /var/lib/kubelet" + docker-machine ssh "${DOCKER_MACHINE_NAME}" "sudo mkdir -p /var/lib/kubelet" + docker-machine ssh "${DOCKER_MACHINE_NAME}" "sudo mount --bind /var/lib/kubelet /var/lib/kubelet" + docker-machine ssh "${DOCKER_MACHINE_NAME}" "sudo mount --make-shared /var/lib/kubelet" + else + mount | grep -o 'on /var/lib/kubelet.* type' | cut -c 4- | rev | cut -c 6- | rev | sort -r | xargs --no-run-if-empty sudo umount + sudo rm -Rf /var/lib/kubelet + sudo mkdir -p /var/lib/kubelet + sudo mount --bind /var/lib/kubelet /var/lib/kubelet + sudo mount --make-shared /var/lib/kubelet + fi +} + +# Helper function to properly remove containers +delete_container() { + local container=("$@") + docker stop "${container[@]}" &>/dev/null || : + docker wait "${container[@]}" &>/dev/null || : + docker rm --force --volumes "${container[@]}" &>/dev/null || : +} + +# Delete master components and resources in kubernetes. +kube_down() { + echo "Deleting all resources in kubernetes..." + $KUBECTL delete replicationcontrollers,services,pods,secrets --all >/dev/null 2>&1 || : + $KUBECTL delete replicationcontrollers,services,pods,secrets --all --namespace=kube-system >/dev/null 2>&1 || : + $KUBECTL delete namespace kube-system >/dev/null 2>&1 || : + + echo "Stopping kubelet..." + delete_container kubelet + + echo "Stopping remaining kubernetes containers..." + local kube_containers=($(docker ps -aqf "name=k8s_")) + if [[ "${#kube_containers[@]}" -gt 0 ]]; then + delete_container "${kube_containers[@]}" + fi +} + +# Start a kubernetes cluster in docker. +kube_up() { + verify_prereqs + + set_master_ip + clean_volumes + setup_firewall + + start_kubernetes + generate_kubeconfig + create_kube_system_namespace + create_kube_dns + + $KUBECTL cluster-info +} + +KUBE_VERSION=${KUBE_VERSION:-$(get_latest_version_number)} + +# Main ------------------------------------------------------------------------- + +main() { + case "$1" in + up|start) + kube_up + ;; + down|stop) + kube_down + ;; + restart) + kube_down + kube_up + ;; + *) + echo "Usage: $0 {up|down|restart}" + ;; + esac +} + +main "${@:-}" + diff --git a/scripts/validate-go.sh b/scripts/validate-go.sh new file mode 100755 index 000000000..1f0aac130 --- /dev/null +++ b/scripts/validate-go.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +set -euo pipefail + +readonly reset=$(tput sgr0) +readonly red=$(tput bold; tput setaf 1) +readonly green=$(tput bold; tput setaf 2) +readonly yellow=$(tput bold; tput setaf 3) + +exit_code=0 + +find_go_files() { + find . -type f -name "*.go" | grep -v vendor +} + +hash golint 2>/dev/null || go get -u github.com/golang/lint/golint +hash godir 2>/dev/null || go get -u github.com/Masterminds/godir + +echo "==> Running golint..." +for pkg in $(godir pkgs | grep -v proto); do + golint_out=$(golint "$pkg" 2>&1) + if [[ -n "$golint_out" ]]; then + echo "${yellow}${golint_out}${reset}" + fi +done + +echo "==> Running go vet..." +echo -n "$red" +go vet $(godir pkgs) 2>&1 | grep -v "^exit status " || exit_code=${PIPESTATUS[0]} +echo -n "$reset" + +echo "==> Running gofmt..." +failed_fmt=$(find_go_files | xargs gofmt -s -l) +if [[ -n "${failed_fmt}" ]]; then + echo -n "${red}" + echo "gofmt check failed:" + echo "$failed_fmt" + gofmt -s -d "${failed_fmt}" + echo -n "${reset}" + exit_code=1 +fi + +exit ${exit_code} diff --git a/versioning.mk b/versioning.mk new file mode 100644 index 000000000..8d96c8629 --- /dev/null +++ b/versioning.mk @@ -0,0 +1,22 @@ +MUTABLE_VERSION ?= canary +VERSION ?= git-$(shell git rev-parse --short HEAD) + +IMAGE := ${DOCKER_REGISTRY}/${IMAGE_PREFIX}/${SHORT_NAME}:${VERSION} +MUTABLE_IMAGE := ${DOCKER_REGISTRY}/${IMAGE_PREFIX}/${SHORT_NAME}:${MUTABLE_VERSION} + +info: + @echo "Build tag: ${VERSION}" + @echo "Registry: ${DOCKER_REGISTRY}" + @echo "Immutable tag: ${IMAGE}" + @echo "Mutable tag: ${MUTABLE_IMAGE}" + +.PHONY: docker-push +docker-push: docker-mutable-push docker-immutable-push + +.PHONY: docker-immutable-push +docker-immutable-push: + docker push ${IMAGE} + +.PHONY: docker-mutable-push +docker-mutable-push: + docker push ${MUTABLE_IMAGE}