diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..56a0a583 --- /dev/null +++ b/Makefile @@ -0,0 +1,98 @@ +GO = go +GO_FLAGS = +GOFMT = gofmt +KUBECFG = kubecfg +DOCKER = docker +CONTROLLER_IMAGE = kubeless-controller-manager:latest +NATS_CONTROLLER_IMAGE = nats-trigger-controller:latest +OS = linux +ARCH = amd64 +BUNDLES = bundles +GO_PACKAGES = ./cmd/... ./pkg/... +GO_FILES := $(shell find $(shell $(GO) list -f '{{.Dir}}' $(GO_PACKAGES)) -name \*.go) + +export KUBECFG_JPATH := $(CURDIR)/ksonnet-lib +export PATH := $(PATH):$(CURDIR)/bats/bin + +.PHONY: all + +KUBELESS_ENVS := \ + -e OS_PLATFORM_ARG \ + -e OS_ARCH_ARG \ + +default: binary + +all: + CGO_ENABLED=1 ./script/make.sh + +binary: + CGO_ENABLED=1 ./script/binary + +binary-cross: + ./script/binary-cli + + +%.yaml: %.jsonnet + $(KUBECFG) show -o yaml $< > $@.tmp + mv $@.tmp $@ + +all-yaml: nats.yaml + +nats.yaml: nats.jsonnet + +nats-controller-build: + ./script/binary-controller -os=$(OS) -arch=$(ARCH) nats-controller github.com/kubeless/nats-trigger/cmd/nats-trigger-controller + +nats-controller-image: docker/nats-controller + $(DOCKER) build -t $(NATS_CONTROLLER_IMAGE) $< + +docker/nats-controller: nats-controller-build + cp $(BUNDLES)/kubeless_$(OS)-$(ARCH)/nats-controller $@ + +update: + ./hack/update-codegen.sh + +test: + $(GO) test $(GO_FLAGS) $(GO_PACKAGES) + +validation: + ./script/validate-vet + ./script/validate-lint + ./script/validate-gofmt + ./script/validate-git-marks + +integration-tests: + ./script/integration-tests minikube deployment + ./script/integration-tests minikube basic + +minikube-rbac-test: + ./script/integration-test-rbac minikube + +fmt: + $(GOFMT) -s -w $(GO_FILES) + +bats: + git clone --depth=1 https://github.com/sstephenson/bats.git + +ksonnet-lib: + git clone --depth=1 https://github.com/ksonnet/ksonnet-lib.git + +.PHONY: bootstrap +bootstrap: bats ksonnet-lib + + go get github.com/mitchellh/gox + go get github.com/golang/lint/golint + + @if ! which kubecfg >/dev/null; then \ + sudo wget -q -O /usr/local/bin/kubecfg https://github.com/ksonnet/kubecfg/releases/download/v0.6.0/kubecfg-$$(go env GOOS)-$$(go env GOARCH); \ + sudo chmod +x /usr/local/bin/kubecfg; \ + fi + + @if ! which kubectl >/dev/null; then \ + KUBECTL_VERSION=$$(wget -qO- https://storage.googleapis.com/kubernetes-release/release/stable.txt); \ + sudo wget -q -O /usr/local/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/$$KUBECTL_VERSION/bin/$$(go env GOOS)/$$(go env GOARCH)/kubectl; \ + sudo chmod +x /usr/local/bin/kubectl; \ + fi + +build_and_test: + ./script/start-test-environment.sh "make binary && make controller-image CONTROLLER_IMAGE=bitnami/kubeless-controller-manager:latest && make integration-tests" diff --git a/docker/nats-controller/Dockerfile b/docker/nats-controller/Dockerfile new file mode 100644 index 00000000..871a8f2e --- /dev/null +++ b/docker/nats-controller/Dockerfile @@ -0,0 +1,5 @@ +FROM bitnami/minideb:jessie + +ADD nats-controller /nats-controller + +ENTRYPOINT ["/nats-controller"] diff --git a/examples/Makefile b/examples/Makefile new file mode 100644 index 00000000..06ce6860 --- /dev/null +++ b/examples/Makefile @@ -0,0 +1,148 @@ +python-nats: + kubeless function deploy python-nats --runtime python2.7 --handler pubsub.handler --from-file python/hellowithdata.py + kubeless trigger nats create python-nats --function-selector created-by=kubeless,function=python-nats --trigger-topic test + +python-nats-verify: + $(eval DATA := $(shell mktemp -u -t XXXXXXXX)) + $(eval NODEPORT := $(shell kubectl get svc nats -n nats-io -o jsonpath="{.spec.ports[0].nodePort}")) + $(eval MINIKUBE_IP := $(shell minikube ip)) + kubeless trigger nats publish --url nats://$(MINIKUBE_IP):$(NODEPORT) --topic test --message '{"payload":"$(DATA)"}' + number="1"; \ + timeout="60"; \ + found=false; \ + while [ $$number -le $$timeout ] ; do \ + pod=`kubectl get po -oname -l function=python-nats`; \ + logs=`kubectl logs $$pod | grep $(DATA)`; \ + if [ "$$logs" != "" ]; then \ + found=true; \ + break; \ + fi; \ + sleep 1; \ + number=`expr $$number + 1`; \ + done; \ + $$found + # Verify event context + logs=`kubectl logs -l function=python-nats`; \ + echo $$logs | grep -q "event-time.*UTC" && \ + echo $$logs | grep -q "event-type.*application/json" && \ + echo $$logs | grep -q "event-namespace.*natstriggers.kubeless.io" && \ + echo $$logs | grep -q "event-id.*" + +nats-python-func1-topic-test: + kubeless function deploy nats-python-func1-topic-test --runtime python2.7 --handler pubsub.handler --from-file python/hellowithdata.py --label topic=nats-topic-test + +nats-python-func2-topic-test: + kubeless function deploy nats-python-func2-topic-test --runtime python2.7 --handler pubsub.handler --from-file python/hellowithdata.py --label topic=nats-topic-test + +nats-python-func-multi-topic: + kubeless function deploy nats-python-func-multi-topic --runtime python2.7 --handler pubsub.handler --from-file python/hellowithdata.py --label func=nats-python-func-multi-topic + +nats-python-trigger-topic-test: + kubeless trigger nats create nats-python-trigger-topic-test --function-selector created-by=kubeless,topic=nats-topic-test --trigger-topic topic-test + +nats-python-trigger-topic1: + kubeless trigger nats create nats-python-trigger-topic1 --function-selector created-by=kubeless,func=nats-python-func-multi-topic --trigger-topic topic1 + +nats-python-trigger-topic2: + kubeless trigger nats create nats-python-trigger-topic2 --function-selector created-by=kubeless,func=nats-python-func-multi-topic --trigger-topic topic2 + +nats-python-func1-topic-test-verify: + $(eval DATA := $(shell mktemp -u -t XXXXXXXX)) + $(eval NODEPORT := $(shell kubectl get svc nats -n nats-io -o jsonpath="{.spec.ports[0].nodePort}")) + $(eval MINIKUBE_IP := $(shell minikube ip)) + kubeless trigger nats publish --url nats://$(MINIKUBE_IP):$(NODEPORT) --topic topic-test --message '{"payload":"$(DATA)"}' + number="1"; \ + timeout="60"; \ + found=false; \ + while [ $$number -le $$timeout ] ; do \ + pod=`kubectl get po -oname -l function=nats-python-func1-topic-test`; \ + logs=`kubectl logs $$pod | grep $(DATA)`; \ + if [ "$$logs" != "" ]; then \ + found=true; \ + break; \ + fi; \ + sleep 1; \ + number=`expr $$number + 1`; \ + done; \ + $$found + # Verify event context + logs=`kubectl logs -l function=nats-python-func1-topic-test`; \ + echo $$logs | grep -q "event-time.*UTC" && \ + echo $$logs | grep -q "event-type.*application/json" && \ + echo $$logs | grep -q "event-namespace.*natstriggers.kubeless.io" && \ + echo $$logs | grep -q "event-id.*" + +nats-python-func2-topic-test-verify: + $(eval DATA := $(shell mktemp -u -t XXXXXXXX)) + $(eval NODEPORT := $(shell kubectl get svc nats -n nats-io -o jsonpath="{.spec.ports[0].nodePort}")) + $(eval MINIKUBE_IP := $(shell minikube ip)) + kubeless trigger nats publish --url nats://$(MINIKUBE_IP):$(NODEPORT) --topic topic-test --message '{"payload":"$(DATA)"}' + number="1"; \ + timeout="60"; \ + found=false; \ + while [ $$number -le $$timeout ] ; do \ + pod=`kubectl get po -oname -l function=nats-python-func2-topic-test`; \ + logs=`kubectl logs $$pod | grep $(DATA)`; \ + if [ "$$logs" != "" ]; then \ + found=true; \ + break; \ + fi; \ + sleep 1; \ + number=`expr $$number + 1`; \ + done; \ + $$found + # Verify event context + logs=`kubectl logs -l function=nats-python-func2-topic-test`; \ + echo $$logs | grep -q "event-time.*UTC" && \ + echo $$logs | grep -q "event-type.*application/json" && \ + echo $$logs | grep -q "event-namespace.*natstriggers.kubeless.io" && \ + echo $$logs | grep -q "event-id.*" + +nats-python-func-multi-topic-verify: + $(eval DATA := $(shell mktemp -u -t XXXXXXXX)) + $(eval NODEPORT := $(shell kubectl get svc nats -n nats-io -o jsonpath="{.spec.ports[0].nodePort}")) + $(eval MINIKUBE_IP := $(shell minikube ip)) + kubeless trigger nats publish --url nats://$(MINIKUBE_IP):$(NODEPORT) --topic topic1 --message '{"payload":"$(DATA)"}' + number="1"; \ + timeout="60"; \ + found=false; \ + while [ $$number -le $$timeout ] ; do \ + pod=`kubectl get po -oname -l function=nats-python-func-multi-topic`; \ + logs=`kubectl logs $$pod | grep $(DATA)`; \ + if [ "$$logs" != "" ]; then \ + found=true; \ + break; \ + fi; \ + sleep 1; \ + number=`expr $$number + 1`; \ + done; \ + $$found + # Verify event context + logs=`kubectl logs -l function=nats-python-func-multi-topic`; \ + echo $$logs | grep -q "event-time.*UTC" && \ + echo $$logs | grep -q "event-type.*application/json" && \ + echo $$logs | grep -q "event-namespace.*natstriggers.kubeless.io" && \ + echo $$logs | grep -q "event-id.*" + + kubeless trigger nats publish --url nats://$(MINIKUBE_IP):$(NODEPORT) --topic topic2 --message '{"payload":"$(DATA)"}' + number="1"; \ + timeout="60"; \ + found=false; \ + while [ $$number -le $$timeout ] ; do \ + pod=`kubectl get po -oname -l function=nats-python-func-multi-topic`; \ + logs=`kubectl logs $$pod | grep $(DATA)`; \ + if [ "$$logs" != "" ]; then \ + found=true; \ + break; \ + fi; \ + sleep 1; \ + number=`expr $$number + 1`; \ + done; \ + $$found + # Verify event context + logs=`kubectl logs -l function=nats-python-func-multi-topic`; \ + echo $$logs | grep -q "event-time.*UTC" && \ + echo $$logs | grep -q "event-type.*application/json" && \ + echo $$logs | grep -q "event-namespace.*natstriggers.kubeless.io" && \ + echo $$logs | grep -q "event-id.*" + diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000..c7b954a9 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,29 @@ +# Examples + +This directory contains basic examples for kubeless. + +Specifically it contains examples that we can test quickly using the `Makefile`. Some of these examples are run during our integration tests. + +Check the [Makefile](Makefile) + +Then run some of the examples like so: + +``` +make post-python +``` + +Or a different runtime: + +``` +make post-dotnetcore +``` + +Or a PubSub example: + +``` +make pubsub-python +``` + +# Looking for more function examples? + +You can find more examples at [https://github.com/kubeless/functions](https://github.com/kubeless/functions) diff --git a/examples/python/hellowithdata.py b/examples/python/hellowithdata.py new file mode 100644 index 00000000..f059de23 --- /dev/null +++ b/examples/python/hellowithdata.py @@ -0,0 +1,3 @@ +def handler(event, context): + print event + return event['data'] diff --git a/examples/python/hellowithdata34.py b/examples/python/hellowithdata34.py new file mode 100644 index 00000000..f468b009 --- /dev/null +++ b/examples/python/hellowithdata34.py @@ -0,0 +1,3 @@ +def handler(event, context): + print (event) + return event['data'] diff --git a/hack/boilerplate.go.txt b/hack/boilerplate.go.txt new file mode 100644 index 00000000..56630899 --- /dev/null +++ b/hack/boilerplate.go.txt @@ -0,0 +1,15 @@ +/* +Copyright (c) 2016-2017 Bitnami + +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/hack/update-codegen.sh b/hack/update-codegen.sh new file mode 100755 index 00000000..ea5cd0c0 --- /dev/null +++ b/hack/update-codegen.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# Copyright 2017 The Kubernetes Authors. +# +# 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. + +set -o errexit +set -o nounset +set -o pipefail + +SCRIPT_ROOT=$(dirname ${BASH_SOURCE})/.. +CODEGEN_PKG=${CODEGEN_PKG:-$(cd ${SCRIPT_ROOT}; ls -d -1 ./vendor/k8s.io/code-generator 2>/dev/null || echo ../code-generator)} + +# generate the code with: +# --output-base because this script should also be able to run inside the vendor dir of +# k8s.io/kubernetes. The output-base is needed for the generators to output into the vendor dir +# instead of the $GOPATH directly. For normal projects this can be dropped. + +### Workaround for issue: https://github.com/kubernetes/code-generator/issues/6 +mkdir -p ${GOPATH}/src/k8s.io/kubernetes/hack/boilerplate +cp ${SCRIPT_ROOT}/hack/boilerplate.go.txt ${GOPATH}/src/k8s.io/kubernetes/hack/boilerplate/ + +${CODEGEN_PKG}/generate-groups.sh "deepcopy,client,informer,lister" \ + github.com/kubeless/nats-trigger/pkg/client github.com/kubeless/nats-trigger/pkg/apis \ + kubeless:v1beta1 diff --git a/manifests/nats/nats-cluster.yaml b/manifests/nats/nats-cluster.yaml new file mode 100644 index 00000000..ed851798 --- /dev/null +++ b/manifests/nats/nats-cluster.yaml @@ -0,0 +1,7 @@ +apiVersion: "nats.io/v1alpha2" +kind: "NatsCluster" +metadata: + name: "nats" +spec: + size: 2 + version: "1.1.0" diff --git a/nats.jsonnet b/nats.jsonnet new file mode 100644 index 00000000..9faba2eb --- /dev/null +++ b/nats.jsonnet @@ -0,0 +1,77 @@ +local k = import "ksonnet.beta.1/k.libsonnet"; +local container = k.core.v1.container; + +local deployment = k.apps.v1beta1.deployment; +local serviceAccount = k.core.v1.serviceAccount; +local objectMeta = k.core.v1.objectMeta; + +local namespace = "kubeless"; +local controller_account_name = "controller-acct"; + +local crd = [ + { + apiVersion: "apiextensions.k8s.io/v1beta1", + kind: "CustomResourceDefinition", + metadata: objectMeta.name("natstriggers.kubeless.io"), + spec: {group: "kubeless.io", version: "v1beta1", scope: "Namespaced", names: {plural: "natstriggers", singular: "natstrigger", kind: "NATSTrigger"}}, + description: "CRD object for NATS trigger type", + }, +]; + +local controllerContainer = + container.default("nats-trigger-controller", "bitnami/nats-trigger-controller:latest") + + container.imagePullPolicy("IfNotPresent"); + +local kubelessLabel = {kubeless: "nats-trigger-controller"}; + +local controllerAccount = + serviceAccount.default(controller_account_name, namespace); + +local controllerDeployment = + deployment.default("nats-trigger-controller", controllerContainer, namespace) + + {metadata+:{labels: kubelessLabel}} + + {spec+: {selector: {matchLabels: kubelessLabel}}} + + {spec+: {template+: {spec+: {serviceAccountName: controllerAccount.metadata.name}}}} + + {spec+: {template+: {metadata: {labels: kubelessLabel}}}}; + +local controller_roles = [ + { + apiGroups: [""], + resources: ["services", "configmaps"], + verbs: ["get", "list"], + }, + { + apiGroups: ["kubeless.io"], + resources: ["functions", "natstriggers"], + verbs: ["get", "list", "watch", "update", "delete"], + }, +]; + +local clusterRole(name, rules) = { + apiVersion: "rbac.authorization.k8s.io/v1beta1", + kind: "ClusterRole", + metadata: objectMeta.name(name), + rules: rules, +}; + +local clusterRoleBinding(name, role, subjects) = { + apiVersion: "rbac.authorization.k8s.io/v1beta1", + kind: "ClusterRoleBinding", + metadata: objectMeta.name(name), + subjects: [{kind: s.kind, namespace: s.metadata.namespace, name: s.metadata.name} for s in subjects], + roleRef: {kind: role.kind, apiGroup: "rbac.authorization.k8s.io", name: role.metadata.name}, +}; + +local controllerClusterRole = clusterRole( + "nats-controller-deployer", controller_roles); + +local controllerClusterRoleBinding = clusterRoleBinding( + "nats-controller-deployer", controllerClusterRole, [controllerAccount] +); + +{ + controller: k.util.prune(controllerDeployment), + crd: k.util.prune(crd), + controllerClusterRole: k.util.prune(controllerClusterRole), + controllerClusterRoleBinding: k.util.prune(controllerClusterRoleBinding), +} diff --git a/script/binary b/script/binary new file mode 100755 index 00000000..90fc90fc --- /dev/null +++ b/script/binary @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +# Copyright (c) 2016-2017 Bitnami +# +# 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. + +set -e + + +GIT_COMMIT=$(git describe --tags --dirty --always) +BUILD_FLAGS=(-ldflags="-w -X github.com/kubeless/nats-trigger/pkg/version.Version=${GIT_COMMIT}") + +# Get rid of existing binary +echo "Removing Old NATS trigger controller binary" +rm -f ${GOPATH%%:*}/bin/nats-trigger-controller + +echo "Build NATS trigger controller binary" +# Build binary +go install \ + "${BUILD_FLAGS[@]}" \ + github.com/kubeless/nats-trigger/cmd/... + +if [ $? -eq 0 ]; then + echo "Build NATS trigger controller binary successful. Program saved at ${GOPATH%%:*}/bin" +else + echo "Build NATS trigger controller failed." +fi diff --git a/script/binary-cli b/script/binary-cli new file mode 100755 index 00000000..78350f56 --- /dev/null +++ b/script/binary-cli @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +# Copyright (c) 2016-2017 Bitnami +# +# 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. + +set -e + +OS_PLATFORM_ARG=(-os="darwin linux windows") +OS_ARCH_ARG=(-arch="386 amd64") + +GIT_COMMIT=$(git describe --tags --dirty) +BUILD_DATE=$(date) +BUILD_FLAGS=(-ldflags="-w -X github.com/kubeless/kubeless/pkg/version.Version=${GIT_COMMIT}") + +# Get rid of existing binaries +rm -rf bundles/kubeless* + +# Build kubeless +gox "${OS_PLATFORM_ARG[@]}" "${OS_ARCH_ARG[@]}" \ + -output="bundles/kubeless_{{.OS}}-{{.Arch}}/kubeless" \ + "${BUILD_FLAGS[@]}" \ + github.com/kubeless/kubeless/cmd/kubeless diff --git a/script/binary-controller b/script/binary-controller new file mode 100755 index 00000000..b5642cd2 --- /dev/null +++ b/script/binary-controller @@ -0,0 +1,55 @@ +#!/usr/bin/env bash + +# Copyright (c) 2016-2017 Bitnami +# +# 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. + +set -e + +if [ -z "$1" ]; then +# TODO: Skip windows at this moment + OS_PLATFORM_ARG=(-os="linux") +else + OS_PLATFORM_ARG=($1) +fi + +if [ -z "$2" ]; then + OS_ARCH_ARG=(-arch="amd64") +else + OS_ARCH_ARG=($2) +fi + +if [ -z "$3" ]; then + TARGET="kubeless-controller-manager" +else + TARGET=($3) +fi + +if [ -z "$4" ]; then + PKG="github.com/kubeless/kubeless/cmd/kubeless-controller-manager" +else + PKG=($4) +fi + + +GIT_COMMIT=$(git describe --tags --dirty --always) +BUILD_FLAGS=(-ldflags="-w -X github.com/kubeless/kubeless/pkg/version.Version=${GIT_COMMIT}") + +# Get rid of existing binaries +rm -rf bundles/kubeless* + +# Build kubeless-controller +gox "${OS_PLATFORM_ARG[@]}" "${OS_ARCH_ARG[@]}" \ + -output="bundles/kubeless_{{.OS}}-{{.Arch}}/$TARGET" \ + "${BUILD_FLAGS[@]}" \ + "$PKG" diff --git a/script/cluster-up-minikube.sh b/script/cluster-up-minikube.sh new file mode 100755 index 00000000..8976101c --- /dev/null +++ b/script/cluster-up-minikube.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash + +# Copyright (c) 2016-2017 Bitnami +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# From minikube howto +export MINIKUBE_WANTUPDATENOTIFICATION=false +export MINIKUBE_WANTREPORTERRORPROMPT=false +export MINIKUBE_HOME=$HOME +export CHANGE_MINIKUBE_NONE_USER=true +mkdir -p ~/.kube +touch ~/.kube/config + +export KUBECONFIG=$HOME/.kube/config +export PATH=${PATH}:${GOPATH:?}/bin + +MINIKUBE_VERSION=${MINIKUBE_VERSION:?} +export KUBELESS_VERSION=$(curl -s https://api.github.com/repos/kubeless/kubeless/releases/latest | grep tag_name | cut -d '"' -f 4) + +install_bin() { + local exe=${1:?} + sudo install -v ${exe} /usr/local/bin || install ${exe} ${GOPATH:?}/bin +} + +# Travis ubuntu trusty env doesn't have nsenter, needed for VM-less minikube +# (--vm-driver=none, runs dockerized) +check_or_build_nsenter() { + which nsenter >/dev/null && return 0 + echo "INFO: Getting 'nsenter' ..." + curl -LO http://mirrors.kernel.org/ubuntu/pool/main/u/util-linux/util-linux_2.30.1-0ubuntu4_amd64.deb + dpkg -x ./util-linux_2.30.1-0ubuntu4_amd64.deb /tmp/out + install_bin /tmp/out/usr/bin/nsenter +} +check_or_install_minikube() { + which minikube || { + wget -q --no-clobber -O minikube \ + https://storage.googleapis.com/minikube/releases/${MINIKUBE_VERSION}/minikube-linux-amd64 + install_bin ./minikube + } +} +check_or_install_kubeless() { + which kubeless || { + sudo wget https://github.com/kubeless/kubeless/releases/download/$KUBELESS_VERSION/kubeless_$(go env GOOS)-$(go env GOARCH).zip + unzip kubeless_$(go env GOOS)-$(go env GOARCH).zip + sudo cp ./bundles/kubeless_$(go env GOOS)-$(go env GOARCH)/kubeless /usr/local/bin/kubeless + sudo chmod +x /usr/local/bin/kubeless + } +} +install_kubeless_manifests() { + wget -q -O kubeless-non-rbac.yaml https://github.com/kubeless/kubeless/releases/download/$KUBELESS_VERSION/kubeless-non-rbac-$KUBELESS_VERSION.yaml + wget -q -O kubeless.yaml https://github.com/kubeless/kubeless/releases/download/$KUBELESS_VERSION/kubeless-$KUBELESS_VERSION.yaml +} + +# Install nsenter if missing +check_or_build_nsenter +# Install minikube if missing +check_or_install_minikube +# Install Kubeless if missing +check_or_install_kubeless +# Install Kubeless manifests +install_kubeless_manifests + +MINIKUBE_BIN=$(which minikube) + +# Start minikube +sudo -E ${MINIKUBE_BIN} start --vm-driver=none \ + --extra-config=apiserver.Authorization.Mode=RBAC \ + --memory 4096 + +# Wait til settles +echo "INFO: Waiting for minikube cluster to be ready ..." +typeset -i cnt=120 +until kubectl --context=minikube get pods >& /dev/null; do + ((cnt=cnt-1)) || exit 1 + sleep 1 +done + +sudo -E ${MINIKUBE_BIN} update-context + +# Enable Nginx Ingress +echo "INFO: Enabling ingress addon to minikube..." +sudo -E ${MINIKUBE_BIN} addons enable ingress +sudo -E ${MINIKUBE_BIN} config set WantUpdateNotification false + +# Give some time for the cluster to become healthy +sleep 10 + +exit 0 +# vim: sw=4 ts=4 et si diff --git a/script/create_release.sh b/script/create_release.sh new file mode 100755 index 00000000..b0e1aa64 --- /dev/null +++ b/script/create_release.sh @@ -0,0 +1,38 @@ +#!/bin/bash +set -e + +REPO_NAME=kubeless +REPO_DOMAIN=nats-trigger +TAG=${1:?} +MANIFESTS=${2:?} # Space separated list of manifests to publish + +PROJECT_DIR=$(cd $(dirname $0)/.. && pwd) + +source $(dirname $0)/release_utils.sh + +if [[ -z "$REPO_NAME" || -z "$REPO_DOMAIN" ]]; then + echo "Github repository not specified" > /dev/stderr + exit 1 +fi + +if [[ -z "$ACCESS_TOKEN" ]]; then + echo "Unable to release: Github Token not specified" > /dev/stderr + exit 1 +fi + +repo_check=`curl -H "Authorization: token $ACCESS_TOKEN" -s https://api.github.com/repos/$REPO_DOMAIN/$REPO_NAME` +if [[ $repo_check == *"Not Found"* ]]; then + echo "Not found a Github repository for $REPO_DOMAIN/$REPO_NAME, it is not possible to publish it" > /dev/stderr + exit 1 +else + RELEASE_ID=$(release_tag $TAG $REPO_DOMAIN $REPO_NAME | jq '.id') +fi + +IFS=' ' read -r -a manifests <<< "$MANIFESTS" +for f in "${manifests[@]}"; do + cp ${PROJECT_DIR}/${f}.yaml ${PROJECT_DIR}/${f}-${TAG}.yaml + upload_asset $REPO_DOMAIN $REPO_NAME "$RELEASE_ID" "${PROJECT_DIR}/${f}-${TAG}.yaml" +done +for f in `ls ${PROJECT_DIR}/bundles/kubeless_*.zip`; do + upload_asset $REPO_DOMAIN $REPO_NAME $RELEASE_ID $f +done diff --git a/script/find_digest.sh b/script/find_digest.sh new file mode 100755 index 00000000..22c0efde --- /dev/null +++ b/script/find_digest.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +# Copyright (c) 2016-2017 Bitnami +# +# 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. + +REPOSITORY=$1 +TARGET_TAG=$2 + +# get authorization token +TOKEN=$(curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:$REPOSITORY:pull" | jq -r .token) + +# find all tags +ALL_TAGS=$(curl -s -H "Authorization: Bearer $TOKEN" https://index.docker.io/v2/$REPOSITORY/tags/list | jq -r .tags[]) + +# get image digest for target +TARGET_DIGEST=$(curl -s -D - -H "Authorization: Bearer $TOKEN" -H "Accept: application/vnd.docker.distribution.manifest.v2+json" https://index.docker.io/v2/$REPOSITORY/manifests/$TARGET_TAG | grep Docker-Content-Digest | cut -d ' ' -f 2) + +# for each tags +for tag in ${ALL_TAGS[@]}; do + # get image digest + digest=$(curl -s -D - -H "Authorization: Bearer $TOKEN" -H "Accept: application/vnd.docker.distribution.manifest.v2+json" https://index.docker.io/v2/$REPOSITORY/manifests/$tag | grep Docker-Content-Digest | cut -d ' ' -f 2) + + # check digest + if [[ $TARGET_DIGEST = $digest ]]; then + echo "$tag $digest" + fi +done diff --git a/script/integration-tests b/script/integration-tests new file mode 100755 index 00000000..7929a175 --- /dev/null +++ b/script/integration-tests @@ -0,0 +1,115 @@ +#!/usr/bin/env bash + +# Copyright (c) 2016-2017 Bitnami +# +# 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. + +# Special case: if ./ksonnet-lib exists, set KUBECFG_JPATH +test -d $PWD/ksonnet-lib && export KUBECFG_JPATH=$PWD/ksonnet-lib + +# We require below env +: ${GOPATH:?} ${KUBECFG_JPATH:?} +export PATH=${PATH}:${GOPATH}/bin + +# Default kubernetes context - if it's "dind" or "minikube" will +# try to bring up a local (dockerized) cluster +test -n "${TRAVIS_K8S_CONTEXT}" && set -- ${TRAVIS_K8S_CONTEXT} +# minikube seems to be more stable than dind, sp for kafka +INTEGRATION_TESTS_CTX=${1:-minikube} + +INTEGRATION_TESTS_TARGET=${2:-default} + +# Check for some needed tools, install (some) if missing +which bats > /dev/null || { + echo "ERROR: 'bats' is required to run these tests," \ + "install it from https://github.com/sstephenson/bats" + exit 255 +} + +# Start a k8s cluster (minikube, dind) if not running +kubectl get nodes --context=${INTEGRATION_TESTS_CTX:?} || { + cluster_up=./script/cluster-up-${INTEGRATION_TESTS_CTX}.sh + test -f ${cluster_up} || { + echo "FATAL: bringing up k8s cluster '${INTEGRATION_TESTS_CTX}' not supported" + exit 255 + } + ${cluster_up} +} + +# Both RBAC'd dind and minikube seem to be missing rules to make kube-dns work properly +# add some (granted) broad ones: +kubectl --context=${INTEGRATION_TESTS_CTX:?} get clusterrolebinding kube-dns-admin >& /dev/null || \ + kubectl --context=${INTEGRATION_TESTS_CTX:?} create clusterrolebinding kube-dns-admin --serviceaccount=kube-system:default --clusterrole=cluster-admin + +# Prep: load test library, save current k8s default context (and restore it at exit), +# as kubeless doesn't support --context +export TEST_CONTEXT=${INTEGRATION_TESTS_CTX} +source script/libtest.bash +trap k8s_context_restore 0 +k8s_context_save + +# Run the tests thru bats: +kubectl create namespace kubeless +case $INTEGRATION_TESTS_TARGET in +deployment) + bats tests/deployment-tests.bats + ;; +basic) + bats tests/integration-tests.bats + ;; +kafka) + bats tests/integration-tests-kafka.bats + ;; +nats) + bats tests/integration-tests-nats.bats + ;; +kinesis) + bats tests/integration-tests-kinesis.bats + ;; +http) + bats tests/integration-tests-http.bats + ;; +cronjob) + bats tests/integration-tests-cronjob.bats + ;; +prebuilt_functions) + bats tests/integration-tests-prebuilt.bats + ;; +*) + bats tests/deployment-tests.bats && \ + bats tests/integration-tests.bats && \ + bats tests/integration-tests-http.bats && \ + bats tests/integration-tests-cronjob.bats && \ + bats tests/integration-tests-kafka.bats + ;; +esac +exit_code=$? + +# Just showing remaining k8s objects +kubectl get all --all-namespaces + +if [ ${exit_code} -ne 0 -o -n "${TRAVIS_DUMP_LOGS}" ]; then + echo "INFO: Build ERRORed, dumping logs: ##" + for ns in kubeless default; do + echo "### LOGs: namespace: ${ns} ###" + kubectl get pod -n ${ns} -oname|xargs -I@ sh -xc "kubectl logs -n ${ns} @|sed 's|^|@: |'" + done + echo "INFO: Description" + kubectl describe pod -l created-by=kubeless + echo "INFO: LOGs: pod: kube-dns ###" + kubectl logs -n kube-system -l k8s-app=kube-dns -c kubedns + echo "INFO: LOGs: END" +fi +[ ${exit_code} -eq 0 ] && echo "INFO: $0: SUCCESS" || echo "ERROR: $0: FAILED" +exit ${exit_code} +# vim: sw=4 ts=4 et si diff --git a/script/kafka-controller.sh b/script/kafka-controller.sh new file mode 100755 index 00000000..2601dfaa --- /dev/null +++ b/script/kafka-controller.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash + +# Copyright (c) 2016-2017 Bitnami +# +# 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. + +set -e + +if [ -z "$1" ]; then +# TODO: Skip windows at this moment + OS_PLATFORM_ARG=(-os="linux") +else + OS_PLATFORM_ARG=($1) +fi + +if [ -z "$2" ]; then + OS_ARCH_ARG=(-arch="amd64") +else + OS_ARCH_ARG=($2) +fi + + +GIT_COMMIT=$(git describe --tags --dirty) +BUILD_DATE=$(date) +BUILD_FLAGS=(-ldflags="-w -X github.com/kubeless/kubeless/pkg/version.Version=${GIT_COMMIT}") + +# Get rid of existing binaries +rm -rf bundles/kubeless* + +# Build kafka-controller +gox "${OS_PLATFORM_ARG[@]}" "${OS_ARCH_ARG[@]}" \ + -output="bundles/kubeless_{{.OS}}-{{.Arch}}/kafka-controller" \ + "${BUILD_FLAGS[@]}" \ + github.com/kubeless/kubeless/cmd/kafka-trigger-controller diff --git a/script/kubeless_uninstall.sh b/script/kubeless_uninstall.sh new file mode 100755 index 00000000..7cfe44cc --- /dev/null +++ b/script/kubeless_uninstall.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +kubectl get po --all-namespaces +kubectl delete statefulsets --namespace kubeless kafka +kubectl delete statefulsets --namespace kubeless zoo +kubectl delete deployment --namespace kubeless kubeless-controller +kubectl delete svc --namespace kubeless broker +kubectl delete svc --namespace kubeless kafka +kubectl delete svc --namespace kubeless zoo +kubectl delete svc --namespace kubeless zookeeper +#kubectl delete customresourcedefinition function.k8s.io diff --git a/script/libtest.bash b/script/libtest.bash new file mode 100644 index 00000000..2a91b545 --- /dev/null +++ b/script/libtest.bash @@ -0,0 +1,602 @@ +# Copyright (c) 2016-2017 Bitnami +# +# 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. + +# k8s and kubeless helpers, specially "wait"-ers on pod ready/deleted/etc + +KUBELESS_MANIFEST=kubeless-non-rbac.yaml +KUBELESS_MANIFEST_RBAC=kubeless.yaml +KAFKA_MANIFEST=kafka-zookeeper.yaml +NATS_MANIFEST=nats.yaml +KINESIS_MANIFEST=kinesis.yaml + +KUBECTL_BIN=$(which kubectl) +: ${KUBECTL_BIN:?ERROR: missing binary: kubectl} + +export TEST_MAX_WAIT_SEC=300 + +# Workaround 'bats' lack of forced output support, dup() stderr fd +exec 9>&2 +echo_info() { + test -z "$TEST_DEBUG" && return 0 + echo "INFO: $*" >&9 +} +export -f echo_info + +kubectl() { + ${KUBECTL_BIN:?} --context=${TEST_CONTEXT:?} "$@" +} + +## k8s specific Helper functions +k8s_wait_for_pod_ready() { + echo_info "Waiting for pod '${@}' to be ready ... " + local -i cnt=${TEST_MAX_WAIT_SEC:?} + + # Retries just in case it is not stable + local -i successCount=0 + while [ "$successCount" -lt "3" ]; do + if kubectl get pod "${@}" |&grep -q Running; then + ((successCount=successCount+1)) + fi + ((cnt=cnt-1)) || return 1 + sleep 1 + done +} +k8s_wait_for_pod_count() { + local pod_cnt=${1:?}; shift + echo_info "Waiting for pod '${@}' to have count==${pod_cnt} running ... " + local -i cnt=${TEST_MAX_WAIT_SEC:?} + # Retries just in case it is not stable + local -i successCount=0 + while [ "$successCount" -lt "3" ]; do + if [[ $(kubectl get pod "${@}" -ogo-template='{{.items|len}}') == ${pod_cnt} ]]; then + ((successCount=successCount+1)) + fi + ((cnt=cnt-1)) || return 1 + sleep 1 + done + k8s_wait_for_pod_ready "${@}" + echo "Finished waiting" +} +k8s_wait_for_uniq_pod() { + k8s_wait_for_pod_count 1 "$@" +} +k8s_wait_for_pod_gone() { + echo_info "Waiting for pod '${@}' to be gone ... " + local -i cnt=${TEST_MAX_WAIT_SEC:?} + until kubectl get pod "${@}" |&grep -q No.resources.found; do + ((cnt=cnt-1)) || return 1 + sleep 1 + done +} +k8s_wait_for_pod_logline() { + local string="${1:?}"; shift + local -i cnt=${TEST_MAX_WAIT_SEC:?} + echo_info "Waiting for '${@}' to show logline '${string}' ..." + until kubectl logs "${@}"|&grep -q "${string}"; do + ((cnt=cnt-1)) || return 1 + sleep 1 + done +} +k8s_wait_for_cluster_ready() { + echo_info "Waiting for k8s cluster to be ready (context=${TEST_CONTEXT}) ..." + _wait_for_cmd_ok kubectl get po 2>/dev/null && \ + k8s_wait_for_pod_ready -n kube-system -l component=kube-addon-manager && \ + k8s_wait_for_pod_ready -n kube-system -l k8s-app=kube-dns && \ + return 0 + return 1 +} +k8s_log_all_pods() { + local namespaces=${*:?} ns + for ns in ${*}; do + echo "### namespace: ${ns} ###" + kubectl get pod -n ${ns} -oname|xargs -I@ sh -xc "kubectl logs -n ${ns} @|sed 's|^|@: |'" + done +} +k8s_context_save() { + TEST_CONTEXT_SAVED=$(${KUBECTL_BIN} config current-context) + # Kubeless doesn't support contexts yet, save+restore it + # Don't save current_context if it's the same already + [[ $TEST_CONTEXT_SAVED == $TEST_CONTEXT ]] && TEST_CONTEXT_SAVED="" + + # Save current_context + [[ $TEST_CONTEXT_SAVED != "" ]] && \ + echo_info "Saved context: '${TEST_CONTEXT_SAVED}'" && \ + ${KUBECTL_BIN} config use-context ${TEST_CONTEXT} +} +k8s_context_restore() { + # Restore saved context + [[ $TEST_CONTEXT_SAVED != "" ]] && \ + echo_info "Restoring context: '${TEST_CONTEXT_SAVED}'" && \ + ${KUBECTL_BIN} config use-context ${TEST_CONTEXT_SAVED} +} +_wait_for_cmd_ok() { + local cmd="${*:?}"; shift + local -i cnt=${TEST_MAX_WAIT_SEC:?} + echo_info "Waiting for '${*}' to successfully exit ..." + until env ${cmd}; do + ((cnt=cnt-1)) || return 1 + sleep 1 + done +} + +## Specific for kubeless +kubeless_recreate() { + local manifest_del=${1:?missing delete manifest} manifest_upd=${2:?missing update manifest} + local -i cnt=${TEST_MAX_WAIT_SEC:?} + echo_info "Delete kubeless namespace, wait to be gone ... " + kubectl delete -f ${manifest_del} || true + kubectl delete namespace kubeless >& /dev/null || true + while kubectl get namespace kubeless >& /dev/null; do + ((cnt=cnt-1)) || return 1 + sleep 1 + done + kubectl create namespace kubeless + kubectl create -f ${manifest_upd} +} +kubeless_function_delete() { + local func=${1:?}; shift + echo_info "Deleting function "${func}" in case still present ... " + kubeless function ls |grep -w "${func}" && kubeless function delete "${func}" >& /dev/null || true + echo_info "Wait for function "${func}" to be deleted " + local -i cnt=${TEST_MAX_WAIT_SEC:?} + while kubectl get functions "${func}" >& /dev/null; do + ((cnt=cnt-1)) || return 1 + sleep 1 + done +} +kubeless_kafka_trigger_delete() { + local trigger=${1:?}; shift + echo_info "Deleting kafka trigger "${trigger}" in case still present ... " + kubeless trigger kafka list |grep -w "${trigger}" && kubeless trigger kafka delete "${trigger}" >& /dev/null || true +} +kubeless_nats_trigger_delete() { + local trigger=${1:?}; shift + echo_info "Deleting NATS trigger "${trigger}" in case still present ... " + kubeless trigger nats list |grep -w "${trigger}" && kubeless trigger nats delete "${trigger}" >& /dev/null || true +} +kubeless_function_deploy() { + local func=${1:?}; shift + echo_info "Deploying function ..." + kubeless function deploy ${func} ${@} +} +_wait_for_kubeless_controller_ready() { + echo_info "Waiting for kubeless controller to be ready ... " + k8s_wait_for_pod_ready -n kubeless -l kubeless=controller + _wait_for_cmd_ok kubectl get functions 2>/dev/null +} +_wait_for_kubeless_controller_logline() { + local string="${1:?}" + k8s_wait_for_pod_logline "${string}" -n kubeless -l kubeless=controller +} +wait_for_ingress() { + echo_info "Waiting until Nginx pod is ready ..." + local -i cnt=${TEST_MAX_WAIT_SEC:?} + until kubectl get pods -l name=nginx-ingress-controller -n kube-system>& /dev/null; do + ((cnt=cnt-1)) || exit 1 + sleep 1 + done +} +wait_for_kubeless_kafka_server_ready() { + [[ $(kubectl get pod -n kubeless kafka-0 -ojsonpath='{.metadata.annotations.ready}') == true ]] && return 0 + echo_info "Waiting for kafka-0 to be ready ..." + k8s_wait_for_pod_logline "Kafka.*Server.*started" -n kubeless kafka-0 + echo_info "Waiting for kafka-trigger-controller pod to be ready ..." + k8s_wait_for_pod_ready -n kubeless -l kubeless=kafka-trigger-controller + _wait_for_cmd_ok kubectl get kafkatriggers 2>/dev/null + kubectl annotate pods --overwrite -n kubeless kafka-0 ready=true +} +wait_for_kubeless_nats_operator_ready() { + echo_info "Waiting for NATS operator pod to be ready ..." + k8s_wait_for_pod_ready -n nats-io -l name=nats-operator +} +wait_for_kubeless_nats_cluster_ready() { + echo_info "Waiting for NATS cluster pods to be ready ..." + k8s_wait_for_pod_ready -n nats-io -l nats_cluster=nats +} +wait_for_kubeless_nats_controller_ready() { + echo_info "Waiting for NATS controller pods to be ready ..." + k8s_wait_for_pod_ready -n kubeless -l kubeless=nats-trigger-controller +} +_wait_for_kubeless_kafka_topic_ready() { + local topic=${1:?} + local -i cnt=${TEST_MAX_WAIT_SEC:?} + echo_info "Waiting for kafka-0 topic='${topic}' to be ready ..." + # zomg enter kafka-0 container to peek for topic already present + until \ + kubectl exec -n kubeless kafka-0 -- sh -c \ + '/opt/bitnami/kafka/bin/kafka-topics.sh --list --zookeeper $( + sed -n s/zookeeper.connect=//p /bitnami/kafka/conf/server.properties)'| \ + grep -qw ${topic} + do + ((cnt=cnt-1)) || return 1 + sleep 1 + done +} +_wait_for_simple_function_pod_ready() { + k8s_wait_for_pod_ready -l function=get-python +} +_deploy_simple_function() { + make -C examples get-python +} +_call_simple_function() { + # Artifact to dodge 'bats' lack of support for positively testing _for_ errors + case "${1:?}" in + 1) make -C examples get-python-verify |& egrep Error.1;; + 0) make -C examples get-python-verify;; + esac +} +_delete_simple_function() { + kubeless_function_delete get-python +} + +## Entry points used by 'bats' tests: +verify_k8s_tools() { + local tools="kubectl kubecfg kubeless" + for exe in $tools; do + which ${exe} >/dev/null && continue + echo "ERROR: '${exe}' needs to be installed" + return 1 + done +} +verify_rbac_mode() { + kubectl api-versions |&grep -q rbac && return 0 + echo "ERROR: Please run w/RBAC, eg minikube as: minikube start --extra-config=apiserver.Authorization.Mode=RBAC" + return 1 +} + +wait_for_endpoint() { + local func=${1:?} + local -i cnt=${TEST_MAX_WAIT_SEC:?} + local endpoint=$(kubectl get endpoints -l function=$func | grep $func | awk '{print $2}') + echo_info "Waiting for the endpoint ${endpoint}' to be ready ..." + until curl -s $endpoint; do + ((cnt=cnt-1)) || return 1 + sleep 1 + done +} +wait_for_autoscale() { + local func=${1:?} + local -i cnt=${TEST_MAX_WAIT_SEC:?} + local hap=$() + echo_info "Waiting for HAP ${func} to be ready ..." + until kubectl get horizontalpodautoscalers | grep $func; do + ((cnt=cnt-1)) || return 1 + sleep 1 + done +} +wait_for_job() { + local func=${1:?} + local -i cnt=${TEST_MAX_WAIT_SEC:?} + echo_info "Waiting for build job of ${func} to be finished ..." + until kubectl get job -l function=${func} -o yaml | grep "succeeded: 1"; do + ((cnt=cnt-1)) || return 1 + sleep 1 + done +} +test_must_fail_without_rbac_roles() { + echo_info "RBAC TEST: function deploy/call must fail without RBAC roles" + _delete_simple_function + kubeless_recreate $KUBELESS_MANIFEST_RBAC $KUBELESS_MANIFEST + _wait_for_kubeless_controller_logline "User.*cannot" +} +redeploy_with_rbac_roles() { + kubeless_recreate $KUBELESS_MANIFEST_RBAC $KUBELESS_MANIFEST_RBAC + _wait_for_kubeless_controller_ready + _wait_for_kubeless_controller_logline "controller synced and ready" +} + +deploy_kafka() { + echo_info "Deploy kafka ... " + kubectl create -f $KAFKA_MANIFEST +} + +deploy_nats_operator() { + echo_info "Deploy NATS operator ... " + kubectl apply -f https://raw.githubusercontent.com/nats-io/nats-operator/master/example/deployment-rbac.yaml +} + +deploy_nats_cluster() { + echo_info "Deploy NATS cluster ... " + kubectl apply -f ./manifests/nats/nats-cluster.yaml -n nats-io +} + +deploy_nats_trigger_controller() { + echo_info "Deploy NATS trigger controller ... " + kubectl create -f $NATS_MANIFEST +} + +expose_nats_service() { + kubectl get svc nats -n nats-io -o yaml | sed 's/ClusterIP/NodePort/' | kubectl replace -f - +} + +deploy_kinesis_trigger_controller() { + echo_info "Deploy Kinesis trigger controller ... " + kubectl create -f $KINESIS_MANIFEST +} + +wait_for_kubeless_kinesis_controller_ready() { + echo_info "Waiting for Kinesis trigger controller pods to be ready ..." + k8s_wait_for_pod_ready -n kubeless -l kubeless=kinesis-trigger-controller +} + +deploy_kinesalite() { + echo_info "Deploy Kinesalite a AWS Kinesis mock server ... " + kubectl apply -f ./manifests/kinesis/kinesalite.yaml +} + +wait_for_kinesalite_pod() { + echo_info "Waiting for Kinesalite pod to be ready ..." + k8s_wait_for_pod_ready -l app=kinesis +} + +deploy_function() { + local func=${1:?} func_topic + echo_info "TEST: $func" + kubeless_function_delete ${func} + make -sC examples ${func} +} + +deploy_kafka_trigger() { + local trigger=${1:?} + echo_info "TEST: $trigger" + kubeless_kafka_trigger_delete ${trigger} + make -sC examples ${trigger} +} + +deploy_nats_trigger() { + local trigger=${1:?} + echo_info "TEST: $trigger" + kubeless_nats_trigger_delete ${trigger} + make -sC examples ${trigger} +} + +verify_function() { + local func=${1:?} + local make_task=${2:-${func}-verify} + echo_info "Init logs: $(kubectl logs -l function=${func} -c prepare)" + k8s_wait_for_pod_ready -l function=${func} + case "${func}" in + *pubsub*) + func_topic=$(kubectl get kafkatrigger "${func}" -o yaml|sed -n 's/topic: //p') + echo_info "FUNC TOPIC: $func_topic" + esac + local -i counter=0 + until make -sC examples ${make_task}; do + echo_info "FUNC ${func} failed. Retrying..." + ((counter=counter+1)) + if [ "$counter" -ge 3 ]; then + echo_info "FUNC ${func} failed ${counter} times. Exiting" + return 1; + fi + sleep `expr 10 \* $counter` + done +} +test_kubeless_function() { + local func=${1:?} + deploy_function $func + verify_function $func +} +update_function() { + local func=${1:?} func_topic + echo_info "UPDATE: $func" + make -sC examples ${func}-update + sleep 10 + k8s_wait_for_uniq_pod -l function=${func} +} +restart_function() { + local func=${1:?} + echo_info "Restarting: $func" + kubectl delete pod -l function=${func} + k8s_wait_for_uniq_pod -l function=${func} +} +test_kubeless_function_update() { + local func=${1:?} + update_function $func + verify_function $func ${func}-update-verify +} +create_basic_auth_secret() { + local secret=${1:?}; shift + htpasswd -cb auth foo bar + kubectl create secret generic $secret --from-file=auth +} +create_tls_secret_from_key_cert() { + local secret=${1:?}; shift + openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /tmp/tls.key -out /tmp/tls.crt -subj "/CN=foo.bar.com" + kubectl create secret tls $secret --key /tmp/tls.key --cert /tmp/tls.crt +} +create_http_trigger_with_tls_secret(){ + local func=${1:?}; shift + local domain=${1-""}; + local subpath=${2-""}; + local secret=${3-""}; + delete_http_trigger ${func} + echo_info "TEST: Creating HTTP trigger" + local command="kubeless trigger http create ing-${func} --function-name ${func}" + if [ -n "$domain" ]; then + command="$command --hostname ${domain}" + fi + if [ -n "$subpath" ]; then + command="$command --path ${subpath}" + fi + if [ -n "$secret" ]; then + command="$command --tls-secret ${secret}" + fi + eval $command +} +create_http_trigger(){ + local func=${1:?}; shift + local domain=${1-""}; + local subpath=${2-""}; + local basicauth=${3-""}; + local gateway=${4-""}; + delete_http_trigger ${func} + echo_info "TEST: Creating HTTP trigger" + local command="kubeless trigger http create ing-${func} --function-name ${func}" + if [ -n "$domain" ]; then + command="$command --hostname ${domain}" + fi + if [ -n "$subpath" ]; then + command="$command --path ${subpath}" + fi + if [ -n "$basicauth" ]; then + command="$command --basic-auth-secret ${basicauth}" + fi + if [ -n "$gateway" ]; then + command="$command --gateway ${gateway}" + fi + eval $command +} +update_http_trigger(){ + local func=${1:?}; shift + local domain=${1:-""} + local subpath=${2:-""}; + echo_info "TEST: Updating HTTP trigger" + local command="kubeless trigger http update ing-${func} --function-name ${func}" + if [ -n "$domain" ]; then + command="$command --hostname ${domain}" + fi + if [ -n "$subpath" ]; then + command="$command --path ${subpath}" + fi + eval $command +} +verify_http_trigger(){ + local func=${1:?}; shift + local ip=${1:?}; shift + local expected_response=${1:?}; shift + local domain=${1:?}; shift + local subpath=${1:-""}; + kubeless trigger http list | grep ${func} + local -i cnt=${TEST_MAX_WAIT_SEC:?} + echo_info "Waiting for ingress to be ready..." + until kubectl get ingress | grep $func | grep "$domain" | awk '{print $3}' | grep "$ip"; do + ((cnt=cnt-1)) || return 1 + sleep 1 + done + sleep 3 + curl -vv --header "Host: $domain" $ip\/$subpath | grep "${expected_response}" +} +verify_http_trigger_basic_auth(){ + local func=${1:?}; shift + local ip=${1:?}; shift + local expected_response=${1:?}; shift + local domain=${1:?}; shift + local subpath=${1:?}; shift + local auth=${1:-""}; + kubeless trigger http list | grep ${func} + local -i cnt=${TEST_MAX_WAIT_SEC:?} + echo_info "Waiting for ingress to be ready..." + until kubectl get ingress | grep $func | grep "$domain" | awk '{print $3}' | grep "$ip"; do + ((cnt=cnt-1)) || return 1 + sleep 1 + done + sleep 3 + curl -v --header "Host: $domain" $ip\/$subpath | grep "401 Authorization Required" + curl -v --header "Host: $domain" -u $auth $ip\/$subpath | grep "${expected_response}" +} +verify_https_trigger(){ + local func=${1:?}; shift + local ip=${1:?}; shift + local expected_response=${1:?}; shift + local domain=${1:?}; shift + local subpath=${1:-""}; + kubeless trigger http list | grep ${func} + local -i cnt=${TEST_MAX_WAIT_SEC:?} + echo_info "Waiting for ingress to be ready..." + until kubectl get ingress | grep $func | grep "$domain" | awk '{print $3}' | grep "$ip"; do + ((cnt=cnt-1)) || return 1 + sleep 1 + done + sleep 3 + curl -k -vv --header "Host: $domain" https:\/\/$ip\/$subpath | grep "${expected_response}" +} +delete_http_trigger() { + local func=${1:?}; shift + kubeless trigger http list |grep -w ing-${func} && kubeless trigger http delete ing-${func} >& /dev/null || true +} +create_cronjob_trigger(){ + local func=${1:?}; shift + local schedule=${1:?}; + delete_cronjob_trigger ${func} + echo_info "TEST: Creating CronJob trigger" + kubeless trigger cronjob create ${func} --function ${func} --schedule "${schedule}" +} +update_cronjob_trigger(){ + local func=${1:?}; shift + local schedule=${1:?}; + echo_info "TEST: Updating CronJob trigger" + kubeless trigger cronjob update ${func} --function ${func} --schedule "${schedule}" +} +verify_cronjob_trigger(){ + local func=${1:?}; shift + local schedule=${1:?}; shift + local expected_log=${1:?} + local -i cnt=${TEST_MAX_WAIT_SEC:?} + kubeless trigger cronjob list | grep ${func} | grep "${schedule}" + echo_info "Waiting for CronJob to be executed..." + until kubectl logs -l function=${func} | grep "$expected_log"; do + ((cnt=cnt-1)) || return 1 + sleep 1 + done +} +delete_cronjob_trigger() { + local func=${1:?}; shift + kubeless trigger cronjob list |grep -w ${func} && kubeless trigger cronjob delete ${func} >& /dev/null || true +} +test_kubeless_autoscale() { + local func=${1:?} exp_autoscale act_autoscale + # Use some fixed values + local val=10 num=3 + echo_info "TEST: autoscale ${func}" + kubeless autoscale create ${func} --value ${val:?} --min ${num:?} --max ${num:?} + wait_for_autoscale ${func} + kubeless autoscale list | fgrep -w ${func} + act_autoscale=$(kubectl get horizontalpodautoscaler -ojsonpath='{range .items[*].spec}{@.scaleTargetRef.name}:{@.targetCPUUtilizationPercentage}:{@.minReplicas}:{@.maxReplicas}{end}') + exp_autoscale="${func}:${val}:${num}:${num}" + [[ ${act_autoscale} == ${exp_autoscale} ]] + k8s_wait_for_pod_count ${num} -l function="${func}" + kubeless autoscale delete ${func} +} +test_topic_deletion() { + local topic=$RANDOM + local topic_count=0 + kubeless topic create $topic + kubeless topic delete $topic + topic_count=$(kubeless topic list | grep $topic | wc -l) + if [ ${topic_count} -gt 0 ] ; then + echo_info "Topic $topic still exists" + exit 200 + fi +} +sts_restart() { + local num=1 + kubectl delete pod kafka-0 -n kubeless + kubectl delete pod zoo-0 -n kubeless + k8s_wait_for_uniq_pod -l kubeless=zookeeper -n kubeless + k8s_wait_for_uniq_pod -l kubeless=kafka -n kubeless + wait_for_kubeless_kafka_server_ready +} +verify_clean_object() { + local type=${1:?}; shift + local name=${1:?}; shift + echo_info "Checking if "${type}" exists for function "${name}"... " + local -i cnt=${TEST_MAX_WAIT_SEC:?} + until [[ ! $(kubectl get ${type} 2>&1 | grep ${name}) ]]; do + ((cnt=cnt-1)) || return 1 + sleep 1 + echo_info "$(kubectl get ${type} 2>&1 | grep ${name})" + done + echo_info "${type}/${name} is gone" +} +# vim: sw=4 ts=4 et si diff --git a/script/make.sh b/script/make.sh new file mode 100755 index 00000000..974f73ff --- /dev/null +++ b/script/make.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash + +# Copyright (c) 2016-2017 Bitnami +# +# 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. + +set -e + +export KUBELESS_PKG='github.com/kubeless/kubeless' + +# List of bundles to create when no argument is passed +DEFAULT_BUNDLES=( + validate-test + validate-gofmt + validate-git-marks + validate-lint + validate-vet + binary +) +bundle() { + local bundle="$1"; shift + echo "---> Making bundle: $(basename "$bundle") (in $DEST)" + source "script/$bundle" "$@" +} + +if [ $# -lt 1 ]; then + bundles=(${DEFAULT_BUNDLES[@]}) +else + bundles=($@) +fi +for bundle in ${bundles[@]}; do + export DEST=. + ABS_DEST="$(cd "$DEST" && pwd -P)" + bundle "$bundle" + echo +done diff --git a/script/pull-or-build-image.sh b/script/pull-or-build-image.sh new file mode 100755 index 00000000..fe0422e8 --- /dev/null +++ b/script/pull-or-build-image.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +set -e + +TARGET=${1:?} + +function push() { + local image=${1:?} + if [[ -n "$DOCKER_USERNAME" && -n "$DOCKER_PASSWORD" ]]; then + docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD" + docker push $image + fi +} + +case "${TARGET}" in + "controller-image") + image=${CONTROLLER_IMAGE:?} + docker pull $image || make $TARGET CONTROLLER_IMAGE=$image + push $image + ;; + "kafka-controller-image") + image=${KAFKA_CONTROLLER_IMAGE:?} + docker pull $image || make $TARGET KAFKA_CONTROLLER_IMAGE=$image + push $image + ;; + "nats-controller-image") + image=${NATS_CONTROLLER_IMAGE:?} + docker pull $image || make $TARGET NATS_CONTROLLER_IMAGE=$image + push $image + ;; + "kinesis-controller-image") + image=${KINESIS_CONTROLLER_IMAGE:?} + docker pull $image || make $TARGET KINESIS_CONTROLLER_IMAGE=$image + push $image + ;; + "function-image-builder") + image=${FUNCTION_IMAGE_BUILDER:?} + docker pull $image || make $TARGET FUNCTION_IMAGE_BUILDER=$image + push $image + ;; + "default") + echo "Unsupported target" + exit 1 +esac diff --git a/script/release_utils.sh b/script/release_utils.sh new file mode 100755 index 00000000..08f51693 --- /dev/null +++ b/script/release_utils.sh @@ -0,0 +1,99 @@ +#!/bin/bash +set -e + +function commit_list { + local tag=${1:?} + local repo_domain=${2:?} + local repo_name=${3:?} + git fetch --tags + local previous_tag=`curl -H "Authorization: token $ACCESS_TOKEN" -s https://api.github.com/repos/$repo_domain/$repo_name/tags | jq --raw-output '.[1].name'` + local release_notes=`git log $previous_tag..$tag --oneline` + local parsed_release_notes=$(echo "$release_notes" | sed -n -e 'H;${x;s/\n/\\n- /g;s/^\\n//;s/"/\\"/g;p;}') + echo $parsed_release_notes +} + +function get_release_notes { + local tag=${1:?} + local repo_domain=${2:?} + local repo_name=${3:?} + commits=`commit_list $tag $repo_domain $repo_name` + notes=$(echo "\ +This release includes the following commits and features:\\n\ +$commits\\n\\n\ +To install this latest version, use the manifest that is part of the release:\\n\ +\\n\ +**WITH RBAC ENABLED:**\\n\ +\\n\ +\`\`\`console\\n\ +kubectl create ns kubeless\\n\ +kubectl create -f https://github.com/kubeless/kubeless/releases/download/$tag/kubeless-$tag.yaml \\n\ +\`\`\`\\n\ +\\n\ +**WITHOUT RBAC:**\\n\ +\\n\ +\`\`\`console\\n\ +kubectl create ns kubeless\\n\ +kubectl create -f https://github.com/kubeless/kubeless/releases/download/$tag/kubeless-non-rbac-$tag.yaml \\n\ +\`\`\`\\n\ +**OPENSHIFT:**\\n\ +\\n\ +\`\`\`console\\n\ +oc create ns kubeless\\n\ +oc create -f https://github.com/kubeless/kubeless/releases/download/$tag/kubeless-openshift-$tag.yaml \\n\ +# Kafka\\n\ +oc create -f https://github.com/kubeless/kubeless/releases/download/$tag/kafka-zookeeper-openshift-$tag.yaml \\n\ +\`\`\`\\n\ +") + echo "${notes}" +} + +function get_release_body { + local tag=${1:?} + local repo_domain=${2:?} + local repo_name=${3:?} + local release_notes=$(get_release_notes $tag $repo_domain $repo_name) + echo '{ + "tag_name": "'$tag'", + "target_commitish": "master", + "name": "'$tag'", + "body": "'$release_notes'", + "draft": true, + "prerelease": false + }' +} + +function update_release_tag { + local tag=${1:?} + local repo_domain=${2:?} + local repo_name=${3:?} + local release_id=$(curl -H "Authorization: token $ACCESS_TOKEN" -s https://api.github.com/repos/$repo_domain/$repo_name/releases | jq --raw-output '.[0].id') + local body=$(get_release_body $tag $repo_domain $repo_name) + local release=`curl -H "Authorization: token $ACCESS_TOKEN" -s --request PATCH --data $body https://api.github.com/repos/$repo_domain/$repo_name/releases/$release_id` + echo $release +} + +function release_tag { + local tag=$1 + local repo_domain=${2:?} + local repo_name=${3:?} + local body=$(get_release_body $tag $repo_domain $repo_name) + local release=`curl -H "Authorization: token $ACCESS_TOKEN" -s --request POST --data "$body" https://api.github.com/repos/$repo_domain/$repo_name/releases` + echo $release +} + +function upload_asset { + local repo_domain=${1:?} + local repo_name=${2:?} + local release_id=${3:?} + local asset=${4:?} + local filename=$(basename $asset) + if [[ "$filename" == *".zip" ]]; then + local content_type="application/zip" + elif [[ "$filename" == *".yaml" ]]; then + local content_type="text/yaml" + fi + curl -H "Authorization: token $ACCESS_TOKEN" \ + -H "Content-Type: $content_type" \ + --data-binary @"$asset" \ + "https://uploads.github.com/repos/$repo_domain/$repo_name/releases/$release_id/assets?name=$filename" +} diff --git a/script/start-test-environment.sh b/script/start-test-environment.sh new file mode 100755 index 00000000..5608610c --- /dev/null +++ b/script/start-test-environment.sh @@ -0,0 +1,34 @@ +#!/bin/bash +set -e +SCRIPT=$0 +if [ -h $SCRIPT ]; then + SCRIPT=`readlink $SCRIPT` +fi +ROOTDIR=`cd $(dirname $SCRIPT)/.. && pwd` + +COMMAND="${@:-bash}" + +if ! minikube status | grep -q "minikube: $"; then + echo "Unable to start the test environment with an existing instance of minikube" + echo "Delete the current profile executing 'minikube delete' or create a new one" + echo "executing 'minikube profile new_profile'" + exit 1 +fi + +minikube start --extra-config=apiserver.Authorization.Mode=RBAC --insecure-registry 0.0.0.0/0 +eval $(minikube docker-env) + +CONTEXT=$(kubectl config current-context) + +# Both RBAC'd dind and minikube seem to be missing rules to make kube-dns work properly +# add some (granted) broad ones: +kubectl --context=${CONTEXT} get clusterrolebinding kube-dns-admin >& /dev/null || \ + kubectl --context=${CONTEXT} create clusterrolebinding kube-dns-admin --serviceaccount=kube-system:default --clusterrole=cluster-admin + +docker run --privileged -it \ + -v $ROOTDIR:/go/src/github.com/kubeless/kubeless \ + -v $HOME/.kube:/root/.kube \ + -v $HOME/.minikube:$HOME/.minikube \ + -e TEST_CONTEXT=$(kubectl config current-context) \ + -e TEST_DEBUG=1 \ + kubeless/dev-environment:latest bash -c "$COMMAND" diff --git a/script/upload_release_notes.sh b/script/upload_release_notes.sh new file mode 100755 index 00000000..508a0f34 --- /dev/null +++ b/script/upload_release_notes.sh @@ -0,0 +1,25 @@ +#!/bin/bash +set -e + +REPO_NAME=kubeless +REPO_DOMAIN=kubeless + +source $(dirname $0)/release_utils.sh + +if [[ -z "$REPO_NAME" || -z "$REPO_DOMAIN" ]]; then + echo "Github repository not specified" > /dev/stderr + exit 1 +fi + +if [[ -z "$ACCESS_TOKEN" ]]; then + echo "Unable to release: Github Token not specified" > /dev/stderr + exit 1 +fi + +repo_check=`curl -H "Authorization: token $ACCESS_TOKEN" -s https://api.github.com/repos/$REPO_DOMAIN/$REPO_NAME` +if [[ $repo_check == *"Not Found"* ]]; then + echo "Not found a Github repository for $REPO_DOMAIN/$REPO_NAME, it is not possible to publish it" > /dev/stderr + exit 1 +else + update_release_tag $1 $REPO_DOMAIN $REPO_DOMAIN +fi diff --git a/script/validate-git-marks b/script/validate-git-marks new file mode 100755 index 00000000..c01828de --- /dev/null +++ b/script/validate-git-marks @@ -0,0 +1,58 @@ +#!/usr/bin/env bash + +# Copyright (c) 2016-2017 Bitnami +# +# 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. + +source "$(dirname "$BASH_SOURCE")/.validate" + +# folders=$(find * -type d | egrep -v '^Godeps|bundles|.git') + +IFS=$'\n' +files=( $(validate_diff --diff-filter=ACMR --name-only -- '*' | grep -v '^vendor/' || true) ) +unset IFS + +badFiles=() +for f in "${files[@]}"; do + if [ $(grep -r "^<<<<<<<" $f) ]; then + badFiles+=( "$f" ) + continue + fi + + if [ $(grep -r "^>>>>>>>" $f) ]; then + badFiles+=( "$f" ) + continue + fi + + if [ $(grep -r "^=======$" $f) ]; then + badFiles+=( "$f" ) + continue + fi + set -e +done + + +if [ ${#badFiles[@]} -eq 0 ]; then + echo 'Congratulations! There is no conflict.' +else + { + echo "There is trace of conflict(s) in the following files :" + for f in "${badFiles[@]}"; do + echo " - $f" + done + echo + echo 'Please fix the conflict(s) commit the result.' + echo + } >&2 + false +fi diff --git a/script/validate-gofmt b/script/validate-gofmt new file mode 100755 index 00000000..addd8522 --- /dev/null +++ b/script/validate-gofmt @@ -0,0 +1,44 @@ +#!/bin/bash + +# Copyright (c) 2016-2017 Bitnami +# +# 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. + +source "$(dirname "$BASH_SOURCE")/.validate" + +IFS=$'\n' +files=( $(validate_diff --diff-filter=ACMR --name-only -- '*.go' | grep -v '^vendor/\|kubeless.tpl.go' || true) ) +unset IFS + +badFiles=() +for f in "${files[@]}"; do + # we use "git show" here to validate that what's committed is formatted + if [ "$(git show "$VALIDATE_HEAD:$f" | gofmt -s -l)" ]; then + badFiles+=( "$f" ) + fi +done + +if [ ${#badFiles[@]} -eq 0 ]; then + echo 'Congratulations! All Go source files are properly formatted.' +else + { + echo "These files are not properly gofmt'd:" + for f in "${badFiles[@]}"; do + echo " - $f" + done + echo + echo 'Please reformat the above files using "gofmt -s -w" and commit the result.' + echo + } >&2 + false +fi diff --git a/script/validate-lint b/script/validate-lint new file mode 100755 index 00000000..8387072d --- /dev/null +++ b/script/validate-lint @@ -0,0 +1,48 @@ +#!/usr/bin/env bash + +# Copyright (c) 2016-2017 Bitnami +# +# 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. + +source "$(dirname "$BASH_SOURCE")/.validate" + +# We will eventually get to the point where packages should be the complete list +# of subpackages, vendoring excluded, as given by: +# +IFS=$'\n' +files=( $(validate_diff --diff-filter=ACMR --name-only -- '*.go' | grep -v '^vendor/\|^pkg/client/\|^pkg/apis/kubeless/v1beta1/zz_generated.deepcopy.go\|^integration\|kubeless.tpl.go' || true) ) +unset IFS + +errors=() +for f in "${files[@]}"; do + # we use "git show" here to validate that what's committed passes go lint + failedLint=$(golint "$f") + if [ "$failedLint" ]; then + errors+=( "$failedLint" ) + fi +done + +if [ ${#errors[@]} -eq 0 ]; then + echo 'Congratulations! All Go source files have been linted.' +else + { + echo "Errors from golint:" + for err in "${errors[@]}"; do + echo "$err" + done + echo + echo 'Please fix the above errors. You can test via "golint" and commit the result.' + echo + } >&2 + false +fi diff --git a/script/validate-test b/script/validate-test new file mode 100755 index 00000000..8fbeff33 --- /dev/null +++ b/script/validate-test @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +# Copyright (c) 2016-2017 Bitnami +# +# 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. + +# TODO: Simplify once `./...` ignores `vendor/` + +go test \ + github.com/kubeless/kubeless/cmd/... \ + github.com/kubeless/kubeless/pkg/... \ + github.com/kubeless/kubeless/version/... diff --git a/script/validate-vet b/script/validate-vet new file mode 100755 index 00000000..8585e67a --- /dev/null +++ b/script/validate-vet @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +# Copyright (c) 2016-2017 Bitnami +# +# 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. + +source "$(dirname "$BASH_SOURCE")/.validate" + +IFS=$'\n' +files=( $(validate_diff --diff-filter=ACMR --name-only -- '*.go' | grep -v '^vendor/\|kubeless.tpl.go' || true) ) +unset IFS + +failed=0 +for f in "${files[@]}"; do + # we use "git show" here to validate that what's committed passes go vet + if ! go vet "$f"; then + failed=1 + fi +done + +exit $failed diff --git a/tests/deployment-tests.bats b/tests/deployment-tests.bats new file mode 100644 index 00000000..c6608e9d --- /dev/null +++ b/tests/deployment-tests.bats @@ -0,0 +1,31 @@ +# Copyright (c) 2016-2017 Bitnami +# +# 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. + +load ../script/libtest + +@test "Verify TEST_CONTEXT envvar" { + : ${TEST_CONTEXT:?} +} +@test "Verify needed kubernetes tools installed" { + verify_k8s_tools +} +@test "Verify k8s RBAC mode" { + verify_rbac_mode +} +@test "Test simple function failure without RBAC rules" { + test_must_fail_without_rbac_roles +} +@test "Redeploy with proper RBAC rules" { + redeploy_with_rbac_roles +} diff --git a/tests/integration-tests-nats.bats b/tests/integration-tests-nats.bats new file mode 100644 index 00000000..9504d735 --- /dev/null +++ b/tests/integration-tests-nats.bats @@ -0,0 +1,49 @@ +#!/usr/bin/env bats + +# Copyright (c) 2016-2017 Bitnami +# +# 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. + +load ../script/libtest + +# 'bats' lacks loop support, unroll-them-all -> +@test "Deploy and wait for NATS" { + deploy_nats_operator + wait_for_kubeless_nats_operator_ready + deploy_nats_trigger_controller + wait_for_kubeless_nats_controller_ready + deploy_nats_cluster + wait_for_kubeless_nats_cluster_ready + expose_nats_service +} +@test "Test function: pubsub-python-nats" { + deploy_function python-nats + verify_function python-nats + kubeless_function_delete python-nats +} +@test "Test 1:n association between NATS trigger and functions" { + deploy_function nats-python-func1-topic-test + deploy_function nats-python-func2-topic-test + deploy_nats_trigger nats-python-trigger-topic-test + verify_function nats-python-func1-topic-test + verify_function nats-python-func2-topic-test + kubeless_function_delete nats-python-func1-topic-test + kubeless_function_delete nats-python-func2-topic-test +} +@test "Test 1:n association between function and NATS triggers" { + deploy_function nats-python-func-multi-topic + deploy_nats_trigger nats-python-trigger-topic1 + deploy_nats_trigger nats-python-trigger-topic2 + verify_function nats-python-func-multi-topic + kubeless_function_delete nats-python-func-multi-topic +} \ No newline at end of file