event.data;
}
================================================
FILE: examples/dotnetcore/dependency-yaml.cs
================================================
using System;
using Kubeless.Functions;
using YamlDotNet.Serialization;
public class module
{
public string handler(Event k8Event, Context k8Context)
{
var person = new Person()
{
Name = "Michael J. Fox",
Age = 56
};
var serializer = new SerializerBuilder().Build();
return serializer.Serialize(person); // yaml
}
}
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
================================================
FILE: examples/dotnetcore/dependency-yaml.csproj
================================================
netstandard2.0
================================================
FILE: examples/dotnetcore/fibonacci.cs
================================================
using System;
using Kubeless.Functions;
public class module
{
public int handler(Event k8Event, Context k8Context)
{
var n = int.Parse(k8Event.Data.ToString());
return fibonacci(n);
}
public int fibonacci(int n)
{
if ((n == 0) || (n == 1))
return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
}
================================================
FILE: examples/dotnetcore/fibonacci.csproj
================================================
netstandard2.0
================================================
FILE: examples/dotnetcore/helloget.cs
================================================
using System;
using Kubeless.Functions;
public class module
{
public string handler(Event k8Event, Context k8Context)
{
return "hello world";
}
}
================================================
FILE: examples/dotnetcore/helloget.csproj
================================================
netstandard2.0
================================================
FILE: examples/dotnetcore/hellowithdata.cs
================================================
using System;
using Kubeless.Functions;
public class module
{
public object handler(Event k8Event, Context k8Context)
{
return k8Event.Data;
}
}
================================================
FILE: examples/dotnetcore/hellowithdata.csproj
================================================
netstandard2.0
================================================
FILE: examples/golang/go.mod
================================================
module function
go 1.14
require (
github.com/sirupsen/logrus v1.6.0
)
================================================
FILE: examples/golang/helloget.go
================================================
package kubeless
import (
"github.com/kubeless/kubeless/pkg/functions"
)
// Foo sample function
func Foo(event functions.Event, context functions.Context) (string, error) {
return "Hello world!", nil
}
================================================
FILE: examples/golang/hellowithdata.go
================================================
package kubeless
import (
"fmt"
"github.com/kubeless/kubeless/pkg/functions"
)
// Handler sample function with data
func Handler(event functions.Event, context functions.Context) (string, error) {
fmt.Println(event)
return event.Data, nil
}
================================================
FILE: examples/golang/hellowithdeps.go
================================================
package kubeless
import (
"github.com/kubeless/kubeless/pkg/functions"
"github.com/sirupsen/logrus"
)
// Hello sample function with dependencies
func Hello(event functions.Event, context functions.Context) (string, error) {
logrus.Info(event.Data)
return "Hello world!", nil
}
================================================
FILE: examples/java/HelloGet.java
================================================
package io.kubeless;
import io.kubeless.Event;
import io.kubeless.Context;
public class Foo {
public String foo(io.kubeless.Event event, io.kubeless.Context context) {
return "Hello world!";
}
}
================================================
FILE: examples/java/HelloWithData.java
================================================
package io.kubeless;
import io.kubeless.Event;
import io.kubeless.Context;
public class Foo {
public String foo(io.kubeless.Event event, io.kubeless.Context context) {
System.out.println(event.Data);
return event.Data;
}
}
================================================
FILE: examples/java/HelloWithDeps.java
================================================
package io.kubeless;
import io.kubeless.Event;
import io.kubeless.Context;
import org.joda.time.LocalTime;
public class Hello {
public String sayHello(io.kubeless.Event event, io.kubeless.Context context) {
System.out.println(event.Data);
LocalTime currentTime = new LocalTime();
return "Hello world! Current local time is: " + currentTime;
}
}
================================================
FILE: examples/java/pom.xml
================================================
4.0.0
function
function
1.0-SNAPSHOT
joda-time
joda-time
2.9.2
io.kubeless
params
1.0-SNAPSHOT
io.kubeless
kubeless
1.0-SNAPSHOT
================================================
FILE: examples/jvm/Readme.md
================================================
# JVM exampels
These are examples to run compiled jvm code in kubeless.
They should serve as a template to be able to use other languages.
================================================
FILE: examples/jvm/java/Readme.md
================================================
# Java on runtime JVM
`gradle shadowJar` Build the jar with all deps
`kubeless function deploy test --runtime jvm1.8 --from-file build/libs/jvm-test-0.1-all.jar --handler io_ino_Handler.sayHello` The package name use `_` instead of `.` for the path.
================================================
FILE: examples/jvm/java/build.gradle
================================================
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.4'
}
}
apply plugin: 'java'
apply plugin: 'com.github.johnrengelman.shadow'
version = '0.1'
jar {
manifest {
attributes 'Implementation-Title': 'jvm-test',
'Implementation-Version': version
}
}
repositories {
mavenCentral()
}
dependencies {
compile group: 'log4j', name: 'log4j', version: '1.2.17'
compile group: 'de.inoio.kubeless', name: 'jvm-runtime', version: '0.1'
testCompile group: 'junit', name: 'junit', version: '4.+'
}
================================================
FILE: examples/jvm/java/src/main/java/io/ino/Handler.java
================================================
package io.ino;
public class Handler {
public String sayHello(io.kubeless.Event event, io.kubeless.Context context) {
System.out.println(event.toString());
return "Hello world! AFDFCH";
}
}
================================================
FILE: examples/jvm/scala/Readme.md
================================================
# Scala on runtime JVM
!! WIP the jar-file is to large for the storage backend, you have to pass a url to the jar file.
`sbt assembly` Build the jar with all deps
`kubeless function deploy testscala --runtime jvm1.8 --from-file target/scala-2.12/scala-test.jar --handler de_inoio_Handler.fooBar` The package name use `_` instead of `.` for the path.
================================================
FILE: examples/jvm/scala/build.sbt
================================================
assemblyJarName in assembly := "scala-test.jar"
organization := "de.inoio"
scalaVersion := "2.12.1"
libraryDependencies += "de.inoio.kubeless" % "jvm-runtime" % "0.1"
================================================
FILE: examples/jvm/scala/project/assembly.sbt
================================================
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.7")
================================================
FILE: examples/jvm/scala/project/build.properties
================================================
sbt.version=0.13.15
================================================
FILE: examples/jvm/scala/src/main/scala/de/inoio/Handler.scala
================================================
package de.inoio
import io.kubeless.{Context, Event}
class Handler {
def fooBar(event: Event, context: Context): String = {
"FOO Bar aus Hamburg"
}
}
================================================
FILE: examples/nodejs/function.yaml
================================================
---
apiVersion: kubeless.io/v1beta1
kind: Function
metadata:
name: hello
spec:
handler: handler.hello
runtime: nodejs6
function: |
module.exports = {
hello: function(event, context) {
return 'Hello World'
}
}
================================================
FILE: examples/nodejs/function1.yaml
================================================
---
apiVersion: kubeless.io/v1beta1
kind: Function
metadata:
name: hello
spec:
handler: handler.foobar
runtime: nodejs8
function: |
module.exports = {
foobar: function (event, context) {
return(event.data)
}
}
================================================
FILE: examples/nodejs/helloget.js
================================================
module.exports = {
foo: function (event, context) {
return 'hello world!';
}
}
================================================
FILE: examples/nodejs/hellostream.js
================================================
const from = require('from2');
const eos = require('end-of-stream');
function fromString(string) {
return from(function(size, next) {
if (string.length <= 0) return next(null, null);
const chunk = string.slice(0, size);
string = string.slice(size);
next(null, chunk);
});
}
module.exports = {
foo: function(event, context) {
return new Promise((resolve, reject) => {
const {response} = event.extensions;
const stream = fromString('hello world!');
eos(stream, err => err ? reject(err) : resolve(stream));
response.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
stream.pipe(response);
});
}
}
================================================
FILE: examples/nodejs/hellowithdata.js
================================================
module.exports = {
handler: (event, context) => {
console.log(event);
return event.data;
},
};
================================================
FILE: examples/nodejs/hellowithdeps.js
================================================
'use strict';
const _ = require('lodash');
module.exports = {
handler: (event, context) => {
_.assign(event.data, {date: new Date().toTimeString()})
return JSON.stringify(event.data);
},
};
================================================
FILE: examples/nodejs/index.js
================================================
'use strict';
module.exports = {
helloGet: require('./helloget').foo,
helloWithData: require('./hellowithdata').handler,
}
================================================
FILE: examples/nodejs/package.json
================================================
{
"name": "hellowithdeps",
"version": "0.0.1",
"dependencies": {
"end-of-stream": "^1.4.1",
"from2": "^2.3.0",
"lodash": "^4.17.5"
}
}
================================================
FILE: examples/php/composer.json
================================================
{
"require": {
"monolog/monolog": "^1.23"
}
}
================================================
FILE: examples/php/helloget.php
================================================
data);
}
================================================
FILE: examples/php/hellowithdeps.php
================================================
pushHandler(new StreamHandler("php://stdout", Logger::INFO));
// add records to the log
$log->info('Hello');
$log->info('World');
return "hello world";
}
================================================
FILE: examples/python/Dockerfile
================================================
# Create a custom image with a python function
FROM kubeless/python@sha256:565bebecb08d9a7b804c588105677a3572f10ff2032cef7727975061a653fb98
ENV FUNC_HANDLER=foo \
MOD_NAME=helloget
ADD helloget.py /
RUN mkdir -p /kubeless/
RUN chown 1000:1000 /kubeless
ENTRYPOINT [ "bash", "-c", "cp /helloget.py /kubeless/ && python3.7 /kubeless.py"]
================================================
FILE: examples/python/function.yaml
================================================
---
apiVersion: k8s.io/v1
kind: Function
metadata:
name: function
spec:
handler: hello.handler
runtime: python3.7
function: |
import json
def handler():
return "hello world"
================================================
FILE: examples/python/function1.yaml
================================================
---
apiVersion: kubeless.io/v1beta1
kind: Function
metadata:
name: function1
spec:
handler: hello.foobar
runtime: python3.7
deps: |
cowpy
function: |
import time
import random
from cowpy import cow
def foobar():
# NB: delay will be negative and sleep will raise an error
# occasionally. This is a feature for demoing errors.
delay = random.normalvariate(0.3, 0.2)
time.sleep(delay)
msg = "hello world - with a %0.2fs artificial delay" % delay
c = cow.get_cow()
return c().milk(msg)
================================================
FILE: examples/python/helloget.py
================================================
def foo(event, context):
return "hello world"
================================================
FILE: examples/python/hellowithdata.py
================================================
def handler(event, context):
print(event)
return event['data']
================================================
FILE: examples/python/hellowithdeps.py
================================================
from hellowithdepshelper import foo
================================================
FILE: examples/python/hellowithdepshelper.py
================================================
from bs4 import BeautifulSoup
import urllib.request
def foo(event, context):
page = urllib.request.urlopen("https://www.google.com/").read()
soup = BeautifulSoup(page, 'html.parser')
return soup.title.string
================================================
FILE: examples/python/requirements.txt
================================================
bs4
================================================
FILE: examples/ruby/Gemfile
================================================
source 'https://rubygems.org'
gem 'logging'
================================================
FILE: examples/ruby/function.yaml
================================================
---
apiVersion: kubeless.io/v1beta1
kind: Function
metadata:
name: function
spec:
handler: test.run
runtime: ruby2.4
function: |
# Obtains the latest Kubeless release published
def run(event, context)
require "net/https"
require "uri"
require "json"
# Fetch release info
uri = URI.parse("https://api.github.com/repos/kubeless/kubeless/releases")
http = Net::HTTP.new(uri.host, uri.port)
request = Net::HTTP::Get.new(uri.request_uri)
http.use_ssl = true
response = http.request(request)
# Parse response
output = JSON.parse(response.body)
# Create a Hash for output
output_hash = { version: output.first['name'] }
# Print the stuff (JSON)
puts JSON.pretty_generate(output_hash)
return output_hash[:version]
end
================================================
FILE: examples/ruby/helloget.rb
================================================
def foo(event, context)
"hello world"
end
================================================
FILE: examples/ruby/hellowithdata.rb
================================================
def handler(event, context)
puts event
JSON.generate(event[:data])
end
================================================
FILE: examples/ruby/hellowithdeps.rb
================================================
require 'logging'
def foo(event, context)
logging = Logging.logger(STDOUT)
logging.info "it works!"
"hello world"
end
================================================
FILE: examples/ruby/latest.rb
================================================
# Obtains the latest Kubeless release published
def handler(event, context)
require "net/https"
require "uri"
require "json"
# Fetch release info
uri = URI.parse("https://api.github.com/repos/kubeless/kubeless/releases")
http = Net::HTTP.new(uri.host, uri.port)
request = Net::HTTP::Get.new(uri.request_uri)
http.use_ssl = true
response = http.request(request)
# Parse response
output = JSON.parse(response.body)
# Create a Hash for output
output_hash = { version: output.first['name'] }
# Print the stuff (JSON)
puts JSON.pretty_generate(output_hash)
end
================================================
FILE: go.mod
================================================
module github.com/kubeless/kubeless
go 1.12
require (
github.com/Azure/go-autorest v8.0.0+incompatible // indirect
github.com/aws/aws-sdk-go v1.16.26
github.com/coreos/prometheus-operator v0.0.0-20171201110357-197eb012d973
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect
github.com/googleapis/gnostic v0.2.0 // indirect
github.com/gophercloud/gophercloud v0.0.0-20190130105114-cc9c99918988 // indirect
github.com/gosuri/uitable v0.0.0-20160404203958-36ee7e946282
github.com/imdario/mergo v0.3.7
github.com/kubeless/cronjob-trigger v1.0.2
github.com/kubeless/http-trigger v1.0.0
github.com/kubeless/kafka-trigger v1.0.1
github.com/kubeless/kinesis-trigger v0.0.0-20180817123215-a548c3d1cbd9
github.com/kubeless/nats-trigger v0.0.0-20180817123246-372a5fa547dc
github.com/mattn/go-runewidth v0.0.4 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/nats-io/gnatsd v1.4.1 // indirect
github.com/nats-io/go-nats v1.7.0
github.com/nats-io/nkeys v0.0.2 // indirect
github.com/nats-io/nuid v1.0.0 // indirect
github.com/pkg/errors v0.8.1 // indirect
github.com/prometheus/client_golang v0.9.3
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 // indirect
github.com/prometheus/common v0.4.0
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967
github.com/sirupsen/logrus v1.2.0
github.com/spf13/cobra v1.1.1
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d // indirect
golang.org/x/net v0.0.0-20190620200207-3b0461eec859
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/api v0.0.0-20180308224125-73d903622b73
k8s.io/apiextensions-apiserver v0.0.0-20180327033742-750feebe2038
k8s.io/apimachinery v0.0.0-20180228050457-302974c03f7e
k8s.io/client-go v7.0.0+incompatible
)
================================================
FILE: go.sum
================================================
cloud.google.com/go v0.0.0-20160913182117-3b1ae45394a2/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.35.1 h1:LMe/Btq0Eijsc97JyBwMc0KMXOe0orqAMdg7/EkywN8=
cloud.google.com/go v0.35.1/go.mod h1:wfjPZNvXCBYESy3fIynybskMP48KVPrjSPCnXiK7Prg=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3 h1:AVXDdKsrtX33oR9fbCMu/+c1o8Ofjq6Ku/MInaLVg5Y=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
github.com/Azure/go-autorest v8.0.0+incompatible h1:lgmv/yX7Zgt1TJEYG8DHCqc0zw5FkYevByNVIm77JNM=
github.com/Azure/go-autorest v8.0.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aws/aws-sdk-go v1.16.26 h1:GWkl3rkRO/JGRTWoLLIqwf7AWC4/W/1hMOUZqmX0js4=
github.com/aws/aws-sdk-go v1.16.26/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/coreos/prometheus-operator v0.0.0-20171201110357-197eb012d973 h1:7a78CgFQnnKoQomLoxGgKMaUp7QO9amd/IrifrECbmY=
github.com/coreos/prometheus-operator v0.0.0-20171201110357-197eb012d973/go.mod h1:SO+r5yZUacDFPKHfPoUjI3hMsH+ZUdiuNNhuSq3WoSg=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/emicklei/go-restful-swagger12 v0.0.0-20170208215640-dcef7f557305/go.mod h1:qr0VowGBT4CS4Q8vFF8BSeKz34PuqKGxs/L0IAQA9DQ=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 h1:Mn26/9ZMNWSw9C9ERFA1PUxfmGpolnw2v0bKOREu5ew=
github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v0.0.0-20170330071051-c0656edd0d9e/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.0 h1:xU6/SpYbvkNYiptHJYEDRseDLvYE7wSqhYYNy0QSUzI=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20141105023935-44145f04b68c/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef h1:veQD95Isof8w9/WXiA+pa3tz3fJXkt5B7QaRBrM62gk=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v0.0.0-20160524151835-7d79101e329e/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck=
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
github.com/googleapis/gnostic v0.2.0 h1:l6N3VoaVzTncYYW+9yOz2LJJammFZGBO13sqgEhpy9g=
github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
github.com/gophercloud/gophercloud v0.0.0-20190130105114-cc9c99918988 h1:fajr0WpQtCjYtwtH5zivs/sXvMcPcT/ebx+HdyD11NA=
github.com/gophercloud/gophercloud v0.0.0-20190130105114-cc9c99918988/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gosuri/uitable v0.0.0-20160404203958-36ee7e946282 h1:KFqmdzEPbU7Uck2tn50t+HQXZNVkxe8M9qRb/ZoSHaE=
github.com/gosuri/uitable v0.0.0-20160404203958-36ee7e946282/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo=
github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.0.0-20160207214719-a0d98a5f2880/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c h1:kQWxfPIHVLbgLzphqk3QUflDy9QdksZR4ygR807bpy0=
github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/imdario/mergo v0.0.0-20180119215619-163f41321a19/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI=
github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v0.0.0-20170829155851-36b14963da70/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE=
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/juju/ratelimit v0.0.0-20170523012141-5b9ff8664717/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kubeless/cronjob-trigger v1.0.2 h1:/hAkCMY7dTeP8oPo2lmPWmuEdQVcydPmUc10EfwGYaQ=
github.com/kubeless/cronjob-trigger v1.0.2/go.mod h1:Ktn0pfVcg2EG6XoV7MNBlsiyKm/RyUu87oRqdpMR1qM=
github.com/kubeless/http-trigger v1.0.0 h1:CciPHVu1Rf8oi67GOdMmhySILHYxxQDndiDDm+VoYfw=
github.com/kubeless/http-trigger v1.0.0/go.mod h1:a3DdjXl1CXccRLyiM4BeYpwW4Pt6q2viEm0mI6Wvaps=
github.com/kubeless/kafka-trigger v1.0.1 h1:XcKCe92i/+hww8fb2gNNAgCBKQNzuing9h97d1G8Jeg=
github.com/kubeless/kafka-trigger v1.0.1/go.mod h1:giDA+x4a/T6o0vWhHvZkas6N4B/cMjOv7fb3hnorMUI=
github.com/kubeless/kinesis-trigger v0.0.0-20180817123215-a548c3d1cbd9 h1:D+VuPkR46FGkP2dvH49fTF0dF/+Kz98H3Wy9BD3+ZGg=
github.com/kubeless/kinesis-trigger v0.0.0-20180817123215-a548c3d1cbd9/go.mod h1:Zz4cU6vaCS71yy+DpAx3/Y2HeV0RsJ3f+/5WCSeYq24=
github.com/kubeless/kubeless v1.0.0-alpha.6/go.mod h1:eBSqNpFBgiemDH1gmDcIndBDbGgoZJobww4ZaFK9N1k=
github.com/kubeless/nats-trigger v0.0.0-20180817123246-372a5fa547dc h1:64KDKAkb6xOt+Os36M9nblvBdWpMKNsQdc542wurU0E=
github.com/kubeless/nats-trigger v0.0.0-20180817123246-372a5fa547dc/go.mod h1:VgX8QhZAcW/DUMyMZbdfodjzLfZCZwSxd2q8XKNi1rs=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/matttproud/golang_protobuf_extensions v0.0.0-20150406173934-fc2b8d3a73c4/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nats-io/gnatsd v1.4.1 h1:RconcfDeWpKCD6QIIwiVFcvForlXpWeJP7i5/lDLy44=
github.com/nats-io/gnatsd v1.4.1/go.mod h1:nqco77VO78hLCJpIcVfygDP2rPGfsEHkGTUk94uh5DQ=
github.com/nats-io/go-nats v1.7.0 h1:oQOfHcLr8hb43QG8yeVyY2jtarIaTjOv41CGdF3tTvQ=
github.com/nats-io/go-nats v1.7.0/go.mod h1:+t7RHT5ApZebkrQdnn6AhQJmhJJiKAvJUio1PiiCtj0=
github.com/nats-io/nkeys v0.0.2 h1:+qM7QpgXnvDDixitZtQUBDY9w/s9mu1ghS+JIbsrx6M=
github.com/nats-io/nkeys v0.0.2/go.mod h1:dab7URMsZm6Z/jp9Z5UGa87Uutgc2mVpXLC4B7TDb/4=
github.com/nats-io/nuid v1.0.0 h1:44QGdhbiANq8ZCbUkdn6W5bqtg+mHuDE4wOUuxxndFs=
github.com/nats-io/nuid v1.0.0/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pkg/errors v0.0.0-20180311214515-816c9085562c/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.1 h1:K47Rk0v/fkEfwfQet2KWhscE0cJzjgCCDBG2KHZoVno=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20150212101744-fa8ad6fec335/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20170427095455-13ba4ddd0caa/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.2.0 h1:kUZDBDTdBVBYBj5Tmh2NZLlF60mfjA27rM34b+cVwNU=
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190129233650-316cf8ccfec5 h1:Etei0Wx6pooT/DeOKcGTr1M/01ggz95Ajq8BBwCOKBU=
github.com/prometheus/procfs v0.0.0-20190129233650-316cf8ccfec5/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967 h1:x7xEyJDP7Hv3LVgvWhzioQqbC/KtuUhTigKlH/8ehhE=
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
github.com/sirupsen/logrus v0.0.0-20180129181852-768a92a02685/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.1/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4=
github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
golang.org/x/crypto v0.0.0-20170825220121-81e90905daef/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190130090550-b01c7a725664 h1:YbZJ76lQ1BqNhVe7dKTSB67wDrc2VPRR75IyGyyPDX8=
golang.org/x/crypto v0.0.0-20190130090550-b01c7a725664/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3 h1:ulvT7fqt0yHWzpJwI57MezWnYDVpCAYBVuYst/L+fAY=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20170412232759-a6bd8cefa181/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190130055435-99b60b757ec1 h1:VeAkjQVzKLmu+JnFcK96TPbkuaTIqwGGAzQ9hgwPjVg=
golang.org/x/oauth2 v0.0.0-20190130055435-99b60b757ec1/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190130150945-aca44879d564 h1:o6ENHFwwr1TZ9CUPQcfo1HGvLP1OPsPOTB7xCIOPNmU=
golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 h1:HyfiK1WMnHj5FXFXatD+Qs1A/xC2Run6RzeW1SyHxpc=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
google.golang.org/genproto v0.0.0-20190122154452-ba6ebe99b011/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
k8s.io/api v0.0.0-20180103175015-389dfa299845/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA=
k8s.io/api v0.0.0-20180308224125-73d903622b73 h1:5Z+PFfTIOXwKmOhQtZ0WBykbpGBBOuvbDx2YNAqIoYc=
k8s.io/api v0.0.0-20180308224125-73d903622b73/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA=
k8s.io/apiextensions-apiserver v0.0.0-20180103181712-d0becfa6529e/go.mod h1:IxkesAMoaCRoLrPJdZNZUQp9NfZnzqaVzLhb2VEQzXE=
k8s.io/apiextensions-apiserver v0.0.0-20180327033742-750feebe2038 h1:VcfogrrvSU1RneMsMUOMf+1o5fN+SFcSrMw3I/yv3LU=
k8s.io/apiextensions-apiserver v0.0.0-20180327033742-750feebe2038/go.mod h1:IxkesAMoaCRoLrPJdZNZUQp9NfZnzqaVzLhb2VEQzXE=
k8s.io/apimachinery v0.0.0-20180103174757-bc110fd540ab/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0=
k8s.io/apimachinery v0.0.0-20180228050457-302974c03f7e h1:CsgbEA8905OlpVLNKWD4GacPex50kFbqhotVNPew+dU=
k8s.io/apimachinery v0.0.0-20180228050457-302974c03f7e/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0=
k8s.io/client-go v5.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s=
k8s.io/client-go v7.0.0+incompatible h1:kiH+Y6hn+pc78QS/mtBfMJAMIIaWevHi++JvOGEEQp4=
k8s.io/client-go v7.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s=
k8s.io/kube-openapi v0.0.0-20170830100654-868f2f29720b/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
================================================
FILE: hack/boilerplate.go.txt
================================================
/*
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.
*/
================================================
FILE: hack/update-codegen.sh
================================================
#!/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/kubeless/pkg/client github.com/kubeless/kubeless/pkg/apis \
kubeless:v1beta1
================================================
FILE: kubeless-non-rbac.jsonnet
================================================
local k = import "ksonnet.beta.1/k.libsonnet";
local runtimesSrc = import "runtimes.jsonnet";
local objectMeta = k.core.v1.objectMeta;
local deployment = k.apps.v1beta1.deployment;
local container = k.core.v1.container;
local service = k.core.v1.service;
local serviceAccount = k.core.v1.serviceAccount;
local configMap = k.core.v1.configMap;
local namespace = "kubeless";
local controller_account_name = "controller-acct";
local controllerEnv = [
{
name: "KUBELESS_INGRESS_ENABLED",
valueFrom: {configMapKeyRef: {"name": "kubeless-config", key: "ingress-enabled"}}
},
{
name: "KUBELESS_SERVICE_TYPE",
valueFrom: {configMapKeyRef: {"name": "kubeless-config", key: "service-type"}}
},
{
name: "KUBELESS_NAMESPACE",
valueFrom: {fieldRef: {fieldPath: "metadata.namespace"}}
},
{
name: "KUBELESS_CONFIG",
value: "kubeless-config"
},
];
local functionControllerContainer =
container.default("kubeless-function-controller", "kubeless/function-controller:latest") +
container.imagePullPolicy("IfNotPresent") +
container.env(controllerEnv);
local httpTriggerControllerContainer =
container.default("http-trigger-controller", "kubeless/http-trigger-controller:v1.0.3") +
container.imagePullPolicy("IfNotPresent") +
container.env(controllerEnv);
local cronjobTriggerContainer =
container.default("cronjob-trigger-controller", "kubeless/cronjob-trigger-controller:v1.0.3") +
container.imagePullPolicy("IfNotPresent") +
container.env(controllerEnv);
local kubelessLabel = {kubeless: "controller"};
local controllerAccount =
serviceAccount.default(controller_account_name, namespace);
local controllerDeployment =
deployment.default("kubeless-controller-manager", [functionControllerContainer, httpTriggerControllerContainer, cronjobTriggerContainer], namespace) +
{apiVersion: "apps/v1"} +
{metadata+:{labels: kubelessLabel}} +
{spec+: {selector: {matchLabels: kubelessLabel}}} +
{spec+: {template+: {spec+: {serviceAccountName: controllerAccount.metadata.name}}}} +
{spec+: {template+: {metadata: {labels: kubelessLabel}}}};
local crd = [
{
apiVersion: "apiextensions.k8s.io/v1beta1",
kind: "CustomResourceDefinition",
metadata: objectMeta.name("functions.kubeless.io"),
spec: {group: "kubeless.io", version: "v1beta1", scope: "Namespaced", names: {plural: "functions", singular: "function", kind: "Function"}},
},
{
apiVersion: "apiextensions.k8s.io/v1beta1",
kind: "CustomResourceDefinition",
metadata: objectMeta.name("httptriggers.kubeless.io"),
spec: {group: "kubeless.io", version: "v1beta1", scope: "Namespaced", names: {plural: "httptriggers", singular: "httptrigger", kind: "HTTPTrigger"}},
},
{
apiVersion: "apiextensions.k8s.io/v1beta1",
kind: "CustomResourceDefinition",
metadata: objectMeta.name("cronjobtriggers.kubeless.io"),
spec: {group: "kubeless.io", version: "v1beta1", scope: "Namespaced", names: {plural: "cronjobtriggers", singular: "cronjobtrigger", kind: "CronJobTrigger"}},
}
];
local deploymentConfig = '{}';
local kubelessConfig = configMap.default("kubeless-config", namespace) +
configMap.data({"ingress-enabled": "false"}) +
configMap.data({"service-type": "ClusterIP"})+
configMap.data({"deployment": std.toString(deploymentConfig)})+
configMap.data({"runtime-images": std.toString(runtimesSrc)})+
configMap.data({"enable-build-step": "false"})+
configMap.data({"function-registry-tls-verify": "true"})+
configMap.data({"provision-image": "kubeless/unzip@sha256:e867f9b366ffb1a25f14baf83438db426ced4f7add56137b7300d32507229b5a"})+
configMap.data({"provision-image-secret": ""})+
configMap.data({"builder-image": "kubeless/function-image-builder:latest"})+
configMap.data({"builder-image-secret": ""});
{
controllerAccount: k.util.prune(controllerAccount),
controller: k.util.prune(controllerDeployment),
crd: k.util.prune(crd),
cfg: k.util.prune(kubelessConfig),
}
================================================
FILE: kubeless-openshift.jsonnet
================================================
# Builds on kubeless.ksonnet to produce a deployable manifest on OpenShift 1.5
# Modifies apiVersion for kubeless-controller Deployment to extensions/v1beta1
# Modifies ClusterRole and ClusterRoleBinding apiVersions to v1
local k = import "ksonnet.beta.1/k.libsonnet";
local kubeless = import "kubeless.jsonnet";
local config = kubeless.cfg + k.core.v1.configMap.data({"deployment":'{"spec":{"template":{"spec":{"securityContext":{}}}}}'});
kubeless + {
controller: kubeless.controller + { apiVersion: "extensions/v1beta1" },
controllerClusterRole: kubeless.controllerClusterRole + { apiVersion: "v1" },
controllerClusterRoleBinding: kubeless.controllerClusterRoleBinding + { apiVersion: "v1" },
cfg: config,
}
================================================
FILE: kubeless.jsonnet
================================================
# Add RBAC role and binding on top of kubeless.jsonnet, to allow
# kubeless controller to deploy/update/etc functions on any namespace
local k = import "ksonnet.beta.1/k.libsonnet";
local objectMeta = k.core.v1.objectMeta;
local kubeless = import "kubeless-non-rbac.jsonnet";
local controller_account = kubeless.controller_account;
local controller_roles = [
{
apiGroups: [""],
resources: ["services", "configmaps"],
verbs: ["create", "get", "delete", "list", "update", "patch"],
},
{
apiGroups: ["apps", "extensions"],
resources: ["deployments"],
verbs: ["create", "get", "delete", "list", "update", "patch"],
},
{
apiGroups: [""],
resources: ["pods"],
verbs: ["list", "delete"],
},
{
apiGroups: [""],
resources: ["secrets"],
resourceNames: ["kubeless-registry-credentials"],
verbs: ["get"],
},
{
apiGroups: ["kubeless.io"],
resources: ["functions", "httptriggers", "cronjobtriggers"],
verbs: ["get", "list", "watch", "update", "delete"],
},
{
apiGroups: ["batch"],
resources: ["cronjobs", "jobs"],
verbs: ["create", "get", "delete", "deletecollection", "list", "update", "patch"],
},
{
apiGroups: ["autoscaling"],
resources: ["horizontalpodautoscalers"],
verbs: ["create", "get", "delete", "list", "update", "patch"],
},
{
apiGroups: ["apiextensions.k8s.io"],
resources: ["customresourcedefinitions"],
verbs: ["get", "list"],
},
{
apiGroups: ["monitoring.coreos.com"],
resources: ["alertmanagers", "prometheuses", "servicemonitors"],
verbs: ["*"],
},
{
apiGroups: ["extensions"],
resources: ["ingresses"],
verbs: ["create", "get", "list", "update", "delete"],
},
];
local controllerAccount = kubeless.controllerAccount;
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(
"kubeless-controller-deployer", controller_roles);
local controllerClusterRoleBinding = clusterRoleBinding(
"kubeless-controller-deployer", controllerClusterRole, [controllerAccount]
);
kubeless + {
controllerClusterRole: controllerClusterRole,
controllerClusterRoleBinding: controllerClusterRoleBinding,
}
================================================
FILE: manifests/README.md
================================================
# Collection of manifests for development
**NOTE: TO INSTALL KUBELESS USE A RELEASED MANIFEST AT https://github.com/kubeless/kubeless/releases"
In this folder you can find several manifest that you can deploy to extend the base functionality of Kubeless.
================================================
FILE: manifests/autoscaling/custom-metrics.yaml
================================================
kind: Namespace
apiVersion: v1
metadata:
name: custom-metrics
---
kind: ServiceAccount
apiVersion: v1
metadata:
name: custom-metrics-apiserver
namespace: custom-metrics
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: custom-metrics:system:auth-delegator
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:auth-delegator
subjects:
- kind: ServiceAccount
name: custom-metrics-apiserver
namespace: custom-metrics
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
name: custom-metrics-auth-reader
namespace: kube-system
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: extension-apiserver-authentication-reader
subjects:
- kind: ServiceAccount
name: custom-metrics-apiserver
namespace: custom-metrics
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: custom-metrics-read
rules:
- apiGroups:
- ""
resources:
- namespaces
- pods
- services
verbs:
- get
- list
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: custom-metrics-read
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: custom-metrics-read
subjects:
- kind: ServiceAccount
name: custom-metrics-apiserver
namespace: custom-metrics
---
apiVersion: apps/v1beta1
kind: Deployment
metadata:
name: custom-metrics-apiserver
namespace: custom-metrics
labels:
app: custom-metrics-apiserver
spec:
replicas: 1
template:
metadata:
name: custom-metrics-apiserver
labels:
app: custom-metrics-apiserver
spec:
serviceAccountName: custom-metrics-apiserver
containers:
- name: custom-metrics-server
image: luxas/k8s-prometheus-adapter
args:
- --prometheus-url=http://sample-metrics-prom.default.svc:9090
- --metrics-relist-interval=30s
- --rate-interval=60s
- --v=10
- --logtostderr=true
ports:
- containerPort: 443
securityContext:
runAsUser: 0
---
apiVersion: v1
kind: Service
metadata:
name: api
namespace: custom-metrics
spec:
ports:
- port: 443
targetPort: 443
selector:
app: custom-metrics-apiserver
---
apiVersion: apiregistration.k8s.io/v1beta1
kind: APIService
metadata:
name: v1alpha1.custom-metrics.metrics.k8s.io
spec:
insecureSkipTLSVerify: true
group: custom-metrics.metrics.k8s.io
groupPriorityMinimum: 1000
versionPriority: 5
service:
name: api
namespace: custom-metrics
version: v1alpha1
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: custom-metrics-server-resources
rules:
- apiGroups:
- custom-metrics.metrics.k8s.io
resources: ["*"]
verbs: ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: hpa-controller-custom-metrics
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: custom-metrics-server-resources
subjects:
- kind: ServiceAccount
name: horizontal-pod-autoscaler
namespace: kube-system
================================================
FILE: manifests/autoscaling/prometheus-operator.yaml
================================================
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: prometheus-operator
rules:
- apiGroups:
- extensions
resources:
- thirdpartyresources
verbs:
- create
- apiGroups:
- monitoring.coreos.com
resources:
- alertmanagers
- prometheuses
- servicemonitors
verbs:
- "*"
- apiGroups:
- apps
resources:
- statefulsets
verbs: ["*"]
- apiGroups: [""]
resources:
- configmaps
- secrets
verbs: ["*"]
- apiGroups: [""]
resources:
- pods
verbs: ["list", "delete"]
- apiGroups: [""]
resources:
- services
- endpoints
verbs: ["get", "create", "update"]
- apiGroups: [""]
resources:
- nodes
verbs: ["list", "watch"]
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: prometheus-operator
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: prometheus-operator
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: prometheus-operator
subjects:
- kind: ServiceAccount
name: prometheus-operator
namespace: default
---
apiVersion: apps/v1beta1
kind: Deployment
metadata:
name: prometheus-operator
labels:
operator: prometheus
spec:
replicas: 1
template:
metadata:
labels:
operator: prometheus
spec:
serviceAccountName: prometheus-operator
containers:
- name: prometheus-operator
image: luxas/prometheus-operator:v0.10.1
resources:
requests:
cpu: 100m
memory: 50Mi
limits:
cpu: 200m
memory: 100Mi
================================================
FILE: manifests/autoscaling/sample-metrics-app.yaml
================================================
apiVersion: apps/v1beta1
kind: Deployment
metadata:
labels:
app: sample-metrics-app
name: sample-metrics-app
spec:
replicas: 2
template:
metadata:
labels:
app: sample-metrics-app
spec:
containers:
- image: luxas/autoscale-demo:v0.1.2
name: sample-metrics-app
ports:
- name: web
containerPort: 8080
readinessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 3
periodSeconds: 5
livenessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 3
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: sample-metrics-app
labels:
app: sample-metrics-app
spec:
ports:
- name: web
port: 80
targetPort: 8080
selector:
app: sample-metrics-app
---
apiVersion: monitoring.coreos.com/v1alpha1
kind: ServiceMonitor
metadata:
name: sample-metrics-app
labels:
service-monitor: function
spec:
selector:
matchLabels:
app: sample-metrics-app
endpoints:
- port: web
---
kind: HorizontalPodAutoscaler
apiVersion: autoscaling/v2alpha1
metadata:
name: sample-metrics-app-hpa
spec:
scaleTargetRef:
kind: Deployment
name: sample-metrics-app
minReplicas: 2
maxReplicas: 10
metrics:
- type: Object
object:
target:
kind: Service
name: sample-metrics-app
metricName: http_requests
targetValue: 100
================================================
FILE: manifests/autoscaling/sample-prometheus-instance.yaml
================================================
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: prometheus
rules:
- apiGroups:
- ""
resources:
- nodes
- services
- endpoints
- pods
verbs:
- get
- list
- watch
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: prometheus
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: prometheus
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: prometheus
subjects:
- kind: ServiceAccount
name: prometheus
namespace: default
---
apiVersion: monitoring.coreos.com/v1alpha1
kind: Prometheus
metadata:
name: sample-metrics-prom
labels:
app: sample-metrics-prom
prometheus: sample-metrics-prom
spec:
replicas: 1
baseImage: prom/prometheus
version: v1.7.1
serviceAccountName: prometheus
serviceMonitorSelector:
matchLabels:
service-monitor: function
resources:
requests:
memory: 300Mi
#storage:
# resources:
# requests:
# storage: 3Gi
---
apiVersion: v1
kind: Service
metadata:
name: sample-metrics-prom
labels:
app: sample-metrics-prom
prometheus: sample-metrics-prom
spec:
type: NodePort
ports:
- name: web
nodePort: 30999
port: 9090
targetPort: web
selector:
prometheus: sample-metrics-prom
================================================
FILE: manifests/kinesis/kinesalite.yaml
================================================
---
apiVersion: v1
kind: Service
metadata:
annotations:
name: kinesis
labels:
app: kinesis
spec:
type: NodePort
ports:
- port: 4567
selector:
app: kinesis
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: kinesis
spec:
replicas: 1
template:
metadata:
labels:
app: kinesis
spec:
containers:
- name: kinesis
image: saikocat/kinesalite:1.11.5
ports:
- containerPort: 4567
args:
- --port=4567
================================================
FILE: manifests/monitoring/grafana-configmap.yaml
================================================
apiVersion: v1
data:
grafana-net-2-dashboard.json: |
{
"__inputs": [{
"name": "DS_PROMETHEUS",
"label": "Prometheus",
"description": "",
"type": "datasource",
"pluginId": "prometheus",
"pluginName": "Prometheus"
}],
"__requires": [{
"type": "panel",
"id": "singlestat",
"name": "Singlestat",
"version": ""
}, {
"type": "panel",
"id": "text",
"name": "Text",
"version": ""
}, {
"type": "panel",
"id": "graph",
"name": "Graph",
"version": ""
}, {
"type": "grafana",
"id": "grafana",
"name": "Grafana",
"version": "3.1.0"
}, {
"type": "datasource",
"id": "prometheus",
"name": "Prometheus",
"version": "1.0.0"
}],
"id": null,
"title": "Prometheus Stats",
"tags": [],
"style": "dark",
"timezone": "browser",
"editable": true,
"hideControls": true,
"sharedCrosshair": false,
"rows": [{
"collapse": false,
"editable": true,
"height": 178,
"panels": [{
"cacheTimeout": null,
"colorBackground": false,
"colorValue": false,
"colors": ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"],
"datasource": "${DS_PROMETHEUS}",
"decimals": 1,
"editable": true,
"error": false,
"format": "s",
"id": 5,
"interval": null,
"links": [],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"span": 3,
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": false
},
"targets": [{
"expr": "(time() - process_start_time_seconds{job=\"prometheus\"})",
"intervalFactor": 2,
"refId": "A",
"step": 4
}],
"thresholds": "",
"title": "Uptime",
"type": "singlestat",
"valueFontSize": "80%",
"valueMaps": [{
"op": "=",
"text": "N/A",
"value": "null"
}],
"valueName": "current",
"mappingTypes": [{
"name": "value to text",
"value": 1
}, {
"name": "range to text",
"value": 2
}],
"rangeMaps": [{
"from": "null",
"to": "null",
"text": "N/A"
}],
"mappingType": 1,
"gauge": {
"show": false,
"minValue": 0,
"maxValue": 100,
"thresholdMarkers": true,
"thresholdLabels": false
}
}, {
"cacheTimeout": null,
"colorBackground": false,
"colorValue": false,
"colors": ["rgba(50, 172, 45, 0.97)", "rgba(237, 129, 40, 0.89)", "rgba(245, 54, 54, 0.9)"],
"datasource": "${DS_PROMETHEUS}",
"editable": true,
"error": false,
"format": "none",
"id": 6,
"interval": null,
"links": [],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"span": 3,
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": true
},
"targets": [{
"expr": "prometheus_local_storage_memory_series",
"intervalFactor": 2,
"refId": "A",
"step": 4
}],
"thresholds": "1,5",
"title": "Local Storage Memory Series",
"type": "singlestat",
"valueFontSize": "70%",
"valueMaps": [],
"valueName": "current",
"mappingTypes": [{
"name": "value to text",
"value": 1
}, {
"name": "range to text",
"value": 2
}],
"rangeMaps": [{
"from": "null",
"to": "null",
"text": "N/A"
}],
"mappingType": 1,
"gauge": {
"show": false,
"minValue": 0,
"maxValue": 100,
"thresholdMarkers": true,
"thresholdLabels": false
}
}, {
"cacheTimeout": null,
"colorBackground": false,
"colorValue": true,
"colors": ["rgba(50, 172, 45, 0.97)", "rgba(237, 129, 40, 0.89)", "rgba(245, 54, 54, 0.9)"],
"datasource": "${DS_PROMETHEUS}",
"editable": true,
"error": false,
"format": "none",
"id": 7,
"interval": null,
"links": [],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"span": 3,
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": true
},
"targets": [{
"expr": "prometheus_local_storage_indexing_queue_length",
"intervalFactor": 2,
"refId": "A",
"step": 4
}],
"thresholds": "500,4000",
"title": "Interal Storage Queue Length",
"type": "singlestat",
"valueFontSize": "70%",
"valueMaps": [{
"op": "=",
"text": "Empty",
"value": "0"
}],
"valueName": "current",
"mappingTypes": [{
"name": "value to text",
"value": 1
}, {
"name": "range to text",
"value": 2
}],
"rangeMaps": [{
"from": "null",
"to": "null",
"text": "N/A"
}],
"mappingType": 1,
"gauge": {
"show": false,
"minValue": 0,
"maxValue": 100,
"thresholdMarkers": true,
"thresholdLabels": false
}
}, {
"content": "
\nPrometheus\n\nYou're using Prometheus, an open-source systems monitoring and alerting toolkit originally built at SoundCloud. For more information, check out the Grafana and Prometheus projects.
",
"editable": true,
"error": false,
"id": 9,
"links": [],
"mode": "html",
"span": 3,
"style": {},
"title": "",
"transparent": true,
"type": "text"
}],
"title": "New row"
}, {
"collapse": false,
"editable": true,
"height": 227,
"panels": [{
"aliasColors": {
"prometheus": "#C15C17",
"{instance=\"localhost:9090\",job=\"prometheus\"}": "#C15C17"
},
"bars": false,
"datasource": "${DS_PROMETHEUS}",
"editable": true,
"error": false,
"fill": 1,
"grid": {
"threshold1": null,
"threshold1Color": "rgba(216, 200, 27, 0.27)",
"threshold2": null,
"threshold2Color": "rgba(234, 112, 112, 0.22)"
},
"id": 3,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 2,
"links": [],
"nullPointMode": "connected",
"percentage": false,
"pointradius": 2,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"span": 9,
"stack": false,
"steppedLine": false,
"targets": [{
"expr": "rate(prometheus_local_storage_ingested_samples_total[5m])",
"interval": "",
"intervalFactor": 2,
"legendFormat": "{{job}}",
"metric": "",
"refId": "A",
"step": 2
}],
"timeFrom": null,
"timeShift": null,
"title": "Samples ingested (rate-5m)",
"tooltip": {
"shared": true,
"value_type": "cumulative",
"ordering": "alphabetical",
"msResolution": false
},
"type": "graph",
"yaxes": [{
"show": true,
"min": null,
"max": null,
"logBase": 1,
"format": "short"
}, {
"show": true,
"min": null,
"max": null,
"logBase": 1,
"format": "short"
}],
"xaxis": {
"show": true
}
}, {
"content": "#### Samples Ingested\nThis graph displays the count of samples ingested by the Prometheus server, as measured over the last 5 minutes, per time series in the range vector. When troubleshooting an issue on IRC or Github, this is often the first stat requested by the Prometheus team. ",
"editable": true,
"error": false,
"id": 8,
"links": [],
"mode": "markdown",
"span": 2.995914043583536,
"style": {},
"title": "",
"transparent": true,
"type": "text"
}],
"title": "New row"
}, {
"collapse": false,
"editable": true,
"height": "250px",
"panels": [{
"aliasColors": {
"prometheus": "#F9BA8F",
"{instance=\"localhost:9090\",interval=\"5s\",job=\"prometheus\"}": "#F9BA8F"
},
"bars": false,
"datasource": "${DS_PROMETHEUS}",
"editable": true,
"error": false,
"fill": 1,
"grid": {
"threshold1": null,
"threshold1Color": "rgba(216, 200, 27, 0.27)",
"threshold2": null,
"threshold2Color": "rgba(234, 112, 112, 0.22)"
},
"id": 2,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 2,
"links": [],
"nullPointMode": "connected",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"span": 5,
"stack": false,
"steppedLine": false,
"targets": [{
"expr": "rate(prometheus_target_interval_length_seconds_count[5m])",
"intervalFactor": 2,
"legendFormat": "{{job}}",
"refId": "A",
"step": 2
}],
"timeFrom": null,
"timeShift": null,
"title": "Target Scrapes (last 5m)",
"tooltip": {
"shared": true,
"value_type": "cumulative",
"ordering": "alphabetical",
"msResolution": false
},
"type": "graph",
"yaxes": [{
"show": true,
"min": null,
"max": null,
"logBase": 1,
"format": "short"
}, {
"show": true,
"min": null,
"max": null,
"logBase": 1,
"format": "short"
}],
"xaxis": {
"show": true
}
}, {
"aliasColors": {},
"bars": false,
"datasource": "${DS_PROMETHEUS}",
"editable": true,
"error": false,
"fill": 1,
"grid": {
"threshold1": null,
"threshold1Color": "rgba(216, 200, 27, 0.27)",
"threshold2": null,
"threshold2Color": "rgba(234, 112, 112, 0.22)"
},
"id": 14,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 2,
"links": [],
"nullPointMode": "connected",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"span": 4,
"stack": false,
"steppedLine": false,
"targets": [{
"expr": "prometheus_target_interval_length_seconds{quantile!=\"0.01\", quantile!=\"0.05\"}",
"interval": "",
"intervalFactor": 2,
"legendFormat": "{{quantile}} ({{interval}})",
"metric": "",
"refId": "A",
"step": 2
}],
"timeFrom": null,
"timeShift": null,
"title": "Scrape Duration",
"tooltip": {
"shared": true,
"value_type": "cumulative",
"ordering": "alphabetical",
"msResolution": false
},
"type": "graph",
"yaxes": [{
"show": true,
"min": null,
"max": null,
"logBase": 1,
"format": "short"
}, {
"show": true,
"min": null,
"max": null,
"logBase": 1,
"format": "short"
}],
"xaxis": {
"show": true
}
}, {
"content": "#### Scrapes\nPrometheus scrapes metrics from instrumented jobs, either directly or via an intermediary push gateway for short-lived jobs. Target scrapes will show how frequently targets are scraped, as measured over the last 5 minutes, per time series in the range vector. Scrape Duration will show how long the scrapes are taking, with percentiles available as series. ",
"editable": true,
"error": false,
"id": 11,
"links": [],
"mode": "markdown",
"span": 3,
"style": {},
"title": "",
"transparent": true,
"type": "text"
}],
"title": "New row"
}, {
"collapse": false,
"editable": true,
"height": "250px",
"panels": [{
"aliasColors": {},
"bars": false,
"datasource": "${DS_PROMETHEUS}",
"decimals": null,
"editable": true,
"error": false,
"fill": 1,
"grid": {
"threshold1": null,
"threshold1Color": "rgba(216, 200, 27, 0.27)",
"threshold2": null,
"threshold2Color": "rgba(234, 112, 112, 0.22)"
},
"id": 12,
"legend": {
"alignAsTable": false,
"avg": false,
"current": false,
"hideEmpty": true,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 2,
"links": [],
"nullPointMode": "connected",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"span": 9,
"stack": false,
"steppedLine": false,
"targets": [{
"expr": "prometheus_evaluator_duration_milliseconds{quantile!=\"0.01\", quantile!=\"0.05\"}",
"interval": "",
"intervalFactor": 2,
"legendFormat": "{{quantile}}",
"refId": "A",
"step": 2
}],
"timeFrom": null,
"timeShift": null,
"title": "Rule Eval Duration",
"tooltip": {
"shared": true,
"value_type": "cumulative",
"ordering": "alphabetical",
"msResolution": false
},
"type": "graph",
"yaxes": [{
"show": true,
"min": null,
"max": null,
"logBase": 1,
"format": "percentunit",
"label": ""
}, {
"show": true,
"min": null,
"max": null,
"logBase": 1,
"format": "short"
}],
"xaxis": {
"show": true
}
}, {
"content": "#### Rule Evaluation Duration\nThis graph panel plots the duration for all evaluations to execute. The 50th percentile, 90th percentile and 99th percentile are shown as three separate series to help identify outliers that may be skewing the data.",
"editable": true,
"error": false,
"id": 15,
"links": [],
"mode": "markdown",
"span": 3,
"style": {},
"title": "",
"transparent": true,
"type": "text"
}],
"title": "New row"
}],
"time": {
"from": "now-5m",
"to": "now"
},
"timepicker": {
"now": true,
"refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"],
"time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"]
},
"templating": {
"list": []
},
"annotations": {
"list": []
},
"refresh": false,
"schemaVersion": 12,
"version": 0,
"links": [{
"icon": "info",
"tags": [],
"targetBlank": true,
"title": "Grafana Docs",
"tooltip": "",
"type": "link",
"url": "http://www.grafana.org/docs"
}, {
"icon": "info",
"tags": [],
"targetBlank": true,
"title": "Prometheus Docs",
"type": "link",
"url": "http://prometheus.io/docs/introduction/overview/"
}],
"gnetId": 2,
"description": "The official, pre-built Prometheus Stats Dashboard."
}
grafana-net-737-dashboard.json: |
{
"__inputs": [{
"name": "DS_PROMETHEUS",
"label": "prometheus",
"description": "",
"type": "datasource",
"pluginId": "prometheus",
"pluginName": "Prometheus"
}],
"__requires": [{
"type": "panel",
"id": "singlestat",
"name": "Singlestat",
"version": ""
}, {
"type": "panel",
"id": "graph",
"name": "Graph",
"version": ""
}, {
"type": "grafana",
"id": "grafana",
"name": "Grafana",
"version": "3.1.0"
}, {
"type": "datasource",
"id": "prometheus",
"name": "Prometheus",
"version": "1.0.0"
}],
"id": null,
"title": "Kubernetes Pod Resources",
"description": "Shows resource usage of Kubernetes pods.",
"tags": [
"kubernetes"
],
"style": "dark",
"timezone": "browser",
"editable": true,
"hideControls": false,
"sharedCrosshair": false,
"rows": [{
"collapse": false,
"editable": true,
"height": "250px",
"panels": [{
"cacheTimeout": null,
"colorBackground": false,
"colorValue": true,
"colors": [
"rgba(50, 172, 45, 0.97)",
"rgba(237, 129, 40, 0.89)",
"rgba(245, 54, 54, 0.9)"
],
"datasource": "${DS_PROMETHEUS}",
"editable": true,
"error": false,
"format": "percent",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": true,
"thresholdLabels": false,
"thresholdMarkers": true
},
"height": "180px",
"id": 4,
"interval": null,
"isNew": true,
"links": [],
"mappingType": 1,
"mappingTypes": [{
"name": "value to text",
"value": 1
}, {
"name": "range to text",
"value": 2
}],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"rangeMaps": [{
"from": "null",
"text": "N/A",
"to": "null"
}],
"span": 4,
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": false
},
"targets": [{
"expr": "sum (container_memory_working_set_bytes{id=\"/\",instance=~\"^$instance$\"}) / sum (machine_memory_bytes{instance=~\"^$instance$\"}) * 100",
"interval": "",
"intervalFactor": 2,
"legendFormat": "",
"refId": "A",
"step": 2
}],
"thresholds": "65, 90",
"timeFrom": "1m",
"timeShift": null,
"title": "Memory Working Set",
"transparent": false,
"type": "singlestat",
"valueFontSize": "80%",
"valueMaps": [{
"op": "=",
"text": "N/A",
"value": "null"
}],
"valueName": "current"
}, {
"cacheTimeout": null,
"colorBackground": false,
"colorValue": true,
"colors": [
"rgba(50, 172, 45, 0.97)",
"rgba(237, 129, 40, 0.89)",
"rgba(245, 54, 54, 0.9)"
],
"datasource": "${DS_PROMETHEUS}",
"decimals": 2,
"editable": true,
"error": false,
"format": "percent",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": true,
"thresholdLabels": false,
"thresholdMarkers": true
},
"height": "180px",
"id": 6,
"interval": null,
"isNew": true,
"links": [],
"mappingType": 1,
"mappingTypes": [{
"name": "value to text",
"value": 1
}, {
"name": "range to text",
"value": 2
}],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"rangeMaps": [{
"from": "null",
"text": "N/A",
"to": "null"
}],
"span": 4,
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": false
},
"targets": [{
"expr": "sum(rate(container_cpu_usage_seconds_total{id=\"/\",instance=~\"^$instance$\"}[1m])) / sum (machine_cpu_cores{instance=~\"^$instance$\"}) * 100",
"interval": "10s",
"intervalFactor": 1,
"refId": "A",
"step": 10
}],
"thresholds": "65, 90",
"timeFrom": "1m",
"timeShift": null,
"title": "Cpu Usage",
"type": "singlestat",
"valueFontSize": "80%",
"valueMaps": [{
"op": "=",
"text": "N/A",
"value": "null"
}],
"valueName": "current"
}, {
"cacheTimeout": null,
"colorBackground": false,
"colorValue": true,
"colors": [
"rgba(50, 172, 45, 0.97)",
"rgba(237, 129, 40, 0.89)",
"rgba(245, 54, 54, 0.9)"
],
"datasource": "${DS_PROMETHEUS}",
"decimals": 2,
"editable": true,
"error": false,
"format": "percent",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": true,
"thresholdLabels": false,
"thresholdMarkers": true
},
"height": "180px",
"id": 7,
"interval": null,
"isNew": true,
"links": [],
"mappingType": 1,
"mappingTypes": [{
"name": "value to text",
"value": 1
}, {
"name": "range to text",
"value": 2
}],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"rangeMaps": [{
"from": "null",
"text": "N/A",
"to": "null"
}],
"span": 4,
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": false
},
"targets": [{
"expr": "sum(container_fs_usage_bytes{id=\"/\",instance=~\"^$instance$\"}) / sum(container_fs_limit_bytes{id=\"/\",instance=~\"^$instance$\"}) * 100",
"interval": "10s",
"intervalFactor": 1,
"legendFormat": "",
"metric": "",
"refId": "A",
"step": 10
}],
"thresholds": "65, 90",
"timeFrom": "1m",
"timeShift": null,
"title": "Filesystem Usage",
"type": "singlestat",
"valueFontSize": "80%",
"valueMaps": [{
"op": "=",
"text": "N/A",
"value": "null"
}],
"valueName": "current"
}, {
"cacheTimeout": null,
"colorBackground": false,
"colorValue": false,
"colors": [
"rgba(50, 172, 45, 0.97)",
"rgba(237, 129, 40, 0.89)",
"rgba(245, 54, 54, 0.9)"
],
"datasource": "${DS_PROMETHEUS}",
"decimals": 2,
"editable": true,
"error": false,
"format": "bytes",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"height": "1px",
"hideTimeOverride": true,
"id": 9,
"interval": null,
"isNew": true,
"links": [],
"mappingType": 1,
"mappingTypes": [{
"name": "value to text",
"value": 1
}, {
"name": "range to text",
"value": 2
}],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"postfix": "",
"postfixFontSize": "20%",
"prefix": "",
"prefixFontSize": "20%",
"rangeMaps": [{
"from": "null",
"text": "N/A",
"to": "null"
}],
"span": 2,
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": false
},
"targets": [{
"expr": "sum(container_memory_working_set_bytes{id=\"/\",instance=~\"^$instance$\"})",
"interval": "10s",
"intervalFactor": 1,
"refId": "A",
"step": 10
}],
"thresholds": "",
"timeFrom": "1m",
"title": "Used",
"type": "singlestat",
"valueFontSize": "50%",
"valueMaps": [{
"op": "=",
"text": "N/A",
"value": "null"
}],
"valueName": "current"
}, {
"cacheTimeout": null,
"colorBackground": false,
"colorValue": false,
"colors": [
"rgba(50, 172, 45, 0.97)",
"rgba(237, 129, 40, 0.89)",
"rgba(245, 54, 54, 0.9)"
],
"datasource": "${DS_PROMETHEUS}",
"decimals": 2,
"editable": true,
"error": false,
"format": "bytes",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"height": "1px",
"hideTimeOverride": true,
"id": 10,
"interval": null,
"isNew": true,
"links": [],
"mappingType": 1,
"mappingTypes": [{
"name": "value to text",
"value": 1
}, {
"name": "range to text",
"value": 2
}],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"rangeMaps": [{
"from": "null",
"text": "N/A",
"to": "null"
}],
"span": 2,
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": false
},
"targets": [{
"expr": "sum (machine_memory_bytes{instance=~\"^$instance$\"})",
"interval": "10s",
"intervalFactor": 1,
"refId": "A",
"step": 10
}],
"thresholds": "",
"timeFrom": "1m",
"title": "Total",
"type": "singlestat",
"valueFontSize": "50%",
"valueMaps": [{
"op": "=",
"text": "N/A",
"value": "null"
}],
"valueName": "current"
}, {
"cacheTimeout": null,
"colorBackground": false,
"colorValue": false,
"colors": [
"rgba(50, 172, 45, 0.97)",
"rgba(237, 129, 40, 0.89)",
"rgba(245, 54, 54, 0.9)"
],
"datasource": "${DS_PROMETHEUS}",
"decimals": 2,
"editable": true,
"error": false,
"format": "none",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"height": "1px",
"hideTimeOverride": true,
"id": 11,
"interval": null,
"isNew": true,
"links": [],
"mappingType": 1,
"mappingTypes": [{
"name": "value to text",
"value": 1
}, {
"name": "range to text",
"value": 2
}],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"postfix": " cores",
"postfixFontSize": "30%",
"prefix": "",
"prefixFontSize": "50%",
"rangeMaps": [{
"from": "null",
"text": "N/A",
"to": "null"
}],
"span": 2,
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": false
},
"targets": [{
"expr": "sum (rate (container_cpu_usage_seconds_total{id=\"/\",instance=~\"^$instance$\"}[1m]))",
"interval": "10s",
"intervalFactor": 1,
"refId": "A",
"step": 10
}],
"thresholds": "",
"timeFrom": "1m",
"timeShift": null,
"title": "Used",
"type": "singlestat",
"valueFontSize": "50%",
"valueMaps": [{
"op": "=",
"text": "N/A",
"value": "null"
}],
"valueName": "current"
}, {
"cacheTimeout": null,
"colorBackground": false,
"colorValue": false,
"colors": [
"rgba(50, 172, 45, 0.97)",
"rgba(237, 129, 40, 0.89)",
"rgba(245, 54, 54, 0.9)"
],
"datasource": "${DS_PROMETHEUS}",
"decimals": 2,
"editable": true,
"error": false,
"format": "none",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"height": "1px",
"hideTimeOverride": true,
"id": 12,
"interval": null,
"isNew": true,
"links": [],
"mappingType": 1,
"mappingTypes": [{
"name": "value to text",
"value": 1
}, {
"name": "range to text",
"value": 2
}],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"postfix": " cores",
"postfixFontSize": "30%",
"prefix": "",
"prefixFontSize": "50%",
"rangeMaps": [{
"from": "null",
"text": "N/A",
"to": "null"
}],
"span": 2,
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": false
},
"targets": [{
"expr": "sum (machine_cpu_cores{instance=~\"^$instance$\"})",
"interval": "10s",
"intervalFactor": 1,
"refId": "A",
"step": 10
}],
"thresholds": "",
"timeFrom": "1m",
"title": "Total",
"type": "singlestat",
"valueFontSize": "50%",
"valueMaps": [{
"op": "=",
"text": "N/A",
"value": "null"
}],
"valueName": "current"
}, {
"cacheTimeout": null,
"colorBackground": false,
"colorValue": false,
"colors": [
"rgba(50, 172, 45, 0.97)",
"rgba(237, 129, 40, 0.89)",
"rgba(245, 54, 54, 0.9)"
],
"datasource": "${DS_PROMETHEUS}",
"decimals": 2,
"editable": true,
"error": false,
"format": "bytes",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"height": "1px",
"hideTimeOverride": true,
"id": 13,
"interval": null,
"isNew": true,
"links": [],
"mappingType": 1,
"mappingTypes": [{
"name": "value to text",
"value": 1
}, {
"name": "range to text",
"value": 2
}],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"rangeMaps": [{
"from": "null",
"text": "N/A",
"to": "null"
}],
"span": 2,
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": false
},
"targets": [{
"expr": "sum(container_fs_usage_bytes{id=\"/\",instance=~\"^$instance$\"})",
"interval": "10s",
"intervalFactor": 1,
"refId": "A",
"step": 10
}],
"thresholds": "",
"timeFrom": "1m",
"title": "Used",
"type": "singlestat",
"valueFontSize": "50%",
"valueMaps": [{
"op": "=",
"text": "N/A",
"value": "null"
}],
"valueName": "current"
}, {
"cacheTimeout": null,
"colorBackground": false,
"colorValue": false,
"colors": [
"rgba(50, 172, 45, 0.97)",
"rgba(237, 129, 40, 0.89)",
"rgba(245, 54, 54, 0.9)"
],
"datasource": "${DS_PROMETHEUS}",
"decimals": 2,
"editable": true,
"error": false,
"format": "bytes",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"height": "1px",
"hideTimeOverride": true,
"id": 14,
"interval": null,
"isNew": true,
"links": [],
"mappingType": 1,
"mappingTypes": [{
"name": "value to text",
"value": 1
}, {
"name": "range to text",
"value": 2
}],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"rangeMaps": [{
"from": "null",
"text": "N/A",
"to": "null"
}],
"span": 2,
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": false
},
"targets": [{
"expr": "sum (container_fs_limit_bytes{id=\"/\",instance=~\"^$instance$\"})",
"interval": "10s",
"intervalFactor": 1,
"refId": "A",
"step": 10
}],
"thresholds": "",
"timeFrom": "1m",
"title": "Total",
"type": "singlestat",
"valueFontSize": "50%",
"valueMaps": [{
"op": "=",
"text": "N/A",
"value": "null"
}],
"valueName": "current"
}, {
"aliasColors": {},
"bars": false,
"datasource": "${DS_PROMETHEUS}",
"decimals": 2,
"editable": true,
"error": false,
"fill": 1,
"grid": {
"threshold1": null,
"threshold1Color": "rgba(216, 200, 27, 0.27)",
"threshold2": null,
"threshold2Color": "rgba(234, 112, 112, 0.22)",
"thresholdLine": false
},
"height": "200px",
"id": 32,
"isNew": true,
"legend": {
"alignAsTable": true,
"avg": true,
"current": true,
"max": false,
"min": false,
"rightSide": true,
"show": true,
"sideWidth": 200,
"sort": "current",
"sortDesc": true,
"total": false,
"values": true
},
"lines": true,
"linewidth": 2,
"links": [],
"nullPointMode": "connected",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"span": 12,
"stack": false,
"steppedLine": false,
"targets": [{
"expr": "sum(rate(container_network_receive_bytes_total{instance=~\"^$instance$\",namespace=~\"^$namespace$\"}[1m]))",
"interval": "",
"intervalFactor": 2,
"legendFormat": "receive",
"metric": "network",
"refId": "A",
"step": 240
}, {
"expr": "- sum(rate(container_network_transmit_bytes_total{instance=~\"^$instance$\",namespace=~\"^$namespace$\"}[1m]))",
"interval": "",
"intervalFactor": 2,
"legendFormat": "transmit",
"metric": "network",
"refId": "B",
"step": 240
}],
"timeFrom": null,
"timeShift": null,
"title": "Network",
"tooltip": {
"msResolution": false,
"shared": true,
"sort": 0,
"value_type": "cumulative"
},
"transparent": false,
"type": "graph",
"xaxis": {
"show": true
},
"yaxes": [{
"format": "Bps",
"label": "transmit / receive",
"logBase": 1,
"max": null,
"min": null,
"show": true
}, {
"format": "Bps",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": false
}]
}],
"showTitle": true,
"title": "all pods"
}, {
"collapse": false,
"editable": true,
"height": "250px",
"panels": [{
"aliasColors": {},
"bars": false,
"datasource": "${DS_PROMETHEUS}",
"decimals": 3,
"editable": true,
"error": false,
"fill": 0,
"grid": {
"threshold1": null,
"threshold1Color": "rgba(216, 200, 27, 0.27)",
"threshold2": null,
"threshold2Color": "rgba(234, 112, 112, 0.22)"
},
"height": "",
"id": 17,
"isNew": true,
"legend": {
"alignAsTable": true,
"avg": true,
"current": true,
"hideEmpty": true,
"hideZero": true,
"max": false,
"min": false,
"rightSide": true,
"show": true,
"sideWidth": null,
"sort": "current",
"sortDesc": true,
"total": false,
"values": true
},
"lines": true,
"linewidth": 2,
"links": [],
"nullPointMode": "connected",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"span": 12,
"stack": false,
"steppedLine": false,
"targets": [{
"expr": "sum(rate(container_cpu_usage_seconds_total{image!=\"\",name=~\"^k8s_.*\",instance=~\"^$instance$\",namespace=~\"^$namespace$\"}[1m])) by (pod_name)",
"interval": "",
"intervalFactor": 2,
"legendFormat": "{{ pod_name }}",
"metric": "container_cpu",
"refId": "A",
"step": 240
}],
"timeFrom": null,
"timeShift": null,
"title": "Cpu Usage",
"tooltip": {
"msResolution": true,
"shared": false,
"sort": 2,
"value_type": "cumulative"
},
"transparent": false,
"type": "graph",
"xaxis": {
"show": true
},
"yaxes": [{
"format": "none",
"label": "cores",
"logBase": 1,
"max": null,
"min": null,
"show": true
}, {
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": false
}]
}, {
"aliasColors": {},
"bars": false,
"datasource": "${DS_PROMETHEUS}",
"decimals": 2,
"editable": true,
"error": false,
"fill": 0,
"grid": {
"threshold1": null,
"threshold1Color": "rgba(216, 200, 27, 0.27)",
"threshold2": null,
"threshold2Color": "rgba(234, 112, 112, 0.22)"
},
"id": 33,
"isNew": true,
"legend": {
"alignAsTable": true,
"avg": true,
"current": true,
"hideEmpty": true,
"hideZero": true,
"max": false,
"min": false,
"rightSide": true,
"show": true,
"sideWidth": null,
"sort": "current",
"sortDesc": true,
"total": false,
"values": true
},
"lines": true,
"linewidth": 2,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"span": 12,
"stack": false,
"steppedLine": false,
"targets": [{
"expr": "sum (container_memory_working_set_bytes{image!=\"\",name=~\"^k8s_.*\",instance=~\"^$instance$\",namespace=~\"^$namespace$\"}) by (pod_name)",
"interval": "",
"intervalFactor": 2,
"legendFormat": "{{ pod_name }}",
"metric": "",
"refId": "A",
"step": 240
}],
"timeFrom": null,
"timeShift": null,
"title": "Memory Working Set",
"tooltip": {
"msResolution": false,
"shared": false,
"sort": 2,
"value_type": "cumulative"
},
"type": "graph",
"xaxis": {
"show": true
},
"yaxes": [{
"format": "bytes",
"label": "used",
"logBase": 1,
"max": null,
"min": null,
"show": true
}, {
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": false
}]
}, {
"aliasColors": {},
"bars": false,
"datasource": "${DS_PROMETHEUS}",
"decimals": 2,
"editable": true,
"error": false,
"fill": 1,
"grid": {
"threshold1": null,
"threshold1Color": "rgba(216, 200, 27, 0.27)",
"threshold2": null,
"threshold2Color": "rgba(234, 112, 112, 0.22)"
},
"id": 16,
"isNew": true,
"legend": {
"alignAsTable": true,
"avg": true,
"current": true,
"hideEmpty": true,
"hideZero": true,
"max": false,
"min": false,
"rightSide": true,
"show": true,
"sideWidth": 200,
"sort": "avg",
"sortDesc": true,
"total": false,
"values": true
},
"lines": true,
"linewidth": 2,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"span": 12,
"stack": false,
"steppedLine": false,
"targets": [{
"expr": "sum (rate (container_network_receive_bytes_total{image!=\"\",name=~\"^k8s_.*\",instance=~\"^$instance$\",namespace=~\"^$namespace$\"}[1m])) by (pod_name)",
"interval": "",
"intervalFactor": 2,
"legendFormat": "{{ pod_name }} < in",
"metric": "network",
"refId": "A",
"step": 240
}, {
"expr": "- sum (rate (container_network_transmit_bytes_total{image!=\"\",name=~\"^k8s_.*\",instance=~\"^$instance$\",namespace=~\"^$namespace$\"}[1m])) by (pod_name)",
"interval": "",
"intervalFactor": 2,
"legendFormat": "{{ pod_name }} > out",
"metric": "network",
"refId": "B",
"step": 240
}],
"timeFrom": null,
"timeShift": null,
"title": "Network",
"tooltip": {
"msResolution": false,
"shared": false,
"sort": 2,
"value_type": "cumulative"
},
"type": "graph",
"xaxis": {
"show": true
},
"yaxes": [{
"format": "Bps",
"label": "transmit / receive",
"logBase": 1,
"max": null,
"min": null,
"show": true
}, {
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": false
}]
}, {
"aliasColors": {},
"bars": false,
"datasource": "${DS_PROMETHEUS}",
"decimals": 2,
"editable": true,
"error": false,
"fill": 1,
"grid": {
"threshold1": null,
"threshold1Color": "rgba(216, 200, 27, 0.27)",
"threshold2": null,
"threshold2Color": "rgba(234, 112, 112, 0.22)"
},
"id": 34,
"isNew": true,
"legend": {
"alignAsTable": true,
"avg": true,
"current": true,
"hideEmpty": true,
"hideZero": true,
"max": false,
"min": false,
"rightSide": true,
"show": true,
"sideWidth": 200,
"sort": "current",
"sortDesc": true,
"total": false,
"values": true
},
"lines": true,
"linewidth": 2,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"span": 12,
"stack": false,
"steppedLine": false,
"targets": [{
"expr": "sum(container_fs_usage_bytes{image!=\"\",name=~\"^k8s_.*\",instance=~\"^$instance$\",namespace=~\"^$namespace$\"}) by (pod_name)",
"interval": "",
"intervalFactor": 2,
"legendFormat": "{{ pod_name }}",
"metric": "network",
"refId": "A",
"step": 240
}],
"timeFrom": null,
"timeShift": null,
"title": "Filesystem",
"tooltip": {
"msResolution": false,
"shared": false,
"sort": 2,
"value_type": "cumulative"
},
"type": "graph",
"xaxis": {
"show": true
},
"yaxes": [{
"format": "bytes",
"label": "used",
"logBase": 1,
"max": null,
"min": null,
"show": true
}, {
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": false
}]
}],
"showTitle": true,
"title": "each pod"
}],
"time": {
"from": "now-3d",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
]
},
"templating": {
"list": [{
"allValue": ".*",
"current": {},
"datasource": "${DS_PROMETHEUS}",
"hide": 0,
"includeAll": true,
"label": "Instance",
"multi": false,
"name": "instance",
"options": [],
"query": "label_values(instance)",
"refresh": 1,
"regex": "",
"type": "query"
}, {
"current": {},
"datasource": "${DS_PROMETHEUS}",
"hide": 0,
"includeAll": true,
"label": "Namespace",
"multi": true,
"name": "namespace",
"options": [],
"query": "label_values(namespace)",
"refresh": 1,
"regex": "",
"type": "query"
}]
},
"annotations": {
"list": []
},
"refresh": false,
"schemaVersion": 12,
"version": 8,
"links": [],
"gnetId": 737
}
prometheus-datasource.json: |
{
"name": "prometheus",
"type": "prometheus",
"url": "http://prometheus:9090",
"access": "proxy",
"basicAuth": false
}
kind: ConfigMap
metadata:
creationTimestamp: null
name: grafana-import-dashboards
namespace: monitoring
================================================
FILE: manifests/monitoring/grafana-deployment.yaml
================================================
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: grafana-core
namespace: monitoring
labels:
app: grafana
component: core
spec:
replicas: 1
template:
metadata:
labels:
app: grafana
component: core
spec:
containers:
- image: grafana/grafana:3.1.1
name: grafana-core
# env:
resources:
# keep request = limit to keep this container in guaranteed class
limits:
cpu: 100m
memory: 100Mi
requests:
cpu: 100m
memory: 100Mi
env:
# The following env variables set up basic auth twith the default admin user and admin password.
- name: GF_AUTH_BASIC_ENABLED
value: "true"
- name: GF_AUTH_ANONYMOUS_ENABLED
value: "false"
# - name: GF_AUTH_ANONYMOUS_ORG_ROLE
# value: Admin
# does not really work, because of template variables in exported dashboards:
# - name: GF_DASHBOARDS_JSON_ENABLED
# value: "true"
volumeMounts:
- name: grafana-persistent-storage
mountPath: /var
volumes:
- name: grafana-persistent-storage
emptyDir: {}
================================================
FILE: manifests/monitoring/grafana-job.yaml
================================================
apiVersion: batch/v1
kind: Job
metadata:
name: grafana-import-dashboards
namespace: monitoring
labels:
app: grafana
component: import-dashboards
spec:
template:
metadata:
name: grafana-import-dashboards
labels:
app: grafana
component: import-dashboards
spec:
containers:
- name: grafana-import-dashboards
image: giantswarm/tiny-tools
command: ["/bin/sh", "-c"]
workingDir: /opt/grafana-import-dashboards
args:
# FIXME use kubernetes probe instead of "until curl"
- >
until $(curl --silent --fail --show-error --output /dev/null http://admin:admin@grafana:3000/api/datasources); do
printf '.' ; sleep 1 ;
done ;
for file in *-datasource.json ; do
if [ -e "$file" ] ; then
echo "importing $file" &&
curl --silent --fail --show-error \
--request POST http://admin:admin@grafana:3000/api/datasources \
--header "Content-Type: application/json" \
--data-binary "@$file" ;
echo "" ;
fi
done ;
for file in *-dashboard.json ; do
if [ -e "$file" ] ; then
echo "importing $file" &&
cat "$file" \
| xargs -0 printf '{"dashboard":%s,"overwrite":true,"inputs":[{"name":"DS_PROMETHEUS","type":"datasource","pluginId":"prometheus","value":"prometheus"}]}' \
| jq -c '.' \
| curl --silent --fail --show-error \
--request POST http://admin:admin@grafana:3000/api/dashboards/import \
--header "Content-Type: application/json" \
--data-binary "@-" ;
echo "" ;
fi
done
volumeMounts:
- name: config-volume
mountPath: /opt/grafana-import-dashboards
restartPolicy: Never
volumes:
- name: config-volume
configMap:
name: grafana-import-dashboards
================================================
FILE: manifests/monitoring/grafana-service.yaml
================================================
apiVersion: v1
kind: Service
metadata:
name: grafana
namespace: monitoring
labels:
app: grafana
component: core
spec:
type: NodePort
ports:
- port: 3000
selector:
app: grafana
component: core
================================================
FILE: manifests/monitoring/prometheus.yaml
================================================
apiVersion: v1
kind: Namespace
metadata:
name: monitoring
---
kind: ConfigMap
metadata:
name: prometheus-config
namespace: monitoring
apiVersion: v1
data:
prometheus.yml: |-
global:
scrape_interval: 30s
scrape_timeout: 30s
scrape_configs:
- job_name: 'kubernetes-cluster'
scheme: https
tls_config:
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
kubernetes_sd_configs:
- api_servers:
- 'https://kubernetes.default.svc'
in_cluster: true
role: apiserver
- job_name: 'kubernetes-nodes'
scheme: https
tls_config:
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
insecure_skip_verify: true
kubernetes_sd_configs:
- api_servers:
- 'https://kubernetes.default.svc'
in_cluster: true
role: node
relabel_configs:
- action: labelmap
regex: __meta_kubernetes_node_label_(.+)
- job_name: 'kubernetes-service-endpoints'
scheme: http
kubernetes_sd_configs:
- api_servers:
- 'https://kubernetes.default.svc'
in_cluster: true
role: endpoint
relabel_configs:
- source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scrape]
action: keep
regex: true
- source_labels: [__meta_kubernetes_service_namespace]
action: replace
target_label: kubernetes_namespace
- source_labels: [__meta_kubernetes_service_name]
action: replace
target_label: kubernetes_name
- job_name: 'kubernetes-services'
scheme: http
kubernetes_sd_configs:
- api_servers:
- 'https://kubernetes.default.svc'
in_cluster: true
role: service
relabel_configs:
- source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scrape]
action: keep
regex: true
- source_labels: [__meta_kubernetes_service_namespace]
target_label: kubernetes_namespace
- source_labels: [__meta_kubernetes_service_name]
target_label: kubernetes_name
- job_name: 'kubernetes-pods'
scheme: http
kubernetes_sd_configs:
- api_servers:
- 'https://kubernetes.default.svc'
in_cluster: true
role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)
- source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
action: replace
regex: (.+):(?:\\d+);(\\d+)
replacement: ${1}:${2}
target_label: __address__
- action: labelmap
regex: __meta_kubernetes_pod_label_(.+)
- source_labels: [__meta_kubernetes_pod_namespace]
action: replace
target_label: kubernetes_namespace
- source_labels: [__meta_kubernetes_pod_name]
action: replace
target_label: kubernetes_pod_name
- source_labels: [__meta_kubernetes_pod_node_name]
action: replace
target_label: kubernetes_pod_node_name
---
apiVersion: v1
kind: Service
metadata:
name: prometheus
namespace: monitoring
spec:
ports:
- port: 9090
protocol: TCP
targetPort: 9090
selector:
name: prometheus
type: NodePort
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
labels:
name: prometheus
name: prometheus
namespace: monitoring
spec:
replicas: 1
selector:
matchLabels:
name: prometheus
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 1
type: RollingUpdate
template:
metadata:
creationTimestamp: null
labels:
name: prometheus
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9090"
spec:
containers:
- args:
- -config.file=/etc/prometheus/prometheus.yml
- -storage.local.path=/prometheus
- -storage.local.retention=24h
command:
- /bin/prometheus
image: quay.io/prometheus/prometheus:v1.1.3
imagePullPolicy: IfNotPresent
name: prometheus
ports:
- containerPort: 9090
protocol: TCP
resources:
limits:
cpu: 500m
memory: 2500Mi
requests:
cpu: 100m
memory: 100Mi
volumeMounts:
- mountPath: /prometheus
name: data
- mountPath: /etc/prometheus
name: config-volume
restartPolicy: Always
securityContext: {}
terminationGracePeriodSeconds: 30
volumes:
- emptyDir: {}
name: data
- configMap:
name: prometheus-config
name: config-volume
================================================
FILE: manifests/nats/nats-cluster.yaml
================================================
apiVersion: "nats.io/v1alpha2"
kind: "NatsCluster"
metadata:
name: "nats"
spec:
size: 2
version: "1.1.0"
================================================
FILE: manifests/ui/README.md
================================================
# Kubeless UI
You can find the latest manifest for deploying the UI in the releases page of the kubeless-ui repository:
https://github.com/kubeless/kubeless-ui/releases
================================================
FILE: pkg/apis/kubeless/register.go
================================================
package kubeless
const (
// GroupName is ApiGroup for the Kubeless API
GroupName = "kubeless.io"
)
================================================
FILE: pkg/apis/kubeless/v1beta1/doc.go
================================================
/*
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:deepcopy-gen=package
// Package v1beta1 is the v1beta1 version of the Kubeless API
// +groupName=kubeless.io
package v1beta1
================================================
FILE: pkg/apis/kubeless/v1beta1/function.go
================================================
/*
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.
*/
package v1beta1
import (
appsv1 "k8s.io/api/apps/v1"
"k8s.io/api/autoscaling/v2beta1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// Function object
type Function struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata"`
Spec FunctionSpec `json:"spec"`
}
// FunctionSpec contains func specification
type FunctionSpec struct {
Handler string `json:"handler"` // Function handler: "file.function"
Function string `json:"function"` // Function file content or URL of the function
FunctionContentType string `json:"function-content-type"` // Function file content type (plain text, url, base64, zip or compressedtar)
Checksum string `json:"checksum"` // Checksum of the file
Runtime string `json:"runtime"` // Function runtime to use
Timeout string `json:"timeout"` // Maximum timeout for the function to complete its execution
Deps string `json:"deps"` // Function dependencies
Deployment appsv1.Deployment `json:"deployment" protobuf:"bytes,3,opt,name=template"`
ServiceSpec v1.ServiceSpec `json:"service"`
HorizontalPodAutoscaler v2beta1.HorizontalPodAutoscaler `json:"horizontalPodAutoscaler" protobuf:"bytes,3,opt,name=horizontalPodAutoscaler"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// FunctionList contains map of functions
type FunctionList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata"`
// Items is a list of third party objects
Items []*Function `json:"items"`
}
================================================
FILE: pkg/apis/kubeless/v1beta1/register.go
================================================
package v1beta1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
kubeless "github.com/kubeless/kubeless/pkg/apis/kubeless"
)
// SchemeGroupVersion is group version used to register these objects
var SchemeGroupVersion = schema.GroupVersion{Group: kubeless.GroupName, Version: "v1beta1"}
// Kind takes an unqualified kind and returns back a Group qualified GroupKind
func Kind(kind string) schema.GroupKind {
return SchemeGroupVersion.WithKind(kind).GroupKind()
}
// Resource takes an unqualified resource and returns a Group qualified GroupResource
func Resource(resource string) schema.GroupResource {
return SchemeGroupVersion.WithResource(resource).GroupResource()
}
var (
// SchemeBuilder collects the scheme builder functions for the Kubeless API
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
// AddToScheme applies the SchemeBuilder functions to a specified scheme
AddToScheme = SchemeBuilder.AddToScheme
)
// Adds the list of known types to Scheme.
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&Function{},
&FunctionList{},
)
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
return nil
}
================================================
FILE: pkg/apis/kubeless/v1beta1/zz_generated.deepcopy.go
================================================
// +build !ignore_autogenerated
/*
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.
*/
// This file was autogenerated by deepcopy-gen. Do not edit it manually!
package v1beta1
import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Function) DeepCopyInto(out *Function) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Function.
func (in *Function) DeepCopy() *Function {
if in == nil {
return nil
}
out := new(Function)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Function) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *FunctionList) DeepCopyInto(out *FunctionList) {
*out = *in
out.TypeMeta = in.TypeMeta
out.ListMeta = in.ListMeta
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]*Function, len(*in))
for i := range *in {
if (*in)[i] == nil {
(*out)[i] = nil
} else {
(*out)[i] = new(Function)
(*in)[i].DeepCopyInto((*out)[i])
}
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FunctionList.
func (in *FunctionList) DeepCopy() *FunctionList {
if in == nil {
return nil
}
out := new(FunctionList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *FunctionList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *FunctionSpec) DeepCopyInto(out *FunctionSpec) {
*out = *in
in.Deployment.DeepCopyInto(&out.Deployment)
in.ServiceSpec.DeepCopyInto(&out.ServiceSpec)
in.HorizontalPodAutoscaler.DeepCopyInto(&out.HorizontalPodAutoscaler)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FunctionSpec.
func (in *FunctionSpec) DeepCopy() *FunctionSpec {
if in == nil {
return nil
}
out := new(FunctionSpec)
in.DeepCopyInto(out)
return out
}
================================================
FILE: pkg/client/clientset/versioned/clientset.go
================================================
/*
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.
*/
package versioned
import (
glog "github.com/golang/glog"
kubelessv1beta1 "github.com/kubeless/kubeless/pkg/client/clientset/versioned/typed/kubeless/v1beta1"
discovery "k8s.io/client-go/discovery"
rest "k8s.io/client-go/rest"
flowcontrol "k8s.io/client-go/util/flowcontrol"
)
type Interface interface {
Discovery() discovery.DiscoveryInterface
KubelessV1beta1() kubelessv1beta1.KubelessV1beta1Interface
// Deprecated: please explicitly pick a version if possible.
Kubeless() kubelessv1beta1.KubelessV1beta1Interface
}
// Clientset contains the clients for groups. Each group has exactly one
// version included in a Clientset.
type Clientset struct {
*discovery.DiscoveryClient
kubelessV1beta1 *kubelessv1beta1.KubelessV1beta1Client
}
// KubelessV1beta1 retrieves the KubelessV1beta1Client
func (c *Clientset) KubelessV1beta1() kubelessv1beta1.KubelessV1beta1Interface {
return c.kubelessV1beta1
}
// Deprecated: Kubeless retrieves the default version of KubelessClient.
// Please explicitly pick a version.
func (c *Clientset) Kubeless() kubelessv1beta1.KubelessV1beta1Interface {
return c.kubelessV1beta1
}
// Discovery retrieves the DiscoveryClient
func (c *Clientset) Discovery() discovery.DiscoveryInterface {
if c == nil {
return nil
}
return c.DiscoveryClient
}
// NewForConfig creates a new Clientset for the given config.
func NewForConfig(c *rest.Config) (*Clientset, error) {
configShallowCopy := *c
if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 {
configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst)
}
var cs Clientset
var err error
cs.kubelessV1beta1, err = kubelessv1beta1.NewForConfig(&configShallowCopy)
if err != nil {
return nil, err
}
cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfig(&configShallowCopy)
if err != nil {
glog.Errorf("failed to create the DiscoveryClient: %v", err)
return nil, err
}
return &cs, nil
}
// NewForConfigOrDie creates a new Clientset for the given config and
// panics if there is an error in the config.
func NewForConfigOrDie(c *rest.Config) *Clientset {
var cs Clientset
cs.kubelessV1beta1 = kubelessv1beta1.NewForConfigOrDie(c)
cs.DiscoveryClient = discovery.NewDiscoveryClientForConfigOrDie(c)
return &cs
}
// New creates a new Clientset for the given RESTClient.
func New(c rest.Interface) *Clientset {
var cs Clientset
cs.kubelessV1beta1 = kubelessv1beta1.New(c)
cs.DiscoveryClient = discovery.NewDiscoveryClient(c)
return &cs
}
================================================
FILE: pkg/client/clientset/versioned/doc.go
================================================
/*
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.
*/
// This package has the automatically generated clientset.
package versioned
================================================
FILE: pkg/client/clientset/versioned/fake/clientset_generated.go
================================================
/*
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.
*/
package fake
import (
clientset "github.com/kubeless/kubeless/pkg/client/clientset/versioned"
kubelessv1beta1 "github.com/kubeless/kubeless/pkg/client/clientset/versioned/typed/kubeless/v1beta1"
fakekubelessv1beta1 "github.com/kubeless/kubeless/pkg/client/clientset/versioned/typed/kubeless/v1beta1/fake"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/discovery"
fakediscovery "k8s.io/client-go/discovery/fake"
"k8s.io/client-go/testing"
)
// NewSimpleClientset returns a clientset that will respond with the provided objects.
// It's backed by a very simple object tracker that processes creates, updates and deletions as-is,
// without applying any validations and/or defaults. It shouldn't be considered a replacement
// for a real clientset and is mostly useful in simple unit tests.
func NewSimpleClientset(objects ...runtime.Object) *Clientset {
o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder())
for _, obj := range objects {
if err := o.Add(obj); err != nil {
panic(err)
}
}
fakePtr := testing.Fake{}
fakePtr.AddReactor("*", "*", testing.ObjectReaction(o))
fakePtr.AddWatchReactor("*", testing.DefaultWatchReactor(watch.NewFake(), nil))
return &Clientset{fakePtr, &fakediscovery.FakeDiscovery{Fake: &fakePtr}}
}
// Clientset implements clientset.Interface. Meant to be embedded into a
// struct to get a default implementation. This makes faking out just the method
// you want to test easier.
type Clientset struct {
testing.Fake
discovery *fakediscovery.FakeDiscovery
}
func (c *Clientset) Discovery() discovery.DiscoveryInterface {
return c.discovery
}
var _ clientset.Interface = &Clientset{}
// KubelessV1beta1 retrieves the KubelessV1beta1Client
func (c *Clientset) KubelessV1beta1() kubelessv1beta1.KubelessV1beta1Interface {
return &fakekubelessv1beta1.FakeKubelessV1beta1{Fake: &c.Fake}
}
// Kubeless retrieves the KubelessV1beta1Client
func (c *Clientset) Kubeless() kubelessv1beta1.KubelessV1beta1Interface {
return &fakekubelessv1beta1.FakeKubelessV1beta1{Fake: &c.Fake}
}
================================================
FILE: pkg/client/clientset/versioned/fake/doc.go
================================================
/*
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.
*/
// This package has the automatically generated fake clientset.
package fake
================================================
FILE: pkg/client/clientset/versioned/fake/register.go
================================================
/*
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.
*/
package fake
import (
kubelessv1beta1 "github.com/kubeless/kubeless/pkg/apis/kubeless/v1beta1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
schema "k8s.io/apimachinery/pkg/runtime/schema"
serializer "k8s.io/apimachinery/pkg/runtime/serializer"
)
var scheme = runtime.NewScheme()
var codecs = serializer.NewCodecFactory(scheme)
var parameterCodec = runtime.NewParameterCodec(scheme)
func init() {
v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"})
AddToScheme(scheme)
}
// AddToScheme adds all types of this clientset into the given scheme. This allows composition
// of clientsets, like in:
//
// import (
// "k8s.io/client-go/kubernetes"
// clientsetscheme "k8s.io/client-go/kuberentes/scheme"
// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
// )
//
// kclientset, _ := kubernetes.NewForConfig(c)
// aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme)
//
// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types
// correctly.
func AddToScheme(scheme *runtime.Scheme) {
kubelessv1beta1.AddToScheme(scheme)
}
================================================
FILE: pkg/client/clientset/versioned/scheme/doc.go
================================================
/*
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.
*/
// This package contains the scheme of the automatically generated clientset.
package scheme
================================================
FILE: pkg/client/clientset/versioned/scheme/register.go
================================================
/*
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.
*/
package scheme
import (
kubelessv1beta1 "github.com/kubeless/kubeless/pkg/apis/kubeless/v1beta1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
schema "k8s.io/apimachinery/pkg/runtime/schema"
serializer "k8s.io/apimachinery/pkg/runtime/serializer"
)
var Scheme = runtime.NewScheme()
var Codecs = serializer.NewCodecFactory(Scheme)
var ParameterCodec = runtime.NewParameterCodec(Scheme)
func init() {
v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"})
AddToScheme(Scheme)
}
// AddToScheme adds all types of this clientset into the given scheme. This allows composition
// of clientsets, like in:
//
// import (
// "k8s.io/client-go/kubernetes"
// clientsetscheme "k8s.io/client-go/kuberentes/scheme"
// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
// )
//
// kclientset, _ := kubernetes.NewForConfig(c)
// aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme)
//
// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types
// correctly.
func AddToScheme(scheme *runtime.Scheme) {
kubelessv1beta1.AddToScheme(scheme)
}
================================================
FILE: pkg/client/clientset/versioned/typed/kubeless/v1beta1/doc.go
================================================
/*
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.
*/
// This package has the automatically generated typed clients.
package v1beta1
================================================
FILE: pkg/client/clientset/versioned/typed/kubeless/v1beta1/fake/doc.go
================================================
/*
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.
*/
// Package fake has the automatically generated clients.
package fake
================================================
FILE: pkg/client/clientset/versioned/typed/kubeless/v1beta1/fake/fake_function.go
================================================
/*
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.
*/
package fake
import (
v1beta1 "github.com/kubeless/kubeless/pkg/apis/kubeless/v1beta1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
labels "k8s.io/apimachinery/pkg/labels"
schema "k8s.io/apimachinery/pkg/runtime/schema"
types "k8s.io/apimachinery/pkg/types"
watch "k8s.io/apimachinery/pkg/watch"
testing "k8s.io/client-go/testing"
)
// FakeFunctions implements FunctionInterface
type FakeFunctions struct {
Fake *FakeKubelessV1beta1
ns string
}
var functionsResource = schema.GroupVersionResource{Group: "kubeless.io", Version: "v1beta1", Resource: "functions"}
var functionsKind = schema.GroupVersionKind{Group: "kubeless.io", Version: "v1beta1", Kind: "Function"}
// Get takes name of the function, and returns the corresponding function object, and an error if there is any.
func (c *FakeFunctions) Get(name string, options v1.GetOptions) (result *v1beta1.Function, err error) {
obj, err := c.Fake.
Invokes(testing.NewGetAction(functionsResource, c.ns, name), &v1beta1.Function{})
if obj == nil {
return nil, err
}
return obj.(*v1beta1.Function), err
}
// List takes label and field selectors, and returns the list of Functions that match those selectors.
func (c *FakeFunctions) List(opts v1.ListOptions) (result *v1beta1.FunctionList, err error) {
obj, err := c.Fake.
Invokes(testing.NewListAction(functionsResource, functionsKind, c.ns, opts), &v1beta1.FunctionList{})
if obj == nil {
return nil, err
}
label, _, _ := testing.ExtractFromListOptions(opts)
if label == nil {
label = labels.Everything()
}
list := &v1beta1.FunctionList{}
for _, item := range obj.(*v1beta1.FunctionList).Items {
if label.Matches(labels.Set(item.Labels)) {
list.Items = append(list.Items, item)
}
}
return list, err
}
// Watch returns a watch.Interface that watches the requested functions.
func (c *FakeFunctions) Watch(opts v1.ListOptions) (watch.Interface, error) {
return c.Fake.
InvokesWatch(testing.NewWatchAction(functionsResource, c.ns, opts))
}
// Create takes the representation of a function and creates it. Returns the server's representation of the function, and an error, if there is any.
func (c *FakeFunctions) Create(function *v1beta1.Function) (result *v1beta1.Function, err error) {
obj, err := c.Fake.
Invokes(testing.NewCreateAction(functionsResource, c.ns, function), &v1beta1.Function{})
if obj == nil {
return nil, err
}
return obj.(*v1beta1.Function), err
}
// Update takes the representation of a function and updates it. Returns the server's representation of the function, and an error, if there is any.
func (c *FakeFunctions) Update(function *v1beta1.Function) (result *v1beta1.Function, err error) {
obj, err := c.Fake.
Invokes(testing.NewUpdateAction(functionsResource, c.ns, function), &v1beta1.Function{})
if obj == nil {
return nil, err
}
return obj.(*v1beta1.Function), err
}
// Delete takes name of the function and deletes it. Returns an error if one occurs.
func (c *FakeFunctions) Delete(name string, options *v1.DeleteOptions) error {
_, err := c.Fake.
Invokes(testing.NewDeleteAction(functionsResource, c.ns, name), &v1beta1.Function{})
return err
}
// DeleteCollection deletes a collection of objects.
func (c *FakeFunctions) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error {
action := testing.NewDeleteCollectionAction(functionsResource, c.ns, listOptions)
_, err := c.Fake.Invokes(action, &v1beta1.FunctionList{})
return err
}
// Patch applies the patch and returns the patched function.
func (c *FakeFunctions) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1beta1.Function, err error) {
obj, err := c.Fake.
Invokes(testing.NewPatchSubresourceAction(functionsResource, c.ns, name, data, subresources...), &v1beta1.Function{})
if obj == nil {
return nil, err
}
return obj.(*v1beta1.Function), err
}
================================================
FILE: pkg/client/clientset/versioned/typed/kubeless/v1beta1/fake/fake_kubeless_client.go
================================================
/*
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.
*/
package fake
import (
v1beta1 "github.com/kubeless/kubeless/pkg/client/clientset/versioned/typed/kubeless/v1beta1"
rest "k8s.io/client-go/rest"
testing "k8s.io/client-go/testing"
)
type FakeKubelessV1beta1 struct {
*testing.Fake
}
func (c *FakeKubelessV1beta1) Functions(namespace string) v1beta1.FunctionInterface {
return &FakeFunctions{c, namespace}
}
// RESTClient returns a RESTClient that is used to communicate
// with API server by this client implementation.
func (c *FakeKubelessV1beta1) RESTClient() rest.Interface {
var ret *rest.RESTClient
return ret
}
================================================
FILE: pkg/client/clientset/versioned/typed/kubeless/v1beta1/function.go
================================================
/*
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.
*/
package v1beta1
import (
v1beta1 "github.com/kubeless/kubeless/pkg/apis/kubeless/v1beta1"
scheme "github.com/kubeless/kubeless/pkg/client/clientset/versioned/scheme"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
types "k8s.io/apimachinery/pkg/types"
watch "k8s.io/apimachinery/pkg/watch"
rest "k8s.io/client-go/rest"
)
// FunctionsGetter has a method to return a FunctionInterface.
// A group's client should implement this interface.
type FunctionsGetter interface {
Functions(namespace string) FunctionInterface
}
// FunctionInterface has methods to work with Function resources.
type FunctionInterface interface {
Create(*v1beta1.Function) (*v1beta1.Function, error)
Update(*v1beta1.Function) (*v1beta1.Function, error)
Delete(name string, options *v1.DeleteOptions) error
DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error
Get(name string, options v1.GetOptions) (*v1beta1.Function, error)
List(opts v1.ListOptions) (*v1beta1.FunctionList, error)
Watch(opts v1.ListOptions) (watch.Interface, error)
Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1beta1.Function, err error)
FunctionExpansion
}
// functions implements FunctionInterface
type functions struct {
client rest.Interface
ns string
}
// newFunctions returns a Functions
func newFunctions(c *KubelessV1beta1Client, namespace string) *functions {
return &functions{
client: c.RESTClient(),
ns: namespace,
}
}
// Get takes name of the function, and returns the corresponding function object, and an error if there is any.
func (c *functions) Get(name string, options v1.GetOptions) (result *v1beta1.Function, err error) {
result = &v1beta1.Function{}
err = c.client.Get().
Namespace(c.ns).
Resource("functions").
Name(name).
VersionedParams(&options, scheme.ParameterCodec).
Do().
Into(result)
return
}
// List takes label and field selectors, and returns the list of Functions that match those selectors.
func (c *functions) List(opts v1.ListOptions) (result *v1beta1.FunctionList, err error) {
result = &v1beta1.FunctionList{}
err = c.client.Get().
Namespace(c.ns).
Resource("functions").
VersionedParams(&opts, scheme.ParameterCodec).
Do().
Into(result)
return
}
// Watch returns a watch.Interface that watches the requested functions.
func (c *functions) Watch(opts v1.ListOptions) (watch.Interface, error) {
opts.Watch = true
return c.client.Get().
Namespace(c.ns).
Resource("functions").
VersionedParams(&opts, scheme.ParameterCodec).
Watch()
}
// Create takes the representation of a function and creates it. Returns the server's representation of the function, and an error, if there is any.
func (c *functions) Create(function *v1beta1.Function) (result *v1beta1.Function, err error) {
result = &v1beta1.Function{}
err = c.client.Post().
Namespace(c.ns).
Resource("functions").
Body(function).
Do().
Into(result)
return
}
// Update takes the representation of a function and updates it. Returns the server's representation of the function, and an error, if there is any.
func (c *functions) Update(function *v1beta1.Function) (result *v1beta1.Function, err error) {
result = &v1beta1.Function{}
err = c.client.Put().
Namespace(c.ns).
Resource("functions").
Name(function.Name).
Body(function).
Do().
Into(result)
return
}
// Delete takes name of the function and deletes it. Returns an error if one occurs.
func (c *functions) Delete(name string, options *v1.DeleteOptions) error {
return c.client.Delete().
Namespace(c.ns).
Resource("functions").
Name(name).
Body(options).
Do().
Error()
}
// DeleteCollection deletes a collection of objects.
func (c *functions) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error {
return c.client.Delete().
Namespace(c.ns).
Resource("functions").
VersionedParams(&listOptions, scheme.ParameterCodec).
Body(options).
Do().
Error()
}
// Patch applies the patch and returns the patched function.
func (c *functions) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1beta1.Function, err error) {
result = &v1beta1.Function{}
err = c.client.Patch(pt).
Namespace(c.ns).
Resource("functions").
SubResource(subresources...).
Name(name).
Body(data).
Do().
Into(result)
return
}
================================================
FILE: pkg/client/clientset/versioned/typed/kubeless/v1beta1/generated_expansion.go
================================================
/*
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.
*/
package v1beta1
type FunctionExpansion interface{}
================================================
FILE: pkg/client/clientset/versioned/typed/kubeless/v1beta1/kubeless_client.go
================================================
/*
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.
*/
package v1beta1
import (
v1beta1 "github.com/kubeless/kubeless/pkg/apis/kubeless/v1beta1"
"github.com/kubeless/kubeless/pkg/client/clientset/versioned/scheme"
serializer "k8s.io/apimachinery/pkg/runtime/serializer"
rest "k8s.io/client-go/rest"
)
type KubelessV1beta1Interface interface {
RESTClient() rest.Interface
FunctionsGetter
}
// KubelessV1beta1Client is used to interact with features provided by the kubeless.io group.
type KubelessV1beta1Client struct {
restClient rest.Interface
}
func (c *KubelessV1beta1Client) Functions(namespace string) FunctionInterface {
return newFunctions(c, namespace)
}
// NewForConfig creates a new KubelessV1beta1Client for the given config.
func NewForConfig(c *rest.Config) (*KubelessV1beta1Client, error) {
config := *c
if err := setConfigDefaults(&config); err != nil {
return nil, err
}
client, err := rest.RESTClientFor(&config)
if err != nil {
return nil, err
}
return &KubelessV1beta1Client{client}, nil
}
// NewForConfigOrDie creates a new KubelessV1beta1Client for the given config and
// panics if there is an error in the config.
func NewForConfigOrDie(c *rest.Config) *KubelessV1beta1Client {
client, err := NewForConfig(c)
if err != nil {
panic(err)
}
return client
}
// New creates a new KubelessV1beta1Client for the given RESTClient.
func New(c rest.Interface) *KubelessV1beta1Client {
return &KubelessV1beta1Client{c}
}
func setConfigDefaults(config *rest.Config) error {
gv := v1beta1.SchemeGroupVersion
config.GroupVersion = &gv
config.APIPath = "/apis"
config.NegotiatedSerializer = serializer.DirectCodecFactory{CodecFactory: scheme.Codecs}
if config.UserAgent == "" {
config.UserAgent = rest.DefaultKubernetesUserAgent()
}
return nil
}
// RESTClient returns a RESTClient that is used to communicate
// with API server by this client implementation.
func (c *KubelessV1beta1Client) RESTClient() rest.Interface {
if c == nil {
return nil
}
return c.restClient
}
================================================
FILE: pkg/client/informers/externalversions/factory.go
================================================
/*
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.
*/
// This file was automatically generated by informer-gen
package externalversions
import (
reflect "reflect"
sync "sync"
time "time"
versioned "github.com/kubeless/kubeless/pkg/client/clientset/versioned"
internalinterfaces "github.com/kubeless/kubeless/pkg/client/informers/externalversions/internalinterfaces"
kubeless "github.com/kubeless/kubeless/pkg/client/informers/externalversions/kubeless"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
schema "k8s.io/apimachinery/pkg/runtime/schema"
cache "k8s.io/client-go/tools/cache"
)
type sharedInformerFactory struct {
client versioned.Interface
namespace string
tweakListOptions internalinterfaces.TweakListOptionsFunc
lock sync.Mutex
defaultResync time.Duration
informers map[reflect.Type]cache.SharedIndexInformer
// startedInformers is used for tracking which informers have been started.
// This allows Start() to be called multiple times safely.
startedInformers map[reflect.Type]bool
}
// NewSharedInformerFactory constructs a new instance of sharedInformerFactory
func NewSharedInformerFactory(client versioned.Interface, defaultResync time.Duration) SharedInformerFactory {
return NewFilteredSharedInformerFactory(client, defaultResync, v1.NamespaceAll, nil)
}
// NewFilteredSharedInformerFactory constructs a new instance of sharedInformerFactory.
// Listers obtained via this SharedInformerFactory will be subject to the same filters
// as specified here.
func NewFilteredSharedInformerFactory(client versioned.Interface, defaultResync time.Duration, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerFactory {
return &sharedInformerFactory{
client: client,
namespace: namespace,
tweakListOptions: tweakListOptions,
defaultResync: defaultResync,
informers: make(map[reflect.Type]cache.SharedIndexInformer),
startedInformers: make(map[reflect.Type]bool),
}
}
// Start initializes all requested informers.
func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) {
f.lock.Lock()
defer f.lock.Unlock()
for informerType, informer := range f.informers {
if !f.startedInformers[informerType] {
go informer.Run(stopCh)
f.startedInformers[informerType] = true
}
}
}
// WaitForCacheSync waits for all started informers' cache were synced.
func (f *sharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool {
informers := func() map[reflect.Type]cache.SharedIndexInformer {
f.lock.Lock()
defer f.lock.Unlock()
informers := map[reflect.Type]cache.SharedIndexInformer{}
for informerType, informer := range f.informers {
if f.startedInformers[informerType] {
informers[informerType] = informer
}
}
return informers
}()
res := map[reflect.Type]bool{}
for informType, informer := range informers {
res[informType] = cache.WaitForCacheSync(stopCh, informer.HasSynced)
}
return res
}
// InternalInformerFor returns the SharedIndexInformer for obj using an internal
// client.
func (f *sharedInformerFactory) InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer {
f.lock.Lock()
defer f.lock.Unlock()
informerType := reflect.TypeOf(obj)
informer, exists := f.informers[informerType]
if exists {
return informer
}
informer = newFunc(f.client, f.defaultResync)
f.informers[informerType] = informer
return informer
}
// SharedInformerFactory provides shared informers for resources in all known
// API group versions.
type SharedInformerFactory interface {
internalinterfaces.SharedInformerFactory
ForResource(resource schema.GroupVersionResource) (GenericInformer, error)
WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool
Kubeless() kubeless.Interface
}
func (f *sharedInformerFactory) Kubeless() kubeless.Interface {
return kubeless.New(f, f.namespace, f.tweakListOptions)
}
================================================
FILE: pkg/client/informers/externalversions/generic.go
================================================
/*
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.
*/
// This file was automatically generated by informer-gen
package externalversions
import (
"fmt"
v1beta1 "github.com/kubeless/kubeless/pkg/apis/kubeless/v1beta1"
schema "k8s.io/apimachinery/pkg/runtime/schema"
cache "k8s.io/client-go/tools/cache"
)
// GenericInformer is type of SharedIndexInformer which will locate and delegate to other
// sharedInformers based on type
type GenericInformer interface {
Informer() cache.SharedIndexInformer
Lister() cache.GenericLister
}
type genericInformer struct {
informer cache.SharedIndexInformer
resource schema.GroupResource
}
// Informer returns the SharedIndexInformer.
func (f *genericInformer) Informer() cache.SharedIndexInformer {
return f.informer
}
// Lister returns the GenericLister.
func (f *genericInformer) Lister() cache.GenericLister {
return cache.NewGenericLister(f.Informer().GetIndexer(), f.resource)
}
// ForResource gives generic access to a shared informer of the matching type
// TODO extend this to unknown resources with a client pool
func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) {
switch resource {
// Group=kubeless.io, Version=v1beta1
case v1beta1.SchemeGroupVersion.WithResource("functions"):
return &genericInformer{resource: resource.GroupResource(), informer: f.Kubeless().V1beta1().Functions().Informer()}, nil
}
return nil, fmt.Errorf("no informer found for %v", resource)
}
================================================
FILE: pkg/client/informers/externalversions/internalinterfaces/factory_interfaces.go
================================================
/*
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.
*/
// This file was automatically generated by informer-gen
package internalinterfaces
import (
time "time"
versioned "github.com/kubeless/kubeless/pkg/client/clientset/versioned"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
cache "k8s.io/client-go/tools/cache"
)
type NewInformerFunc func(versioned.Interface, time.Duration) cache.SharedIndexInformer
// SharedInformerFactory a small interface to allow for adding an informer without an import cycle
type SharedInformerFactory interface {
Start(stopCh <-chan struct{})
InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer
}
type TweakListOptionsFunc func(*v1.ListOptions)
================================================
FILE: pkg/client/informers/externalversions/kubeless/interface.go
================================================
/*
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.
*/
// This file was automatically generated by informer-gen
package kubeless
import (
internalinterfaces "github.com/kubeless/kubeless/pkg/client/informers/externalversions/internalinterfaces"
v1beta1 "github.com/kubeless/kubeless/pkg/client/informers/externalversions/kubeless/v1beta1"
)
// Interface provides access to each of this group's versions.
type Interface interface {
// V1beta1 provides access to shared informers for resources in V1beta1.
V1beta1() v1beta1.Interface
}
type group struct {
factory internalinterfaces.SharedInformerFactory
namespace string
tweakListOptions internalinterfaces.TweakListOptionsFunc
}
// New returns a new Interface.
func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface {
return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
}
// V1beta1 returns a new v1beta1.Interface.
func (g *group) V1beta1() v1beta1.Interface {
return v1beta1.New(g.factory, g.namespace, g.tweakListOptions)
}
================================================
FILE: pkg/client/informers/externalversions/kubeless/v1beta1/function.go
================================================
/*
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.
*/
// This file was automatically generated by informer-gen
package v1beta1
import (
time "time"
kubeless_v1beta1 "github.com/kubeless/kubeless/pkg/apis/kubeless/v1beta1"
versioned "github.com/kubeless/kubeless/pkg/client/clientset/versioned"
internalinterfaces "github.com/kubeless/kubeless/pkg/client/informers/externalversions/internalinterfaces"
v1beta1 "github.com/kubeless/kubeless/pkg/client/listers/kubeless/v1beta1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
watch "k8s.io/apimachinery/pkg/watch"
cache "k8s.io/client-go/tools/cache"
)
// FunctionInformer provides access to a shared informer and lister for
// Functions.
type FunctionInformer interface {
Informer() cache.SharedIndexInformer
Lister() v1beta1.FunctionLister
}
type functionInformer struct {
factory internalinterfaces.SharedInformerFactory
tweakListOptions internalinterfaces.TweakListOptionsFunc
namespace string
}
// NewFunctionInformer constructs a new informer for Function type.
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewFunctionInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
return NewFilteredFunctionInformer(client, namespace, resyncPeriod, indexers, nil)
}
// NewFilteredFunctionInformer constructs a new informer for Function type.
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewFilteredFunctionInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
return cache.NewSharedIndexInformer(
&cache.ListWatch{
ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
}
return client.KubelessV1beta1().Functions(namespace).List(options)
},
WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
}
return client.KubelessV1beta1().Functions(namespace).Watch(options)
},
},
&kubeless_v1beta1.Function{},
resyncPeriod,
indexers,
)
}
func (f *functionInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
return NewFilteredFunctionInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
}
func (f *functionInformer) Informer() cache.SharedIndexInformer {
return f.factory.InformerFor(&kubeless_v1beta1.Function{}, f.defaultInformer)
}
func (f *functionInformer) Lister() v1beta1.FunctionLister {
return v1beta1.NewFunctionLister(f.Informer().GetIndexer())
}
================================================
FILE: pkg/client/informers/externalversions/kubeless/v1beta1/interface.go
================================================
/*
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.
*/
// This file was automatically generated by informer-gen
package v1beta1
import (
internalinterfaces "github.com/kubeless/kubeless/pkg/client/informers/externalversions/internalinterfaces"
)
// Interface provides access to all the informers in this group version.
type Interface interface {
// Functions returns a FunctionInformer.
Functions() FunctionInformer
}
type version struct {
factory internalinterfaces.SharedInformerFactory
namespace string
tweakListOptions internalinterfaces.TweakListOptionsFunc
}
// New returns a new Interface.
func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface {
return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
}
// Functions returns a FunctionInformer.
func (v *version) Functions() FunctionInformer {
return &functionInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
}
================================================
FILE: pkg/client/listers/kubeless/v1beta1/expansion_generated.go
================================================
/*
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.
*/
// This file was automatically generated by lister-gen
package v1beta1
// FunctionListerExpansion allows custom methods to be added to
// FunctionLister.
type FunctionListerExpansion interface{}
// FunctionNamespaceListerExpansion allows custom methods to be added to
// FunctionNamespaceLister.
type FunctionNamespaceListerExpansion interface{}
================================================
FILE: pkg/client/listers/kubeless/v1beta1/function.go
================================================
/*
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.
*/
// This file was automatically generated by lister-gen
package v1beta1
import (
v1beta1 "github.com/kubeless/kubeless/pkg/apis/kubeless/v1beta1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/tools/cache"
)
// FunctionLister helps list Functions.
type FunctionLister interface {
// List lists all Functions in the indexer.
List(selector labels.Selector) (ret []*v1beta1.Function, err error)
// Functions returns an object that can list and get Functions.
Functions(namespace string) FunctionNamespaceLister
FunctionListerExpansion
}
// functionLister implements the FunctionLister interface.
type functionLister struct {
indexer cache.Indexer
}
// NewFunctionLister returns a new FunctionLister.
func NewFunctionLister(indexer cache.Indexer) FunctionLister {
return &functionLister{indexer: indexer}
}
// List lists all Functions in the indexer.
func (s *functionLister) List(selector labels.Selector) (ret []*v1beta1.Function, err error) {
err = cache.ListAll(s.indexer, selector, func(m interface{}) {
ret = append(ret, m.(*v1beta1.Function))
})
return ret, err
}
// Functions returns an object that can list and get Functions.
func (s *functionLister) Functions(namespace string) FunctionNamespaceLister {
return functionNamespaceLister{indexer: s.indexer, namespace: namespace}
}
// FunctionNamespaceLister helps list and get Functions.
type FunctionNamespaceLister interface {
// List lists all Functions in the indexer for a given namespace.
List(selector labels.Selector) (ret []*v1beta1.Function, err error)
// Get retrieves the Function from the indexer for a given namespace and name.
Get(name string) (*v1beta1.Function, error)
FunctionNamespaceListerExpansion
}
// functionNamespaceLister implements the FunctionNamespaceLister
// interface.
type functionNamespaceLister struct {
indexer cache.Indexer
namespace string
}
// List lists all Functions in the indexer for a given namespace.
func (s functionNamespaceLister) List(selector labels.Selector) (ret []*v1beta1.Function, err error) {
err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) {
ret = append(ret, m.(*v1beta1.Function))
})
return ret, err
}
// Get retrieves the Function from the indexer for a given namespace and name.
func (s functionNamespaceLister) Get(name string) (*v1beta1.Function, error) {
obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name)
if err != nil {
return nil, err
}
if !exists {
return nil, errors.NewNotFound(v1beta1.Resource("function"), name)
}
return obj.(*v1beta1.Function), nil
}
================================================
FILE: pkg/controller/function_controller.go
================================================
/*
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.
*/
package controller
import (
"crypto/sha256"
"fmt"
"net/url"
"time"
monitoringv1alpha1 "github.com/coreos/prometheus-operator/pkg/client/monitoring/v1alpha1"
"github.com/sirupsen/logrus"
appsv1 "k8s.io/api/apps/v1"
"k8s.io/api/autoscaling/v2beta1"
corev1 "k8s.io/api/core/v1"
apiequality "k8s.io/apimachinery/pkg/api/equality"
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue"
"github.com/ghodss/yaml"
kubelessApi "github.com/kubeless/kubeless/pkg/apis/kubeless/v1beta1"
"github.com/kubeless/kubeless/pkg/client/clientset/versioned"
kv1beta1 "github.com/kubeless/kubeless/pkg/client/informers/externalversions/kubeless/v1beta1"
"github.com/kubeless/kubeless/pkg/langruntime"
"github.com/kubeless/kubeless/pkg/registry"
"github.com/kubeless/kubeless/pkg/utils"
)
const (
maxRetries = 5
funcKind = "Function"
funcAPIVersion = "kubeless.io/v1beta1"
functionFinalizer = "kubeless.io/function"
)
// FunctionController object
type FunctionController struct {
logger *logrus.Entry
clientset kubernetes.Interface
kubelessclient versioned.Interface
smclient *monitoringv1alpha1.MonitoringV1alpha1Client
Functions map[string]*kubelessApi.Function
queue workqueue.RateLimitingInterface
informer cache.SharedIndexInformer
config *corev1.ConfigMap
langRuntime *langruntime.Langruntimes
imagePullSecrets []corev1.LocalObjectReference
}
// Config contains k8s client of a controller
type Config struct {
KubeCli kubernetes.Interface
FunctionClient versioned.Interface
}
// NewFunctionController returns a new *FunctionController
func NewFunctionController(cfg Config, smclient *monitoringv1alpha1.MonitoringV1alpha1Client) *FunctionController {
queue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter())
apiExtensionsClientset := utils.GetAPIExtensionsClientInCluster()
config, err := utils.GetKubelessConfig(cfg.KubeCli, apiExtensionsClientset)
if err != nil {
logrus.Fatalf("Unable to read the configmap: %s", err)
}
informer := kv1beta1.NewFunctionInformer(cfg.FunctionClient, config.Data["functions-namespace"], 0, cache.Indexers{})
informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
key, err := cache.MetaNamespaceKeyFunc(obj)
if err == nil {
queue.Add(key)
}
},
UpdateFunc: func(old, new interface{}) {
key, err := cache.MetaNamespaceKeyFunc(new)
if err == nil {
newFunctionObj := new.(*kubelessApi.Function)
oldFunctionObj := old.(*kubelessApi.Function)
if functionObjChanged(oldFunctionObj, newFunctionObj) {
queue.Add(key)
}
}
},
DeleteFunc: func(obj interface{}) {
key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
if err == nil {
queue.Add(key)
}
},
})
var lr = langruntime.New(config)
lr.ReadConfigMap()
imagePullSecrets := utils.GetSecretsAsLocalObjectReference(config.Data["provision-image-secret"], config.Data["builder-image-secret"])
if config.Data["enable-build-step"] == "true" {
imagePullSecrets = append(imagePullSecrets, utils.GetSecretsAsLocalObjectReference("kubeless-registry-credentials")...)
}
return &FunctionController{
logger: logrus.WithField("pkg", "function-controller"),
clientset: cfg.KubeCli,
smclient: smclient,
kubelessclient: cfg.FunctionClient,
informer: informer,
queue: queue,
config: config,
langRuntime: lr,
imagePullSecrets: imagePullSecrets,
}
}
// Run starts the kubeless controller
func (c *FunctionController) Run(stopCh <-chan struct{}) {
defer utilruntime.HandleCrash()
defer c.queue.ShutDown()
c.logger.Info("Starting Function controller")
go c.informer.Run(stopCh)
if !cache.WaitForCacheSync(stopCh, c.HasSynced) {
utilruntime.HandleError(fmt.Errorf("Timed out waiting for caches to sync"))
return
}
c.logger.Info("Function controller synced and ready")
wait.Until(c.runWorker, time.Second, stopCh)
}
// HasSynced is required for the cache.Controller interface.
func (c *FunctionController) HasSynced() bool {
return c.informer.HasSynced()
}
// LastSyncResourceVersion is required for the cache.Controller interface.
func (c *FunctionController) LastSyncResourceVersion() string {
return c.informer.LastSyncResourceVersion()
}
func (c *FunctionController) runWorker() {
for c.processNextItem() {
// continue looping
}
}
func (c *FunctionController) processNextItem() bool {
key, quit := c.queue.Get()
if quit {
return false
}
defer c.queue.Done(key)
err := c.processItem(key.(string))
if err == nil {
// No error, reset the ratelimit counters
c.queue.Forget(key)
} else if c.queue.NumRequeues(key) < maxRetries {
c.logger.Errorf("Error processing %s (will retry): %v", key, err)
c.queue.AddRateLimited(key)
} else {
// err != nil and too many retries
c.logger.Errorf("Error processing %s (giving up): %v", key, err)
c.queue.Forget(key)
utilruntime.HandleError(err)
}
return true
}
func (c *FunctionController) processItem(key string) error {
c.logger.Infof("Processing change to Function %s", key)
ns, name, err := cache.SplitMetaNamespaceKey(key)
if err != nil {
return err
}
obj, exists, err := c.informer.GetIndexer().GetByKey(key)
if err != nil {
return fmt.Errorf("Error fetching object with key %s from store: %v", key, err)
}
// this is an update when Function API object is actually deleted, we dont need to process anything here
if !exists {
c.logger.Infof("Function object %s not found in the cache, ignoring the deletion update", key)
return nil
}
funcObj := obj.(*kubelessApi.Function)
// Function API object is marked for deletion (DeletionTimestamp != nil), so lets process the delete update
if funcObj.ObjectMeta.DeletionTimestamp != nil {
// If finalizer is removed, then we already processed the delete update, so just return
if !utils.FunctionObjHasFinalizer(funcObj, functionFinalizer) {
return nil
}
// Function object should be deleted, so cleanup the associated resources and remove the finalizer
err := c.deleteK8sResources(ns, name)
if err != nil {
c.logger.Errorf("Can't delete function: %v", err)
return err
}
// remove finalizer from the function object, so that we dont have to process any further and object can be deleted
err = utils.FunctionObjRemoveFinalizer(c.kubelessclient, funcObj, functionFinalizer)
if err != nil {
c.logger.Errorf("Failed to remove function controller as finalizer to Function Obj: %s object due to: %v: ", key, err)
return err
}
c.logger.Infof("Function object %s has been successfully processed and marked for deletion", key)
return nil
}
// If function object in not marked with self as finalizer, then add the finalizer
if !utils.FunctionObjHasFinalizer(funcObj, functionFinalizer) {
err = utils.FunctionObjAddFinalizer(c.kubelessclient, funcObj, functionFinalizer)
if err != nil {
c.logger.Errorf("Error adding Function controller as finalizer to Function Obj: %s CRD due to: %v: ", key, err)
return err
}
}
err = c.ensureK8sResources(funcObj)
if err != nil {
c.logger.Errorf("Function can not be created/updated: %v", err)
return err
}
c.logger.Infof("Processed change to function: %s", key)
return nil
}
// startImageBuildJob creates (if necessary) a job that will build an image for the given function
// returns the name of the image, a boolean indicating if the build job has been created and an error
func (c *FunctionController) startImageBuildJob(funcObj *kubelessApi.Function, or []metav1.OwnerReference) (string, bool, error) {
imagePullSecret, err := c.clientset.CoreV1().Secrets(funcObj.ObjectMeta.Namespace).Get("kubeless-registry-credentials", metav1.GetOptions{})
if err != nil {
return "", false, fmt.Errorf("Unable to locate registry credentials to build function image: %v", err)
}
reg, err := registry.New(*imagePullSecret)
if err != nil {
return "", false, fmt.Errorf("Unable to retrieve registry information: %v", err)
}
// Use function content and deps as tag (digested)
tag := fmt.Sprintf("%x", sha256.Sum256([]byte(fmt.Sprintf("%v%v", funcObj.Spec.Function, funcObj.Spec.Deps))))
imageName := fmt.Sprintf("%s/%s", reg.Creds.Username, funcObj.ObjectMeta.Name)
// Check if image already exists
exists, err := reg.ImageExists(imageName, tag)
if err != nil {
return "", false, fmt.Errorf("Unable to check is target image exists: %v", err)
}
regURL, err := url.Parse(reg.Endpoint)
if err != nil {
return "", false, fmt.Errorf("Unable to parse registry URL: %v", err)
}
image := fmt.Sprintf("%s/%s:%s", regURL.Host, imageName, tag)
if !exists {
tlsVerify := true
if c.config.Data["function-registry-tls-verify"] == "false" {
tlsVerify = false
}
err = utils.EnsureFuncImage(c.clientset, funcObj, c.langRuntime, or, imageName, tag, c.config.Data["builder-image"], regURL.Host, imagePullSecret.Name, c.config.Data["provision-image"], tlsVerify, c.imagePullSecrets)
if err != nil {
return "", false, fmt.Errorf("Unable to create image build job: %v", err)
}
} else {
// Image already exists
return image, false, nil
}
return image, true, nil
}
// ensureK8sResources creates/updates k8s objects (deploy, svc, configmap) for the function
func (c *FunctionController) ensureK8sResources(funcObj *kubelessApi.Function) error {
if len(funcObj.ObjectMeta.Labels) == 0 {
funcObj.ObjectMeta.Labels = make(map[string]string)
}
funcObj.ObjectMeta.Labels["function"] = funcObj.ObjectMeta.Name
deployment := appsv1.Deployment{}
if deploymentConfigData, ok := c.config.Data["deployment"]; ok {
err := yaml.UnmarshalStrict([]byte(deploymentConfigData), &deployment, yaml.DisallowUnknownFields)
if err != nil {
logrus.Errorf("Error parsing Deployment data in ConfigMap kubeless-function-deployment-config: %v", err)
return err
}
err = utils.MergeDeployments(&funcObj.Spec.Deployment, &deployment)
if err != nil {
logrus.Errorf(" Error while merging function.Spec.Deployment and Deployment from ConfigMap: %v", err)
return err
}
}
or, err := utils.GetOwnerReference(funcKind, funcAPIVersion, funcObj.Name, funcObj.UID)
if err != nil {
return err
}
err = utils.EnsureFuncConfigMap(c.clientset, funcObj, or, c.langRuntime)
if err != nil {
return err
}
err = utils.EnsureFuncService(c.clientset, funcObj, or)
if err != nil {
return err
}
prebuiltImage := ""
if len(funcObj.Spec.Deployment.Spec.Template.Spec.Containers) > 0 && funcObj.Spec.Deployment.Spec.Template.Spec.Containers[0].Image != "" {
prebuiltImage = funcObj.Spec.Deployment.Spec.Template.Spec.Containers[0].Image
}
// Skip image build step if using a custom runtime
if prebuiltImage == "" {
if c.config.Data["enable-build-step"] == "true" {
var isBuilding bool
prebuiltImage, isBuilding, err = c.startImageBuildJob(funcObj, or)
if err != nil {
logrus.Errorf("Unable to build function: %v", err)
} else {
if isBuilding {
logrus.Infof("Started build process for function %s", funcObj.ObjectMeta.Name)
} else {
logrus.Infof("Found existing image %s", prebuiltImage)
}
}
}
} else {
logrus.Infof("Skipping image-build step for %s", funcObj.ObjectMeta.Name)
}
err = utils.EnsureFuncDeployment(c.clientset, funcObj, or, c.langRuntime, prebuiltImage, c.config.Data["provision-image"], c.imagePullSecrets)
if err != nil {
return err
}
if funcObj.Spec.HorizontalPodAutoscaler.Name != "" && funcObj.Spec.HorizontalPodAutoscaler.Spec.ScaleTargetRef.Name != "" {
funcObj.Spec.HorizontalPodAutoscaler.OwnerReferences = or
if funcObj.Spec.HorizontalPodAutoscaler.Spec.Metrics[0].Type == v2beta1.ObjectMetricSourceType {
// A service monitor is needed when the metric is an object
err = utils.CreateServiceMonitor(*c.smclient, funcObj, funcObj.ObjectMeta.Namespace, or)
if err != nil {
return err
}
}
err = utils.CreateAutoscale(c.clientset, funcObj.Spec.HorizontalPodAutoscaler)
if err != nil && k8sErrors.IsAlreadyExists(err) {
err = utils.UpdateAutoscale(c.clientset, funcObj.Spec.HorizontalPodAutoscaler)
}
if err != nil {
return err
}
} else {
// HorizontalPodAutoscaler doesn't exists, try to delete if it already existed
err = c.deleteAutoscale(funcObj.ObjectMeta.Namespace, funcObj.ObjectMeta.Name)
if err != nil && !k8sErrors.IsNotFound(err) {
return err
}
}
return nil
}
func (c *FunctionController) deleteAutoscale(ns, name string) error {
if c.smclient != nil {
// Delete Service monitor if the client is available
err := utils.DeleteServiceMonitor(*c.smclient, name, ns)
if err != nil && !k8sErrors.IsNotFound(err) {
return err
}
}
// delete autoscale
err := utils.DeleteAutoscale(c.clientset, name, ns)
if err != nil && !k8sErrors.IsNotFound(err) {
return err
}
return nil
}
// deleteK8sResources removes k8s objects of the function
func (c *FunctionController) deleteK8sResources(ns, name string) error {
// delete deployment
deletePolicy := metav1.DeletePropagationBackground
err := c.clientset.Extensions().Deployments(ns).Delete(name, &metav1.DeleteOptions{PropagationPolicy: &deletePolicy})
if err != nil && !k8sErrors.IsNotFound(err) {
return err
}
// delete svc
err = c.clientset.Core().Services(ns).Delete(name, &metav1.DeleteOptions{})
if err != nil && !k8sErrors.IsNotFound(err) {
return err
}
// delete cm
err = c.clientset.Core().ConfigMaps(ns).Delete(name, &metav1.DeleteOptions{})
if err != nil && !k8sErrors.IsNotFound(err) {
return err
}
// delete service monitor
err = c.deleteAutoscale(ns, name)
if err != nil && !k8sErrors.IsNotFound(err) {
return err
}
// delete build job
err = c.clientset.BatchV1().Jobs(ns).DeleteCollection(&metav1.DeleteOptions{}, metav1.ListOptions{
LabelSelector: fmt.Sprintf("created-by=kubeless,function=%s", name),
})
if err != nil && !k8sErrors.IsNotFound(err) {
return err
}
return nil
}
func functionObjChanged(oldFunctionObj, newFunctionObj *kubelessApi.Function) bool {
// If the function object's deletion timestamp is set, then process
if oldFunctionObj.DeletionTimestamp != newFunctionObj.DeletionTimestamp {
return true
}
// If the new and old function object's resource version is same
if oldFunctionObj.ResourceVersion == newFunctionObj.ResourceVersion {
return false
}
newSpec := &oldFunctionObj.Spec
oldSpec := &newFunctionObj.Spec
if newSpec.Function != oldSpec.Function ||
// compare checksum since the url content type uses Function field to pass the URL for the function
// comparing the checksum ensures that if the function code has changed but the URL remains the same, the function will get redeployed
newSpec.Checksum != oldSpec.Checksum ||
newSpec.Handler != oldSpec.Handler ||
newSpec.FunctionContentType != oldSpec.FunctionContentType ||
newSpec.Deps != oldSpec.Deps ||
newSpec.Timeout != oldSpec.Timeout {
return true
}
if !apiequality.Semantic.DeepEqual(newSpec.Deployment, oldSpec.Deployment) ||
!apiequality.Semantic.DeepEqual(newSpec.HorizontalPodAutoscaler, oldSpec.HorizontalPodAutoscaler) ||
!apiequality.Semantic.DeepEqual(newSpec.ServiceSpec, oldSpec.ServiceSpec) {
return true
}
return false
}
================================================
FILE: pkg/controller/function_controller_test.go
================================================
package controller
import (
"reflect"
"testing"
"github.com/ghodss/yaml"
kubelessApi "github.com/kubeless/kubeless/pkg/apis/kubeless/v1beta1"
"github.com/kubeless/kubeless/pkg/langruntime"
"github.com/sirupsen/logrus"
appsv1 "k8s.io/api/apps/v1"
"k8s.io/api/autoscaling/v2beta1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/fake"
ktesting "k8s.io/client-go/testing"
)
func findAction(fake *fake.Clientset, verb, resource string) ktesting.Action {
for _, a := range fake.Actions() {
if a.Matches(verb, resource) {
return a
}
}
return nil
}
func hasAction(fake *fake.Clientset, verb, resource string) bool {
return findAction(fake, verb, resource) != nil
}
func TestDeleteK8sResources(t *testing.T) {
myNsFoo := metav1.ObjectMeta{
Namespace: "myns",
Name: "foo",
}
deploy := appsv1.Deployment{
ObjectMeta: myNsFoo,
}
svc := v1.Service{
ObjectMeta: myNsFoo,
}
cm := v1.ConfigMap{
ObjectMeta: myNsFoo,
}
hpa := v2beta1.HorizontalPodAutoscaler{
ObjectMeta: myNsFoo,
}
clientset := fake.NewSimpleClientset(&deploy, &svc, &cm, &hpa)
controller := FunctionController{
clientset: clientset,
}
if err := controller.deleteK8sResources("myns", "foo"); err != nil {
t.Fatalf("Deleting resources returned err: %v", err)
}
t.Log("Actions:", clientset.Actions())
for _, kind := range []string{"services", "configmaps", "deployments", "horizontalpodautoscalers"} {
a := findAction(clientset, "delete", kind)
if a == nil {
t.Errorf("failed to delete %s", kind)
} else if ns := a.GetNamespace(); ns != "myns" {
t.Errorf("deleted %s from wrong namespace (%s)", kind, ns)
} else if n := a.(ktesting.DeleteAction).GetName(); n != "foo" {
t.Errorf("deleted %s with wrong name (%s)", kind, n)
}
}
// Similar with only svc remaining
clientset = fake.NewSimpleClientset(&svc)
controller = FunctionController{
clientset: clientset,
}
if err := controller.deleteK8sResources("myns", "foo"); err != nil {
t.Fatalf("Deleting partial resources returned err: %v", err)
}
t.Log("Actions:", clientset.Actions())
if !hasAction(clientset, "delete", "services") {
t.Errorf("failed to delete service")
}
clientset = fake.NewSimpleClientset(&deploy, &svc, &cm)
controller = FunctionController{
clientset: clientset,
}
if err := controller.deleteK8sResources("myns", "foo"); err != nil {
t.Fatalf("Deleting resources returned err: %v", err)
}
t.Log("Actions:", clientset.Actions())
for _, kind := range []string{"services", "configmaps", "deployments"} {
a := findAction(clientset, "delete", kind)
if a == nil {
t.Errorf("failed to delete %s", kind)
} else if ns := a.GetNamespace(); ns != "myns" {
t.Errorf("deleted %s from wrong namespace (%s)", kind, ns)
}
}
}
func TestEnsureK8sResourcesWithDeploymentDefinitionFromConfigMap(t *testing.T) {
funcObj := testFunc()
deploymentConfigData := `{
"metadata": {
"annotations": {
"foo-from-deploy-cm": "bar-from-deploy-cm",
"xyz": "valuefromcm"
}
},
"spec": {
"replicas": 2,
"template": {
"metadata": {
"annotations": {
"podannotation-from-func-crd": "value-from-container"
}
}
}
}
}`
clientset := fake.NewSimpleClientset()
controller := testController(clientset, funcObj.Namespace, map[string]string{
"deployment": deploymentConfigData,
"runtime-images": testRuntimeImages(),
})
if err := controller.ensureK8sResources(funcObj); err != nil {
t.Fatalf("Creating/Updating resources returned err: %v", err)
}
dpm, _ := clientset.AppsV1().Deployments(funcObj.Namespace).Get(funcObj.Name, metav1.GetOptions{})
expectedAnnotations := map[string]string{
"bar": "foo",
"foo-from-deploy-cm": "bar-from-deploy-cm",
"xyz": "valuefromfunc",
}
for i := range expectedAnnotations {
if dpm.ObjectMeta.Annotations[i] != expectedAnnotations[i] {
t.Errorf("Expecting annotation %s but received %s", expectedAnnotations[i], dpm.ObjectMeta.Annotations[i])
}
}
if *dpm.Spec.Replicas != 10 {
t.Fatalf("Expecting replicas as 10 but received : %d", *dpm.Spec.Replicas)
}
expectedPodAnnotations := map[string]string{
"bar": "foo",
"foo-from-deploy-cm": "bar-from-deploy-cm",
"xyz": "valuefromfunc",
"podannotation-from-func-crd": "value-from-container",
}
for i := range expectedPodAnnotations {
if dpm.Spec.Template.Annotations[i] != expectedPodAnnotations[i] {
t.Fatalf("Expecting annotation %s but received %s", expectedPodAnnotations[i], dpm.ObjectMeta.Annotations[i])
}
}
}
func TestEnsureK8sResourcesWithDeploymentDefinitionFromConfigMapUnknownKey(t *testing.T) {
funcObj := testFunc()
deploymentConfigData := `{
"spec": {
"template": {
"spec": {
"unknown": "property"
}
}
}
}`
controller := testController(fake.NewSimpleClientset(), funcObj.Namespace, map[string]string{
"deployment": deploymentConfigData,
"runtime-images": testRuntimeImages(),
})
if err := controller.ensureK8sResources(funcObj); err == nil {
t.Fatalf("Unknown key in ConfigMap Deployment definition does not fail")
}
}
func TestEnsureK8sResourcesWithLivenessProbeFromConfigMap(t *testing.T) {
funcObj := testFunc()
runtimeImages := `[
{
"ID": "ruby",
"depName": "Gemfile",
"fileNameSuffix": ".rb",
"versions": [
{
"name": "ruby24",
"version": "2.4",
"initImage": "bitnami/ruby:2.4",
"imagePullSecrets":[]
}
],
"livenessProbeInfo":{
"exec": {
"command": [
"curl",
"-f",
"http://localhost:8080/healthz"
],
},
"initialDelaySeconds": 5,
"periodSeconds": 10
}
}
]`
clientset := fake.NewSimpleClientset()
controller := testController(clientset, funcObj.Namespace, map[string]string{
"runtime-images": runtimeImages,
})
if err := controller.ensureK8sResources(funcObj); err != nil {
t.Fatalf("Creating/Updating resources returned err: %v", err)
}
dpm, _ := clientset.AppsV1().Deployments(funcObj.Namespace).Get(funcObj.Name, metav1.GetOptions{})
expectedLivenessProbe := &v1.Probe{
InitialDelaySeconds: int32(5),
PeriodSeconds: int32(10),
Handler: v1.Handler{
Exec: &v1.ExecAction{
Command: []string{"curl", "-f", "http://localhost:8080/healthz"},
},
},
}
if !reflect.DeepEqual(dpm.Spec.Template.Spec.Containers[0].LivenessProbe, expectedLivenessProbe) {
t.Fatalf("LivenessProbe found is '%v', although expected was '%v'", dpm.Spec.Template.Spec.Containers[0].LivenessProbe, expectedLivenessProbe)
}
}
func testFunc() *kubelessApi.Function {
var replicas int32
replicas = 10
funcAnno := map[string]string{
"bar": "foo",
"xyz": "valuefromfunc",
}
return &kubelessApi.Function{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Namespace: "default",
Labels: map[string]string{"foo": "bar"},
UID: "foo-uid",
},
Spec: kubelessApi.FunctionSpec{
Function: "function",
Deps: "deps",
Handler: "foo.bar",
Runtime: "ruby2.4",
Deployment: appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Annotations: funcAnno,
},
Spec: appsv1.DeploymentSpec{
Replicas: &replicas,
Template: v1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Annotations: funcAnno,
},
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Env: []v1.EnvVar{
{
Name: "foo",
Value: "bar",
},
},
},
},
},
},
},
},
},
}
}
func testRuntimeImages() string {
runtimeImages := []langruntime.RuntimeInfo{{
ID: "ruby",
DepName: "Gemfile",
FileNameSuffix: ".rb",
Versions: []langruntime.RuntimeVersion{
{
Name: "ruby24",
Version: "2.4",
Images: []langruntime.Image{
{Phase: "runtime", Image: "bitnami/ruby:2.4"},
},
ImagePullSecrets: []langruntime.ImageSecret{},
},
},
}}
out, err := yaml.Marshal(runtimeImages)
if err != nil {
logrus.Fatal("Canot Marshall runtimeimage")
}
return string(out)
}
func testController(clientset kubernetes.Interface, namespace string, configData map[string]string) *FunctionController {
kubelessConfigMap := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "kubeless-config",
},
Data: configData,
}
_, err := clientset.CoreV1().ConfigMaps(namespace).Create(kubelessConfigMap)
if err != nil {
logrus.Fatal("Unable to create configmap")
}
config, err := clientset.CoreV1().ConfigMaps(namespace).Get("kubeless-config", metav1.GetOptions{})
if err != nil {
logrus.Fatal("Unable to read the configmap")
}
var lr = langruntime.New(config)
lr.ReadConfigMap()
return &FunctionController{
logger: logrus.WithField("pkg", "controller"),
clientset: clientset,
langRuntime: lr,
config: config,
}
}
================================================
FILE: pkg/function-image-builder/image_builder.go
================================================
/*
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.
*/
package main
import (
"bufio"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
lbuilder "github.com/kubeless/kubeless/pkg/function-image-builder/layer-builder"
"github.com/spf13/cobra"
)
var globalUsage = `` //TODO: add explanation
func init() {
layerCmd.Flags().Bool("insecure", false, "Disable TLS verification.")
layerCmd.Flags().StringP("src", "", "", "Source image reference. F.e. dir://path/to/image")
layerCmd.Flags().StringP("src-creds", "", "", "Source image credentials in case it is a private registry. F.e. user:my_pass")
layerCmd.Flags().StringP("dst", "", "", "Destination image reference. F.e. docker://user/image")
layerCmd.Flags().StringP("dst-creds", "", "", "Destination credentials in case it is a docker registry. F.e. user:my_pass")
layerCmd.Flags().StringP("cwd", "", "", "Working directory")
}
func runCommand(command string, args []string) error {
cmd := exec.Command(command, args...)
stdout, _ := cmd.StdoutPipe()
stderr, _ := cmd.StderrPipe()
cmd.Start()
scannerStdout := bufio.NewScanner(stdout)
scannerStdout.Split(bufio.ScanLines)
for scannerStdout.Scan() {
m := scannerStdout.Text()
fmt.Fprintln(os.Stdout, m)
}
scannerStderr := bufio.NewScanner(stderr)
scannerStderr.Split(bufio.ScanLines)
for scannerStderr.Scan() {
m := scannerStderr.Text()
fmt.Fprintln(os.Stderr, m)
}
return cmd.Wait()
}
func skopeoCopy(src, dst, srcCreds, dstCreds string, insecure bool) error {
command := "skopeo"
args := []string{"copy"}
if srcCreds != "" {
args = append(args, "--src-creds", srcCreds)
}
if dstCreds != "" {
args = append(args, "--dest-creds", dstCreds)
}
if insecure {
args = append(args, "--src-tls-verify=false", "--dest-tls-verify=false")
}
args = append(args, src, dst)
return runCommand(command, args)
}
var layerCmd = &cobra.Command{
Use: "add-layer FLAG",
Short: "Add tar as a image layer",
Long: ``,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 1 {
log.Fatal("Need exactly one argument - layer tar")
}
layerTar := args[0]
srcImage, err := cmd.Flags().GetString("src")
if err != nil {
log.Fatal(err)
}
if srcImage == "" {
log.Fatal("Need specify the source image using the flag --src")
}
dstImage, err := cmd.Flags().GetString("dst")
if err != nil {
log.Fatal(err)
}
if dstImage == "" {
log.Fatal("Need specify the destination image using the flag --dst")
}
srcCreds, err := cmd.Flags().GetString("src-creds")
if err != nil {
log.Fatal(err)
}
dstCreds, err := cmd.Flags().GetString("dst-creds")
if err != nil {
log.Fatal(err)
}
workDir, err := cmd.Flags().GetString("cwd")
if err != nil {
log.Fatal(err)
}
if workDir == "" {
workDir, err = ioutil.TempDir("", "build")
if err != nil {
log.Fatal(err)
}
}
insecure, err := cmd.Flags().GetBool("insecure")
if err != nil {
log.Fatal(err)
}
// Store src image
err = skopeoCopy(srcImage, fmt.Sprintf("dir://%s", workDir), srcCreds, dstCreds, insecure)
if err != nil {
log.Fatal(err)
}
log.Println("Succesfully stored base image ", srcImage, " at ", workDir)
// Add layer
err = lbuilder.AddTarToLayer(workDir, layerTar)
if err != nil {
log.Fatal(err)
}
log.Println("Added layer ", layerTar, " in ", workDir)
// Publish new image
err = skopeoCopy(fmt.Sprintf("dir://%s", workDir), dstImage, srcCreds, dstCreds, insecure)
if err != nil {
log.Fatal(err)
}
log.Println("Succesfully stored final image at ", dstImage)
},
}
func newRootCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "imbuilder",
Short: "Pulls an image and push a new one including a tar file as a new layer",
Long: globalUsage,
}
cmd.AddCommand(layerCmd)
return cmd
}
func main() {
cmd := newRootCmd()
if err := cmd.Execute(); err != nil {
os.Exit(1)
}
}
================================================
FILE: pkg/function-image-builder/layer-builder/description.go
================================================
/*
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.
*/
package layerbuilder
import (
"crypto/sha256"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"time"
)
// Config represents a container configuration
type Config struct {
Hostname string
Domainname string
User string
AttachStdin bool
AttachStdout bool
AttachStderr bool
Tty bool
OpenStdin bool
StdinOnce bool
Env []string
Cmd []string
ArgsEscaped bool
Image string
Volumes interface{}
WorkingDir string
Entrypoint interface{}
OnBuild interface{}
Labels interface{}
}
// HistoryEntry represents a layer creation info
type HistoryEntry struct {
Created string `json:"created"`
CreatedBy string `json:"created_by,omitifempty"`
Comment string `json:"comment,omitifempty"`
EmptyLayer bool `json:"empty_layer,omitifempty"`
}
// Rootfs represents the root filesystem of an image
type Rootfs struct {
Type string `json:"type"`
DiffIds []string `json:"diff_ids"`
}
// Description represents the specification of a Docker image
type Description struct {
Arch string `json:"architecture"`
Config Config `json:"config"`
Container string `json:"container"`
ContainerConfig Config `json:"container_config"`
Created string `json:"created"`
DockerVersion string `json:"docker_version"`
History []HistoryEntry `json:"history"`
OS string `json:"os"`
Rootfs Rootfs `json:"rootfs"`
}
// New generates a Description object based on the description file
func (d *Description) New(descriptionFile io.Reader) error {
descriptionContent, err := ioutil.ReadAll(descriptionFile)
if err != nil {
return err
}
return json.Unmarshal(descriptionContent, d)
}
// AddLayer adds a new Layer to the image Description
func (d *Description) AddLayer(newLayer *Layer) {
// Delete some properties that doesn't apply anymore
d.Config.Hostname = ""
d.Config.Image = ""
d.Container = ""
d.ContainerConfig.Hostname = ""
d.ContainerConfig.Image = ""
// Update new properties
d.Created = time.Now().UTC().Format(time.RFC3339)
d.History = append(d.History, HistoryEntry{
Created: time.Now().UTC().Format(time.RFC3339),
Comment: "Created by Kubeless",
})
d.Rootfs.DiffIds = append(d.Rootfs.DiffIds, fmt.Sprintf("sha256:%s", newLayer.Sha256))
}
// Content returns the description content
func (d *Description) Content() ([]byte, error) {
return json.Marshal(*d)
}
// ToLayer returns the Description as a Layer
func (d *Description) ToLayer() (*Layer, error) {
content, err := d.Content()
if err != nil {
return nil, err
}
descriptionNewSize := int64(len(content))
descriptionNewSha := fmt.Sprintf("%x", sha256.Sum256(content))
return &Layer{
Size: descriptionNewSize,
Sha256: descriptionNewSha,
}, nil
}
================================================
FILE: pkg/function-image-builder/layer-builder/description_test.go
================================================
/*
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.
*/
package layerbuilder
import (
"strings"
"testing"
)
func TestNewDescription(t *testing.T) {
descFile := strings.NewReader(`{"architecture":"amd64","config":{"Hostname":"","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["sh"],"ArgsEscaped":true,"Image":"sha256:8cae5980d887cc55ba2f978ae99c662007ee06d79881678d57f33f0473fe0736","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"container":"8d2c840a1a9b2544fe713c2e24b6757d52328f09bdfc9c2ef6219afbf7ae6b59","container_config":{"Hostname":"8d2c840a1a9b","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh","-c","#(nop) "],"ArgsEscaped":true,"Image":"sha256:8cae5980d887cc55ba2f978ae99c662007ee06d79881678d57f33f0473fe0736","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":{}},"created":"2018-02-28T22:14:49.023807051Z","docker_version":"17.06.2-ce","history":[{"created":"2018-02-28T22:14:48.759033366Z","created_by":"/bin/sh -c #(nop) ADD file:327f69fc1ac9a7b6e56e9032f7b8fbd7741dd0b22920761909c6c8e5fa9c5815 in / "},{"created":"2018-02-28T22:14:49.023807051Z","created_by":"/bin/sh -c #(nop) ","empty_layer":true}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:c5183829c43c4698634093dc38f9bee26d1b931dedeba71dbee984f42fe1270d"]}}`)
d := Description{}
err := d.New(descFile)
if err != nil {
t.Errorf("Unexpected error %v", err)
}
}
func TestAddLayerDescription(t *testing.T) {
descFile := strings.NewReader(`{"architecture":"amd64","config":{"Hostname":"","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["sh"],"ArgsEscaped":true,"Image":"sha256:8cae5980d887cc55ba2f978ae99c662007ee06d79881678d57f33f0473fe0736","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"container":"8d2c840a1a9b2544fe713c2e24b6757d52328f09bdfc9c2ef6219afbf7ae6b59","container_config":{"Hostname":"8d2c840a1a9b","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh","-c","#(nop) "],"ArgsEscaped":true,"Image":"sha256:8cae5980d887cc55ba2f978ae99c662007ee06d79881678d57f33f0473fe0736","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":{}},"created":"2018-02-28T22:14:49.023807051Z","docker_version":"17.06.2-ce","history":[{"created":"2018-02-28T22:14:48.759033366Z","created_by":"/bin/sh -c #(nop) ADD file:327f69fc1ac9a7b6e56e9032f7b8fbd7741dd0b22920761909c6c8e5fa9c5815 in / "},{"created":"2018-02-28T22:14:49.023807051Z","created_by":"/bin/sh -c #(nop) ","empty_layer":true}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:c5183829c43c4698634093dc38f9bee26d1b931dedeba71dbee984f42fe1270d"]}}`)
d := Description{}
err := d.New(descFile)
if err != nil {
t.Errorf("Unexpected error %v", err)
}
newLayer := Layer{
Size: 10,
Sha256: "abc123",
}
d.AddLayer(&newLayer)
// Last history entry should be the new layer
if d.History[len(d.History)-1].Comment != "Created by Kubeless" {
t.Errorf("Failed to include new layer: %v", d.History)
}
// Last rootfs.diff_id should be the new layer
if d.Rootfs.DiffIds[len(d.Rootfs.DiffIds)-1] == "abc123" {
t.Error("Failed to include new layer")
}
}
func TestDescriptionToLayer(t *testing.T) {
emptyDesc := Description{}
res, err := emptyDesc.ToLayer()
if err != nil {
t.Fatalf("Unexpected error %v", err)
}
expectedSize := int64(721)
expectedSha := "17263670d4f12e26a270c7ec0a443c3ba8354da1d42f43f8e421634c5965bb6b"
if res.Sha256 != expectedSha {
t.Errorf("Unexpected sha256 %s", res.Sha256)
}
if res.Size != expectedSize {
t.Errorf("Unexpected size %d", res.Size)
}
}
================================================
FILE: pkg/function-image-builder/layer-builder/layer.go
================================================
/*
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.
*/
package layerbuilder
import (
"crypto/sha256"
"fmt"
"io/ioutil"
"os"
)
// Layer represent the size and checksum of a image layer
type Layer struct {
Size int64
Sha256 string
}
// New returns a Layer based on its file
func (f *Layer) New(layerFile *os.File) error {
// Calculate sha256
fContent, err := ioutil.ReadAll(layerFile)
if err != nil {
return err
}
f.Sha256 = fmt.Sprintf("%x", sha256.Sum256(fContent))
// Calculate size
fstat, err := layerFile.Stat()
if err != nil {
return err
}
f.Size = fstat.Size()
return nil
}
================================================
FILE: pkg/function-image-builder/layer-builder/layer_builder.go
================================================
/*
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.
*/
package layerbuilder
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path"
"strings"
)
func copyReader(src io.Reader, dst string) error {
dstFile, err := os.Create(dst)
if err != nil {
return err
}
defer dstFile.Close()
_, err = io.Copy(dstFile, src)
if err != nil {
return err
}
err = dstFile.Sync()
if err != nil {
return err
}
return nil
}
func copyFile(src, dst string) error {
srcFile, err := os.Open(src)
if err != nil {
return err
}
defer srcFile.Close()
return copyReader(srcFile, dst)
}
func getLayer(file string) (*Layer, error) {
layerFile, err := os.Open(file)
if err != nil {
return nil, err
}
defer layerFile.Close()
layer := Layer{}
err = layer.New(layerFile)
if err != nil {
return nil, err
}
return &layer, nil
}
func saveNewDescription(content []byte, dir, contentChecksum string) error {
dLayerFile := path.Join(dir, contentChecksum)
return copyReader(bytes.NewReader(content), dLayerFile)
}
func updateDescription(descriptionDir string, descriptionFile *os.File, newLayer *Layer) (*Description, error) {
d := Description{}
err := d.New(descriptionFile)
if err != nil {
return nil, fmt.Errorf("Unable to parse image description: %v", err)
}
d.AddLayer(newLayer)
if err != nil {
return nil, fmt.Errorf("Unable to update image description: %v", err)
}
return &d, nil
}
// AddTarToLayer copies a tar file into a image directory and update its metadata
func AddTarToLayer(imageDir, tarFile string) error {
tarLayer, err := getLayer(tarFile)
if err != nil {
return err
}
destFile := path.Join(imageDir, tarLayer.Sha256)
err = copyFile(tarFile, destFile)
if err != nil {
return fmt.Errorf("Failed to copy tar file: %v", err)
}
log.Printf("Copied source %s to %s", tarFile, destFile)
// Parse manifest
manifestPath := path.Join(imageDir, "manifest.json")
manifestFile, err := os.Open(manifestPath)
if err != nil {
return err
}
m := Manifest{}
err = m.New(manifestFile)
if err != nil {
return fmt.Errorf("Failed to parse image manifest: %v", err)
}
log.Printf("Parsed manifest")
// Update description
descriptionPath := path.Join(imageDir, strings.Replace(m.Config.Digest, "sha256:", "", -1))
descriptionFile, err := os.Open(descriptionPath)
if err != nil {
return err
}
description, err := updateDescription(imageDir, descriptionFile, tarLayer)
if err != nil {
return err
}
descriptionLayer, err := description.ToLayer()
if err != nil {
return fmt.Errorf("Unable to generate layer from description: %v", err)
}
descriptionContent, err := description.Content()
if err != nil {
return err
}
err = saveNewDescription(descriptionContent, imageDir, descriptionLayer.Sha256)
if err != nil {
return err
}
log.Printf("Added layer to description at %s", descriptionLayer.Sha256)
// Update manifest
m.UpdateConfig(descriptionLayer)
m.AddLayer(tarLayer)
mBytes, err := json.Marshal(m)
if err != nil {
return err
}
err = ioutil.WriteFile(manifestPath, mBytes, 0644)
if err != nil {
return err
}
log.Printf("Updated manifest")
return nil
}
================================================
FILE: pkg/function-image-builder/layer-builder/layer_test.go
================================================
/*
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.
*/
package layerbuilder
import (
"io/ioutil"
"os"
"testing"
)
func TestNewLayer(t *testing.T) {
f, err := ioutil.TempFile("", "")
if err != nil {
t.Fatal(err)
}
defer os.Remove(f.Name())
f.WriteString("test content")
layer := Layer{}
err = layer.New(f)
if err != nil {
t.Fatalf("Unexpected error %v", err)
}
if layer.Sha256 != "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" {
t.Errorf("Wrong sha, expecting patata, received %s", layer.Sha256)
}
if layer.Size != 12 {
t.Errorf("Wrong size, expecting patata, received %d", layer.Size)
}
}
================================================
FILE: pkg/function-image-builder/layer-builder/manifest.go
================================================
/*
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.
*/
package layerbuilder
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
)
type layer struct {
MediaType string `json:"mediaType"`
Size int64 `json:"size"`
Digest string `json:"digest"`
}
// Manifest represent the manifest.json of an image
type Manifest struct {
SchemaVersion int `json:"schemaVersion"`
MediaType string `json:"mediaType"`
Config layer `json:"config"`
Layers []layer `json:"layers"`
}
// New parses an io.Reader into a Manifest
func (m *Manifest) New(manifestFile io.Reader) error {
manifestContent, err := ioutil.ReadAll(manifestFile)
if err != nil {
return err
}
err = json.Unmarshal(manifestContent, m)
if err != nil {
return nil
}
return nil
}
// UpdateConfig overrides the Config information of the manifest with a new Layer
func (m *Manifest) UpdateConfig(newConfig *Layer) {
m.Config.Size = int64(newConfig.Size)
m.Config.Digest = fmt.Sprintf("sha256:%s", newConfig.Sha256)
}
// AddLayer adds a new layer to the list in the Manifest
func (m *Manifest) AddLayer(newLayer *Layer) {
m.Layers = append(m.Layers, layer{
MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
Size: newLayer.Size,
Digest: fmt.Sprintf("sha256:%s", newLayer.Sha256),
})
}
================================================
FILE: pkg/function-image-builder/layer-builder/manifest_test.go
================================================
/*
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.
*/
package layerbuilder
import (
"strings"
"testing"
)
func TestNewManifest(t *testing.T) {
manifestFile := strings.NewReader(`{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","size":1489,"digest":"sha256:c7fc094ddbf9f9335543421b34d8c6f3becd3bb05c9f9a5ca0f0e6065871072d"},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","size":723113,"digest":"sha256:d070b8ef96fc4f2d92ff520a4fe55594e362b4e1076a32bbfeb261dc03322910"}]}`)
m := Manifest{}
err := m.New(manifestFile)
if err != nil {
t.Errorf("Unexpected error %v", err)
}
if m.Config.Size != 1489 {
t.Errorf("Unexpected size %d", m.Config.Size)
}
if m.Config.Digest != "sha256:c7fc094ddbf9f9335543421b34d8c6f3becd3bb05c9f9a5ca0f0e6065871072d" {
t.Errorf("Unexpected digest %s", m.Config.Digest)
}
if len(m.Layers) != 1 {
t.Errorf("Unexpected layers length %d", len(m.Layers))
}
}
func TestAddNewLayer(t *testing.T) {
manifestFile := strings.NewReader(`{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","size":1489,"digest":"sha256:c7fc094ddbf9f9335543421b34d8c6f3becd3bb05c9f9a5ca0f0e6065871072d"},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","size":723113,"digest":"sha256:d070b8ef96fc4f2d92ff520a4fe55594e362b4e1076a32bbfeb261dc03322910"}]}`)
m := Manifest{}
err := m.New(manifestFile)
if err != nil {
t.Errorf("Unexpected error %v", err)
}
m.AddLayer(&Layer{
Size: 10,
Sha256: "Test",
})
if len(m.Layers) != 2 {
t.Errorf("Unexpected layers length %d", len(m.Layers))
}
if m.Layers[1].Size != 10 && m.Layers[1].Digest != "Test" {
t.Errorf("Unexpected layer %v", m.Layers[1])
}
}
func TestUpdateConfig(t *testing.T) {
manifestFile := strings.NewReader(`{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","size":1489,"digest":"sha256:c7fc094ddbf9f9335543421b34d8c6f3becd3bb05c9f9a5ca0f0e6065871072d"},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","size":723113,"digest":"sha256:d070b8ef96fc4f2d92ff520a4fe55594e362b4e1076a32bbfeb261dc03322910"}]}`)
m := Manifest{}
err := m.New(manifestFile)
if err != nil {
t.Errorf("Unexpected error %v", err)
}
m.UpdateConfig(&Layer{
Size: 10,
Sha256: "Test",
})
if m.Config.Size != 10 && m.Config.Digest != "Test" {
t.Errorf("Unexpected layer %v", m.Config)
}
}
================================================
FILE: pkg/function-proxy/Gopkg.toml
================================================
[[constraint]]
name = "github.com/prometheus/client_golang"
revision = "f504d69affe11ec1ccb2e5948127f86878c9fd57"
================================================
FILE: pkg/function-proxy/proxy.go
================================================
/*
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.
*/
package main
import (
"golang.org/x/net/context"
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"
"github.com/kubeless/kubeless/pkg/function-proxy/utils"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func copyHeaders(dst, src http.Header) {
for k, vv := range src {
for _, v := range vv {
dst.Add(k, v)
}
}
}
func handle(ctx context.Context, w http.ResponseWriter, r *http.Request) ([]byte, error) {
client := &http.Client{}
req, err := http.NewRequest(r.Method, "http://localhost:8090", r.Body)
if err != nil {
return []byte{}, err
}
copyHeaders(req.Header, r.Header)
req.ContentLength = r.ContentLength
response, err := client.Do(req)
if err != nil {
return []byte{}, err
}
return ioutil.ReadAll(response.Body)
}
func handler(w http.ResponseWriter, r *http.Request) {
utils.Handler(w, r, handle)
}
func health(w http.ResponseWriter, r *http.Request) {
rr, err := http.Get("http://localhost:8090/healthz")
res, _ := ioutil.ReadAll(rr.Body)
log.Println(string(res))
if err != nil {
log.Fatalln("localhost:8090 not responding")
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Internal Server error"))
} else {
w.Write([]byte("OK"))
}
}
func startNativeDaemon() {
args := os.Getenv("FUNC_PROCESS")
cmd := exec.Command("/bin/sh", "-c", args)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
log.Fatalf("Unable to run %s. Received %v", args, err)
}
}
func main() {
go startNativeDaemon()
mux := http.NewServeMux()
mux.HandleFunc("/", handler)
mux.HandleFunc("/healthz", health)
mux.Handle("/metrics", promhttp.Handler())
server := utils.NewServer(mux)
go func() {
if err := server.ListenAndServe(); err != http.ErrServerClosed {
panic(err)
}
}()
utils.GracefulShutdown(server)
}
================================================
FILE: pkg/function-proxy/utils/proxy-utils.go
================================================
/*
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.
*/
package utils
import (
"fmt"
"golang.org/x/net/context"
"log"
"net/http"
"os"
"os/signal"
"strconv"
"syscall"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
timeout = os.Getenv("FUNC_TIMEOUT")
funcPort = os.Getenv("FUNC_PORT")
shutdownTimeout = os.Getenv("SHUTDOWN_TIMEOUT")
intTimeout int
intShutdownTimeout int
funcHistogram = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Name: "function_duration_seconds",
Help: "Duration of user function in seconds",
}, []string{"method"})
funcCalls = prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "function_calls_total",
Help: "Number of calls to user function",
}, []string{"method"})
funcErrors = prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "function_failures_total",
Help: "Number of exceptions in user function",
}, []string{"method"})
)
// PromHTTPHandler to expose the metrics, invoked in the golang runtime
func PromHTTPHandler() http.Handler {
return promhttp.Handler()
}
func init() {
if timeout == "" {
timeout = "180"
}
if funcPort == "" {
funcPort = "8080"
}
if shutdownTimeout == "" {
shutdownTimeout = "10"
}
var err error
intTimeout, err = strconv.Atoi(timeout)
if err != nil {
panic(err)
}
intShutdownTimeout, err = strconv.Atoi(shutdownTimeout)
if err != nil {
panic(err)
}
prometheus.MustRegister(funcHistogram, funcCalls, funcErrors)
}
// Logging Functions, required to expose statusCode property
type loggingResponseWriter struct {
http.ResponseWriter
statusCode int
}
func newLoggingResponseWriter(w http.ResponseWriter) *loggingResponseWriter {
return &loggingResponseWriter{w, http.StatusOK}
}
func (lrw *loggingResponseWriter) WriteHeader(code int) {
lrw.statusCode = code
lrw.ResponseWriter.WriteHeader(code)
}
func logReq(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
lrw := newLoggingResponseWriter(w)
handler.ServeHTTP(lrw, r)
log.Printf("%s \"%s %s %s\" %d %s", r.RemoteAddr, r.Method, r.RequestURI, r.Proto, lrw.statusCode, r.UserAgent())
if lrw.statusCode == 408 {
go func() {
// Give time to return timeout response
time.Sleep(time.Second)
log.Fatal("Request timeout. Forcing exit")
}()
}
})
}
func copyHeaders(dst, src http.Header) {
for k, vv := range src {
for _, v := range vv {
dst.Add(k, v)
}
}
}
// Handle type receive the context elements of a HTTP request to process it
type Handle func(ctx context.Context, w http.ResponseWriter, r *http.Request) ([]byte, error)
// Handler receives an HTTP request and response and a handler function
// It manages timeouts and prometheus metrics
func Handler(w http.ResponseWriter, r *http.Request, h Handle) {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(intTimeout)*time.Second)
defer cancel()
funcChannel := make(chan struct {
res string
err error
}, 1)
go func() {
funcCalls.With(prometheus.Labels{"method": r.Method}).Inc()
start := time.Now()
res, err := h(ctx, w, r)
funcHistogram.With(prometheus.Labels{"method": r.Method}).Observe(time.Since(start).Seconds())
pack := struct {
res string
err error
}{string(res), err}
funcChannel <- pack
}()
select {
case respPack := <-funcChannel:
if respPack.err != nil {
funcErrors.With(prometheus.Labels{"method": r.Method}).Inc()
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(fmt.Sprintf("Error: %v", respPack.err)))
} else {
w.Write([]byte(respPack.res))
}
// Send Timeout response
case <-ctx.Done():
funcErrors.With(prometheus.Labels{"method": r.Method}).Inc()
w.WriteHeader(http.StatusRequestTimeout)
w.Write([]byte("Timeout exceeded"))
}
}
// NewServer returns an HTTP server ready to listen on the configured port
// and with logReq mixed in for logging.
func NewServer(mux *http.ServeMux) *http.Server {
return &http.Server{Addr: fmt.Sprintf(":%s", funcPort), Handler: logReq(mux)}
}
// GracefulShutdown accepts a server reference and triggers a graceful shutdown
// for it when either SIGINT or SIGTERM is received.
func GracefulShutdown(server *http.Server) {
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
<-stop
timeoutDuration := time.Duration(intShutdownTimeout) * time.Second
ctx, cancel := context.WithTimeout(context.Background(), timeoutDuration)
defer cancel()
log.Printf("Shuting down with timeout: %s\n", timeoutDuration)
if err := server.Shutdown(ctx); err != nil {
log.Printf("Error: %v\n", err)
} else {
log.Println("Server stopped")
}
}
================================================
FILE: pkg/functions/params.go
================================================
/*
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.
*/
package functions
import (
"golang.org/x/net/context"
"net/http"
)
// Extension includes a reference to the Event request and its Context (to handle timeouts)
type Extension struct {
Request *http.Request
Response http.ResponseWriter
Context context.Context
}
// Event includes information about the event source
type Event struct {
Data string
EventID string
EventType string
EventTime string
EventNamespace string
Extensions Extension
}
// Context includes information about the function environment
type Context struct {
FunctionName string
Timeout string
Runtime string
MemoryLimit string
}
================================================
FILE: pkg/langruntime/langruntime.go
================================================
package langruntime
import (
"fmt"
"os"
"path"
"regexp"
"strings"
"github.com/ghodss/yaml"
"github.com/sirupsen/logrus"
appsv1 "k8s.io/api/apps/v1"
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/intstr"
)
const (
// PhaseInstallation - Installation phase name
PhaseInstallation = "installation"
// PhaseCompilation - Compilation phase name
PhaseCompilation = "compilation"
// PhaseRuntime - Runtime phase name
PhaseRuntime = "runtime"
)
// Langruntimes struct for getting configmap
type Langruntimes struct {
kubelessConfig *v1.ConfigMap
AvailableRuntimes []RuntimeInfo
}
// Image represents the information about a runtime phase
type Image struct {
Phase string `yaml:"phase"`
Image string `yaml:"image"`
Command string `yaml:"command,omitempty"`
Env map[string]string `yaml:"env,omitempty"`
Secrets []Secret `yaml:"secrets,omitempty"`
}
// Secret is a reference to a secret.
type Secret struct {
Name string `yaml:"name,omitempty"`
}
// RuntimeVersion is a struct with all the info about the images and secrets
type RuntimeVersion struct {
Name string `yaml:"name"`
Version string `yaml:"version"`
Images []Image `yaml:"runtimeImage"`
ImagePullSecrets []ImageSecret `yaml:"imagePullSecrets,omitempty"`
}
// ImageSecret for pulling the image
type ImageSecret struct {
ImageSecret string `yaml:"imageSecret,omitempty"`
}
// RuntimeInfo describe the runtime specifics (typical file suffix and dependency file name)
// and the supported versions
type RuntimeInfo struct {
ID string `yaml:"ID"`
Versions []RuntimeVersion `yaml:"versions"`
LivenessProbeInfo *v1.Probe `yaml:"livenessProbeInfo,omitempty"`
DepName string `yaml:"depName"`
FileNameSuffix string `yaml:"fileNameSuffix"`
}
// New initializes a langruntime object
func New(config *v1.ConfigMap) *Langruntimes {
var ri []RuntimeInfo
return &Langruntimes{
kubelessConfig: config,
AvailableRuntimes: ri,
}
}
// ReadConfigMap reads the configmap
func (l *Langruntimes) ReadConfigMap() {
if runtimeImages, ok := l.kubelessConfig.Data["runtime-images"]; ok {
err := yaml.Unmarshal([]byte(runtimeImages), &l.AvailableRuntimes)
if err != nil {
logrus.Fatalf("Unable to get the runtime images: %v", err)
}
}
}
// GetRuntimes returns the list of available runtimes as strings
func (l *Langruntimes) GetRuntimes() []string {
result := []string{}
for _, runtimeInf := range l.AvailableRuntimes {
for _, runtime := range runtimeInf.Versions {
result = append(result, runtimeInf.ID+runtime.Version)
}
}
return result
}
// IsValidRuntime returns true if passed runtime name is valid runtime
func (l *Langruntimes) IsValidRuntime(runtime string) bool {
for _, validRuntime := range l.GetRuntimes() {
if runtime == validRuntime {
return true
}
}
return false
}
func (l *Langruntimes) getAvailableRuntimesPerTrigger(imageType string) []string {
var runtimeList []string
for i := range l.AvailableRuntimes {
for j := range l.AvailableRuntimes[i].Versions {
if l.findImage(PhaseRuntime, l.AvailableRuntimes[i].Versions[j]) != nil {
runtimeList = append(runtimeList, l.AvailableRuntimes[i].ID+l.AvailableRuntimes[i].Versions[j].Version)
}
}
}
return runtimeList
}
// extract the branch number from the runtime string
func (l *Langruntimes) getVersionFromRuntime(runtime string) string {
re := regexp.MustCompile("[0-9.]+$")
return re.FindString(runtime)
}
// GetRuntimeInfo returns all the info regarding a runtime
func (l *Langruntimes) GetRuntimeInfo(runtime string) (RuntimeInfo, error) {
runtimeID := regexp.MustCompile("^[a-zA-Z_-]+").FindString(runtime)
for _, runtimeInf := range l.AvailableRuntimes {
if runtimeInf.ID == runtimeID {
return runtimeInf, nil
}
}
return RuntimeInfo{}, fmt.Errorf("Unable to find %s as runtime", runtime)
}
// GetLivenessProbeInfo returs the liveness probe info regarding a runtime
func (l *Langruntimes) GetLivenessProbeInfo(runtime string, port int) *v1.Probe {
livenessProbe := &v1.Probe{
InitialDelaySeconds: int32(3),
PeriodSeconds: int32(30),
Handler: v1.Handler{
HTTPGet: &v1.HTTPGetAction{
Path: "/healthz",
Port: intstr.FromInt(port),
},
},
}
runtimeID := regexp.MustCompile("^[a-zA-Z]+").FindString(runtime)
for _, runtimeInf := range l.AvailableRuntimes {
if runtimeInf.ID == runtimeID {
if runtimeInf.LivenessProbeInfo != nil {
return runtimeInf.LivenessProbeInfo
}
return livenessProbe
}
}
return livenessProbe
}
func (l *Langruntimes) findRuntimeVersion(runtimeWithVersion string) (RuntimeVersion, error) {
version := l.getVersionFromRuntime(runtimeWithVersion)
runtimeInf, err := l.GetRuntimeInfo(runtimeWithVersion)
if err != nil {
return RuntimeVersion{}, err
}
for _, versionInf := range runtimeInf.Versions {
if versionInf.Version == version {
return versionInf, nil
}
}
return RuntimeVersion{}, fmt.Errorf("The given runtime and version %s is not valid", runtimeWithVersion)
}
// Returns the image information of a phase or null if the phase is not found
func (l *Langruntimes) findImage(phase string, runtime RuntimeVersion) *Image {
for _, imageInf := range runtime.Images {
if imageInf.Phase == phase {
return &imageInf
}
}
return nil
}
// GetFunctionImage returns the image ID depending on the runtime, its version and function type
func (l *Langruntimes) GetFunctionImage(runtime string) (string, error) {
runtimeInf, err := l.GetRuntimeInfo(runtime)
if err != nil {
return "", err
}
imageNameEnvVar := strings.ToUpper(runtimeInf.ID) + l.getVersionFromRuntime(runtime) + "_RUNTIME"
imageName := os.Getenv(imageNameEnvVar)
if imageName == "" {
versionInf, err := l.findRuntimeVersion(runtime)
if err != nil {
return "", err
}
runtimeImage := l.findImage(PhaseRuntime, versionInf)
if runtimeImage == nil {
err = fmt.Errorf("The given runtime and version '%s' does not have a valid image for HTTP based functions. Available runtimes are: %s", runtime, strings.Join(l.getAvailableRuntimesPerTrigger("HTTP")[:], ", "))
} else {
imageName = runtimeImage.Image
}
}
return imageName, nil
}
// GetImageSecrets gets the secrets to pull the runtime image
func (l *Langruntimes) GetImageSecrets(runtime string) ([]v1.LocalObjectReference, error) {
var secrets []string
runtimeInf, err := l.findRuntimeVersion(runtime)
if err != nil {
return []v1.LocalObjectReference{}, err
}
if len(runtimeInf.ImagePullSecrets) == 0 {
return []v1.LocalObjectReference{}, nil
}
for _, s := range runtimeInf.ImagePullSecrets {
secrets = append(secrets, s.ImageSecret)
}
var lors []v1.LocalObjectReference
if len(secrets) > 0 {
for _, s := range secrets {
lor := v1.LocalObjectReference{Name: s}
lors = append(lors, lor)
}
}
return lors, nil
}
// GetInitContainerSecrets gets the secrets of the init container with name
func (l *Langruntimes) GetInitContainerSecrets(runtime, name string) ([]v1.LocalObjectReference, error) {
runtimeInf, err := l.findRuntimeVersion(runtime)
if err != nil {
return nil, err
}
if len(runtimeInf.Images) == 0 {
return nil, nil
}
var secrets []Secret
phase := name2phase(name)
for _, i := range runtimeInf.Images {
if i.Phase == phase {
secrets = append(secrets, i.Secrets...)
break
}
}
var refs []v1.LocalObjectReference
for _, s := range secrets {
refs = append(refs, v1.LocalObjectReference{Name: s.Name})
}
return refs, nil
}
func appendToCommand(orig string, command ...string) string {
if len(orig) > 0 {
return fmt.Sprintf("%s && %s", orig, strings.Join(command, " && "))
}
return strings.Join(command, " && ")
}
func parseEnv(env map[string]string) []v1.EnvVar {
res := []v1.EnvVar{}
for key, value := range env {
res = append(res, v1.EnvVar{Name: key, Value: value})
}
return res
}
// GetBuildContainer returns a Container definition based on a runtime
func (l *Langruntimes) GetBuildContainer(runtime, depsChecksum string, env []v1.EnvVar, installVolume v1.VolumeMount, resources v1.ResourceRequirements) (v1.Container, error) {
runtimeInf, err := l.GetRuntimeInfo(runtime)
if err != nil {
return v1.Container{}, err
}
depsFile := path.Join(installVolume.MountPath, runtimeInf.DepName)
versionInf, err := l.findRuntimeVersion(runtime)
if err != nil {
return v1.Container{}, err
}
imageInf := l.findImage(PhaseInstallation, versionInf)
if imageInf == nil {
// The runtime doesn't have an installation hook
return v1.Container{}, nil
}
var command string
// Validate deps checksum
shaFile := "/tmp/deps.sha256"
// if checksum exist, check sum
if depsChecksum != "" {
command = appendToCommand(command,
fmt.Sprintf("echo '%s %s' > %s", depsChecksum, depsFile, shaFile),
fmt.Sprintf("sha256sum -c %s", shaFile),
imageInf.Command,
)
} else {
command = appendToCommand(command, imageInf.Command)
}
env = append(
env,
v1.EnvVar{Name: "KUBELESS_INSTALL_VOLUME", Value: installVolume.MountPath},
v1.EnvVar{Name: "KUBELESS_DEPS_FILE", Value: depsFile},
)
env = append(env, parseEnv(imageInf.Env)...)
return v1.Container{
Name: "install",
Image: imageInf.Image,
Command: []string{"sh", "-c"},
Args: []string{command},
VolumeMounts: []v1.VolumeMount{installVolume},
ImagePullPolicy: v1.PullIfNotPresent,
WorkingDir: installVolume.MountPath,
Env: env,
Resources: resources,
}, nil
}
// UpdateDeployment object in case of custom runtime
func (l *Langruntimes) UpdateDeployment(dpm *appsv1.Deployment, volPath, runtime string) {
versionInf, err := l.findRuntimeVersion(runtime)
if err != nil {
// Not found an image for the given runtime
return
}
dpm.Spec.Template.Spec.Containers[0].Env = append(
dpm.Spec.Template.Spec.Containers[0].Env,
v1.EnvVar{Name: "KUBELESS_INSTALL_VOLUME", Value: volPath},
)
imageInf := l.findImage(PhaseRuntime, versionInf)
if imageInf == nil {
// Not found an image for the given runtime
return
}
dpm.Spec.Template.Spec.Containers[0].Env = append(
dpm.Spec.Template.Spec.Containers[0].Env,
parseEnv(imageInf.Env)...,
)
}
// GetCompilationContainer returns a Container definition based on a runtime
func (l *Langruntimes) GetCompilationContainer(runtime, funcName string, env []v1.EnvVar, installVolume v1.VolumeMount, resources v1.ResourceRequirements) (*v1.Container, error) {
versionInf, err := l.findRuntimeVersion(runtime)
if err != nil {
return nil, err
}
imageInf := l.findImage(PhaseCompilation, versionInf)
if imageInf == nil {
// The runtime doesn't have a compilation hook
return nil, nil
}
env = append(
env,
v1.EnvVar{Name: "KUBELESS_INSTALL_VOLUME", Value: installVolume.MountPath},
v1.EnvVar{Name: "KUBELESS_FUNC_NAME", Value: funcName},
)
env = append(env, parseEnv(imageInf.Env)...)
return &v1.Container{
Name: "compile",
Image: imageInf.Image,
Command: []string{"sh", "-c"},
Args: []string{imageInf.Command},
Env: env,
VolumeMounts: []v1.VolumeMount{installVolume},
ImagePullPolicy: v1.PullIfNotPresent,
WorkingDir: installVolume.MountPath,
Resources: resources,
}, nil
}
// name2phase returns the phase of an init container
func name2phase(name string) string {
switch name {
case "compile":
return PhaseCompilation
case "install":
return PhaseInstallation
}
return name
}
================================================
FILE: pkg/langruntime/langruntime_test.go
================================================
package langruntime
import (
"os"
"reflect"
"regexp"
"strings"
"testing"
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/client-go/kubernetes/fake"
)
var clientset = fake.NewSimpleClientset()
func TestMain(m *testing.M) {
AddFakeConfig(clientset)
os.Exit(m.Run())
}
func check(clientset *fake.Clientset, lr *Langruntimes, runtime, fname string, values []string, t *testing.T) {
info, err := lr.GetRuntimeInfo(runtime)
if err != nil {
t.Fatal(err)
}
if info.DepName != values[0] {
t.Fatalf("Retrieving the image returned a wrong dependencies file. Received " + info.DepName + " while expecting " + values[0])
}
if fname+info.FileNameSuffix != values[1] {
t.Fatalf("Retrieving the image returned a wrong file name. Received " + fname + info.FileNameSuffix + " while expecting " + values[1])
}
}
func TestGetFunctionFileNames(t *testing.T) {
lr := SetupLangRuntime(clientset)
lr.ReadConfigMap()
expectedValues := []string{"requirements.txt", "test.py"}
check(clientset, lr, "python2.7", "test", expectedValues, t)
}
func TestGetFunctionImage(t *testing.T) {
lr := SetupLangRuntime(clientset)
lr.ReadConfigMap()
// Throws an error if the runtime doesn't exist
_, err := lr.GetFunctionImage("unexistent")
if err == nil {
t.Fatalf("Retrieving data for 'unexistent' should return an error")
}
// Throws an error if the runtime version doesn't exist
_, err = lr.GetFunctionImage("python10")
expectedErrMsg := regexp.MustCompile("The given runtime and version python10 is not valid")
if expectedErrMsg.FindString(err.Error()) == "" {
t.Fatalf("Retrieving data for 'python10' should return an error. Received: %s", err)
}
expectedImageName := "ruby-test-image"
os.Setenv("PYTHON2.7_RUNTIME", expectedImageName)
imageR, errR := lr.GetFunctionImage("python2.7")
if errR != nil {
t.Errorf("Retrieving the image returned err: %v", errR)
}
if imageR != expectedImageName {
t.Errorf("Expecting " + imageR + " to be set to " + expectedImageName)
}
os.Unsetenv("PYTHON2.7_RUNTIME")
}
func TestGetLivenessProbe(t *testing.T) {
lr := SetupLangRuntime(clientset)
lr.ReadConfigMap()
livenessProbe := lr.GetLivenessProbeInfo("python", 8080)
expectedLivenessProbe := &v1.Probe{
InitialDelaySeconds: int32(5),
PeriodSeconds: int32(10),
Handler: v1.Handler{
Exec: &v1.ExecAction{
Command: []string{"curl", "-f", "http://localhost:8080/healthz"},
},
},
}
if !reflect.DeepEqual(livenessProbe, expectedLivenessProbe) {
t.Fatalf("Expected livenessProbeInfo to be %v, but found %v", expectedLivenessProbe, livenessProbe)
}
}
func TestGetRuntimes(t *testing.T) {
lr := SetupLangRuntime(clientset)
lr.ReadConfigMap()
runtimes := strings.Join(lr.GetRuntimes(), ", ")
expectedRuntimes := "python2.7"
if runtimes != expectedRuntimes {
t.Errorf("Expected %s but got %s", expectedRuntimes, runtimes)
}
}
func TestGetBuildContainer(t *testing.T) {
lr := SetupLangRuntime(clientset)
lr.ReadConfigMap()
// It should throw an error if there is not an image available
_, err := lr.GetBuildContainer("notExists", "", []v1.EnvVar{}, v1.VolumeMount{}, v1.ResourceRequirements{})
if err == nil {
t.Error("Expected to throw an error")
}
// It should return the proper build image for python
vol1 := v1.VolumeMount{Name: "v1", MountPath: "/v1"}
resources := v1.ResourceRequirements{Limits: v1.ResourceList{v1.ResourceLimitsCPU: resource.MustParse("100m")}}
c, err := lr.GetBuildContainer("python2.7", "abc123", []v1.EnvVar{}, vol1, resources)
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
expectedContainer := v1.Container{
Name: "install",
Image: "python:2.7",
Command: []string{"sh", "-c"},
Args: []string{"echo 'abc123 /v1/requirements.txt' > /tmp/deps.sha256 && sha256sum -c /tmp/deps.sha256 && foo"},
VolumeMounts: []v1.VolumeMount{vol1},
WorkingDir: "/v1",
ImagePullPolicy: v1.PullIfNotPresent,
Env: []v1.EnvVar{
{Name: "KUBELESS_INSTALL_VOLUME", Value: "/v1"},
{Name: "KUBELESS_DEPS_FILE", Value: "/v1/requirements.txt"},
},
Resources: v1.ResourceRequirements{Limits: v1.ResourceList{v1.ResourceLimitsCPU: resource.MustParse("100m")}},
}
if !reflect.DeepEqual(expectedContainer, c) {
t.Errorf("Unexpected result. Expecting:\n %+v\nReceived:\n %+v", expectedContainer, c)
}
}
func TestGetBuildContainerWithBundledDeps(t *testing.T) {
lr := SetupLangRuntime(clientset)
lr.ReadConfigMap()
// It should return the proper build image for python
vol1 := v1.VolumeMount{Name: "v1", MountPath: "/v1"}
resources := v1.ResourceRequirements{Limits: v1.ResourceList{v1.ResourceLimitsCPU: resource.MustParse("100m")}}
c, err := lr.GetBuildContainer("python2.7", "", []v1.EnvVar{}, vol1, resources)
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
expectedContainer := v1.Container{
Name: "install",
Image: "python:2.7",
Command: []string{"sh", "-c"},
Args: []string{"foo"},
VolumeMounts: []v1.VolumeMount{vol1},
WorkingDir: "/v1",
ImagePullPolicy: v1.PullIfNotPresent,
Env: []v1.EnvVar{
{Name: "KUBELESS_INSTALL_VOLUME", Value: "/v1"},
{Name: "KUBELESS_DEPS_FILE", Value: "/v1/requirements.txt"},
},
Resources: v1.ResourceRequirements{Limits: v1.ResourceList{v1.ResourceLimitsCPU: resource.MustParse("100m")}},
}
if !reflect.DeepEqual(expectedContainer, c) {
t.Errorf("Unexpected result. Expecting:\n %+v\nReceived:\n %+v", expectedContainer, c)
}
}
================================================
FILE: pkg/langruntime/langruntimetestutils.go
================================================
package langruntime
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/sirupsen/logrus"
"k8s.io/api/core/v1"
"k8s.io/client-go/kubernetes/fake"
)
// AddFakeConfig initializes configmap for unit tests with fake configuration.
func AddFakeConfig(clientset *fake.Clientset) {
runtimeImages := `[
{
"ID": "python",
"compiled": false,
"depName": "requirements.txt",
"fileNameSuffix": ".py",
"livenessProbeInfo": {
"exec": {
"command": ["curl", "-f", "http://localhost:8080/healthz"]
},
"initialDelaySeconds": 5,
"periodseconds": 10
},
"versions": [
{
"images": [
{
"command": "foo",
"image": "python:2.7",
"phase": "installation",
"secrets": [{"name": "my-secret"}]
},
{
"image": "bar",
"phase": "runtime",
"env": {"PYTHONPATH": "/kubeless/lib/python2.7/site-packages:/kubeless"}
}
],
"name": "python27",
"version": "2.7",
"imagePullSecrets": [{"ImageSecret": "p1"}, {"ImageSecret": "p2"}]
}
]
}
]`
cm := v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "kubeless-config",
Namespace: "kubeless",
},
Data: map[string]string{
"runtime-images": runtimeImages,
},
}
_, err := clientset.CoreV1().ConfigMaps("kubeless").Create(&cm)
if err != nil {
logrus.Fatal("Unable to create configmap")
}
}
// SetupLangRuntime Sets up Langruntime struct
func SetupLangRuntime(clientset *fake.Clientset) *Langruntimes {
config, err := clientset.CoreV1().ConfigMaps("kubeless").Get("kubeless-config", metav1.GetOptions{})
if err != nil {
logrus.Fatal("Unable to read the configmap")
}
var lr = New(config)
return lr
}
================================================
FILE: pkg/registry/registry.go
================================================
/*
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.
*/
package registry
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"reflect"
"regexp"
"time"
"k8s.io/api/core/v1"
)
// Credentials represent the required credentials to authenticate against a Docker registry
type Credentials struct {
Username string `json:"username"`
Password string `json:"password"`
Email string `json:"email,omitifempty"`
Auth string `json:"auth,omitifempty"`
}
// Registry struct represents a Docker Registry
type Registry struct {
Endpoint string
Version string
Creds Credentials
}
type tagv1 struct {
Layer string `json:"layer"`
Name string `json:"name"`
}
type tagListV2 struct {
Name string `json:"name"`
Tags []string `json:"tags"`
}
type dockerCfg struct {
Auths map[string]Credentials `json:"auths"`
}
// New returns a Registry struct parsing its URL and storing the required credentials
func New(config v1.Secret) (*Registry, error) {
// Parse secret
cfg := dockerCfg{}
err := json.Unmarshal(config.Data[".dockerconfigjson"], &cfg)
if err != nil {
return nil, err
}
regs := reflect.ValueOf(cfg.Auths).MapKeys()
if len(regs) > 1 {
return nil, fmt.Errorf("Found several registries: %q, unable to decide which one to use", regs)
}
registryURL := regs[0].String()
re := regexp.MustCompile("(https?://.*)/(v[0-9]+)/?")
parsedURL := re.FindStringSubmatch(registryURL)
if len(parsedURL) == 0 {
return nil, fmt.Errorf("Unable to parse registry URL %s", registryURL)
}
reg := Registry{
Endpoint: parsedURL[1],
Version: parsedURL[2],
Creds: cfg.Auths[registryURL],
}
return ®, err
}
// getTags return the list of tags from an HTTP response to the tag/list API endpoint
func (r *Registry) getTags(body []byte) ([]string, error) {
switch r.Version {
case "v1":
response := []tagv1{}
err := json.Unmarshal(body, &response)
if err != nil {
return nil, err
}
tags := []string{}
for _, tag := range response {
tags = append(tags, tag.Name)
}
return tags, nil
case "v2":
response := tagListV2{}
err := json.Unmarshal(body, &response)
if err != nil {
return nil, err
}
return response.Tags, nil
default:
return nil, fmt.Errorf("API version %s not supported", r.Version)
}
}
// tagURL return the URL of the endpoint for listing existing tags
func (r *Registry) tagURL(img string) (string, error) {
switch r.Version {
case "v1":
return fmt.Sprintf("%s/%s/repositories/%s/tags", r.Endpoint, r.Version, img), nil
case "v2":
return fmt.Sprintf("%s/%s/%s/tags/list", r.Endpoint, r.Version, img), nil
default:
return "", fmt.Errorf("API version %s not supported", r.Version)
}
}
// findProperty returns the value of a property from a list witht the format 'foo="bar",bar="foo"'
func findProperty(src, property string) (string, error) {
re := regexp.MustCompile(fmt.Sprintf("%s=\"([^\"]*)\"", property))
res := re.FindStringSubmatch(src)
if len(res) != 2 {
return "", fmt.Errorf("Unable to find the property %s in %s", property, src)
}
return res[1], nil
}
type authResponse struct {
Token string `json:"token"`
}
// doRequestWithAuth does an HTTP GET agains the given url parsing the authInfo given
func doRequestWithAuth(authInfo, url string, client *http.Client) ([]byte, error) {
bearer, err := findProperty(authInfo, "Bearer realm")
if err != nil {
return nil, fmt.Errorf("Unable to extract auth info: %v", err)
}
service, err := findProperty(authInfo, "service")
if err != nil {
return nil, fmt.Errorf("Unable to extract auth info: %v", err)
}
scope, err := findProperty(authInfo, "scope")
if err != nil {
return nil, fmt.Errorf("Unable to extract auth info: %v", err)
}
authResp, err := client.Get(fmt.Sprintf("%s?service=%s&scope=%s", bearer, service, scope))
if err != nil {
return nil, fmt.Errorf("Unable to obtain auth token: %v", err)
}
defer authResp.Body.Close()
authb, err := ioutil.ReadAll(authResp.Body)
if err != nil {
return nil, err
}
authr := authResponse{}
err = json.Unmarshal(authb, &authr)
if err != nil {
return nil, fmt.Errorf("Unable to parse auth token: %v", err)
}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", authr.Token))
respWithAuth, err := client.Do(req)
if err != nil {
return nil, err
}
defer respWithAuth.Body.Close()
body, err := ioutil.ReadAll(respWithAuth.Body)
if err != nil {
return nil, err
}
return body, nil
}
func (r *Registry) doRequest(url string) ([]byte, error) {
tr := &http.Transport{
MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second,
DisableCompression: true,
}
client := &http.Client{
Transport: tr,
}
resp, err := client.Get(url)
if err != nil {
return nil, err
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
// Handle auth if needed
if resp.StatusCode == 401 {
// Get auth info from headers
authInfo := resp.Header.Get("Www-Authenticate")
if authInfo == "" {
return nil, fmt.Errorf("Failed to authenticate: unknown authentication format: %v", body)
}
body, err = doRequestWithAuth(authInfo, url, client)
if err != nil {
return nil, err
}
}
return body, nil
}
// ImageExists checks if a certain image:tag exists in the registry
func (r *Registry) ImageExists(id, tag string) (bool, error) {
url, err := r.tagURL(id)
if err != nil {
return false, err
}
body, err := r.doRequest(url)
if err != nil {
return false, err
}
if match, _ := regexp.MatchString("Resource not found", string(body)); match {
// There is no image with that ID yet
return false, nil
}
tags, err := r.getTags(body)
if err != nil {
return false, err
}
for _, t := range tags {
if t == tag {
return true, nil
}
}
return false, nil
}
================================================
FILE: pkg/registry/registry_test.go
================================================
package registry
import (
"reflect"
"testing"
"k8s.io/api/core/v1"
)
func TestNew(t *testing.T) {
s := v1.Secret{
Data: map[string][]byte{
".dockerconfigjson": []byte("{\"auths\":{\"https://index.docker.io/v1/\":{\"username\":\"test\",\"password\":\"pass\"}}}"),
},
}
r, err := New(s)
if err != nil {
t.Error(err)
}
if r.Endpoint != "https://index.docker.io" {
t.Errorf("Unexpected endpoint %s, expecting https://index.docker.io", r.Endpoint)
}
if r.Version != "v1" {
t.Errorf("Unexpected version %s, expecting v1", r.Version)
}
if r.Creds.Username != "test" {
t.Errorf("Unexpected username %s, expecting test", r.Creds.Username)
}
if r.Creds.Password != "pass" {
t.Errorf("Unexpected password %s, expecting pass", r.Creds.Password)
}
}
func TestTagURLV1(t *testing.T) {
r := Registry{
Endpoint: "https://registry-1.docker.io",
Version: "v1",
}
url, err := r.tagURL("test/image")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if url != "https://registry-1.docker.io/v1/repositories/test/image/tags" {
t.Errorf("Unexpected URL %s", url)
}
}
func TestTagURLV2(t *testing.T) {
r := Registry{
Endpoint: "https://registry-1.docker.io",
Version: "v2",
}
url, err := r.tagURL("test/image")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if url != "https://registry-1.docker.io/v2/test/image/tags/list" {
t.Errorf("Unexpected URL %s", url)
}
}
func TestGetTagsV1(t *testing.T) {
r := Registry{
Endpoint: "https://registry-1.docker.io",
Version: "v1",
}
body := []byte("[{\"later\": \"\", \"name\": \"latest\"}]")
tags, err := r.getTags(body)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
expectedTags := []string{"latest"}
if !reflect.DeepEqual(tags, expectedTags) {
t.Errorf("Unexpected tags: %v", tags)
}
}
func TestGetTagsV2(t *testing.T) {
r := Registry{
Endpoint: "https://registry-1.docker.io",
Version: "v2",
}
body := []byte("{\"name\": \"test\", \"tags\":[\"latest\"]}")
tags, err := r.getTags(body)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
expectedTags := []string{"latest"}
if !reflect.DeepEqual(tags, expectedTags) {
t.Errorf("Unexpected tags: %v", tags)
}
}
================================================
FILE: pkg/utils/configlocation.go
================================================
package utils
// ConfigLocation is a struct to store the location of kubeless configuration specific ConfigMap
type ConfigLocation struct {
Name string
Namespace string
}
================================================
FILE: pkg/utils/exec.go
================================================
package utils
import (
"crypto/tls"
"fmt"
"io"
"net/http"
"sync"
"github.com/sirupsen/logrus"
"golang.org/x/net/websocket"
"k8s.io/api/core/v1"
"k8s.io/client-go/kubernetes/scheme"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/rest"
)
const (
stdinChannel = 0
stdoutChannel = 1
stderrChannel = 2
errChannel = 3
)
// Cmd stores information relevant to an individual remote command being run
type Cmd struct {
Stdin io.Reader
Stdout io.Writer
Stderr io.Writer
}
// RoundTripCallback is suitable to use with `ExecRoundTripper` and will
// copy data to/from stdio channels. The returned `Response` is
// currently always `nil`.
func (c *Cmd) RoundTripCallback(conn *websocket.Conn) (*http.Response, error) {
errChan := make(chan error, 3)
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
defer wg.Done()
if c.Stdin == nil {
return
}
buf := make([]byte, 1025) // NB: first byte is fixed
buf[0] = stdinChannel
for {
n, err := c.Stdin.Read(buf[1:])
err2 := websocket.Message.Send(conn, buf[:n+1])
if err == nil && err2 != nil {
err = err2
}
if err == io.EOF {
break
} else if err != nil {
errChan <- err
return
}
}
const closeStatusNormal = 1000
conn.WriteClose(closeStatusNormal)
}()
go func() {
defer wg.Done()
for {
var buf []byte
err := websocket.Message.Receive(conn, &buf)
if err == io.EOF {
break
} else if err != nil {
errChan <- err
return
}
if len(buf) == 0 {
logrus.Debug("Received empty message, skipping")
continue
}
logrus.Debugf("Received %dB message for channel %d", len(buf)-1, buf[0])
var w io.Writer
switch buf[0] {
case stdoutChannel:
w = c.Stdout
case stderrChannel:
w = c.Stderr
case errChannel:
errChan <- fmt.Errorf("Error from remote command: %s", buf[1:])
return
default:
logrus.Infof("Ignoring message for unknown channel %d", buf[0])
continue
}
if w == nil {
logrus.Infof("Ignoring message for nil channel %d", buf[0])
continue
}
_, err = w.Write(buf[1:])
if err != nil {
errChan <- err
return
}
}
}()
wg.Wait()
close(errChan)
err := <-errChan
return &http.Response{
Status: "OK",
StatusCode: 200,
}, err
}
// A RoundTripCallback is used to process the websocket from an
// individual command execution.
type RoundTripCallback func(conn *websocket.Conn) (*http.Response, error)
// WebsocketRoundTripper is an http.RoundTripper that invokes a
// callback on a websocket connection.
type WebsocketRoundTripper struct {
TLSConfig *tls.Config
Do RoundTripCallback
}
// RoundTrip implements the http.RoundTripper interface.
func (d *WebsocketRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
referrer := r.Referer()
if referrer == "" {
referrer = "http://localhost/"
}
wsconf, err := websocket.NewConfig(r.URL.String(), referrer)
if err != nil {
return nil, err
}
wsconf.TlsConfig = d.TLSConfig
wsconf.Header = r.Header
wsconf.Protocol = []string{"channel.k8s.io"}
conn, err := websocket.DialConfig(wsconf)
if err != nil {
return nil, err
}
conn.PayloadType = websocket.BinaryFrame
defer conn.Close()
return d.Do(conn)
}
// ExecRoundTripper creates a wrapped WebsocketRoundTripper
func ExecRoundTripper(conf *rest.Config, f RoundTripCallback) (http.RoundTripper, error) {
tlsConfig, err := rest.TLSConfigFor(conf)
if err != nil {
return nil, err
}
rt := &WebsocketRoundTripper{
Do: f,
TLSConfig: tlsConfig,
}
return rest.HTTPWrappersForConfig(conf, rt)
}
// Exec returns an "exec" Request suitable for ExecRoundTripper.
func Exec(client corev1.CoreV1Interface, pod, namespace string, opts v1.PodExecOptions) (*http.Request, error) {
cl := client.RESTClient()
req := cl.Verb("ignored").
Namespace(namespace).
Resource("pods").
Name(pod).
SubResource("exec").
VersionedParams(&opts, scheme.ParameterCodec)
url := req.URL()
switch url.Scheme {
case "http":
url.Scheme = "ws"
case "https":
url.Scheme = "wss"
default:
return nil, fmt.Errorf("Unrecognised URL scheme in %v", url)
}
// NB: Only some fields are honoured by our RoundTrip implementation
return &http.Request{
URL: url,
}, nil
}
================================================
FILE: pkg/utils/exec_test.go
================================================
package utils
import (
"testing"
"k8s.io/api/core/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)
func TestExecURL(t *testing.T) {
conf := rest.Config{
Host: "https://example.com/",
}
clientset := kubernetes.NewForConfigOrDie(&conf)
opts := v1.PodExecOptions{
Container: "ctr",
Stderr: true,
Command: []string{"a", "b"},
}
req, err := Exec(clientset.Core(), "mypod", "myns", opts)
if err != nil {
t.Fatal("Exec error:", err)
}
t.Logf("Got URL %v", req.URL)
if req.URL.String() != "wss://example.com/api/v1/namespaces/myns/pods/mypod/exec?command=a&command=b&container=ctr&stderr=true" {
t.Error("Unexpected url:", req.URL)
}
}
================================================
FILE: pkg/utils/k8sutil.go
================================================
/*
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.
*/
package utils
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"net/url"
"os"
"path/filepath"
kubelessApi "github.com/kubeless/kubeless/pkg/apis/kubeless/v1beta1"
"github.com/sirupsen/logrus"
appsv1 "k8s.io/api/apps/v1"
"k8s.io/api/autoscaling/v2beta1"
v1 "k8s.io/api/core/v1"
clientsetAPIExtensions "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
monitoringv1alpha1 "github.com/coreos/prometheus-operator/pkg/client/monitoring/v1alpha1"
// Auth plugins
_ "k8s.io/client-go/plugin/pkg/client/auth"
"github.com/imdario/mergo"
"github.com/kubeless/kubeless/pkg/client/clientset/versioned"
)
const (
defaultTimeout = "180"
)
// GetClient returns a k8s clientset to the request from inside of cluster
func GetClient() kubernetes.Interface {
config, err := GetInClusterConfig()
if err != nil {
logrus.Fatalf("Can not get kubernetes config: %v", err)
}
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
logrus.Fatalf("Can not create kubernetes client: %v", err)
}
return clientset
}
// BuildOutOfClusterConfig returns k8s config
func BuildOutOfClusterConfig() (*rest.Config, error) {
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
kubeconfigEnv := os.Getenv("KUBECONFIG")
if kubeconfigEnv == "" {
home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
if home == "" {
for _, h := range []string{"HOME", "USERPROFILE"} {
if home = os.Getenv(h); home != "" {
break
}
}
}
kubeconfigPath := filepath.Join(home, ".kube", "config")
loadingRules.ExplicitPath = kubeconfigPath
}
config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
loadingRules, &clientcmd.ConfigOverrides{}).ClientConfig()
if err != nil {
return nil, err
}
return config, nil
}
// GetClientOutOfCluster returns a k8s clientset to the request from outside of cluster
func GetClientOutOfCluster() kubernetes.Interface {
config, err := BuildOutOfClusterConfig()
if err != nil {
logrus.Fatalf("Can not get kubernetes config: %v", err)
}
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
logrus.Fatalf("Can not get kubernetes client: %v", err)
}
return clientset
}
// GetAPIExtensionsClientOutOfCluster returns a k8s clientset to access APIExtensions from outside of cluster
func GetAPIExtensionsClientOutOfCluster() clientsetAPIExtensions.Interface {
config, err := BuildOutOfClusterConfig()
if err != nil {
logrus.Fatalf("Can not get kubernetes config: %v", err)
}
clientset, err := clientsetAPIExtensions.NewForConfig(config)
if err != nil {
logrus.Fatalf("Can not get kubernetes client: %v", err)
}
return clientset
}
// GetAPIExtensionsClientInCluster returns a k8s clientset to access APIExtensions from inside of cluster
func GetAPIExtensionsClientInCluster() clientsetAPIExtensions.Interface {
config, err := GetInClusterConfig()
if err != nil {
logrus.Fatalf("Can not get kubernetes config: %v", err)
}
clientset, err := clientsetAPIExtensions.NewForConfig(config)
if err != nil {
logrus.Fatalf("Can not get kubernetes client: %v", err)
}
return clientset
}
// GetFunctionClientInCluster returns function clientset to the request from inside of cluster
func GetFunctionClientInCluster() (versioned.Interface, error) {
config, err := GetInClusterConfig()
if err != nil {
return nil, err
}
kubelessClient, err := versioned.NewForConfig(config)
if err != nil {
return nil, err
}
return kubelessClient, nil
}
// GetKubelessClientOutCluster returns kubeless clientset to make kubeless API request from outside of cluster
func GetKubelessClientOutCluster() (versioned.Interface, error) {
config, err := BuildOutOfClusterConfig()
if err != nil {
return nil, err
}
kubelessClient, err := versioned.NewForConfig(config)
if err != nil {
return nil, err
}
return kubelessClient, nil
}
// GetDefaultNamespace returns the namespace set in current cluster context
func GetDefaultNamespace() string {
rules := clientcmd.NewDefaultClientConfigLoadingRules()
rules.DefaultClientConfig = &clientcmd.DefaultClientConfig
overrides := &clientcmd.ConfigOverrides{ClusterDefaults: clientcmd.ClusterDefaults}
if ns, _, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, overrides).Namespace(); err == nil {
return ns
}
return v1.NamespaceDefault
}
// GetFunction returns specification of a function
func GetFunction(funcName, ns string) (kubelessApi.Function, error) {
kubelessClient, err := GetKubelessClientOutCluster()
if err != nil {
return kubelessApi.Function{}, err
}
f, err := kubelessClient.KubelessV1beta1().Functions(ns).Get(funcName, metav1.GetOptions{})
if err != nil {
if k8sErrors.IsNotFound(err) {
logrus.Fatalf("Function %s is not found", funcName)
}
return kubelessApi.Function{}, err
}
return *f, nil
}
// CreateFunctionCustomResource will create a custom function object
func CreateFunctionCustomResource(kubelessClient versioned.Interface, f *kubelessApi.Function) error {
_, err := kubelessClient.KubelessV1beta1().Functions(f.Namespace).Create(f)
if err != nil {
return err
}
return nil
}
// UpdateFunctionCustomResource applies changes to the function custom object
func UpdateFunctionCustomResource(kubelessClient versioned.Interface, f *kubelessApi.Function) error {
_, err := kubelessClient.KubelessV1beta1().Functions(f.Namespace).Update(f)
return err
}
// PatchFunctionCustomResource applies changes to the function custom object
func PatchFunctionCustomResource(kubelessClient versioned.Interface, f *kubelessApi.Function) error {
data, err := json.Marshal(f)
if err != nil {
return err
}
_, err = kubelessClient.KubelessV1beta1().Functions(f.Namespace).Patch(f.Name, types.MergePatchType, data)
return err
}
// DeleteFunctionCustomResource will delete custom function object
func DeleteFunctionCustomResource(kubelessClient versioned.Interface, funcName, ns string) error {
err := kubelessClient.KubelessV1beta1().Functions(ns).Delete(funcName, &metav1.DeleteOptions{})
if err != nil {
return err
}
return nil
}
// GetFunctionCustomResource will delete custom function object
func GetFunctionCustomResource(kubelessClient versioned.Interface, funcName, ns string) (*kubelessApi.Function, error) {
functionObj, err := kubelessClient.KubelessV1beta1().Functions(ns).Get(funcName, metav1.GetOptions{})
if err != nil {
return nil, err
}
return functionObj, nil
}
// GetPodsByLabel returns list of pods which match the label
// We use this to returns pods to which the function is deployed or pods running controllers
func GetPodsByLabel(c kubernetes.Interface, ns, k, v string) (*v1.PodList, error) {
pods, err := c.Core().Pods(ns).List(metav1.ListOptions{
LabelSelector: k + "=" + v,
})
if err != nil {
return nil, err
}
return pods, nil
}
// GetReadyPod returns the first pod has passed the liveness probe check
func GetReadyPod(pods *v1.PodList) (v1.Pod, error) {
for _, pod := range pods.Items {
isPodRunning := true
for _, containerStatus := range pod.Status.ContainerStatuses {
if !containerStatus.Ready {
isPodRunning = false
break
}
}
if isPodRunning {
return pod, nil
}
}
return v1.Pod{}, fmt.Errorf("there is no pod ready")
}
// GetLocalHostname returns hostname
func GetLocalHostname(config *rest.Config, funcName string) (string, error) {
url, err := url.Parse(config.Host)
if err != nil {
return "", err
}
host := url.Hostname()
return fmt.Sprintf("%s.%s.nip.io", funcName, host), nil
}
func doRESTReq(restIface rest.Interface, groupVersion, verb, resource, elem, namespace string, body interface{}, result interface{}) error {
var req *rest.Request
bodyJSON := []byte{}
var err error
if body != nil {
bodyJSON, err = json.Marshal(body)
if err != nil {
return err
}
}
switch verb {
case "get":
req = restIface.Get().Name(elem)
break
case "create":
req = restIface.Post().Body(bodyJSON)
break
case "update":
req = restIface.Put().Name(elem).Body(bodyJSON)
break
default:
return fmt.Errorf("Verb %s not supported", verb)
}
rawResponse, err := req.AbsPath("apis", groupVersion, "namespaces", namespace, resource).DoRaw()
if err != nil {
return err
}
if result != nil {
err = json.Unmarshal(rawResponse, result)
if err != nil {
return err
}
}
return nil
}
// CreateAutoscale creates HPA object for function
func CreateAutoscale(client kubernetes.Interface, hpa v2beta1.HorizontalPodAutoscaler) error {
_, err := client.AutoscalingV2beta1().HorizontalPodAutoscalers(hpa.ObjectMeta.Namespace).Create(&hpa)
return err
}
// UpdateAutoscale updates an existing HPA object for a function
func UpdateAutoscale(client kubernetes.Interface, hpa v2beta1.HorizontalPodAutoscaler) error {
_, err := client.AutoscalingV2beta1().HorizontalPodAutoscalers(hpa.ObjectMeta.Namespace).Update(&hpa)
return err
}
// DeleteAutoscale deletes an autoscale rule
func DeleteAutoscale(client kubernetes.Interface, name, ns string) error {
err := client.AutoscalingV2beta1().HorizontalPodAutoscalers(ns).Delete(name, &metav1.DeleteOptions{})
if err != nil && !k8sErrors.IsNotFound(err) {
return err
}
return nil
}
// DeleteServiceMonitor cleans the sm if it exists
func DeleteServiceMonitor(smclient monitoringv1alpha1.MonitoringV1alpha1Client, name, ns string) error {
err := smclient.ServiceMonitors(ns).Delete(name, &metav1.DeleteOptions{})
if err != nil && !k8sErrors.IsNotFound(err) {
return err
}
return nil
}
// InitializeEmptyMapsInDeployment initializes all nil maps in a Deployment object
// This is done to counteract with side-effects of github.com/imdario/mergo which panics when provided with a nil map in a struct
func initializeEmptyMapsInDeployment(deployment *appsv1.Deployment) {
if deployment.ObjectMeta.Annotations == nil {
deployment.Annotations = make(map[string]string)
}
if deployment.ObjectMeta.Labels == nil {
deployment.ObjectMeta.Labels = make(map[string]string)
}
if deployment.Spec.Selector != nil && deployment.Spec.Selector.MatchLabels == nil {
deployment.ObjectMeta.Labels = make(map[string]string)
}
if deployment.Spec.Template.ObjectMeta.Annotations == nil {
deployment.Spec.Template.ObjectMeta.Annotations = make(map[string]string)
}
if deployment.Spec.Template.ObjectMeta.Labels == nil {
deployment.Spec.Template.ObjectMeta.Labels = make(map[string]string)
}
if deployment.Spec.Template.Spec.NodeSelector == nil {
deployment.Spec.Template.Spec.NodeSelector = make(map[string]string)
}
}
// MergeDeployments merges two deployment objects
func MergeDeployments(destinationDeployment *appsv1.Deployment, sourceDeployment *appsv1.Deployment) error {
// Initializing nil maps in deployment objects else github.com/imdario/mergo panics
initializeEmptyMapsInDeployment(destinationDeployment)
initializeEmptyMapsInDeployment(sourceDeployment)
err := mergo.Merge(destinationDeployment, sourceDeployment)
// Merge containers
if err == nil && len(sourceDeployment.Spec.Template.Spec.Containers) > 0 {
srcContainers := sourceDeployment.Spec.Template.Spec.Containers
dstContainers := destinationDeployment.Spec.Template.Spec.Containers
// Merge each container individually
for i, srcContainer := range srcContainers {
if i >= len(dstContainers) {
destinationDeployment.Spec.Template.Spec.Containers[i] = srcContainer
continue
}
dstContainer := dstContainers[i]
// Use mergo.WithAppendSlice to append extra volumeMount/env/port definitions
err = mergo.Merge(&dstContainer, srcContainer, mergo.WithAppendSlice)
if err != nil {
break
}
destinationDeployment.Spec.Template.Spec.Containers[i] = dstContainer
}
}
return err
}
// FunctionObjAddFinalizer add specified finalizer string to function object
func FunctionObjAddFinalizer(kubelessClient versioned.Interface, funcObj *kubelessApi.Function, finalizerString string) error {
funcObjClone := funcObj.DeepCopy()
funcObjClone.ObjectMeta.Finalizers = append(funcObjClone.ObjectMeta.Finalizers, finalizerString)
return UpdateFunctionCustomResource(kubelessClient, funcObjClone)
}
// FunctionObjHasFinalizer checks if function object already has the Function controller finalizer
func FunctionObjHasFinalizer(funcObj *kubelessApi.Function, finalizerString string) bool {
currentFinalizers := funcObj.ObjectMeta.Finalizers
for _, f := range currentFinalizers {
if f == finalizerString {
return true
}
}
return false
}
// FunctionObjRemoveFinalizer removes the finalizer from the function object
func FunctionObjRemoveFinalizer(kubelessClient versioned.Interface, funcObj *kubelessApi.Function, finalizerString string) error {
funcObjClone := funcObj.DeepCopy()
newSlice := make([]string, 0)
for _, item := range funcObj.ObjectMeta.Finalizers {
if item == finalizerString {
continue
}
newSlice = append(newSlice, item)
}
if len(newSlice) == 0 {
newSlice = nil
}
funcObjClone.ObjectMeta.Finalizers = newSlice
err := UpdateFunctionCustomResource(kubelessClient, funcObjClone)
return err
}
// GetAnnotationsFromCRD gets annotations from a CustomResourceDefinition
func GetAnnotationsFromCRD(clientset clientsetAPIExtensions.Interface, name string) (map[string]string, error) {
crd, err := clientset.ApiextensionsV1beta1().CustomResourceDefinitions().Get(name, metav1.GetOptions{})
if err != nil {
return nil, err
}
return crd.GetAnnotations(), nil
}
// GetRandString returns a random string of lenght N
func GetRandString(n int) (string, error) {
b := make([]byte, n)
if _, err := rand.Read(b); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(b), nil
}
// GetSecretsAsLocalObjectReference returns a list of LocalObjectReference based on secret names
func GetSecretsAsLocalObjectReference(secrets ...string) []v1.LocalObjectReference {
res := []v1.LocalObjectReference{}
for _, secret := range secrets {
if secret != "" {
res = append(res, v1.LocalObjectReference{Name: secret})
}
}
return res
}
================================================
FILE: pkg/utils/k8sutil_test.go
================================================
package utils
import (
"bytes"
"encoding/json"
"io"
"io/ioutil"
"testing"
appsv1 "k8s.io/api/apps/v1"
v2beta1 "k8s.io/api/autoscaling/v2beta1"
corev1 "k8s.io/api/core/v1"
extensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
fakeextensionsapi "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake"
resource "k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/kubernetes/fake"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
ktesting "k8s.io/client-go/testing"
)
func objBody(object interface{}) io.ReadCloser {
output, err := json.Marshal(object)
if err != nil {
panic(err)
}
return ioutil.NopCloser(bytes.NewReader([]byte(output)))
}
func fakeConfig() *rest.Config {
return &rest.Config{
Host: "https://example.com:443",
ContentConfig: rest.ContentConfig{
GroupVersion: &schema.GroupVersion{
Group: "",
Version: "v1",
},
NegotiatedSerializer: scheme.Codecs,
},
}
}
func TestGetLocalHostname(t *testing.T) {
config := fakeConfig()
expectedHostName := "foobar.example.com.nip.io"
actualHostName, err := GetLocalHostname(config, "foobar")
if err != nil {
t.Error(err)
}
if expectedHostName != actualHostName {
t.Errorf("Expected %s but got %s", expectedHostName, actualHostName)
}
}
func TestCreateAutoscaleResource(t *testing.T) {
clientset := fake.NewSimpleClientset()
name := "foo"
ns := "myns"
hpaDef := v2beta1.HorizontalPodAutoscaler{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: ns,
},
}
if err := CreateAutoscale(clientset, hpaDef); err != nil {
t.Fatalf("Creating autoscale returned err: %v", err)
}
hpa, err := clientset.AutoscalingV2beta1().HorizontalPodAutoscalers(ns).Get(name, metav1.GetOptions{})
if err != nil {
t.Fatalf("Creating autoscale returned err: %v", err)
}
if hpa.ObjectMeta.Name != "foo" {
t.Fatalf("Creating wrong scale target name")
}
}
func TestUpdateAutoscaleResource(t *testing.T) {
clientset := fake.NewSimpleClientset()
name := "foo"
ns := "myns"
// Create a pre-existing HPA
hpaDef := v2beta1.HorizontalPodAutoscaler{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: ns,
},
}
if err := CreateAutoscale(clientset, hpaDef); err != nil {
t.Fatalf("Creating autoscale returned err: %v", err)
}
// Perform an update
hpaDef = v2beta1.HorizontalPodAutoscaler{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: ns,
Labels: map[string]string{
"baz": "qux",
},
},
}
if err := UpdateAutoscale(clientset, hpaDef); err != nil {
t.Fatalf("Updating autoscale returned err: %v", err)
}
hpa, err := clientset.AutoscalingV2beta1().HorizontalPodAutoscalers(ns).Get(name, metav1.GetOptions{})
if err != nil {
t.Fatalf("Updating autoscale returned err: %v", err)
}
if hpa.ObjectMeta.Name != "foo" {
t.Fatalf("Updating wrong scale target name")
}
}
func TestDeleteAutoscaleResource(t *testing.T) {
myNsFoo := metav1.ObjectMeta{
Namespace: "myns",
Name: "foo",
}
as := v2beta1.HorizontalPodAutoscaler{
ObjectMeta: myNsFoo,
}
clientset := fake.NewSimpleClientset(&as)
if err := DeleteAutoscale(clientset, "foo", "myns"); err != nil {
t.Fatalf("Deleting autoscale returned err: %v", err)
}
a := clientset.Actions()
if ns := a[0].GetNamespace(); ns != "myns" {
t.Errorf("deleted autoscale from wrong namespace (%s)", ns)
}
if name := a[0].(ktesting.DeleteAction).GetName(); name != "foo" {
t.Errorf("deleted autoscale with wrong name (%s)", name)
}
}
func TestInitializeEmptyMapsInDeployment(t *testing.T) {
deployment := appsv1.Deployment{}
deployment.Spec.Selector = &metav1.LabelSelector{}
initializeEmptyMapsInDeployment(&deployment)
if deployment.ObjectMeta.Annotations == nil {
t.Fatal("ObjectMeta.Annotations map is nil")
}
if deployment.ObjectMeta.Labels == nil {
t.Fatal("ObjectMeta.Labels map is nil")
}
if deployment.Spec.Selector == nil && deployment.Spec.Selector.MatchLabels == nil {
t.Fatal("deployment.Spec.Selector.MatchLabels is nil")
}
if deployment.Spec.Template.ObjectMeta.Labels == nil {
t.Fatal("deployment.Spec.Template.ObjectMeta.Labels map is nil")
}
if deployment.Spec.Template.ObjectMeta.Annotations == nil {
t.Fatal("deployment.Spec.Template.ObjectMeta.Annotations map is nil")
}
if deployment.Spec.Template.Spec.NodeSelector == nil {
t.Fatal("deployment.Spec.Template.Spec.NodeSelector map is nil")
}
}
func TestMergeDeployments(t *testing.T) {
var dstReplicas int32
dstReplicas = 10
destinationDeployment := appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
"foo1-deploy": "bar",
},
},
Spec: appsv1.DeploymentSpec{
Replicas: &dstReplicas,
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
VolumeMounts: []corev1.VolumeMount{
{
Name: "foo",
MountPath: "/bar",
},
},
Resources: corev1.ResourceRequirements{},
},
},
},
},
},
}
var srcReplicas int32
srcReplicas = 8
sourceDeployment := appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
"foo2-deploy": "bar",
},
},
Spec: appsv1.DeploymentSpec{
Replicas: &srcReplicas,
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
VolumeMounts: []corev1.VolumeMount{
{
Name: "baz",
MountPath: "/qux",
},
},
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceName(corev1.ResourceCPU): resource.MustParse("100m"),
corev1.ResourceName(corev1.ResourceMemory): resource.MustParse("100Mi"),
},
},
},
},
},
},
},
}
var expectedReplicas int32
expectedReplicas = 10
expectedDeployment := appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
"foo1-deploy": "bar",
"foo2-deploy": "bar",
},
},
Spec: appsv1.DeploymentSpec{
Replicas: &expectedReplicas,
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
VolumeMounts: []corev1.VolumeMount{
{
Name: "foo",
MountPath: "/bar",
},
{
Name: "baz",
MountPath: "/qux",
},
},
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceName(corev1.ResourceCPU): resource.MustParse("100m"),
corev1.ResourceName(corev1.ResourceMemory): resource.MustParse("100Mi"),
},
},
},
},
},
},
},
}
MergeDeployments(&destinationDeployment, &sourceDeployment)
mergedContainerCount := len(destinationDeployment.Spec.Template.Spec.Containers)
if mergedContainerCount != 1 {
t.Fatalf("Expecting 1 container but received %v", mergedContainerCount)
}
expectedAnnotations := expectedDeployment.ObjectMeta.Annotations
mergedAnnotations := destinationDeployment.ObjectMeta.Annotations
for i := range expectedAnnotations {
if mergedAnnotations[i] != expectedAnnotations[i] {
t.Fatalf("Expecting annotation %s but received %s", expectedAnnotations[i], mergedAnnotations[i])
}
}
mergedReplicas := *destinationDeployment.Spec.Replicas
if mergedReplicas != expectedReplicas {
t.Fatalf("Expecting 8 replicas but received %v", *destinationDeployment.Spec.Replicas)
}
expectedVolumeMountCount := 2
mergedVolumeMountCount := len(destinationDeployment.Spec.Template.Spec.Containers[0].VolumeMounts)
if mergedVolumeMountCount != expectedVolumeMountCount {
t.Fatalf("Expecting %v volumeMounts but received %v", expectedVolumeMountCount, mergedVolumeMountCount)
}
expectedCPURequest := expectedDeployment.Spec.Template.Spec.Containers[0].Resources.Requests[corev1.ResourceName(corev1.ResourceCPU)]
mergedCPURequest := destinationDeployment.Spec.Template.Spec.Containers[0].Resources.Requests[corev1.ResourceName(corev1.ResourceCPU)]
if mergedCPURequest != expectedCPURequest {
t.Fatalf(
"Expecting %s cpu resource request but received %s",
expectedCPURequest.String(),
mergedCPURequest.String(),
)
}
expectedMemoryRequest := expectedDeployment.Spec.Template.Spec.Containers[0].Resources.Requests[corev1.ResourceName(corev1.ResourceMemory)]
mergedMemoryRequest := destinationDeployment.Spec.Template.Spec.Containers[0].Resources.Requests[corev1.ResourceName(corev1.ResourceMemory)]
if mergedMemoryRequest != expectedMemoryRequest {
t.Fatalf(
"Expecting %s memory resource request but received %s",
expectedMemoryRequest.String(),
mergedMemoryRequest.String(),
)
}
}
func TestGetAnnotationsFromCRD(t *testing.T) {
crdWithoutAnnotationName := "crdWithoutAnnotation"
crdWithAnnotationName := "crdWithAnnotation"
expectedAnnotations := map[string]string{
"foo": "bar",
}
crdWithAnnotation := &extensionsv1beta1.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
"foo": "bar",
},
Name: crdWithAnnotationName,
},
Spec: extensionsv1beta1.CustomResourceDefinitionSpec{
Group: "foo.group.io",
Names: extensionsv1beta1.CustomResourceDefinitionNames{
Plural: "foos",
Singular: "foo",
Kind: "fooKind",
ListKind: "fooList",
},
},
}
clientset := fakeextensionsapi.NewSimpleClientset()
_, err := clientset.ApiextensionsV1beta1().CustomResourceDefinitions().Create(crdWithAnnotation)
if err != nil {
t.Fatalf("Error while creating CRD: %v", err)
}
annotations, err := GetAnnotationsFromCRD(clientset, crdWithAnnotationName)
if err != nil {
t.Fatalf("Error while fetching CRD: %v", err)
}
for i := range expectedAnnotations {
if annotations[i] != expectedAnnotations[i] {
t.Errorf("Expecting annotation %s but received %s", expectedAnnotations[i], annotations[i])
}
}
crdWithoutAnnotation := &extensionsv1beta1.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{},
Name: crdWithoutAnnotationName,
},
Spec: extensionsv1beta1.CustomResourceDefinitionSpec{
Group: "foo.group.io",
Names: extensionsv1beta1.CustomResourceDefinitionNames{
Plural: "foos",
Singular: "foo",
Kind: "fooKind",
ListKind: "fooList",
},
},
}
_, err = clientset.ApiextensionsV1beta1().CustomResourceDefinitions().Create(crdWithoutAnnotation)
if err != nil {
t.Fatalf("Error while creating CRD: %v", err)
}
annotations, err = GetAnnotationsFromCRD(clientset, crdWithoutAnnotationName)
if err != nil {
t.Fatalf("Error while fetching annotations from CRD: %v", err)
}
if len(annotations) != 0 {
t.Errorf("Expecting annotations of length 0 but received length %d", len(annotations))
}
}
================================================
FILE: pkg/utils/kubelessutil.go
================================================
/*
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.
*/
package utils
import (
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"unicode/utf8"
monitoringv1alpha1 "github.com/coreos/prometheus-operator/pkg/client/monitoring/v1alpha1"
"github.com/ghodss/yaml"
kubelessApi "github.com/kubeless/kubeless/pkg/apis/kubeless/v1beta1"
"github.com/kubeless/kubeless/pkg/langruntime"
"github.com/sirupsen/logrus"
appsv1 "k8s.io/api/apps/v1"
batchv1 "k8s.io/api/batch/v1"
v1 "k8s.io/api/core/v1"
clientsetAPIExtensions "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)
// secretsMountPath is the file system path where volumes populated with secrets are mounted.
const secretsMountPath = "/var/run/secrets/kubeless.io"
// GetFunctionPort returns the port for a function service
func GetFunctionPort(clientset kubernetes.Interface, namespace, functionName string) (string, error) {
svc, err := clientset.CoreV1().Services(namespace).Get(functionName, metav1.GetOptions{})
if err != nil {
return "", fmt.Errorf("Unable to find the service for function %s", functionName)
}
return strconv.Itoa(int(svc.Spec.Ports[0].Port)), nil
}
// IsJSON returns true if the string is json
func IsJSON(s string) bool {
var js map[string]interface{}
return json.Unmarshal([]byte(s), &js) == nil
}
func appendToCommand(orig string, command ...string) string {
if len(orig) > 0 {
return fmt.Sprintf("%s && %s", orig, strings.Join(command, " && "))
}
return strings.Join(command, " && ")
}
func getProvisionContainer(function, checksum, fileName, handler, contentType, runtime, prepareImage string, runtimeVolume, depsVolume v1.VolumeMount, resources v1.ResourceRequirements, lr *langruntime.Langruntimes) (v1.Container, error) {
prepareCommand := ""
originFile := path.Join(depsVolume.MountPath, fileName)
// Prepare Function file and dependencies
if strings.Contains(contentType, "base64") {
// File is encoded in base64
decodedFile := "/tmp/func.decoded"
prepareCommand = appendToCommand(prepareCommand, fmt.Sprintf("base64 -d < %s > %s", originFile, decodedFile))
originFile = decodedFile
} else if strings.Contains(contentType, "url") {
fromURLFile := "/tmp/func.fromurl"
prepareCommand = appendToCommand(prepareCommand, fmt.Sprintf("curl '%s' -L --silent --output %s", function, fromURLFile))
originFile = fromURLFile
} else if strings.Contains(contentType, "text") || contentType == "" {
// Assumming that function is plain text
// So we don't need to preprocess it
} else {
return v1.Container{}, fmt.Errorf("Unable to prepare function of type %s: Unknown format", contentType)
}
// Validate checksum
if checksum == "" {
// DEPRECATED: Checksum may be empty
} else {
checksumInfo := strings.Split(checksum, ":")
switch checksumInfo[0] {
case "sha256":
shaFile := "/tmp/func.sha256"
prepareCommand = appendToCommand(prepareCommand,
fmt.Sprintf("echo '%s %s' > %s", checksumInfo[1], originFile, shaFile),
fmt.Sprintf("sha256sum -c %s", shaFile),
)
break
default:
return v1.Container{}, fmt.Errorf("Unable to verify checksum %s: Unknown format", checksum)
}
}
if strings.Contains(contentType, "zip") {
// Extract content in case it is a Zip file
prepareCommand = appendToCommand(prepareCommand,
fmt.Sprintf("unzip -o %s -d %s", originFile, runtimeVolume.MountPath),
)
} else if strings.Contains(contentType, "compressedtar") {
// Extract content in case it is a compressed tar file.
// The `tar` command auto-detects the compression type.
prepareCommand = appendToCommand(prepareCommand,
fmt.Sprintf("tar xf %s -C %s", originFile, runtimeVolume.MountPath),
)
} else {
// Copy the target as a single file
destFileName, err := getFileName(handler, contentType, runtime, lr)
if err != nil {
return v1.Container{}, err
}
dest := path.Join(runtimeVolume.MountPath, destFileName)
prepareCommand = appendToCommand(prepareCommand,
fmt.Sprintf("cp %s %s", originFile, dest),
)
}
// Copy deps file to the installation path
runtimeInf, err := lr.GetRuntimeInfo(runtime)
if err == nil && runtimeInf.DepName != "" && !strings.Contains(contentType, "deps") {
depsFile := path.Join(depsVolume.MountPath, runtimeInf.DepName)
prepareCommand = appendToCommand(prepareCommand,
fmt.Sprintf("cp %s %s", depsFile, runtimeVolume.MountPath),
)
}
return v1.Container{
Name: "prepare",
Image: prepareImage,
Command: []string{"sh", "-c"},
Args: []string{prepareCommand},
VolumeMounts: []v1.VolumeMount{runtimeVolume, depsVolume},
ImagePullPolicy: v1.PullIfNotPresent,
Resources: resources,
}, nil
}
func addDefaultLabel(labels map[string]string) map[string]string {
if labels == nil {
labels = make(map[string]string)
}
labels["created-by"] = "kubeless"
return labels
}
func hasDefaultLabel(labels map[string]string) bool {
if labels == nil || labels["created-by"] != "kubeless" {
return false
}
return true
}
func splitHandler(handler string) (string, string, error) {
str := strings.Split(handler, ".")
if len(str) != 2 {
return "", "", fmt.Errorf("failed: incorrect handler format. It should be module_name.handler_name")
}
return str[0], str[1], nil
}
// getFileName returns a file name based on a handler identifier
func getFileName(handler, funcContentType, runtime string, lr *langruntime.Langruntimes) (string, error) {
modName, _, err := splitHandler(handler)
if err != nil {
return "", err
}
filename := modName
if funcContentType == "text" || funcContentType == "" || funcContentType == "url" || funcContentType == "base64" {
// We can only guess the extension if the function is specified as plain text
runtimeInf, err := lr.GetRuntimeInfo(runtime)
if err == nil {
filename = modName + runtimeInf.FileNameSuffix
}
}
return filename, nil
}
// EnsureFuncConfigMap creates/updates a config map with a function specification
func EnsureFuncConfigMap(client kubernetes.Interface, funcObj *kubelessApi.Function, or []metav1.OwnerReference, lr *langruntime.Langruntimes) error {
configMapData := map[string]string{}
var err error
if funcObj.Spec.Handler != "" {
fileName, err := getFileName(funcObj.Spec.Handler, funcObj.Spec.FunctionContentType, funcObj.Spec.Runtime, lr)
if err != nil {
return err
}
configMapData = map[string]string{
"handler": funcObj.Spec.Handler,
fileName: funcObj.Spec.Function,
}
runtimeInfo, err := lr.GetRuntimeInfo(funcObj.Spec.Runtime)
if err == nil && runtimeInfo.DepName != "" {
configMapData[runtimeInfo.DepName] = funcObj.Spec.Deps
}
}
configMap := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: funcObj.ObjectMeta.Name,
Labels: addDefaultLabel(funcObj.ObjectMeta.Labels),
OwnerReferences: or,
},
Data: configMapData,
}
_, err = client.Core().ConfigMaps(funcObj.ObjectMeta.Namespace).Create(configMap)
if err != nil && k8sErrors.IsAlreadyExists(err) {
// In case the ConfigMap already exists we should update
// just certain fields (to avoid race conditions)
var newConfigMap *v1.ConfigMap
newConfigMap, err = client.Core().ConfigMaps(funcObj.ObjectMeta.Namespace).Get(funcObj.ObjectMeta.Name, metav1.GetOptions{})
if err != nil {
return err
}
if !hasDefaultLabel(newConfigMap.ObjectMeta.Labels) {
return fmt.Errorf("Found a conflicting configmap object %s/%s. Aborting", funcObj.ObjectMeta.Namespace, funcObj.ObjectMeta.Name)
}
newConfigMap.ObjectMeta.Labels = funcObj.ObjectMeta.Labels
newConfigMap.ObjectMeta.OwnerReferences = or
newConfigMap.Data = configMap.Data
_, err = client.Core().ConfigMaps(funcObj.ObjectMeta.Namespace).Update(newConfigMap)
if err != nil && k8sErrors.IsAlreadyExists(err) {
// The configmap may already exist and there is nothing to update
return nil
}
}
return err
}
// this function resolves backward incompatibility in case user uses old client which doesn't include serviceSpec into funcSpec.
// if serviceSpec is empty, we will use the default serviceSpec whose port is 8080
func serviceSpec(funcObj *kubelessApi.Function) v1.ServiceSpec {
if len(funcObj.Spec.ServiceSpec.Ports) == 0 {
return v1.ServiceSpec{
Ports: []v1.ServicePort{
{
// Note: Prefix: "http-" is added to adapt to Istio so that it can discover the function services
Name: "http-function-port",
Protocol: v1.ProtocolTCP,
Port: 8080,
TargetPort: intstr.FromInt(8080),
},
},
Selector: funcObj.ObjectMeta.Labels,
Type: v1.ServiceTypeClusterIP,
}
}
return funcObj.Spec.ServiceSpec
}
// EnsureFuncService creates/updates a function service
func EnsureFuncService(client kubernetes.Interface, funcObj *kubelessApi.Function, or []metav1.OwnerReference) error {
svc := &v1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: funcObj.ObjectMeta.Name,
Labels: addDefaultLabel(funcObj.ObjectMeta.Labels),
OwnerReferences: or,
},
Spec: serviceSpec(funcObj),
}
_, err := client.Core().Services(funcObj.ObjectMeta.Namespace).Create(svc)
if err != nil && k8sErrors.IsAlreadyExists(err) {
// In case the SVC already exists we should update
// just certain fields (to avoid race conditions)
var newSvc *v1.Service
newSvc, err = client.Core().Services(funcObj.ObjectMeta.Namespace).Get(funcObj.ObjectMeta.Name, metav1.GetOptions{})
if err != nil {
return err
}
if !hasDefaultLabel(newSvc.ObjectMeta.Labels) {
return fmt.Errorf("Found a conflicting service object %s/%s. Aborting", funcObj.ObjectMeta.Namespace, funcObj.ObjectMeta.Name)
}
newSvc.ObjectMeta.Labels = funcObj.ObjectMeta.Labels
newSvc.ObjectMeta.OwnerReferences = or
newSvc.Spec.Ports = svc.Spec.Ports
_, err = client.Core().Services(funcObj.ObjectMeta.Namespace).Update(newSvc)
if err != nil && k8sErrors.IsAlreadyExists(err) {
// The service may already exist and there is nothing to update
return nil
}
}
return err
}
func getRuntimeVolumeMount(name string) v1.VolumeMount {
return v1.VolumeMount{
Name: name,
MountPath: "/kubeless",
}
}
func getChecksum(content string) (string, error) {
h := sha256.New()
_, err := h.Write([]byte(content))
if err != nil {
return "", nil
}
return hex.EncodeToString(h.Sum(nil)), nil
}
// populatePodSpec populates a basic Pod Spec that uses init containers to populate
// the runtime container with the function content and its dependencies.
// The caller should define the runtime container(s).
// It accepts a prepopulated podSpec with default information and volume that the
// runtime container should mount
func populatePodSpec(funcObj *kubelessApi.Function, lr *langruntime.Langruntimes, podSpec *v1.PodSpec, runtimeVolumeMount v1.VolumeMount, provisionImage string, imagePullSecrets []v1.LocalObjectReference) error {
depsVolumeName := funcObj.ObjectMeta.Name + "-deps"
result := podSpec
if len(imagePullSecrets) > 0 {
result.ImagePullSecrets = imagePullSecrets
}
result.Volumes = append(podSpec.Volumes,
v1.Volume{
Name: runtimeVolumeMount.Name,
VolumeSource: v1.VolumeSource{
EmptyDir: &v1.EmptyDirVolumeSource{},
},
},
v1.Volume{
Name: depsVolumeName,
VolumeSource: v1.VolumeSource{
ConfigMap: &v1.ConfigMapVolumeSource{
LocalObjectReference: v1.LocalObjectReference{
Name: funcObj.ObjectMeta.Name,
},
},
},
},
)
// prepare init-containers if some function is specified
resources := v1.ResourceRequirements{}
if len(funcObj.Spec.Deployment.Spec.Template.Spec.InitContainers) > 0 {
resources = funcObj.Spec.Deployment.Spec.Template.Spec.InitContainers[0].Resources
}
if funcObj.Spec.Function != "" {
fileName, err := getFileName(funcObj.Spec.Handler, funcObj.Spec.FunctionContentType, funcObj.Spec.Runtime, lr)
if err != nil {
return err
}
srcVolumeMount := v1.VolumeMount{
Name: depsVolumeName,
MountPath: "/src",
}
provisionContainer, err := getProvisionContainer(
funcObj.Spec.Function,
funcObj.Spec.Checksum,
fileName,
funcObj.Spec.Handler,
funcObj.Spec.FunctionContentType,
funcObj.Spec.Runtime,
provisionImage,
runtimeVolumeMount,
srcVolumeMount,
resources,
lr,
)
if err != nil {
return err
}
result.InitContainers = []v1.Container{provisionContainer}
}
// add the image secrets if present to pull images from private docker registry
if funcObj.Spec.Runtime != "" {
imageSecrets, err := lr.GetImageSecrets(funcObj.Spec.Runtime)
if err != nil {
return fmt.Errorf("Unable to fetch ImagePullSecrets, %v", err)
}
result.ImagePullSecrets = append(result.ImagePullSecrets, imageSecrets...)
}
// ensure that the runtime is supported for installing dependencies
_, err := lr.GetRuntimeInfo(funcObj.Spec.Runtime)
envVars := []v1.EnvVar{}
if len(result.Containers) > 0 {
envVars = result.Containers[0].Env
}
hasDeps := funcObj.Spec.Deps != "" || strings.Contains(funcObj.Spec.FunctionContentType, "deps")
if hasDeps && err != nil {
return fmt.Errorf("Unable to install dependencies for the runtime %s", funcObj.Spec.Runtime)
} else if hasDeps {
depsChecksum := ""
if funcObj.Spec.Deps != "" {
depsChecksum, err = getChecksum(funcObj.Spec.Deps)
if err != nil {
return fmt.Errorf("Unable to obtain dependencies checksum: %v", err)
}
}
depsInstallContainer, err := lr.GetBuildContainer(funcObj.Spec.Runtime, depsChecksum, envVars, runtimeVolumeMount, resources)
if err != nil {
return err
}
if depsInstallContainer.Name != "" {
result.InitContainers = append(
result.InitContainers,
depsInstallContainer,
)
}
}
// add compilation init container if needed
_, funcName, _ := splitHandler(funcObj.Spec.Handler)
compContainer, err := lr.GetCompilationContainer(funcObj.Spec.Runtime, funcName, envVars, runtimeVolumeMount, resources)
if err != nil {
return err
}
if compContainer != nil {
result.InitContainers = append(
result.InitContainers,
*compContainer,
)
}
// mount volumes with init container secrets specified in runtime configuration
lr.ReadConfigMap()
for i := 0; i < len(result.InitContainers); i++ {
secrets, err := lr.GetInitContainerSecrets(funcObj.Spec.Runtime, result.InitContainers[i].Name)
if err != nil {
return fmt.Errorf("Unable to fetch init container secrets for runtime %s at phase %s: %v", funcObj.Spec.Runtime, result.InitContainers[i].Name, err)
}
for _, secret := range secrets {
// add volume if not available in the pod spec already
var found bool
for _, vol := range result.Volumes {
if vol.Name == secret.Name && (vol.Secret == nil || vol.Secret.SecretName != secret.Name) {
return fmt.Errorf("Unable to add volume for secret %s, volume already defined %#v", secret.Name, vol)
}
if vol.Name == secret.Name && vol.Secret != nil && vol.Secret.SecretName == secret.Name {
found = true
break
}
}
if !found {
result.Volumes = append(result.Volumes, v1.Volume{
Name: secret.Name,
VolumeSource: v1.VolumeSource{
Secret: &v1.SecretVolumeSource{SecretName: secret.Name},
},
})
}
// add volume mount to the init container
result.InitContainers[i].VolumeMounts = append(result.InitContainers[i].VolumeMounts, v1.VolumeMount{
Name: secret.Name,
ReadOnly: true,
MountPath: filepath.Join(secretsMountPath, secret.Name),
})
}
}
return nil
}
// EnsureFuncImage creates a Job to build a function image
func EnsureFuncImage(client kubernetes.Interface, funcObj *kubelessApi.Function, lr *langruntime.Langruntimes, or []metav1.OwnerReference, imageName, tag, builderImage, registryHost, dockerSecretName, provisionImage string, registryTLSEnabled bool, imagePullSecrets []v1.LocalObjectReference) error {
if len(tag) < 64 {
return errors.New("Expecting sha256 as image tag")
}
jobName := fmt.Sprintf("build-%s-%s", funcObj.ObjectMeta.Name, tag[0:10])
_, err := client.BatchV1().Jobs(funcObj.ObjectMeta.Namespace).Get(jobName, metav1.GetOptions{})
if err == nil {
// The job already exists
logrus.Infof("Found a previous job for building %s:%s", imageName, tag)
return nil
}
podSpec := v1.PodSpec{
RestartPolicy: v1.RestartPolicyOnFailure,
}
runtimeVolumeMount := getRuntimeVolumeMount(funcObj.ObjectMeta.Name)
err = populatePodSpec(funcObj, lr, &podSpec, runtimeVolumeMount, provisionImage, imagePullSecrets)
if err != nil {
return err
}
// Add a final initContainer to create the function bundle.tar
prepareContainer := v1.Container{}
for _, c := range podSpec.InitContainers {
if c.Name == "prepare" {
prepareContainer = c
}
}
podSpec.InitContainers = append(podSpec.InitContainers, v1.Container{
Name: "bundle",
Command: []string{"sh", "-c"},
Args: []string{fmt.Sprintf("tar cvf %s/bundle.tar %s/*", runtimeVolumeMount.MountPath, runtimeVolumeMount.MountPath)},
VolumeMounts: prepareContainer.VolumeMounts,
Image: provisionImage,
})
buildJob := batchv1.Job{
ObjectMeta: metav1.ObjectMeta{
Name: jobName,
Namespace: funcObj.ObjectMeta.Namespace,
OwnerReferences: or,
Labels: addDefaultLabel(map[string]string{
"function": funcObj.ObjectMeta.Name,
}),
},
Spec: batchv1.JobSpec{
Template: v1.PodTemplateSpec{
Spec: podSpec,
},
},
}
baseImage, err := lr.GetFunctionImage(funcObj.Spec.Runtime)
if err != nil {
return err
}
// Registry volume
dockerCredsVol := dockerSecretName
dockerCredsVolMountPath := "/docker"
registryCredsVolume := v1.Volume{
Name: dockerCredsVol,
VolumeSource: v1.VolumeSource{
Secret: &v1.SecretVolumeSource{
SecretName: dockerSecretName,
},
},
}
buildJob.Spec.Template.Spec.Volumes = append(buildJob.Spec.Template.Spec.Volumes, registryCredsVolume)
args := []string{
"/imbuilder",
"add-layer",
}
if !registryTLSEnabled {
args = append(args, "--insecure")
}
args = append(args,
"--src", fmt.Sprintf("docker://%s", baseImage),
"--dst", fmt.Sprintf("docker://%s/%s:%s", registryHost, imageName, tag),
fmt.Sprintf("%s/bundle.tar", podSpec.InitContainers[0].VolumeMounts[0].MountPath),
)
// Add main container
buildJob.Spec.Template.Spec.Containers = []v1.Container{
{
Name: "build",
Image: builderImage,
VolumeMounts: append(prepareContainer.VolumeMounts,
v1.VolumeMount{
Name: dockerCredsVol,
MountPath: dockerCredsVolMountPath,
},
),
Env: []v1.EnvVar{
{
Name: "DOCKER_CONFIG_FOLDER",
Value: dockerCredsVolMountPath,
},
},
Args: args,
},
}
// Create the job if doesn't exists yet
_, err = client.BatchV1().Jobs(funcObj.ObjectMeta.Namespace).Create(&buildJob)
if err == nil {
logrus.Infof("Started function build job %s", jobName)
}
return err
}
func svcTargetPort(funcObj *kubelessApi.Function) int32 {
if len(funcObj.Spec.ServiceSpec.Ports) == 0 {
return int32(8080)
}
return int32(funcObj.Spec.ServiceSpec.Ports[0].TargetPort.IntValue())
}
func mergeMap(dst, src map[string]string) map[string]string {
if len(dst) == 0 {
dst = make(map[string]string)
}
for k, v := range src {
dst[k] = v
}
return dst
}
// EnsureFuncDeployment creates/updates a function deployment
func EnsureFuncDeployment(client kubernetes.Interface, funcObj *kubelessApi.Function, or []metav1.OwnerReference, lr *langruntime.Langruntimes, prebuiltRuntimeImage, provisionImage string, imagePullSecrets []v1.LocalObjectReference) error {
var err error
podAnnotations := map[string]string{
// Attempt to attract the attention of prometheus.
// For runtimes that don't support /metrics,
// prometheus will get a 404 and mostly silently
// ignore the pod (still displayed in the list of
// "targets")
"prometheus.io/scrape": "true",
"prometheus.io/path": "/metrics",
"prometheus.io/port": strconv.Itoa(int(svcTargetPort(funcObj))),
}
maxUnavailable := intstr.FromInt(0)
// add deployment and copy all func's Spec.Deployment to the deployment
dpm := funcObj.Spec.Deployment.DeepCopy()
dpm.OwnerReferences = or
dpm.ObjectMeta.Name = funcObj.ObjectMeta.Name
dpm.Spec.Selector = &metav1.LabelSelector{
MatchLabels: map[string]string{"created-by": funcObj.ObjectMeta.Labels["created-by"], "function": funcObj.ObjectMeta.Labels["function"]},
}
dpm.Spec.Strategy = appsv1.DeploymentStrategy{
RollingUpdate: &appsv1.RollingUpdateDeployment{
MaxUnavailable: &maxUnavailable,
},
}
// append data to dpm deployment
dpm.Labels = addDefaultLabel(mergeMap(dpm.Labels, funcObj.Labels))
dpm.Spec.Template.Labels = mergeMap(dpm.Spec.Template.Labels, funcObj.Labels)
dpm.Annotations = mergeMap(dpm.Annotations, funcObj.Annotations)
dpm.Spec.Template.Annotations = mergeMap(dpm.Spec.Template.Annotations, funcObj.Annotations)
dpm.Spec.Template.Annotations = mergeMap(dpm.Spec.Template.Annotations, podAnnotations)
if len(dpm.Spec.Template.Spec.Containers) == 0 {
dpm.Spec.Template.Spec.Containers = append(dpm.Spec.Template.Spec.Containers, v1.Container{})
}
runtimeVolumeMount := getRuntimeVolumeMount(funcObj.ObjectMeta.Name)
if funcObj.Spec.Handler != "" && funcObj.Spec.Function != "" {
modName, handlerName, err := splitHandler(funcObj.Spec.Handler)
if err != nil {
return err
}
// only resolve the image name and build the function if it has not been built already
if dpm.Spec.Template.Spec.Containers[0].Image == "" && prebuiltRuntimeImage == "" {
err := populatePodSpec(funcObj, lr, &dpm.Spec.Template.Spec, runtimeVolumeMount, provisionImage, imagePullSecrets)
if err != nil {
return err
}
imageName, err := lr.GetFunctionImage(funcObj.Spec.Runtime)
if err != nil {
return err
}
dpm.Spec.Template.Spec.Containers[0].Image = imageName
dpm.Spec.Template.Spec.Containers[0].VolumeMounts = append(dpm.Spec.Template.Spec.Containers[0].VolumeMounts, runtimeVolumeMount)
} else {
if dpm.Spec.Template.Spec.Containers[0].Image == "" {
dpm.Spec.Template.Spec.Containers[0].Image = prebuiltRuntimeImage
}
dpm.Spec.Template.Spec.ImagePullSecrets = imagePullSecrets
}
timeout := funcObj.Spec.Timeout
if timeout == "" {
// Set default timeout to 180 seconds
timeout = defaultTimeout
}
dpm.Spec.Template.Spec.Containers[0].Env = append(dpm.Spec.Template.Spec.Containers[0].Env,
v1.EnvVar{
Name: "FUNC_HANDLER",
Value: handlerName,
},
v1.EnvVar{
Name: "MOD_NAME",
Value: modName,
},
v1.EnvVar{
Name: "FUNC_TIMEOUT",
Value: timeout,
},
v1.EnvVar{
Name: "FUNC_RUNTIME",
Value: funcObj.Spec.Runtime,
},
v1.EnvVar{
Name: "FUNC_MEMORY_LIMIT",
Value: dpm.Spec.Template.Spec.Containers[0].Resources.Limits.Memory().String(),
},
)
} else {
logrus.Warn("Expected non-empty handler and non-empty function content")
}
dpm.Spec.Template.Spec.Containers[0].Env = append(dpm.Spec.Template.Spec.Containers[0].Env,
v1.EnvVar{
Name: "FUNC_PORT",
Value: strconv.Itoa(int(svcTargetPort(funcObj))),
},
)
dpm.Spec.Template.Spec.Containers[0].Name = funcObj.ObjectMeta.Name
dpm.Spec.Template.Spec.Containers[0].Ports = append(dpm.Spec.Template.Spec.Containers[0].Ports, v1.ContainerPort{
ContainerPort: svcTargetPort(funcObj),
})
// update deployment for loading dependencies
lr.UpdateDeployment(dpm, runtimeVolumeMount.MountPath, funcObj.Spec.Runtime)
livenessProbeInfo := lr.GetLivenessProbeInfo(funcObj.Spec.Runtime, int(svcTargetPort(funcObj)))
if dpm.Spec.Template.Spec.Containers[0].LivenessProbe == nil {
dpm.Spec.Template.Spec.Containers[0].LivenessProbe = livenessProbeInfo
}
// Add security context
runtimeUser := int64(1000)
if dpm.Spec.Template.Spec.SecurityContext == nil {
dpm.Spec.Template.Spec.SecurityContext = &v1.PodSecurityContext{
RunAsUser: &runtimeUser,
FSGroup: &runtimeUser,
}
}
// Add soft pod anti affinity
if dpm.Spec.Template.Spec.Affinity == nil {
dpm.Spec.Template.Spec.Affinity = &v1.Affinity{
PodAntiAffinity: &v1.PodAntiAffinity{
PreferredDuringSchedulingIgnoredDuringExecution: []v1.WeightedPodAffinityTerm{
{
Weight: 100,
PodAffinityTerm: v1.PodAffinityTerm{
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"created-by": "kubeless",
"function": funcObj.ObjectMeta.Name,
},
},
TopologyKey: "kubernetes.io/hostname",
},
},
},
},
}
}
_, err = client.AppsV1().Deployments(funcObj.ObjectMeta.Namespace).Create(dpm)
if err != nil && k8sErrors.IsAlreadyExists(err) {
// In case the Deployment already exists we should update
// just certain fields (to avoid race conditions)
var newDpm *appsv1.Deployment
newDpm, err = client.AppsV1().Deployments(funcObj.ObjectMeta.Namespace).Get(funcObj.ObjectMeta.Name, metav1.GetOptions{})
if err != nil {
return err
}
if !hasDefaultLabel(newDpm.ObjectMeta.Labels) {
return fmt.Errorf("Found a conflicting deployment object %s/%s. Aborting", funcObj.ObjectMeta.Namespace, funcObj.ObjectMeta.Name)
}
newDpm.ObjectMeta.Labels = funcObj.ObjectMeta.Labels
newDpm.ObjectMeta.Annotations = funcObj.Spec.Deployment.ObjectMeta.Annotations
newDpm.ObjectMeta.OwnerReferences = or
// We should maintain previous selector to avoid duplicated ReplicaSets
selector := newDpm.Spec.Selector
newDpm.Spec = dpm.Spec
newDpm.Spec.Selector = selector
data, err := json.Marshal(newDpm)
if err != nil {
return err
}
// Use `Patch` to do a rolling update
_, err = client.AppsV1().Deployments(funcObj.ObjectMeta.Namespace).Patch(newDpm.Name, types.MergePatchType, data)
if err != nil {
return err
}
}
return err
}
// CreateServiceMonitor creates a Service Monitor for the given function
func CreateServiceMonitor(smclient monitoringv1alpha1.MonitoringV1alpha1Client, funcObj *kubelessApi.Function, ns string, or []metav1.OwnerReference) error {
_, err := smclient.ServiceMonitors(ns).Get(funcObj.ObjectMeta.Name, metav1.GetOptions{})
if err != nil {
if k8sErrors.IsNotFound(err) {
s := &monitoringv1alpha1.ServiceMonitor{
ObjectMeta: metav1.ObjectMeta{
Name: funcObj.ObjectMeta.Name,
Namespace: ns,
Labels: addDefaultLabel(map[string]string{
"service-monitor": "function",
}),
OwnerReferences: or,
},
Spec: monitoringv1alpha1.ServiceMonitorSpec{
Selector: metav1.LabelSelector{
MatchLabels: map[string]string{
"function": funcObj.ObjectMeta.Name,
},
},
Endpoints: []monitoringv1alpha1.Endpoint{
{
Port: "http-function-port",
},
},
},
}
_, err = smclient.ServiceMonitors(ns).Create(s)
if err != nil {
return err
}
}
return nil
}
return fmt.Errorf("service monitor has already existed")
}
// GetOwnerReference returns ownerRef for appending to objects's metadata
func GetOwnerReference(kind, apiVersion, name string, uid types.UID) ([]metav1.OwnerReference, error) {
if name == "" {
return []metav1.OwnerReference{}, fmt.Errorf("name can't be empty")
}
if uid == "" {
return []metav1.OwnerReference{}, fmt.Errorf("uid can't be empty")
}
return []metav1.OwnerReference{
{
Kind: kind,
APIVersion: apiVersion,
Name: name,
UID: uid,
},
}, nil
}
// GetInClusterConfig returns necessary Config object to authenticate k8s clients if env variable is set
func GetInClusterConfig() (*rest.Config, error) {
config, err := rest.InClusterConfig()
tokenFile := os.Getenv("KUBELESS_TOKEN_FILE_PATH")
if len(tokenFile) == 0 {
return config, err
}
tokenBytes, err := ioutil.ReadFile(tokenFile)
if err != nil {
return nil, fmt.Errorf("unable to read file containing oauth token: %s", err)
}
config.BearerToken = string(tokenBytes)
return config, nil
}
func getConfigLocation(apiExtensionsClientset clientsetAPIExtensions.Interface) (ConfigLocation, error) {
configLocation := ConfigLocation{}
controllerNamespace := os.Getenv("KUBELESS_NAMESPACE")
kubelessConfig := os.Getenv("KUBELESS_CONFIG")
annotationsCRD, err := GetAnnotationsFromCRD(apiExtensionsClientset, "functions.kubeless.io")
if err != nil {
return configLocation, err
}
if len(controllerNamespace) == 0 {
if ns, ok := annotationsCRD["kubeless.io/namespace"]; ok {
controllerNamespace = ns
} else {
controllerNamespace = "kubeless"
}
}
configLocation.Namespace = controllerNamespace
if len(kubelessConfig) == 0 {
if config, ok := annotationsCRD["kubeless.io/config"]; ok {
kubelessConfig = config
} else {
kubelessConfig = "kubeless-config"
}
}
configLocation.Name = kubelessConfig
return configLocation, nil
}
// GetKubelessConfig Returns Kubeless ConfigMap
func GetKubelessConfig(cli kubernetes.Interface, cliAPIExtensions clientsetAPIExtensions.Interface) (*v1.ConfigMap, error) {
configLocation, err := getConfigLocation(cliAPIExtensions)
if err != nil {
return nil, fmt.Errorf("Error while fetching config location: %v", err)
}
controllerNamespace := configLocation.Namespace
kubelessConfig := configLocation.Name
config, err := cli.CoreV1().ConfigMaps(controllerNamespace).Get(kubelessConfig, metav1.GetOptions{})
if err != nil {
return nil, fmt.Errorf("Unable to read the configmap: %s", err)
}
return config, nil
}
// DryRunFmt stringify the given interface in a specific format
func DryRunFmt(format string, trigger interface{}) (string, error) {
switch format {
case "json":
j, err := json.MarshalIndent(trigger, "", " ")
if err != nil {
return "", err
}
return string(j[:]), nil
case "yaml":
y, err := yaml.Marshal(trigger)
if err != nil {
return "", err
}
return string(y[:]), nil
default:
return "", fmt.Errorf("Output format needs to be yaml or json")
}
}
// getCompressionType returns the compression type (if any) of the given file by looking at the file extension
func getCompressionType(filename string) (compressionType string) {
if strings.HasSuffix(filename, ".zip") {
compressionType = "+zip"
}
extensions := []string{".tar.gz", ".taz", ".tgz", ".tar.bz2", ".tb2", ".tbz", ".tbz2", ".tz2", ".tar.xz"}
for _, ext := range extensions {
if strings.HasSuffix(filename, ext) {
compressionType = "+compressedtar"
break
}
}
return
}
// GetContentType Gets the content type of a given filename
func GetContentType(filename string) (string, error) {
var contentType string
if strings.Index(filename, "http://") == 0 || strings.Index(filename, "https://") == 0 {
contentType = "url" + getCompressionType(strings.Split(filename, "?")[0])
} else {
fbytes, err := ioutil.ReadFile(filename)
if err != nil {
return "", err
}
isText := utf8.ValidString(string(fbytes))
if isText {
contentType = "text"
} else {
contentType = "base64"
}
contentType += getCompressionType(filename)
}
return contentType, nil
}
// ParseContent Parses the content of a file as string
func ParseContent(file, contentType string) (string, string, error) {
var checksum, content string
if strings.Contains(contentType, "url") {
functionURL, err := url.Parse(file)
if err != nil {
return "", "", err
}
resp, err := http.Get(functionURL.String())
if err != nil {
return "", "", err
}
defer resp.Body.Close()
functionBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", "", err
}
content = string(functionBytes)
checksum, err = getSha256(functionBytes)
if err != nil {
return "", "", err
}
} else {
functionBytes, err := ioutil.ReadFile(file)
if err != nil {
return "", "", err
}
if contentType == "text" {
content = string(functionBytes)
} else {
content = base64.StdEncoding.EncodeToString(functionBytes)
}
checksum, err = getFileSha256(file)
if err != nil {
return "", "", err
}
}
return content, checksum, nil
}
// Get the checksum of a file using sha256
func getFileSha256(file string) (string, error) {
h := sha256.New()
ff, err := os.Open(file)
if err != nil {
return "", err
}
defer ff.Close()
_, err = io.Copy(h, ff)
if err != nil {
return "", err
}
checksum := hex.EncodeToString(h.Sum(nil))
return "sha256:" + checksum, err
}
// Get the checksum using sha256
func getSha256(bytes []byte) (string, error) {
h := sha256.New()
_, err := h.Write(bytes)
if err != nil {
return "", err
}
checksum := hex.EncodeToString(h.Sum(nil))
return "sha256:" + checksum, nil
}
================================================
FILE: pkg/utils/kubelessutil_test.go
================================================
package utils
import (
"reflect"
"strconv"
"strings"
"testing"
kubelessApi "github.com/kubeless/kubeless/pkg/apis/kubeless/v1beta1"
"github.com/kubeless/kubeless/pkg/langruntime"
appsv1 "k8s.io/api/apps/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/client-go/kubernetes/fake"
)
func getEnvValueFromList(envName string, l []v1.EnvVar) string {
var res v1.EnvVar
for _, env := range l {
if env.Name == envName {
res = env
break
}
}
return res.Value
}
func TestEnsureConfigMap(t *testing.T) {
clientset := fake.NewSimpleClientset()
or := []metav1.OwnerReference{
{
Kind: "Function",
APIVersion: "kubeless.io/v1beta1",
},
}
ns := "default"
funcLabels := map[string]string{
"foo": "bar",
}
f1Name := "f1"
f1 := &kubelessApi.Function{
ObjectMeta: metav1.ObjectMeta{
Name: f1Name,
Namespace: ns,
Labels: funcLabels,
},
Spec: kubelessApi.FunctionSpec{
Function: "function",
Deps: "deps",
Handler: "foo.bar",
Runtime: "python2.7",
},
}
langruntime.AddFakeConfig(clientset)
lr := langruntime.SetupLangRuntime(clientset)
lr.ReadConfigMap()
err := EnsureFuncConfigMap(clientset, f1, or, lr)
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
cm, err := clientset.CoreV1().ConfigMaps(ns).Get(f1Name, metav1.GetOptions{})
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
expectedCM := v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: f1Name,
Namespace: ns,
Labels: funcLabels,
OwnerReferences: or,
},
Data: map[string]string{
"handler": "foo.bar",
"foo.py": "function",
"requirements.txt": "deps",
},
}
if !reflect.DeepEqual(*cm, expectedCM) {
t.Errorf("Unexpected ConfigMap:\n %+v\nExpecting:\n %+v", *cm, expectedCM)
}
}
func TestEnsureFuncMapWithoutDeps(t *testing.T) {
clientset := fake.NewSimpleClientset()
or := []metav1.OwnerReference{
{
Kind: "Function",
APIVersion: "kubeless.io/v1beta1",
},
}
ns := "default"
langruntime.AddFakeConfig(clientset)
lr := langruntime.SetupLangRuntime(clientset)
lr.ReadConfigMap()
// It should skip the dependencies field in case it is not supported
f2Name := "f2"
f2 := &kubelessApi.Function{
ObjectMeta: metav1.ObjectMeta{
Name: f2Name,
Namespace: ns,
},
Spec: kubelessApi.FunctionSpec{
Function: "function",
Handler: "foo.bar",
Runtime: "cobol",
},
}
err := EnsureFuncConfigMap(clientset, f2, or, lr)
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
cm, err := clientset.CoreV1().ConfigMaps(ns).Get(f2Name, metav1.GetOptions{})
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
expectedData := map[string]string{
"handler": "foo.bar",
"foo": "function",
}
if !reflect.DeepEqual(cm.Data, expectedData) {
t.Errorf("Unexpected ConfigMap:\n %+v\nExpecting:\n %+v", cm.Data, expectedData)
}
// If there is already a config map it should update the previous one
f2 = &kubelessApi.Function{
ObjectMeta: metav1.ObjectMeta{
Name: f2Name,
Namespace: ns,
},
Spec: kubelessApi.FunctionSpec{
Function: "function2",
Handler: "foo2.bar2",
Runtime: "python3.4",
},
}
err = EnsureFuncConfigMap(clientset, f2, or, lr)
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
cm, err = clientset.CoreV1().ConfigMaps(ns).Get(f2Name, metav1.GetOptions{})
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
expectedData = map[string]string{
"handler": "foo2.bar2",
"foo2.py": "function2",
"requirements.txt": "",
}
if !reflect.DeepEqual(cm.Data, expectedData) {
t.Errorf("Unexpected ConfigMap:\n %+v\nExpecting:\n %+v", cm.Data, expectedData)
}
}
func TestAvoidConfigMapOverwrite(t *testing.T) {
f1Name := "f1"
clientset, or, ns, lr := prepareDeploymentTest(f1Name)
clientset.CoreV1().ConfigMaps(ns).Create(&v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: f1Name,
Namespace: ns,
},
})
f1 := getDefaultFunc(f1Name, ns)
err := EnsureFuncConfigMap(clientset, f1, or, lr)
if err == nil && strings.Contains(err.Error(), "conflicting object") {
t.Errorf("It should fail because a conflict")
}
}
func TestEnsureFileNames(t *testing.T) {
tests := []struct {
name string
contentType string
fileNameSuffix string
}{
{name: "text", contentType: "text", fileNameSuffix: ".py"},
{name: "empty", contentType: "", fileNameSuffix: ".py"},
{name: "base64", contentType: "base64", fileNameSuffix: ".py"},
{name: "url", contentType: "url", fileNameSuffix: ".py"},
{name: "text+zip", contentType: "text+zip", fileNameSuffix: ""},
{name: "text+compressedtar", contentType: "text+compressedtar", fileNameSuffix: ""},
{name: "base64+zip", contentType: "base64+zip", fileNameSuffix: ""},
{name: "base64+compressedtar", contentType: "base64+compressedtar", fileNameSuffix: ""},
{name: "url+zip", contentType: "url+zip", fileNameSuffix: ""},
{name: "url+compressedtar", contentType: "url+compressedtar", fileNameSuffix: ""},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
clientset := fake.NewSimpleClientset()
or := []metav1.OwnerReference{
{
Kind: "Function",
APIVersion: "kubeless.io/v1beta1",
},
}
ns := "default"
f1Name := "f1"
f1Runtime := "python"
f1 := &kubelessApi.Function{
ObjectMeta: metav1.ObjectMeta{
Name: f1Name,
Namespace: ns,
},
Spec: kubelessApi.FunctionSpec{
Function: "function",
Handler: "foo.bar",
FunctionContentType: test.contentType,
Runtime: f1Runtime,
},
}
langruntime.AddFakeConfig(clientset)
lr := langruntime.SetupLangRuntime(clientset)
lr.ReadConfigMap()
err := EnsureFuncConfigMap(clientset, f1, or, lr)
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
cm, err := clientset.CoreV1().ConfigMaps(ns).Get(f1Name, metav1.GetOptions{})
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
expectedData := map[string]string{
"requirements.txt": "",
"handler": "foo.bar",
"foo" + test.fileNameSuffix: "function",
}
if !reflect.DeepEqual(cm.Data, expectedData) {
t.Errorf("Unexpected ConfigMap:\n %+v\nExpecting:\n %+v", cm.Data, expectedData)
}
})
}
}
func TestEnsureService(t *testing.T) {
fakeSvc := v1.Service{
ObjectMeta: metav1.ObjectMeta{
Namespace: "myns",
Name: "foo",
},
}
clientset := fake.NewSimpleClientset(&fakeSvc)
or := []metav1.OwnerReference{
{
Kind: "Function",
APIVersion: "kubeless.io/v1beta1",
},
}
ns := "default"
funcLabels := map[string]string{
"foo": "bar",
}
f1Name := "f1"
f1 := &kubelessApi.Function{
ObjectMeta: metav1.ObjectMeta{
Name: f1Name,
Namespace: ns,
Labels: funcLabels,
},
Spec: kubelessApi.FunctionSpec{
Function: "function",
Deps: "deps",
Handler: "foo.bar",
Runtime: "python2.7",
},
}
err := EnsureFuncService(clientset, f1, or)
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
svc, err := clientset.CoreV1().Services(ns).Get(f1Name, metav1.GetOptions{})
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
expectedSVC := v1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: f1Name,
Namespace: ns,
Labels: funcLabels,
OwnerReferences: or,
},
Spec: v1.ServiceSpec{
Ports: []v1.ServicePort{
{
Name: "http-function-port",
Port: 8080,
TargetPort: intstr.FromInt(8080),
NodePort: 0,
Protocol: v1.ProtocolTCP,
},
},
Selector: funcLabels,
Type: v1.ServiceTypeClusterIP,
},
}
if !reflect.DeepEqual(*svc, expectedSVC) {
t.Errorf("Unexpected service:\n %+v\nExpecting:\n %+v", *svc, expectedSVC)
}
}
func TestUpdateFuncSvc(t *testing.T) {
fakeSvc := v1.Service{
ObjectMeta: metav1.ObjectMeta{
Namespace: "myns",
Name: "foo",
},
}
clientset := fake.NewSimpleClientset(&fakeSvc)
or := []metav1.OwnerReference{
{
Kind: "Function",
APIVersion: "kubeless.io/v1beta1",
},
}
ns := "default"
// If there is already a service it should update the previous one
funcLabels := map[string]string{
"foo": "bar",
}
f1Name := "f1"
f1 := &kubelessApi.Function{
ObjectMeta: metav1.ObjectMeta{
Name: f1Name,
Namespace: ns,
Labels: funcLabels,
},
Spec: kubelessApi.FunctionSpec{
Function: "function",
Deps: "deps",
Handler: "foo.bar",
Runtime: "python2.7",
},
}
err := EnsureFuncService(clientset, f1, or)
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
newLabels := map[string]string{
"foobar": "barfoo",
}
f1.ObjectMeta.Labels = newLabels
err = EnsureFuncService(clientset, f1, or)
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
svc, err := clientset.CoreV1().Services(ns).Get(f1Name, metav1.GetOptions{})
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
if !reflect.DeepEqual(svc.ObjectMeta.Labels, newLabels) {
t.Error("Unable to update the service")
}
if reflect.DeepEqual(svc.Spec.Selector, newLabels) {
t.Error("It should not update the selector")
}
}
func TestAvoidServiceOverwrite(t *testing.T) {
f1Name := "f1"
ns := "default"
or := []metav1.OwnerReference{
{
Kind: "Function",
APIVersion: "kubeless.io/v1beta1",
},
}
clientset := fake.NewSimpleClientset()
clientset.CoreV1().Services(ns).Create(&v1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: f1Name,
Namespace: ns,
},
})
f1 := getDefaultFunc(f1Name, ns)
err := EnsureFuncService(clientset, f1, or)
if err == nil && strings.Contains(err.Error(), "conflicting object") {
t.Errorf("It should fail because a conflict")
}
}
func TestEnsureImage(t *testing.T) {
clientset := fake.NewSimpleClientset()
langruntime.AddFakeConfig(clientset)
lr := langruntime.SetupLangRuntime(clientset)
lr.ReadConfigMap()
ns := "default"
f1Name := "f1"
or := []metav1.OwnerReference{
{
Kind: "Function",
APIVersion: "kubeless.io/v1beta1",
},
}
f1 := &kubelessApi.Function{
ObjectMeta: metav1.ObjectMeta{
Name: f1Name,
Namespace: ns,
},
Spec: kubelessApi.FunctionSpec{
Function: "function",
Deps: "deps",
Handler: "foo.bar",
Runtime: "python2.7",
},
}
// Testing happy path
pullSecrets := []v1.LocalObjectReference{
{Name: "creds"},
}
err := EnsureFuncImage(clientset, f1, lr, or, "user/image", "4840d87600137157493ba43a24f0b4bb6cf524ebbf095ce96c79f85bf5a3ff5a", "kubeless/builder", "registry.docker.io", "registry-creds", "unzip", true, pullSecrets)
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
jobs, err := clientset.BatchV1().Jobs(ns).List(metav1.ListOptions{})
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
if len(jobs.Items) != 1 {
t.Errorf("It should have created the build job")
}
buildContainer := jobs.Items[0].Spec.Template.Spec.Containers[0]
if buildContainer.Image != "kubeless/builder" {
t.Errorf("Image %s of build job is not recognised", jobs.Items[0].Spec.Template.Spec.Containers[0].Image)
}
dockerConfigFolder := ""
for _, envvar := range buildContainer.Env {
if envvar.Name == "DOCKER_CONFIG_FOLDER" {
dockerConfigFolder = envvar.Value
}
}
if dockerConfigFolder == "" {
t.Error("Builder image relies on the env var DOCKER_CONFIG_FOLDER to authenticate")
}
initContainer := jobs.Items[0].Spec.Template.Spec.InitContainers[0]
if initContainer.Image != "unzip" {
t.Errorf("Unexpected init image %s", initContainer.Image)
}
if reflect.DeepEqual(jobs.Items[0].Spec.Template.Spec.ImagePullSecrets, pullSecrets) {
t.Error("Missing ImagePullSecrets")
}
// ensure my-secret is mounted as /var/run/secrets/kubeless.io/my-secret to install container
var container v1.Container
for _, c := range jobs.Items[0].Spec.Template.Spec.InitContainers {
if c.Name == "install" {
container = c
}
}
if len(container.Name) == 0 {
t.Fatalf("Cannot find init container %q", "install")
}
var found bool
for _, v := range container.VolumeMounts {
if v.MountPath == "/var/run/secrets/kubeless.io/my-secret" {
found = true
}
}
if !found {
t.Fatalf("Cannot find volume mount /var/run/secrets/kubeless.io/my-secret")
}
}
func getDefaultFunc(name, ns string) *kubelessApi.Function {
fPort := int32(8080)
f := kubelessApi.Function{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: ns,
},
Spec: kubelessApi.FunctionSpec{
Function: "function",
Deps: "deps",
Handler: "foo.bar",
Runtime: "python2.7",
ServiceSpec: v1.ServiceSpec{
Ports: []v1.ServicePort{
{
Name: "http-function-port",
Port: fPort,
TargetPort: intstr.FromInt(int(fPort)),
NodePort: 0,
Protocol: v1.ProtocolTCP,
},
},
Type: v1.ServiceTypeClusterIP,
},
Deployment: appsv1.Deployment{
Spec: appsv1.DeploymentSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Env: []v1.EnvVar{
{
Name: "foo",
Value: "bar",
},
},
},
},
},
},
},
},
},
}
return &f
}
func prepareDeploymentTest(funcName string) (*fake.Clientset, []metav1.OwnerReference, string, *langruntime.Langruntimes) {
clientset := fake.NewSimpleClientset()
or := []metav1.OwnerReference{
{
Kind: "Function",
APIVersion: "k8s.io",
},
}
ns := "default"
langruntime.AddFakeConfig(clientset)
lr := langruntime.SetupLangRuntime(clientset)
lr.ReadConfigMap()
return clientset, or, ns, lr
}
func TestEnsureDeployment(t *testing.T) {
f1Name := "f1"
clientset, or, ns, lr := prepareDeploymentTest(f1Name)
funcLabels := map[string]string{
"foo": "bar",
}
funcAnno := map[string]string{
"bar": "foo",
}
f1 := getDefaultFunc(f1Name, ns)
f1.Spec.Deployment.Spec.Template.Spec.InitContainers = []v1.Container{
{
Resources: v1.ResourceRequirements{
Limits: v1.ResourceList{
v1.ResourceLimitsCPU: resource.MustParse("100m"),
},
},
},
}
f1Port := f1.Spec.ServiceSpec.Ports[0].Port
f1.ObjectMeta.Labels = funcLabels
f1.Spec.Deployment.ObjectMeta = metav1.ObjectMeta{
Annotations: funcAnno,
}
f1.Spec.Deployment.Spec.Template.ObjectMeta = metav1.ObjectMeta{
Annotations: funcAnno,
}
// Testing happy path
pullSecrets := []v1.LocalObjectReference{
{Name: "creds"},
}
err := EnsureFuncDeployment(clientset, f1, or, lr, "", "unzip", pullSecrets)
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
dpm, err := clientset.AppsV1().Deployments(ns).Get(f1Name, metav1.GetOptions{})
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
expectedObjectMeta := metav1.ObjectMeta{
Name: f1Name,
Namespace: ns,
Labels: addDefaultLabel(funcLabels),
OwnerReferences: or,
Annotations: funcAnno,
}
if !reflect.DeepEqual(dpm.ObjectMeta, expectedObjectMeta) {
t.Errorf("Unable to set metadata. Received:\n %+v\nExpecting:\n %+v", dpm.ObjectMeta, expectedObjectMeta)
}
expectedAnnotations := map[string]string{
"prometheus.io/scrape": "true",
"prometheus.io/path": "/metrics",
"prometheus.io/port": strconv.Itoa(int(f1Port)),
"bar": "foo",
}
for i := range expectedAnnotations {
if dpm.Spec.Template.Annotations[i] != expectedAnnotations[i] {
t.Errorf("Expecting annotation %s but received %s", expectedAnnotations[i], dpm.Spec.Template.Annotations[i])
}
}
if dpm.Spec.Template.Annotations["bar"] != "foo" {
t.Error("Unable to set annotations")
}
expectedContainer := v1.Container{
Name: f1Name,
Image: "bar",
Ports: []v1.ContainerPort{
{
ContainerPort: int32(f1Port),
},
},
Env: []v1.EnvVar{
{
Name: "foo",
Value: "bar",
},
{
Name: "FUNC_HANDLER",
Value: "bar",
},
{
Name: "MOD_NAME",
Value: "foo",
},
{
Name: "FUNC_TIMEOUT",
Value: "180",
},
{
Name: "FUNC_RUNTIME",
Value: "python2.7",
},
{
Name: "FUNC_MEMORY_LIMIT",
Value: "0",
},
{
Name: "FUNC_PORT",
Value: strconv.Itoa(int(f1Port)),
},
{
Name: "KUBELESS_INSTALL_VOLUME",
Value: "/kubeless",
},
{
Name: "PYTHONPATH",
Value: "/kubeless/lib/python2.7/site-packages:/kubeless",
},
},
VolumeMounts: []v1.VolumeMount{
{
Name: f1Name,
MountPath: "/kubeless",
},
},
LivenessProbe: &v1.Probe{
InitialDelaySeconds: int32(5),
PeriodSeconds: int32(10),
Handler: v1.Handler{
Exec: &v1.ExecAction{
Command: []string{"curl", "-f", "http://localhost:8080/healthz"},
},
},
},
}
if !reflect.DeepEqual(dpm.Spec.Template.Spec.Containers[0], expectedContainer) {
t.Errorf("Unexpected container definition. Received:\n %+v\nExpecting:\n %+v", dpm.Spec.Template.Spec.Containers[0], expectedContainer)
}
expectedAffinity := &v1.Affinity{
PodAntiAffinity: &v1.PodAntiAffinity{
PreferredDuringSchedulingIgnoredDuringExecution: []v1.WeightedPodAffinityTerm{
{
Weight: 100,
PodAffinityTerm: v1.PodAffinityTerm{
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"created-by": "kubeless",
"function": f1Name,
},
},
TopologyKey: "kubernetes.io/hostname",
},
},
},
},
}
if !reflect.DeepEqual(dpm.Spec.Template.Spec.Affinity, expectedAffinity) {
t.Errorf("Unexpected pod affinity definition. Received:\n %+v\nExpecting:\n %+v", dpm.Spec.Template.Spec.Affinity, expectedAffinity)
}
secrets := dpm.Spec.Template.Spec.ImagePullSecrets
if secrets[0].Name != "creds" && secrets[1].Name != "p1" && secrets[2].Name != "p2" {
t.Errorf("Expected first secret to be 'p1' but found %v and second secret to be 'p2' and found %v", secrets[0], secrets[1])
}
// Init containers behavior should be tested with integration tests
if len(dpm.Spec.Template.Spec.InitContainers) < 1 {
t.Errorf("Expecting at least an init container to install deps")
}
if dpm.Spec.Template.Spec.InitContainers[0].Image != "unzip" {
t.Errorf("Unexpected init image %s", dpm.Spec.Template.Spec.InitContainers[0].Image)
}
if dpm.Spec.Template.Spec.InitContainers[0].Resources.Limits == nil {
t.Errorf("Resources must be set for init container")
}
// ensure my-secret is mounted as /var/run/secrets/kubeless.io/my-secret to install container
var container v1.Container
for _, c := range dpm.Spec.Template.Spec.InitContainers {
if c.Name == "install" {
container = c
}
}
if len(container.Name) == 0 {
t.Fatalf("Cannot find init container %q", "install")
}
var found bool
for _, v := range container.VolumeMounts {
if v.MountPath == "/var/run/secrets/kubeless.io/my-secret" {
found = true
}
}
if !found {
t.Fatalf("Cannot find volume mount /var/run/secrets/kubeless.io/my-secret")
}
}
func TestEnsureDeploymentWithoutFuncNorHandler(t *testing.T) {
funcName := "func2"
clientset, or, ns, lr := prepareDeploymentTest(funcName)
// If no handler and function is given it should not fail
f2 := getDefaultFunc(funcName, ns)
f2.Spec.Function = ""
f2.Spec.Handler = ""
err := EnsureFuncDeployment(clientset, f2, or, lr, "", "unzip", []v1.LocalObjectReference{})
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
_, err = clientset.AppsV1().Deployments(ns).Get(funcName, metav1.GetOptions{})
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
}
func TestEnsureDeploymentWithImage(t *testing.T) {
funcName := "func"
clientset, or, ns, lr := prepareDeploymentTest(funcName)
// If the Image has been already provided it should not resolve it
f3 := getDefaultFunc(funcName, ns)
f3.Spec.Deployment.Spec.Template.Spec.Containers[0].Image = "test-image"
err := EnsureFuncDeployment(clientset, f3, or, lr, "", "unzip", []v1.LocalObjectReference{})
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
dpm, err := clientset.AppsV1().Deployments(ns).Get(funcName, metav1.GetOptions{})
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
if dpm.Spec.Template.Spec.Containers[0].Image != "test-image" {
t.Errorf("Unexpected Image Name: %s", dpm.Spec.Template.Spec.Containers[0].Image)
}
}
func TestEnsureDeploymentWithoutFunc(t *testing.T) {
funcName := "func"
clientset, or, ns, lr := prepareDeploymentTest(funcName)
// If no function is given it should not use an init container
f4 := getDefaultFunc(funcName, ns)
f4.Spec.Function = ""
f4.Spec.Deps = ""
err := EnsureFuncDeployment(clientset, f4, or, lr, "", "unzip", []v1.LocalObjectReference{})
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
dpm, err := clientset.AppsV1().Deployments(ns).Get(funcName, metav1.GetOptions{})
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
if len(dpm.Spec.Template.Spec.InitContainers) > 0 {
t.Error("It should not setup an init container")
}
}
func TestEnsureUpdateDeployment(t *testing.T) {
f1Name := "f1"
clientset, or, ns, lr := prepareDeploymentTest(f1Name)
// It should update a deployment if it is already present
funcAnno := map[string]string{
"bar": "foo",
}
f1 := getDefaultFunc(f1Name, ns)
f1.Spec.Deployment.ObjectMeta = metav1.ObjectMeta{
Annotations: funcAnno,
}
f1.Spec.Deployment.Spec.Template.ObjectMeta = metav1.ObjectMeta{
Annotations: funcAnno,
}
err := EnsureFuncDeployment(clientset, f1, or, lr, "", "unzip", []v1.LocalObjectReference{})
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
f6 := kubelessApi.Function{}
f6 = *f1
f6.Spec.Handler = "foo.bar2"
f6.Spec.Deployment.ObjectMeta.Annotations["new-key"] = "value"
err = EnsureFuncDeployment(clientset, &f6, or, lr, "", "unzip", []v1.LocalObjectReference{})
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
// Unable to ensure that the new deployment is patched since fake
// ignores PATCH actions: https://github.com/kubernetes/client-go/issues/364
}
func TestAvoidDeploymentOverwrite(t *testing.T) {
f1Name := "f1"
clientset, or, ns, lr := prepareDeploymentTest(f1Name)
clientset.AppsV1().Deployments(ns).Create(&appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: f1Name,
Namespace: ns,
},
})
f1 := getDefaultFunc(f1Name, ns)
err := EnsureFuncDeployment(clientset, f1, or, lr, "", "unzip", []v1.LocalObjectReference{})
if err == nil && strings.Contains(err.Error(), "conflicting object") {
t.Errorf("It should fail because a conflict")
}
}
func TestDeploymentWithUnsupportedRuntime(t *testing.T) {
funcName := "func"
clientset, or, ns, lr := prepareDeploymentTest(funcName)
// It should return an error if some dependencies are given but the runtime is not supported
f7 := getDefaultFunc("func7", ns)
f7.Spec.Deps = "deps"
f7.Spec.Runtime = "cobol"
err := EnsureFuncDeployment(clientset, f7, or, lr, "", "unzip", []v1.LocalObjectReference{})
if err == nil {
t.Fatal("An error should be thrown")
}
}
func TestDeploymentWithTimeout(t *testing.T) {
funcName := "func"
clientset, or, ns, lr := prepareDeploymentTest(funcName)
// If a timeout is specified it should set an environment variable FUNC_TIMEOUT
f8 := getDefaultFunc(funcName, ns)
f8.Spec.Timeout = "10"
err := EnsureFuncDeployment(clientset, f8, or, lr, "", "unzip", []v1.LocalObjectReference{})
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
dpm, err := clientset.AppsV1().Deployments(ns).Get(funcName, metav1.GetOptions{})
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
if getEnvValueFromList("FUNC_TIMEOUT", dpm.Spec.Template.Spec.Containers[0].Env) != "10" {
t.Error("Unable to set timeout")
}
}
func TestDeploymentWithPrebuiltImage(t *testing.T) {
funcName := "func"
clientset, or, ns, lr := prepareDeploymentTest(funcName)
// If a prebuilt image is specified it should not build the function using init containers
f9 := getDefaultFunc(funcName, ns)
err := EnsureFuncDeployment(clientset, f9, or, lr, "user/image:test", "unzip", []v1.LocalObjectReference{})
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
dpm, err := clientset.AppsV1().Deployments(ns).Get(funcName, metav1.GetOptions{})
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
if dpm.Spec.Template.Spec.Containers[0].Image != "user/image:test" {
t.Errorf("Unexpected image %s, expecting prebuilt user/image:test", dpm.Spec.Template.Spec.Containers[0].Image)
}
if len(dpm.Spec.Template.Spec.InitContainers) != 0 {
t.Error("Unexpected init containers")
}
}
func TestDeploymentWithVolumes(t *testing.T) {
funcName := "func"
clientset, or, ns, lr := prepareDeploymentTest(funcName)
// It should include existing volumes
f10 := getDefaultFunc(funcName, ns)
f10.Spec.Deployment.Spec.Template.Spec.Volumes = []v1.Volume{
{
Name: "test",
VolumeSource: v1.VolumeSource{},
},
}
f10.Spec.Deployment.Spec.Template.Spec.Containers[0].VolumeMounts = []v1.VolumeMount{
{
Name: "test",
MountPath: "/tmp/test",
},
}
err := EnsureFuncDeployment(clientset, f10, or, lr, "", "unzip", []v1.LocalObjectReference{})
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
dpm, err := clientset.AppsV1().Deployments(ns).Get(funcName, metav1.GetOptions{})
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
if dpm.Spec.Template.Spec.Volumes[0].Name != "test" {
t.Error("Should maintain volumen test")
}
if dpm.Spec.Template.Spec.Containers[0].VolumeMounts[0].Name != "test" {
t.Error("Should maintain volumen test")
}
}
func TestEnsureDeploymentWithAffinityOverridden(t *testing.T) {
funcName := "func"
clientset, or, ns, lr := prepareDeploymentTest(funcName)
// If the Image has been already provided it should not resolve it
f3 := getDefaultFunc(funcName, ns)
f3.Spec.Deployment.Spec.Template.Spec.Affinity = &v1.Affinity{}
err := EnsureFuncDeployment(clientset, f3, or, lr, "", "unzip", []v1.LocalObjectReference{})
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
dpm, err := clientset.AppsV1().Deployments(ns).Get(funcName, metav1.GetOptions{})
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
expectedAffinity := &v1.Affinity{NodeAffinity: nil, PodAffinity: nil, PodAntiAffinity: nil}
if *dpm.Spec.Template.Spec.Affinity != *expectedAffinity {
t.Errorf(
"Unexpected Affinity Definition:\nExpecting: %+v\nReceived: %+v",
expectedAffinity,
dpm.Spec.Template.Spec.Affinity,
)
}
}
func doesNotContain(envs []v1.EnvVar, env v1.EnvVar) bool {
for _, e := range envs {
if e == env {
return false
}
}
return true
}
func TestGetProvisionContainer(t *testing.T) {
clientset := fake.NewSimpleClientset()
langruntime.AddFakeConfig(clientset)
lr := langruntime.SetupLangRuntime(clientset)
lr.ReadConfigMap()
rvol := v1.VolumeMount{Name: "runtime", MountPath: "/runtime"}
dvol := v1.VolumeMount{Name: "deps", MountPath: "/deps"}
resources := v1.ResourceRequirements{Limits: v1.ResourceList{v1.ResourceLimitsCPU: resource.MustParse("100m")}}
c, err := getProvisionContainer("test", "sha256:abc1234", "test.func", "test.foo", "text", "python2.7", "unzip", rvol, dvol, resources, lr)
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
expectedContainer := v1.Container{
Name: "prepare",
Image: "unzip",
Command: []string{"sh", "-c"},
Args: []string{"echo 'abc1234 /deps/test.func' > /tmp/func.sha256 && sha256sum -c /tmp/func.sha256 && cp /deps/test.func /runtime/test.py && cp /deps/requirements.txt /runtime"},
VolumeMounts: []v1.VolumeMount{rvol, dvol},
ImagePullPolicy: v1.PullIfNotPresent,
Resources: v1.ResourceRequirements{Limits: v1.ResourceList{v1.ResourceLimitsCPU: resource.MustParse("100m")}},
}
if !reflect.DeepEqual(expectedContainer, c) {
t.Errorf("Unexpected result:\n %+v", c)
}
// If the content type is encoded it should decode it
c, err = getProvisionContainer("Zm9vYmFyCg==", "sha256:abc1234", "test.func", "test.foo", "base64", "python2.7", "unzip", rvol, dvol, resources, lr)
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
if !strings.HasPrefix(c.Args[0], "base64 -d < /deps/test.func > /tmp/func.decoded") {
t.Errorf("Unexpected command: %s", c.Args[0])
}
secrets, err := lr.GetImageSecrets("python2.7")
if err != nil {
t.Errorf("Unable to fetch secrets: %v", err)
}
if secrets[0].Name != "p1" && secrets[1].Name != "p2" {
t.Errorf("Expected first secret to be 'p1' but found %v and second secret to be 'p2' but found %v", secrets[0], secrets[1])
}
// It should skip the dependencies installation if the runtime is not supported
c, err = getProvisionContainer("function", "sha256:abc1234", "test.func", "test.foo", "text", "cobol", "unzip", rvol, dvol, resources, lr)
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
if strings.Contains(c.Args[0], "cp /deps ") {
t.Errorf("Unexpected command: %s", c.Args[0])
}
// It should extract the file in case it is a Zip
c, err = getProvisionContainer("Zm9vYmFyCg==", "sha256:abc1234", "test.zip", "test.foo", "base64+zip", "python2.7", "unzip", rvol, dvol, resources, lr)
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
if !strings.HasPrefix(c.Args[0], "base64 -d < /deps/test.zip > /tmp/func.decoded") {
t.Errorf("Unexpected command: %s", c.Args[0])
}
if !strings.Contains(c.Args[0], "unzip -o /tmp/func.decoded -d /runtime") {
t.Errorf("Unexpected command: %s", c.Args[0])
}
// It should extract the compressed tar file
c, err = getProvisionContainer("Zm9vYmFyCg==", "sha256:abc1234", "test.tar.gz", "test.foo", "base64+compressedtar", "python2.7", "unzip", rvol, dvol, resources, lr)
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
if !strings.HasPrefix(c.Args[0], "base64 -d < /deps/test.tar.gz > /tmp/func.decoded") {
t.Errorf("Unexpected command: %s", c.Args[0])
}
if !strings.Contains(c.Args[0], "tar xf /tmp/func.decoded -C /runtime") {
t.Errorf("Unexpected command: %s", c.Args[0])
}
// If the content type is url it should use curl
c, err = getProvisionContainer("https://raw.githubusercontent.com/test/test/test/test.py", "sha256:abc1234", "", "test.foo", "url", "python2.7", "unzip", rvol, dvol, resources, lr)
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
if !strings.HasPrefix(c.Args[0], "curl 'https://raw.githubusercontent.com/test/test/test/test.py' -L --silent --output /tmp/func.fromurl") {
t.Errorf("Unexpected command: %s", c.Args[0])
}
// If the content type is url+zip it should use curl and unzip
c, err = getProvisionContainer("https://raw.githubusercontent.com/test/test/test/test.zip", "sha256:abc1234", "", "test.foo", "url+zip", "python2.7", "unzip", rvol, dvol, resources, lr)
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
if !strings.HasPrefix(c.Args[0], "curl 'https://raw.githubusercontent.com/test/test/test/test.zip' -L --silent --output /tmp/func.fromurl") {
t.Errorf("Unexpected command: %s", c.Args[0])
}
if !strings.Contains(c.Args[0], "unzip -o /tmp/func.fromurl -d /runtime") {
t.Errorf("Unexpected command: %s", c.Args[0])
}
// If the content type is url+compressedtar it should use curl and tar
c, err = getProvisionContainer("https://raw.githubusercontent.com/test/test/test/test.tar.gz", "sha256:abc1234", "", "test.foo", "url+compressedtar", "python2.7", "unzip", rvol, dvol, resources, lr)
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
if !strings.HasPrefix(c.Args[0], "curl 'https://raw.githubusercontent.com/test/test/test/test.tar.gz' -L --silent --output /tmp/func.fromurl") {
t.Errorf("Unexpected command: %s", c.Args[0])
}
if !strings.Contains(c.Args[0], "tar xf /tmp/func.fromurl -C /runtime") {
t.Errorf("Unexpected command: %s", c.Args[0])
}
// if the function use bundled deps in remote zip file
c, err = getProvisionContainer("https://raw.githubusercontent.com/test/test/test/test.zip", "sha256:abc1234", "", "test.foo", "url+zip+deps", "python2.7", "unzip", rvol, dvol, resources, lr)
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
if !strings.HasPrefix(c.Args[0], "curl 'https://raw.githubusercontent.com/test/test/test/test.zip' -L --silent --output /tmp/func.fromurl") {
t.Errorf("Unexpected command: %s", c.Args[0])
}
if !strings.Contains(c.Args[0], "unzip -o /tmp/func.fromurl -d /runtime") {
t.Errorf("Unexpected command: %s", c.Args[0])
}
// use bundled deps will not copy the requirements.txt to /runtime
if strings.Contains(c.Args[0], "cp /deps/requirements.txt /runtime") {
t.Errorf("Unexpected command: %s", c.Args[0])
}
}
================================================
FILE: pkg/utils/metrics.go
================================================
/*
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.
*/
package utils
import (
"bytes"
"k8s.io/client-go/kubernetes"
"github.com/prometheus/common/expfmt"
)
// Metric contains metrics for a functions
type Metric struct {
FunctionName string `json:"function,omitempty"`
Namespace string `json:"namespace,omitempty"`
Method string `json:"method,omitempty"`
Message string `json:"message,omitempty"`
TotalCalls float64 `json:"total_calls,omitempty"`
TotalFailures float64 `json:"total_failures,omitempty"`
TotalDurationSeconds float64 `json:"total_duration_seconds,omitempty"`
AvgDurationSeconds float64 `json:"avg_duration_seconds,omitempty"`
}
// MetricsRetriever is an interface for retreiving metrics from an endpoint
type MetricsRetriever interface {
GetRawMetrics(kubernetes.Interface, string, string) ([]byte, error)
}
// PrometheusMetricsHandler is a handler for retreiving metrics from Prometheus
type PrometheusMetricsHandler struct{}
func parseMetrics(namespace, functionName string, rawMetrics []byte) ([]*Metric, error) {
parser := expfmt.TextParser{}
parsedData, err := parser.TextToMetricFamilies(bytes.NewReader(rawMetrics))
if err != nil {
return nil, err
}
tmp := map[string]*Metric{}
var parsedMetrics []*Metric
metricsOfInterest := []string{"function_duration_seconds", "function_calls_total", "function_failures_total"}
for _, m := range metricsOfInterest {
for _, metric := range parsedData[m].GetMetric() {
// a function can have metrics for multiple methods (GET, POST, etc.)
// method names can be values other than GET/POST/PUT/DELETE
for _, label := range metric.GetLabel() {
if label.GetName() == "method" {
if _, ok := tmp[label.GetValue()]; !ok {
tmp[label.GetValue()] = &Metric{
FunctionName: functionName,
Namespace: namespace,
Method: label.GetValue(),
}
}
if m == "function_failures_total" {
tmp[label.GetValue()].TotalFailures = metric.GetCounter().GetValue()
}
if m == "function_duration_seconds" {
tmp[label.GetValue()].TotalDurationSeconds = metric.GetHistogram().GetSampleSum()
}
if m == "function_calls_total" {
tmp[label.GetValue()].TotalCalls = metric.GetCounter().GetValue()
if tmp[label.GetValue()].TotalCalls > 0 {
tmp[label.GetValue()].AvgDurationSeconds = float64(tmp[label.GetValue()].TotalDurationSeconds) / tmp[label.GetValue()].TotalCalls
}
}
}
}
}
}
// if the funciton hasn't been invoked, add an item to the list so the function displays in the output
if len(tmp) == 0 {
tmp[""] = &Metric{
FunctionName: functionName,
Namespace: namespace,
}
}
for _, v := range tmp {
parsedMetrics = append(parsedMetrics, v)
}
return parsedMetrics, nil
}
// GetRawMetrics returns the raw metrics for a Prometheus endpoint
func (h *PrometheusMetricsHandler) GetRawMetrics(apiV1Client kubernetes.Interface, namespace, functionName string) ([]byte, error) {
port, err := GetFunctionPort(apiV1Client, namespace, functionName)
if err != nil {
return []byte{}, err
}
req := apiV1Client.CoreV1().RESTClient().Get().Namespace(namespace).Resource("services").SubResource("proxy").Name(functionName + ":" + port).Suffix("/metrics")
return req.Do().Raw()
}
// GetFunctionMetrics returns Prometheus metrics as a slice of *Metrics
func GetFunctionMetrics(apiV1Client kubernetes.Interface, h MetricsRetriever, namespace, functionName string) []*Metric {
res, err := h.GetRawMetrics(apiV1Client, namespace, functionName)
if err != nil {
return []*Metric{
{
FunctionName: functionName,
Namespace: namespace,
Message: "Function does not expose metrics",
},
}
}
metrics, err := parseMetrics(namespace, functionName, res)
if err != nil {
return []*Metric{
{
FunctionName: functionName,
Namespace: namespace,
Message: "Unable to get function metrics",
},
}
}
return metrics
}
================================================
FILE: pkg/version/version.go
================================================
/*
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.
*/
package version
var (
// Version will be set automatically by the build system via -ldflags
Version string
)
================================================
FILE: script/.validate
================================================
#!/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.
if [ -z "$VALIDATE_UPSTREAM" ]; then
# this is kind of an expensive check, so let's not do this twice if we
# are running more than one validate bundlescript
VALIDATE_REPO='https://github.com/kubeless/kubeless.git'
VALIDATE_BRANCH='master'
if [ "$TRAVIS" = 'true' -a "$TRAVIS_PULL_REQUEST" != 'false' ]; then
VALIDATE_REPO="https://github.com/${TRAVIS_REPO_SLUG}.git"
VALIDATE_BRANCH="${TRAVIS_BRANCH}"
fi
VALIDATE_HEAD="$(git rev-parse --verify HEAD)"
git fetch -q "$VALIDATE_REPO" "refs/heads/$VALIDATE_BRANCH"
VALIDATE_UPSTREAM="$(git rev-parse --verify FETCH_HEAD)"
VALIDATE_COMMIT_LOG="$VALIDATE_UPSTREAM..$VALIDATE_HEAD"
VALIDATE_COMMIT_DIFF="$VALIDATE_UPSTREAM...$VALIDATE_HEAD"
validate_diff() {
if [ "$VALIDATE_UPSTREAM" != "$VALIDATE_HEAD" ]; then
git diff "$VALIDATE_COMMIT_DIFF" "$@"
fi
}
validate_log() {
if [ "$VALIDATE_UPSTREAM" != "$VALIDATE_HEAD" ]; then
git log "$VALIDATE_COMMIT_LOG" "$@"
fi
}
fi
================================================
FILE: script/binary
================================================
#!/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)
BUILD_FLAGS=(-ldflags="-w -X github.com/kubeless/kubeless/pkg/version.Version=${GIT_COMMIT}")
# Get rid of existing binary
echo "Removing Old Kubeless binaries"
rm -f ${GOPATH%%:*}/bin/kubeless
rm -f ${GOPATH%%:*}/bin/function-controller
echo "Build Kubeless Components binaries"
# Build binary
go install \
"${BUILD_FLAGS[@]}" \
./cmd/...
if [ $? -eq 0 ]; then
echo "Build Kubeless Components successful. Program saved at ${GOPATH%%:*}/bin"
else
echo "Build Kubeless Components failed."
fi
================================================
FILE: script/binary-cli
================================================
#!/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="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[@]}" \
./cmd/kubeless
================================================
FILE: script/binary-controller
================================================
#!/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-function-controller"
else
TARGET=($3)
fi
if [ -z "$4" ]; then
PKG="./cmd/function-controller"
else
PKG=($4)
fi
GIT_COMMIT=$(git describe --tags --dirty)
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"
================================================
FILE: script/cluster-up-minikube.sh
================================================
#!/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:?}
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
}
}
# Install nsenter if missing
check_or_build_nsenter
# Install minikube if missing
check_or_install_minikube
MINIKUBE_BIN=$(which minikube)
# Start minikube
sudo -E ${MINIKUBE_BIN} start --vm-driver=none \
--extra-config=kubelet.cgroup-driver=cgroupfs \
--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
================================================
FILE: script/create_release.sh
================================================
#!/bin/bash
set -e
REPO_NAME=kubeless
REPO_DOMAIN=kubeless
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
================================================
FILE: script/enable-gcloud.sh
================================================
#!/bin/bash
set -e
BUILD_DIR=${1:?}
export GOOGLE_APPLICATION_CREDENTIALS=$BUILD_DIR/client_secrets.json
echo $GCLOUD_KEY > $GOOGLE_APPLICATION_CREDENTIALS
if [ ! -d $HOME/gcloud/google-cloud-sdk ]; then
mkdir -p $HOME/gcloud &&
wget -q https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-sdk-187.0.0-linux-x86_64.tar.gz --directory-prefix=$HOME/gcloud &&
cd $HOME/gcloud &&
tar xzf google-cloud-sdk-187.0.0-linux-x86_64.tar.gz &&
printf '\ny\n\ny\ny\n' | ./google-cloud-sdk/install.sh &&
sudo ln -s $HOME/gcloud/google-cloud-sdk/bin/gcloud /usr/local/bin/gcloud
cd $BUILD_DIR;
fi
gcloud -q config set project $GKE_PROJECT
if [ -a $GOOGLE_APPLICATION_CREDENTIALS ]; then
gcloud -q auth activate-service-account --key-file $GOOGLE_APPLICATION_CREDENTIALS;
fi
================================================
FILE: script/find_digest.sh
================================================
#!/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
================================================
FILE: script/integration-tests
================================================
#!/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
;;
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
;;
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
================================================
FILE: script/libtest.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.
# 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 -c kubeless-function-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
================================================
FILE: script/pull-or-build-image.sh
================================================
#!/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
"function-controller")
image=${CONTROLLER_IMAGE:?}
docker pull $image || make $TARGET 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
================================================
FILE: script/release_utils.sh
================================================
#!/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"
}
================================================
FILE: script/start-gke-env.sh
================================================
#!/bin/bash
CLUSTER=${1:?}
ZONE=${2:?}
BRANCH=${3:?}
ADMIN=${4:?}
# Resolve latest version from a branch
VERSION=$(gcloud container get-server-config --zone $ZONE --format='yaml(validMasterVersions)' 2> /dev/null | grep $BRANCH | awk '{print $2}' | head -n 1)
function clean() {
local resource=${1:?}
kubectl get $resource | awk '{print $1}' | xargs kubectl delete $resource || true
}
if ! gcloud container clusters list; then
echo "Unable to access gcloud project"
exit 1
fi
if gcloud container clusters list | grep -q $CLUSTER; then
echo "GKE cluster already exits. Deleting resources"
# Cluster already exists, make sure it is clean
gcloud container clusters get-credentials $CLUSTER --zone $ZONE
kubectl delete ns kubeless || true
resources=(
cronjobs
jobs
deployments
horizontalpodautoscalers
)
for res in "${resources[@]}"; do
clean $res
done
echo "Removing clusterroles" >&9
kubectl delete clusterrole kubeless-controller-deployer || true
kubectl delete clusterrole kafka-controller-deployer || true
kubectl delete clusterrolebindings kubeless-controller-deployer || true
kubectl delete clusterrolebindings kafka-controller-deployer || true
echo "Removing customresourcecleanup.apiextensions.k8s.io finalizer from CRD's" >&9
kubectl patch crd/functions.kubeless.io -p '{"metadata":{"finalizers":[]}}' --type=merge || true
kubectl patch crd/cronjobtriggers.kubeless.io -p '{"metadata":{"finalizers":[]}}' --type=merge || true
kubectl patch crd/httptriggers.kubeless.io -p '{"metadata":{"finalizers":[]}}' --type=merge || true
kubectl patch crd/kafkatriggers.kubeless.io -p '{"metadata":{"finalizers":[]}}' --type=merge || true
echo "Removing finalizers from CRD object's and deleting the CRD objects" >&9
functions=$(kubectl get functions -o name)
for func in $functions; do
kubectl patch $func -p '{"metadata":{"finalizers":[]}}' --type=merge || true
kubectl delete $func
done
cronjobtriggers=$(kubectl get cronjobtriggers -o name)
for trigger in $cronjobtriggers; do
kubectl patch $trigger -p '{"metadata":{"finalizers":[]}}' --type=merge || true
kubectl delete $trigger
done
httptriggers=$(kubectl get httptriggers -o name)
for trigger in $httptriggers; do
kubectl patch $trigger -p '{"metadata":{"finalizers":[]}}' --type=merge || true
kubectl delete $trigger
done
kafkatriggers=$(kubectl get kafkatriggers -o name)
for trigger in $kafkatriggers; do
kubectl patch $trigger -p '{"metadata":{"finalizers":[]}}' --type=merge || true
kubectl delete $trigger
done
echo "Deleting CRD's" >&9
kubectl delete crd functions.kubeless.io || true
kubectl delete crd cronjobtriggers.kubeless.io || true
kubectl delete crd httptriggers.kubeless.io || true
kubectl delete crd kafkatriggers.kubeless.io || true
else
echo "Creating cluster $CLUSTER in $ZONE (v$VERSION)"
gcloud container clusters create --cluster-version=$VERSION --zone $ZONE $CLUSTER --num-nodes 5 --machine-type=n1-standard-2
# Wait for the cluster to respond
cnt=20
until kubectl get pods; do
((cnt=cnt-1)) || (echo "Waited 20 seconds but cluster is not reachable" && return 1)
sleep 1
done
kubectl create clusterrolebinding kubeless-cluster-admin --clusterrole=cluster-admin --user=$ADMIN
fi
================================================
FILE: script/start-test-environment.sh
================================================
#!/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"
================================================
FILE: script/upload_release_notes.sh
================================================
#!/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
================================================
FILE: script/validate-git-marks
================================================
#!/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
================================================
FILE: script/validate-gofmt
================================================
#!/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
================================================
FILE: script/validate-lint
================================================
#!/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
================================================
FILE: script/validate-test
================================================
#!/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/...
================================================
FILE: script/validate-vet
================================================
#!/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 tool vet
if ! go vet "$f"; then
failed=1
fi
done
exit $failed
================================================
FILE: tests/deployment-tests.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
@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
}
================================================
FILE: tests/integration-tests-cronjob.bats
================================================
#!/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
@test "Create Cronjob Trigger" {
deploy_function get-python
verify_function get-python
create_cronjob_trigger get-python '* * * * *'
verify_cronjob_trigger get-python '* * * * *' '"GET / HTTP/1.1" 200'
update_cronjob_trigger get-python '*/60 * * * *'
verify_cronjob_trigger get-python '*/60 * * * *' '"GET / HTTP/1.1" 200'
delete_cronjob_trigger get-python
verify_clean_object cronjobtrigger get-python
}
@test "Test no-errors" {
if kubectl logs -n kubeless -l kubeless=controller | grep "level=error"; then
echo "Found errors in the controller logs"
false
fi
}
================================================
FILE: tests/integration-tests-http.bats
================================================
#!/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
@test "Wait for Ingress" {
wait_for_ingress
}
@test "Create HTTP Trigger" {
deploy_function get-python
verify_function get-python
create_http_trigger get-python "test.domain"
verify_http_trigger get-python $(minikube ip) "hello.*world" "test.domain"
update_http_trigger get-python "test.domain-updated"
verify_http_trigger get-python $(minikube ip) "hello.*world" "test.domain-updated"
delete_http_trigger get-python
verify_clean_object httptrigger ing-get-python
verify_clean_object ingress ing-get-python
}
@test "Create HTTP Trigger with a path" {
deploy_function get-python
verify_function get-python
create_http_trigger get-python "test.domain" "get-python"
verify_http_trigger get-python $(minikube ip) "hello.*world" "test.domain" "get-python"
delete_http_trigger get-python
verify_clean_object httptrigger ing-get-python
verify_clean_object ingress ing-get-python
}
@test "Create HTTP Trigger with TLS private key and certificate" {
deploy_function get-python
verify_function get-python
create_tls_secret_from_key_cert foo-secret
create_http_trigger_with_tls_secret get-python "foo.bar.com" "get-python" "foo-secret"
verify_https_trigger get-python $(minikube ip) "hello.*world" "foo.bar.com" "get-python"
delete_http_trigger get-python
verify_clean_object httptrigger ing-get-python
verify_clean_object ingress ing-get-python
}
@test "Create HTTP Trigger with basic auth" {
deploy_function get-python
verify_function get-python
create_basic_auth_secret "basic-auth"
create_http_trigger get-python "test.domain" "get-python" "basic-auth" "nginx"
verify_http_trigger_basic_auth get-python $(minikube ip) "hello.*world" "test.domain" "get-python" "foo:bar"
delete_http_trigger get-python
verify_clean_object httptrigger ing-get-python
verify_clean_object ingress ing-get-python
}
@test "Test no-errors" {
if kubectl logs -n kubeless -l kubeless=controller | grep "level=error"; then
echo "Found errors in the controller logs"
false
fi
}
================================================
FILE: tests/integration-tests-kafka.bats
================================================
#!/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 "Wait for kafka" {
deploy_kafka
wait_for_kubeless_kafka_server_ready
}
@test "Test function: pubsub-python" {
deploy_function pubsub-python
verify_function pubsub-python
kubeless_function_delete pubsub-python
}
@test "Test function: pubsub-python34" {
deploy_function pubsub-python34
verify_function pubsub-python34
kubeless_function_delete pubsub-python34
}
@test "Test 1:n association between Kafka trigger and functions" {
deploy_function kafka-python-func1-topic-s3-python
deploy_function kafka-python-func2-topic-s3-python
deploy_kafka_trigger s3-python-kafka-trigger
verify_function kafka-python-func1-topic-s3-python
verify_function kafka-python-func2-topic-s3-python
kubeless_function_delete kafka-python-func1-topic-s3-python
kubeless_function_delete kafka-python-func2-topic-s3-python
}
@test "Test function: pubsub-nodejs" {
deploy_function pubsub-nodejs
verify_function pubsub-nodejs
test_kubeless_function_update pubsub-nodejs
kubeless_function_delete pubsub-nodejs
}
@test "Test function: pubsub-ruby" {
deploy_function pubsub-ruby
verify_function pubsub-ruby
kubeless_function_delete pubsub-ruby
}
@test "Test function: pubsub-go" {
deploy_function pubsub-go
verify_function pubsub-go
kubeless_function_delete pubsub-go
}
@test "Test topic list" {
wait_for_kubeless_kafka_server_ready
for topic in topic1 topic2; do
kubeless topic create $topic
_wait_for_kubeless_kafka_topic_ready $topic
done
kubeless topic list >$BATS_TMPDIR/kubeless-topic-list
grep -qxF topic1 $BATS_TMPDIR/kubeless-topic-list
grep -qxF topic2 $BATS_TMPDIR/kubeless-topic-list
}
@test "Test topic deletion" {
test_topic_deletion
}
@test "Verify Kafka after restart (if context=='minikube')" {
local topic=$RANDOM
kubeless topic create $topic
sts_restart
kubeless topic list | grep $topic
}
# vim: ts=2 sw=2 si et syntax=sh
================================================
FILE: tests/integration-tests-kinesis.bats
================================================
#!/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 Kinesalite" {
deploy_kinesis_trigger_controller
wait_for_kubeless_kinesis_controller_ready
deploy_kinesalite
wait_for_kinesalite_pod
}
@test "Test function: stream-python-kinesis" {
deploy_function python-kinesis
verify_function python-kinesis
kubeless_function_delete python-kinesis
}
@test "Test function: stream-multi-record-pubish-python-kinesis" {
deploy_function python-kinesis-multi-record
verify_function python-kinesis-multi-record
kubeless_function_delete python-kinesis-multi-record
}
================================================
FILE: tests/integration-tests-nats.bats
================================================
#!/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
}
================================================
FILE: tests/integration-tests-prebuilt.bats
================================================
#!/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
@test "Ensure build step" {
kubectl get -n kubeless configMap kubeless-config -o yaml | grep enable-build-step | grep true
kubectl get -n kubeless configMap kubeless-config -o yaml | grep function-registry-tls-verify | grep false
kubectl get secret kubeless-registry-credentials ||
kubectl create secret docker-registry kubeless-registry-credentials \
--docker-server=http://$(minikube ip):5000/v2 \
--docker-username="user" \
--docker-password="password" \
--docker-email="email"
}
@test "Deploy a function using the build system" {
deploy_function get-python
wait_for_job get-python
curl http://$(minikube ip):5000/v2/_catalog
# Speed up pod start when the image is ready
restart_function get-python
verify_function get-python
kubectl logs -n kubeless -l kubeless=controller -c kubeless-function-controller | grep "Started function build job"
kubectl get deployment -o yaml get-python | grep image | grep $(minikube ip):5000
}
@test "Deploy a Golang function using the build system" {
deploy_function get-go-deps
wait_for_job get-go-deps
# Speed up pod start when the image is ready
restart_function get-go-deps
verify_function get-go-deps
kubectl get deployment -o yaml get-go-deps | grep image | grep $(minikube ip):5000
}
@test "Test no-errors" {
if kubectl logs -n kubeless -l kubeless=controller | grep "level=error"; then
echo "Found errors in the controller logs"
false
fi
}
================================================
FILE: tests/integration-tests.bats
================================================
#!/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 functions to evaluate" {
deploy_function get-python
deploy_function get-python-deps
deploy_function get-python-deps-tar-gz
deploy_function get-python-deps-tar-bz2
deploy_function get-python-deps-tar-xz
deploy_function get-python-custom-port
deploy_function timeout-nodejs
deploy_function get-nodejs-multi
deploy_function get-python-metadata
deploy_function get-python-secrets
deploy_function post-python
deploy_function custom-get-python
deploy_function get-python-url-deps
deploy_function get-node-url-zip
deploy_function get-node-url-tar-gz
deploy_function get-node-url-tar-bz2
deploy_function get-node-url-tar-xz
}
@test "Test function: get-python" {
verify_function get-python
}
@test "Test function: get-python-deps" {
verify_function get-python-deps
}
@test "Test function: get-python-deps-tar-gz" {
verify_function get-python-deps-tar-gz
kubeless_function_delete get-python-deps-tar-gz
}
@test "Test function: get-python-deps-tar-bz2" {
verify_function get-python-deps-tar-bz2
kubeless_function_delete get-python-deps-tar-bz2
}
@test "Test function: get-python-deps-tar-xz" {
verify_function get-python-deps-tar-xz
kubeless_function_delete get-python-deps-tar-xz
}
@test "Test function: get-python-custom-port" {
verify_function get-python-custom-port
}
@test "Test function update: get-python" {
test_kubeless_function_update get-python
}
@test "Test function update: get-python-deps" {
test_kubeless_function_update get-python-deps
kubeless_function_delete get-python-deps
}
@test "Test function autoscale: get-python" {
if kubectl api-versions | tr '\n' ' ' | grep -q -v "autoscaling/v2beta1"; then
skip "Autoscale is only supported for Kubernetes >= 1.8"
fi
test_kubeless_autoscale get-python
kubeless_function_delete get-python
}
@test "Test function: timeout-nodejs" {
verify_function timeout-nodejs
kubeless_function_delete timeout-nodejs
}
@test "Test function: get-nodejs-multi" {
verify_function get-nodejs-multi
kubeless_function_delete get-nodejs-multi
}
@test "Test custom runtime image" {
verify_function custom-get-python
test_kubeless_function_update custom-get-python
kubeless_function_delete custom-get-python
}
@test "Test function: post-python" {
verify_function post-python
kubeless_function_delete post-python
}
@test "Test function: get-python-metadata" {
verify_function get-python-metadata
kubeless_function_delete get-python-metadata
}
@test "Test function: get-python-secrets" {
verify_function get-python-secrets
kubeless_function_delete get-python-secrets
}
@test "Test no-errors" {
if kubectl logs -n kubeless -l kubeless=controller | grep "level=error"; then
echo "Found errors in the controller logs"
false
fi
}
@test "Test function: get-python-url-deps" {
verify_function get-python-url-deps
kubeless_function_delete get-python-url-deps
}
@test "Test function: get-node-url-zip" {
verify_function get-node-url-zip
kubeless_function_delete get-node-url-zip
}
@test "Test function: get-node-url-tar-gz" {
verify_function get-node-url-tar-gz
kubeless_function_delete get-node-url-tar-gz
}
@test "Test function: get-node-url-tar-bz2" {
verify_function get-node-url-tar-bz2
kubeless_function_delete get-node-url-tar-bz2
}
@test "Test function: get-node-url-tar-xz" {
verify_function get-node-url-tar-xz
kubeless_function_delete get-node-url-tar-xz
}
# vim: ts=2 sw=2 si et syntax=sh