Istio integration (#421)

This the very first step for fission to integrate with Istio, which is an open platform to connect, manage, and secure microservices. With Istio, users are able to monitor functions usage and trace requests latency through dashboards. For more information, please visit http://fission.io/docs/
This commit is contained in:
Ta-Ching Chen
2018-02-28 04:09:32 +08:00
committed by GitHub
parent b9a559af2f
commit 23942fdf7d
29 changed files with 731 additions and 98 deletions
@@ -0,0 +1,203 @@
---
title: "Enabling Istio on Fission"
draft: false
weight: 42
---
This is the very first step for fission to integrate with [Istio](https://istio.io/). For those interested in trying to integrate fission with istio, following is the set up tutorial.
## Test Environment
* Google Kubernetes Engine: 1.9.2-gke.1
## Set Up
### Create Kubernetes v1.9+ cluster
Enable both RBAC & initializer features on kubernetes cluster.
``` bash
$ export ZONE=<zone name>
$ gcloud container clusters create istio-demo-1 \
--machine-type=n1-standard-2 \
--num-nodes=1 \
--no-enable-legacy-authorization \
--zone=$ZONE \
--cluster-version=1.9.2-gke.1
```
### Grant cluster admin permissions
Grant admin permission for `system:serviceaccount:kube-system:default` and current user.
``` bash
# for system:serviceaccount:kube-system:default
$ kubectl create clusterrolebinding --user system:serviceaccount:kube-system:default kube-system-cluster-admin --clusterrole cluster-admin
# for current user
$ kubectl create clusterrolebinding cluster-admin-binding --clusterrole=cluster-admin --user=$(gcloud config get-value core/account)
```
### Set up Istio environment
For Istio 0.5.1 you can follow the installation tutorial below. Also, you can follow the latest installation guides on Istio official site: [Quick Start](https://istio.io/docs/setup/kubernetes/quick-start.html) and [Sidecar Injection](https://istio.io/docs/setup/kubernetes/sidecar-injection.html).
Download Istio 0.5.1
``` bash
$ export ISTIO_VERSION=0.5.1
$ curl -L https://git.io/getLatestIstio | sh -
$ cd istio-0.5.1
```
Apply istio related YAML files
``` bash
$ kubectl apply -f install/kubernetes/istio.yaml
```
Automatic sidecar injection
``` bash
$ kubectl api-versions | grep admissionregistration
admissionregistration.k8s.io/v1beta1
```
Installing the webhook
Download the missing files in istio release 0.5.1
``` bash
$ wget https://raw.githubusercontent.com/istio/istio/master/install/kubernetes/webhook-create-signed-cert.sh -P install/kubernetes/
$ wget https://raw.githubusercontent.com/istio/istio/master/install/kubernetes/webhook-patch-ca-bundle.sh -P install/kubernetes/
$ chmod +x install/kubernetes/webhook-create-signed-cert.sh install/kubernetes/webhook-patch-ca-bundle.sh
```
Install the sidecar injection configmap.
``` bash
$ ./install/kubernetes/webhook-create-signed-cert.sh \
--service istio-sidecar-injector \
--namespace istio-system \
--secret sidecar-injector-certs
$ kubectl apply -f install/kubernetes/istio-sidecar-injector-configmap-release.yaml
```
Install the sidecar injector
``` bash
$ cat install/kubernetes/istio-sidecar-injector.yaml | \
./install/kubernetes/webhook-patch-ca-bundle.sh > \
install/kubernetes/istio-sidecar-injector-with-ca-bundle.yaml
$ kubectl apply -f install/kubernetes/istio-sidecar-injector-with-ca-bundle.yaml
# Check sidecar injector status
$ kubectl -n istio-system get deployment -listio=sidecar-injector
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
istio-sidecar-injector 1 1 1 1 26s
```
### Install fission
Set default namespace for helm installation, here we use `fission` as example namespace.
``` bash
$ export FISSION_NAMESPACE=fission
```
Create namespace & add label for Istio sidecar injection.
``` bash
$ kubectl create namespace $FISSION_NAMESPACE
$ kubectl label namespace $FISSION_NAMESPACE istio-injection=enabled
$ kubectl config set-context $(kubectl config current-context) --namespace=$FISSION_NAMESPACE
```
Follow the [installation guide](../../installation/) to install fission with flag `enableIstio` true.
``` bash
$ helm install --namespace $FISSION_NAMESPACE --set enableIstio=true --name istio-demo <chart-fission-all-url>
```
### Create a function
Set environment
``` bash
$ export FISSION_URL=http://$(kubectl --namespace fission get svc controller -o=jsonpath='{..ip}')
$ export FISSION_ROUTER=$(kubectl --namespace fission get svc router -o=jsonpath='{..ip}')
```
Let's create a simple function with Node.js.
``` js
# hello.js
module.exports = async function(context) {
console.log(context.request.headers);
return {
status: 200,
body: "Hello, World!\n"
};
}
```
Create environment
``` bash
$ fission env create --name nodejs --image fission/node-env:latest
```
Create function
``` bash
$ fission fn create --name h1 --env nodejs --code hello.js --method GET
```
Create route
``` bash
$ fission route create --method GET --url /h1 --function h1
```
Access function
``` bash
$ curl http://$FISSION_ROUTER/h1
Hello, World!
```
### Install Istio Add-ons
* Prometheus
``` bash
$ kubectl apply -f istio-0.5.1/install/kubernetes/addons/prometheus.yaml
$ kubectl -n istio-system port-forward $(kubectl -n istio-system get pod -l app=prometheus -o jsonpath='{.items[0].metadata.name}') 9090:9090
```
Web Link: [http://127.0.0.1:9090/graph](http://127.0.0.1:9090/graph)
* Grafana
Please install Prometheus first.
![grafana min](https://user-images.githubusercontent.com/202578/33528556-639493e2-d89d-11e7-9768-976fb9208646.png)
``` bash
$ kubectl apply -f istio-0.5.1/install/kubernetes/addons/grafana.yaml
$ kubectl -n istio-system port-forward $(kubectl -n istio-system get pod -l app=grafana -o jsonpath='{.items[0].metadata.name}') 3000:3000
```
Web Link: [http://127.0.0.1:3000/dashboard/db/istio-dashboard](http://127.0.0.1:3000/dashboard/db/istio-dashboard)
* Jaegar
![jaeger min](https://user-images.githubusercontent.com/202578/33528554-572c4f28-d89d-11e7-8a01-1543fc2aa064.png)
``` bash
$ kubectl apply -n istio-system -f https://raw.githubusercontent.com/jaegertracing/jaeger-kubernetes/master/all-in-one/jaeger-all-in-one-template.yml
$ kubectl port-forward -n istio-system $(kubectl get pod -n istio-system -l app=jaeger -o jsonpath='{.items[0].metadata.name}') 16686:16686
```
Web Link: [http://localhost:16686](http://localhost:16686)
+22 -2
View File
@@ -23,6 +23,7 @@ import (
"log" "log"
"net/http" "net/http"
"strings" "strings"
"time"
"github.com/fission/fission" "github.com/fission/fission"
builder "github.com/fission/fission/builder" builder "github.com/fission/fission/builder"
@@ -45,10 +46,29 @@ func (c *Client) Build(req *builder.PackageBuildRequest) (*builder.PackageBuildR
if err != nil { if err != nil {
return nil, err return nil, err
} }
resp, err := http.Post(c.url, "application/json", bytes.NewReader(body))
if err != nil { maxRetries := 20
var resp *http.Response
for i := 0; i < maxRetries; i++ {
resp, err = http.Post(c.url, "application/json", bytes.NewReader(body))
if err == nil {
if resp.StatusCode == 200 {
break
}
err = fission.MakeErrorFromHTTP(resp)
}
if i < maxRetries-1 {
time.Sleep(50 * time.Duration(2*i) * time.Millisecond)
log.Printf("Error building package (%v), retrying", err)
continue
}
return nil, err return nil, err
} }
defer resp.Body.Close() defer resp.Body.Close()
rBody, err := ioutil.ReadAll(resp.Body) rBody, err := ioutil.ReadAll(resp.Body)
+3 -1
View File
@@ -24,6 +24,7 @@ import (
// Start the buildermgr service. // Start the buildermgr service.
func Start(storageSvcUrl string, envBuilderNamespace string) error { func Start(storageSvcUrl string, envBuilderNamespace string) error {
fissionClient, kubernetesClient, _, err := crd.MakeFissionClient() fissionClient, kubernetesClient, _, err := crd.MakeFissionClient()
if err != nil { if err != nil {
log.Printf("Failed to get kubernetes client: %v", err) log.Printf("Failed to get kubernetes client: %v", err)
@@ -33,7 +34,8 @@ func Start(storageSvcUrl string, envBuilderNamespace string) error {
envWatcher := makeEnvironmentWatcher(fissionClient, kubernetesClient, envBuilderNamespace) envWatcher := makeEnvironmentWatcher(fissionClient, kubernetesClient, envBuilderNamespace)
go envWatcher.watchEnvironments() go envWatcher.watchEnvironments()
pkgWatcher := makePackageWatcher(fissionClient, kubernetesClient.CoreV1().RESTClient(), envBuilderNamespace, storageSvcUrl) pkgWatcher := makePackageWatcher(fissionClient,
kubernetesClient.CoreV1().RESTClient(), envBuilderNamespace, storageSvcUrl)
go pkgWatcher.watchPackages() go pkgWatcher.watchPackages()
select {} select {}
+1 -1
View File
@@ -90,7 +90,7 @@ func buildPackage(fissionClient *crd.FissionClient, builderNamespace string,
buildLogs = buildResp.BuildLogs buildLogs = buildResp.BuildLogs
} }
buildLogs += fmt.Sprintf("%v\n", e) buildLogs += fmt.Sprintf("%v\n", e)
return nil, buildResp.BuildLogs, fission.MakeError(http.StatusInternalServerError, e) return nil, buildLogs, fission.MakeError(http.StatusInternalServerError, e)
} }
log.Printf("Build succeed, source package: %v, deployment package: %v", srcPkgFilename, buildResp.ArtifactFilename) log.Printf("Build succeed, source package: %v, deployment package: %v", srcPkgFilename, buildResp.ArtifactFilename)
+54 -17
View File
@@ -20,6 +20,7 @@ import (
"fmt" "fmt"
"log" "log"
"os" "os"
"strconv"
"time" "time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -30,6 +31,7 @@ import (
apiv1 "k8s.io/client-go/pkg/api/v1" apiv1 "k8s.io/client-go/pkg/api/v1"
"k8s.io/client-go/pkg/apis/extensions/v1beta1" "k8s.io/client-go/pkg/apis/extensions/v1beta1"
"github.com/fission/fission"
"github.com/fission/fission/crd" "github.com/fission/fission/crd"
) )
@@ -70,12 +72,23 @@ type (
kubernetesClient *kubernetes.Clientset kubernetesClient *kubernetes.Clientset
fetcherImage string fetcherImage string
fetcherImagePullPolicy apiv1.PullPolicy fetcherImagePullPolicy apiv1.PullPolicy
useIstio bool
} }
) )
func makeEnvironmentWatcher(fissionClient *crd.FissionClient, func makeEnvironmentWatcher(fissionClient *crd.FissionClient,
kubernetesClient *kubernetes.Clientset, builderNamespace string) *environmentWatcher { kubernetesClient *kubernetes.Clientset, builderNamespace string) *environmentWatcher {
useIstio := false
enableIstio := os.Getenv("ENABLE_ISTIO")
if len(enableIstio) > 0 {
istio, err := strconv.ParseBool(enableIstio)
if err != nil {
log.Println("Failed to parse ENABLE_ISTIO, defaults to false")
}
useIstio = istio
}
fetcherImage := os.Getenv("FETCHER_IMAGE") fetcherImage := os.Getenv("FETCHER_IMAGE")
if len(fetcherImage) == 0 { if len(fetcherImage) == 0 {
fetcherImage = "fission/fetcher" fetcherImage = "fission/fetcher"
@@ -104,6 +117,7 @@ func makeEnvironmentWatcher(fissionClient *crd.FissionClient,
kubernetesClient: kubernetesClient, kubernetesClient: kubernetesClient,
fetcherImage: fetcherImage, fetcherImage: fetcherImage,
fetcherImagePullPolicy: pullPolicy, fetcherImagePullPolicy: pullPolicy,
useIstio: useIstio,
} }
go envWatcher.service() go envWatcher.service()
@@ -129,6 +143,11 @@ func (envw *environmentWatcher) watchEnvironments() {
ResourceVersion: rv, ResourceVersion: rv,
}) })
if err != nil { if err != nil {
if fission.IsNetworkError(err) {
log.Printf("Encounter network error, retrying later: %v", err)
time.Sleep(5 * time.Second)
continue
}
log.Fatalf("Error watching environment list: %v", err) log.Fatalf("Error watching environment list: %v", err)
} }
@@ -152,25 +171,36 @@ func (envw *environmentWatcher) watchEnvironments() {
} }
func (envw *environmentWatcher) sync() { func (envw *environmentWatcher) sync() {
envList, err := envw.fissionClient.Environments(metav1.NamespaceAll).List(metav1.ListOptions{}) maxRetries := 10
if err != nil { for i := 0; i < maxRetries; i++ {
log.Fatalf("Error syncing environment CRD resources: %v", err) envList, err := envw.fissionClient.Environments(metav1.NamespaceAll).List(metav1.ListOptions{})
}
// Create environment builders for all environments
for i := range envList.Items {
env := envList.Items[i]
if env.Spec.Version == 1 || // builder is not supported with v1 interface
len(env.Spec.Builder.Image) == 0 { // ignore env without builder image
continue
}
_, err := envw.getEnvBuilder(&env)
if err != nil { if err != nil {
log.Printf("Error creating builder for %v: %v", env.Metadata.Name, err) if fission.IsNetworkError(err) {
log.Printf("Error syncing environment CRD resources due to network error, retrying later: %v", err)
time.Sleep(50 * time.Duration(2*i) * time.Millisecond)
continue
}
log.Fatalf("Error syncing environment CRD resources: %v", err)
} }
// Create environment builders for all environments
for i := range envList.Items {
env := envList.Items[i]
if env.Spec.Version == 1 || // builder is not supported with v1 interface
len(env.Spec.Builder.Image) == 0 { // ignore env without builder image
continue
}
_, err := envw.getEnvBuilder(&env)
if err != nil {
log.Printf("Error creating builder for %v: %v", env.Metadata.Name, err)
}
}
// Remove environment builders no longer needed
envw.cleanupEnvBuilders(envList.Items)
break
} }
envw.cleanupEnvBuilders(envList.Items)
} }
func (envw *environmentWatcher) service() { func (envw *environmentWatcher) service() {
@@ -423,6 +453,12 @@ func (envw *environmentWatcher) createBuilderDeployment(env *crd.Environment) (*
name := envw.getCacheKey(env.Metadata.Name, env.Metadata.ResourceVersion) name := envw.getCacheKey(env.Metadata.Name, env.Metadata.ResourceVersion)
sel := envw.getLabels(env.Metadata.Name, env.Metadata.ResourceVersion) sel := envw.getLabels(env.Metadata.Name, env.Metadata.ResourceVersion)
var replicas int32 = 1 var replicas int32 = 1
podAnnotation := make(map[string]string)
if envw.useIstio && env.Spec.AllowAccessToExternalNetwork {
podAnnotation["sidecar.istio.io/inject"] = "false"
}
deployment := &v1beta1.Deployment{ deployment := &v1beta1.Deployment{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Namespace: envw.builderNamespace, Namespace: envw.builderNamespace,
@@ -436,7 +472,8 @@ func (envw *environmentWatcher) createBuilderDeployment(env *crd.Environment) (*
}, },
Template: apiv1.PodTemplateSpec{ Template: apiv1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Labels: sel, Labels: sel,
Annotations: podAnnotation,
}, },
Spec: apiv1.PodSpec{ Spec: apiv1.PodSpec{
Volumes: []apiv1.Volume{ Volumes: []apiv1.Volume{
+2 -1
View File
@@ -138,7 +138,8 @@ func (pkgw *packageWatcher) build(buildCache *cache.Cache, pkg *crd.Package) {
break break
} }
uploadResp, buildLogs, err := buildPackage(pkgw.fissionClient, pkgw.builderNamespace, pkgw.storageSvcUrl, pkg) uploadResp, buildLogs, err := buildPackage(pkgw.fissionClient,
pkgw.builderNamespace, pkgw.storageSvcUrl, pkg)
if err != nil { if err != nil {
log.Printf("Error building package %v: %v", pkg.Metadata.Name, err) log.Printf("Error building package %v: %v", pkg.Metadata.Name, err)
updatePackage(pkgw.fissionClient, pkg, fission.BuildStatusFailed, buildLogs, nil) updatePackage(pkgw.fissionClient, pkg, fission.BuildStatusFailed, buildLogs, nil)
@@ -6,6 +6,9 @@ metadata:
labels: labels:
name: fission-function name: fission-function
chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
{{- if .Values.enableIstio }}
istio-injection: enabled
{{- end }}
--- ---
apiVersion: v1 apiVersion: v1
@@ -15,6 +18,9 @@ metadata:
labels: labels:
name: fission-builder name: fission-builder
chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
{{- if .Values.enableIstio }}
istio-injection: enabled
{{- end }}
--- ---
apiVersion: v1 apiVersion: v1
@@ -130,6 +136,11 @@ spec:
imagePullPolicy: {{ .Values.pullPolicy }} imagePullPolicy: {{ .Values.pullPolicy }}
command: ["/fission-bundle"] command: ["/fission-bundle"]
args: ["--controllerPort", "8888"] args: ["--controllerPort", "8888"]
env:
- name: FISSION_FUNCTION_NAMESPACE
value: "{{ .Values.functionNamespace }}"
- name: ENABLE_ISTIO
value: "{{ .Values.enableIstio }}"
readinessProbe: readinessProbe:
httpGet: httpGet:
path: "/healthz" path: "/healthz"
@@ -223,6 +234,8 @@ spec:
value: "{{ .Values.pullPolicy }}" value: "{{ .Values.pullPolicy }}"
- name: RUNTIME_IMAGE_PULL_POLICY - name: RUNTIME_IMAGE_PULL_POLICY
value: "{{ .Values.pullPolicy }}" value: "{{ .Values.pullPolicy }}"
- name: ENABLE_ISTIO
value: "{{ .Values.enableIstio }}"
readinessProbe: readinessProbe:
httpGet: httpGet:
path: "/healthz" path: "/healthz"
@@ -263,6 +276,8 @@ spec:
value: "{{ .Values.fetcherImage }}:{{ .Values.fetcherImageTag }}" value: "{{ .Values.fetcherImage }}:{{ .Values.fetcherImageTag }}"
- name: FETCHER_IMAGE_PULL_POLICY - name: FETCHER_IMAGE_PULL_POLICY
value: "{{ .Values.pullPolicy }}" value: "{{ .Values.pullPolicy }}"
- name: ENABLE_ISTIO
value: "{{ .Values.enableIstio }}"
serviceAccount: fission-svc serviceAccount: fission-svc
--- ---
@@ -22,6 +22,10 @@ spec:
labels: labels:
release: {{ .Release.Name }} release: {{ .Release.Name }}
app: {{ template "name" . }} app: {{ template "name" . }}
annotations:
{{- if .Values.enableIstio }}
"sidecar.istio.io/inject": "false"
{{- end }}
spec: spec:
restartPolicy: Never restartPolicy: Never
containers: containers:
+3
View File
@@ -41,6 +41,9 @@ functionNamespace: fission-function
## the release namespace) ## the release namespace)
builderNamespace: fission-builder builderNamespace: fission-builder
## Enable istio integration
enableIstio: false
## Logger config ## Logger config
logger: logger:
influxdbAdmin: "admin" influxdbAdmin: "admin"
@@ -6,6 +6,9 @@ metadata:
labels: labels:
name: fission-function name: fission-function
chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
{{- if .Values.enableIstio }}
istio-injection: enabled
{{- end }}
--- ---
apiVersion: v1 apiVersion: v1
@@ -15,6 +18,9 @@ metadata:
labels: labels:
name: fission-builder name: fission-builder
chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
{{- if .Values.enableIstio }}
istio-injection: enabled
{{- end }}
--- ---
apiVersion: v1 apiVersion: v1
@@ -130,6 +136,11 @@ spec:
imagePullPolicy: {{ .Values.pullPolicy }} imagePullPolicy: {{ .Values.pullPolicy }}
command: ["/fission-bundle"] command: ["/fission-bundle"]
args: ["--controllerPort", "8888"] args: ["--controllerPort", "8888"]
env:
- name: FISSION_FUNCTION_NAMESPACE
value: "{{ .Values.functionNamespace }}"
- name: ENABLE_ISTIO
value: "{{ .Values.enableIstio }}"
readinessProbe: readinessProbe:
httpGet: httpGet:
path: "/healthz" path: "/healthz"
@@ -221,6 +232,8 @@ spec:
value: "{{ .Values.fetcherImage }}:{{ .Values.fetcherImageTag }}" value: "{{ .Values.fetcherImage }}:{{ .Values.fetcherImageTag }}"
- name: FETCHER_IMAGE_PULL_POLICY - name: FETCHER_IMAGE_PULL_POLICY
value: "{{ .Values.pullPolicy }}" value: "{{ .Values.pullPolicy }}"
- name: ENABLE_ISTIO
value: "{{ .Values.enableIstio }}"
readinessProbe: readinessProbe:
httpGet: httpGet:
path: "/healthz" path: "/healthz"
@@ -261,6 +274,8 @@ spec:
value: "{{ .Values.fetcherImage }}:{{ .Values.fetcherImageTag }}" value: "{{ .Values.fetcherImage }}:{{ .Values.fetcherImageTag }}"
- name: FETCHER_IMAGE_PULL_POLICY - name: FETCHER_IMAGE_PULL_POLICY
value: "{{ .Values.pullPolicy }}" value: "{{ .Values.pullPolicy }}"
- name: ENABLE_ISTIO
value: "{{ .Values.enableIstio }}"
serviceAccount: fission-svc serviceAccount: fission-svc
--- ---
@@ -22,6 +22,10 @@ spec:
labels: labels:
release: {{ .Release.Name }} release: {{ .Release.Name }}
app: {{ template "name" . }} app: {{ template "name" . }}
annotations:
{{- if .Values.enableIstio }}
"sidecar.istio.io/inject": "false"
{{- end }}
spec: spec:
restartPolicy: Never restartPolicy: Never
containers: containers:
+3
View File
@@ -38,6 +38,9 @@ functionNamespace: fission-function
## the release namespace) ## the release namespace)
builderNamespace: fission-builder builderNamespace: fission-builder
## Enable istio integration
enableIstio: false
## Persist data to a persistent volume. ## Persist data to a persistent volume.
persistence: persistence:
enabled: true enabled: true
+12
View File
@@ -18,6 +18,7 @@ package fission
import ( import (
"fmt" "fmt"
"net"
"os" "os"
"os/signal" "os/signal"
"runtime/debug" "runtime/debug"
@@ -40,3 +41,14 @@ func SetupStackTraceHandler() {
os.Exit(1) os.Exit(1)
}() }()
} }
// IsNetworkError returns true if an error is a network error, and false otherwise.
func IsNetworkError(err error) bool {
_, ok := err.(net.Error)
return ok
}
// GetFunctionIstioServiceName return service name of function for istio feature
func GetFunctionIstioServiceName(fnName, fnNamespace string) string {
return fmt.Sprintf("istio-%v-%v", fnName, fnNamespace)
}
+18
View File
@@ -21,6 +21,7 @@ import (
"net/http" "net/http"
"os" "os"
"runtime/debug" "runtime/debug"
"strconv"
"strings" "strings"
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
@@ -41,6 +42,8 @@ type (
storageServiceUrl string storageServiceUrl string
builderManagerUrl string builderManagerUrl string
workflowApiUrl string workflowApiUrl string
functionNamespace string
useIstio bool
} }
logDBConfig struct { logDBConfig struct {
@@ -74,6 +77,21 @@ func MakeAPI() (*API, error) {
api.workflowApiUrl = "http://workflows-apiserver" api.workflowApiUrl = "http://workflows-apiserver"
} }
fnNs := os.Getenv("FISSION_FUNCTION_NAMESPACE")
if len(fnNs) > 0 {
api.functionNamespace = fnNs
} else {
api.functionNamespace = "fission-function"
}
if len(os.Getenv("ENABLE_ISTIO")) > 0 {
istio, err := strconv.ParseBool(os.Getenv("ENABLE_ISTIO"))
if err != nil {
log.Println("Failed to parse ENABLE_ISTIO")
}
api.useIstio = istio
}
return api, err return api, err
} }
+82 -4
View File
@@ -29,14 +29,22 @@ import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/client-go/pkg/api" "k8s.io/client-go/pkg/api"
"k8s.io/client-go/pkg/api/v1" apiv1 "k8s.io/client-go/pkg/api/v1"
restclient "k8s.io/client-go/rest" restclient "k8s.io/client-go/rest"
"github.com/fission/fission" "github.com/fission/fission"
"github.com/fission/fission/crd" "github.com/fission/fission/crd"
) )
func (a *API) getIstioServiceLabels(fnName string) map[string]string {
return map[string]string{
"functionName": fnName,
}
}
func (a *API) FunctionApiList(w http.ResponseWriter, r *http.Request) { func (a *API) FunctionApiList(w http.ResponseWriter, r *http.Request) {
funcs, err := a.fissionClient.Functions(metav1.NamespaceAll).List(metav1.ListOptions{}) funcs, err := a.fissionClient.Functions(metav1.NamespaceAll).List(metav1.ListOptions{})
if err != nil { if err != nil {
@@ -85,6 +93,62 @@ func (a *API) FunctionApiCreate(w http.ResponseWriter, r *http.Request) {
return return
} }
// Since istio only allows accessing pod through k8s service,
// for the functions with executor type "poolmgr" we need to
// create a service for sending requests to pod in pool.
// Functions with executor type "Newdeploy" is specialized at
// pod starts. In this case, just ignore such functions.
fnExecutorType := f.Spec.InvokeStrategy.ExecutionStrategy.ExecutorType
if a.useIstio && fnExecutorType == fission.ExecutorTypePoolmgr {
// create a same name service for function
// since istio only allows the traffic to service
sel := map[string]string{
"functionName": fnew.Metadata.Name,
"functionUid": string(fnew.Metadata.UID),
}
// service for accepting user traffic
svc := apiv1.Service{
ObjectMeta: metav1.ObjectMeta{
Namespace: a.functionNamespace,
Name: fission.GetFunctionIstioServiceName(f.Metadata.Name, f.Metadata.Namespace),
Labels: a.getIstioServiceLabels(f.Metadata.Name),
},
Spec: apiv1.ServiceSpec{
Type: apiv1.ServiceTypeClusterIP,
Ports: []apiv1.ServicePort{
// Service port name should begin with a recognized prefix, or the traffic will be
// treated as TCP traffic. (https://istio.io/docs/setup/kubernetes/sidecar-injection.html)
// Originally the ports' name are similar to "http-fetch" and "http-specialize".
// But for istio 0.5.1, istio-proxy return unexpected 431 error with such naming.
// https://github.com/istio/istio/issues/928
// Workaround: remove prefix
// TODO: prepend prefix once the bug fixed
{
Name: "fetch",
Protocol: apiv1.ProtocolTCP,
Port: 8000,
TargetPort: intstr.FromInt(8000),
},
{
Name: "specialize",
Protocol: apiv1.ProtocolTCP,
Port: 8888,
TargetPort: intstr.FromInt(8888),
},
},
Selector: sel,
},
}
_, err = a.kubernetesClient.CoreV1().Services(a.functionNamespace).Create(&svc)
if err != nil {
a.respondWithError(w, err)
return
}
}
w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusCreated)
a.respondWithSuccess(w, resp) a.respondWithSuccess(w, resp)
} }
@@ -162,6 +226,20 @@ func (a *API) FunctionApiDelete(w http.ResponseWriter, r *http.Request) {
return return
} }
if a.useIstio {
// delete all istio services belong to the function
sel := a.getIstioServiceLabels(name)
svcList, err := a.kubernetesClient.CoreV1().Services(a.functionNamespace).List(metav1.ListOptions{
LabelSelector: labels.Set(sel).AsSelector().String(),
})
for _, svc := range svcList.Items {
err = a.kubernetesClient.CoreV1().Services(a.functionNamespace).Delete(svc.ObjectMeta.Name, &metav1.DeleteOptions{})
// log error and continue
log.Printf("Failed to delete service %v: %v", svc.ObjectMeta.Name, err)
continue
}
}
a.respondWithSuccess(w, []byte("")) a.respondWithSuccess(w, []byte(""))
} }
@@ -219,7 +297,7 @@ func (a *API) FunctionPodLogs(w http.ResponseWriter, r *http.Request) {
// Get function Pods first // Get function Pods first
selector := "functionName=" + fnName selector := "functionName=" + fnName
podList, err := a.kubernetesClient.Core().Pods(ns).List(metav1.ListOptions{LabelSelector: selector}) podList, err := a.kubernetesClient.CoreV1().Pods(ns).List(metav1.ListOptions{LabelSelector: selector})
if err != nil { if err != nil {
a.respondWithError(w, err) a.respondWithError(w, err)
return return
@@ -233,10 +311,10 @@ func (a *API) FunctionPodLogs(w http.ResponseWriter, r *http.Request) {
return itime.After(jtime) return itime.After(jtime)
}) })
podLogOpts := v1.PodLogOptions{Container: envName} // Only the env container, not fetcher podLogOpts := apiv1.PodLogOptions{Container: envName} // Only the env container, not fetcher
var podLogsReq *restclient.Request var podLogsReq *restclient.Request
if len(pods) > 0 { if len(pods) > 0 {
podLogsReq = a.kubernetesClient.Core().Pods(ns).GetLogs(pods[0].ObjectMeta.Name, &podLogOpts) podLogsReq = a.kubernetesClient.CoreV1().Pods(ns).GetLogs(pods[0].ObjectMeta.Name, &podLogOpts)
} else { } else {
a.respondWithError(w, errors.New("No active pods found")) a.respondWithError(w, errors.New("No active pods found"))
return return
+23 -6
View File
@@ -17,6 +17,9 @@ limitations under the License.
package crd package crd
import ( import (
"log"
"time"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
@@ -32,14 +35,28 @@ const (
// needed. (Note that this creates the CRD type; it doesn't create any // needed. (Note that this creates the CRD type; it doesn't create any
// _instances_ of that type.) // _instances_ of that type.)
func ensureCRD(clientset *apiextensionsclient.Clientset, crd *apiextensionsv1beta1.CustomResourceDefinition) error { func ensureCRD(clientset *apiextensionsclient.Clientset, crd *apiextensionsv1beta1.CustomResourceDefinition) error {
_, err := clientset.ApiextensionsV1beta1().CustomResourceDefinitions().Get(crd.ObjectMeta.Name, metav1.GetOptions{}) maxRetries := 5
if err != nil { for i := 0; i < maxRetries; i++ {
if errors.IsNotFound(err) { _, err := clientset.ApiextensionsV1beta1().CustomResourceDefinitions().Get(crd.ObjectMeta.Name, metav1.GetOptions{})
_, err := clientset.ApiextensionsV1beta1().CustomResourceDefinitions().Create(crd)
if err != nil { if err != nil {
return err if errors.IsNotFound(err) {
// crd resource not found error
_, err := clientset.ApiextensionsV1beta1().CustomResourceDefinitions().Create(crd)
if err != nil {
return err
}
} else {
// The requests fail to connect to k8s api server before
// istio-prxoy is ready to serve traffic. Retry again.
log.Printf("Error connecting to kubernetes api service (%v), retrying", err)
time.Sleep(500 * time.Duration(2*i) * time.Millisecond)
continue
} }
} }
// resource already exists
break
} }
return nil return nil
} }
+15 -21
View File
@@ -5,9 +5,7 @@ import (
"encoding/json" "encoding/json"
"io/ioutil" "io/ioutil"
"log" "log"
"net"
"net/http" "net/http"
"net/url"
"time" "time"
"github.com/fission/fission" "github.com/fission/fission"
@@ -38,29 +36,25 @@ func (c *Client) Fetch(fr *fetcher.FetchRequest) error {
for i := 0; i < maxRetries; i++ { for i := 0; i < maxRetries; i++ {
resp, err = http.Post(c.url, "application/json", bytes.NewReader(body)) resp, err = http.Post(c.url, "application/json", bytes.NewReader(body))
if err == nil && resp.StatusCode == 200 { if err == nil {
defer resp.Body.Close() if resp.StatusCode == 200 {
return nil resp.Body.Close()
} return nil
// Only retry for the specific case of a connection error.
if urlErr, ok := err.(*url.Error); ok {
if netErr, ok := urlErr.Err.(*net.OpError); ok {
if netErr.Op == "dial" {
if i < maxRetries-1 {
time.Sleep(50 * time.Duration(2*i) * time.Millisecond)
continue
}
}
} }
err = fission.MakeErrorFromHTTP(resp)
} }
if i < maxRetries-1 {
time.Sleep(50 * time.Duration(2*i) * time.Millisecond)
log.Printf("Error fetching package (%v), retrying", err)
continue
}
log.Printf("Failed to fetch: %v", err)
return err
} }
if err == nil { return nil
err = fission.MakeErrorFromHTTP(resp)
}
log.Printf("Failed to fetch: %v", err)
return err
} }
func (c *Client) Upload(fr *fetcher.UploadRequest) (*fetcher.UploadResponse, error) { func (c *Client) Upload(fr *fetcher.UploadRequest) (*fetcher.UploadResponse, error) {
-9
View File
@@ -128,15 +128,6 @@ func specializePod(f *fetcher.Fetcher, fetchPayload *string, loadPayload *string
if err == nil && resp.StatusCode < 300 { if err == nil && resp.StatusCode < 300 {
// Success // Success
resp.Body.Close() resp.Body.Close()
//On Success creates a file which is used as a readiness probe by Kubernetes for this container/pod
file, err := os.OpenFile("/tmp/ready", os.O_RDONLY|os.O_CREATE, 0666)
if err != nil {
log.Fatalf("Error creating readiness file: %v", err)
}
err = file.Close()
if err != nil {
log.Fatalf("Error closing readiness file: %v", err)
}
break break
} }
+3 -1
View File
@@ -163,7 +163,9 @@ func (fsc *FunctionServiceCache) Add(fsvc FuncSvc) (*FuncSvc, error) {
err = nil err = nil
} }
} }
log.Printf("error caching fsvc: %v", err) if err != nil {
log.Printf("error caching fsvc: %v", err)
}
return nil, err return nil, err
} }
return nil, nil return nil, nil
+55 -7
View File
@@ -19,6 +19,7 @@ package newdeploy
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"log" "log"
"path/filepath" "path/filepath"
"strconv" "strconv"
@@ -55,6 +56,7 @@ func (deploy *NewDeploy) createOrGetDeployment(fn *crd.Function, env *crd.Enviro
} }
targetFilename := "user" targetFilename := "user"
userfunc := "userfunc" userfunc := "userfunc"
var gracePeriodSeconds int64 = 6 * 60
existingDepl, err := deploy.kubernetesClient.ExtensionsV1beta1().Deployments(deploy.namespace).Get(deployName, metav1.GetOptions{}) existingDepl, err := deploy.kubernetesClient.ExtensionsV1beta1().Deployments(deploy.namespace).Get(deployName, metav1.GetOptions{})
if err == nil && existingDepl.Status.ReadyReplicas >= replicas { if err == nil && existingDepl.Status.ReadyReplicas >= replicas {
@@ -83,6 +85,7 @@ func (deploy *NewDeploy) createOrGetDeployment(fn *crd.Function, env *crd.Enviro
if err != nil { if err != nil {
return nil, err return nil, err
} }
loadPayload, err := json.Marshal(loadReq) loadPayload, err := json.Marshal(loadReq)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -94,6 +97,11 @@ func (deploy *NewDeploy) createOrGetDeployment(fn *crd.Function, env *crd.Enviro
return nil, err return nil, err
} }
podAnnotation := make(map[string]string)
if deploy.useIstio && env.Spec.AllowAccessToExternalNetwork {
podAnnotation["sidecar.istio.io/inject"] = "false"
}
deployment := &v1beta1.Deployment{ deployment := &v1beta1.Deployment{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Labels: deployLabels, Labels: deployLabels,
@@ -106,7 +114,8 @@ func (deploy *NewDeploy) createOrGetDeployment(fn *crd.Function, env *crd.Enviro
}, },
Template: apiv1.PodTemplateSpec{ Template: apiv1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Labels: deployLabels, Labels: deployLabels,
Annotations: podAnnotation,
}, },
Spec: apiv1.PodSpec{ Spec: apiv1.PodSpec{
Volumes: []apiv1.Volume{ Volumes: []apiv1.Volume{
@@ -130,6 +139,16 @@ func (deploy *NewDeploy) createOrGetDeployment(fn *crd.Function, env *crd.Enviro
}, },
}, },
Resources: env.Spec.Resources, Resources: env.Spec.Resources,
Lifecycle: &apiv1.Lifecycle{
PreStop: &apiv1.Handler{
Exec: &apiv1.ExecAction{
Command: []string{
"sleep",
fmt.Sprintf("%v", gracePeriodSeconds),
},
},
},
},
}, },
{ {
Name: "fetcher", Name: "fetcher",
@@ -148,6 +167,16 @@ func (deploy *NewDeploy) createOrGetDeployment(fn *crd.Function, env *crd.Enviro
"-secret-dir", deploy.sharedSecretPath, "-secret-dir", deploy.sharedSecretPath,
"-cfgmap-dir", deploy.sharedCfgMapPath, "-cfgmap-dir", deploy.sharedCfgMapPath,
deploy.sharedMountPath}, deploy.sharedMountPath},
Lifecycle: &apiv1.Lifecycle{
PreStop: &apiv1.Handler{
Exec: &apiv1.ExecAction{
Command: []string{
"sleep",
fmt.Sprintf("%v", gracePeriodSeconds),
},
},
},
},
Env: []apiv1.EnvVar{ Env: []apiv1.EnvVar{
{ {
Name: envVersion, Name: envVersion,
@@ -157,17 +186,36 @@ func (deploy *NewDeploy) createOrGetDeployment(fn *crd.Function, env *crd.Enviro
// TBD Use smaller default resources, for now needed to make HPA work // TBD Use smaller default resources, for now needed to make HPA work
Resources: fetcherResources, Resources: fetcherResources,
ReadinessProbe: &apiv1.Probe{ ReadinessProbe: &apiv1.Probe{
Handler: apiv1.Handler{
Exec: &apiv1.ExecAction{
Command: []string{"cat", "/tmp/ready"},
},
},
InitialDelaySeconds: 1, InitialDelaySeconds: 1,
PeriodSeconds: 1, PeriodSeconds: 1,
FailureThreshold: 30,
Handler: apiv1.Handler{
HTTPGet: &apiv1.HTTPGetAction{
Path: "/healthz",
Port: intstr.IntOrString{
Type: intstr.Int,
IntVal: 8000,
},
},
},
},
LivenessProbe: &apiv1.Probe{
InitialDelaySeconds: 35,
PeriodSeconds: 5,
Handler: apiv1.Handler{
HTTPGet: &apiv1.HTTPGetAction{
Path: "/healthz",
Port: intstr.IntOrString{
Type: intstr.Int,
IntVal: 8000,
},
},
},
}, },
}, },
}, },
ServiceAccountName: "fission-fetcher", ServiceAccountName: "fission-fetcher",
TerminationGracePeriodSeconds: &gracePeriodSeconds,
}, },
}, },
}, },
+24 -7
View File
@@ -21,6 +21,7 @@ import (
"fmt" "fmt"
"log" "log"
"os" "os"
"strconv"
"time" "time"
"github.com/fission/fission" "github.com/fission/fission"
@@ -50,6 +51,7 @@ type (
sharedMountPath string sharedMountPath string
sharedSecretPath string sharedSecretPath string
sharedCfgMapPath string sharedCfgMapPath string
useIstio bool
fsCache *fscache.FunctionServiceCache // cache funcSvc's by function, address and podname fsCache *fscache.FunctionServiceCache // cache funcSvc's by function, address and podname
requestChannel chan *fnRequest requestChannel chan *fnRequest
@@ -97,6 +99,15 @@ func MakeNewDeploy(
fetcherImagePullPolicy = "IfNotPresent" fetcherImagePullPolicy = "IfNotPresent"
} }
enableIstio := false
if len(os.Getenv("ENABLE_ISTIO")) > 0 {
istio, err := strconv.ParseBool(os.Getenv("ENABLE_ISTIO"))
if err != nil {
log.Println("Failed to parse ENABLE_ISTIO")
}
enableIstio = istio
}
nd := &NewDeploy{ nd := &NewDeploy{
fissionClient: fissionClient, fissionClient: fissionClient,
kubernetesClient: kubernetesClient, kubernetesClient: kubernetesClient,
@@ -111,6 +122,7 @@ func MakeNewDeploy(
sharedMountPath: "/userfunc", sharedMountPath: "/userfunc",
sharedSecretPath: "/secrets", sharedSecretPath: "/secrets",
sharedCfgMapPath: "/configs", sharedCfgMapPath: "/configs",
useIstio: enableIstio,
requestChannel: make(chan *fnRequest), requestChannel: make(chan *fnRequest),
} }
@@ -249,18 +261,23 @@ func (deploy *NewDeploy) fnCreate(fn *crd.Function) (*fscache.FuncSvc, error) {
"executorType": fission.ExecutorTypeNewdeploy, "executorType": fission.ExecutorTypeNewdeploy,
} }
depl, err := deploy.createOrGetDeployment(fn, env, objName, deployLabels) // Envoy(istio-proxy) returns 404 directly before istio pilot
if err != nil { // propagates latest Envoy-specific configuration.
log.Printf("Error creating the deployment %v: %v", objName, err) // Since newdeploy waits for pods of deployment to be ready,
return fsvc, err // change the order of kubeObject creation (create service first,
} // then deployment) to take advantage of waiting time.
svc, err := deploy.createOrGetSvc(deployLabels, objName) svc, err := deploy.createOrGetSvc(deployLabels, objName)
if err != nil { if err != nil {
log.Printf("Error creating the service %v: %v", objName, err) log.Printf("Error creating the service %v: %v", objName, err)
return fsvc, err return fsvc, err
} }
svcAddress := svc.Spec.ClusterIP svcAddress := fmt.Sprintf("%v.%v", svc.Name, svc.Namespace)
depl, err := deploy.createOrGetDeployment(fn, env, objName, deployLabels)
if err != nil {
log.Printf("Error creating the deployment %v: %v", objName, err)
return fsvc, err
}
hpa, err := deploy.createOrGetHpa(objName, &fn.Spec.InvokeStrategy.ExecutionStrategy, depl) hpa, err := deploy.createOrGetHpa(objName, &fn.Spec.InvokeStrategy.ExecutionStrategy, depl)
if err != nil { if err != nil {
+137 -14
View File
@@ -28,6 +28,7 @@ import (
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"time" "time"
@@ -60,7 +61,8 @@ type (
idlePodReapTime time.Duration // pods unused for idlePodReapTime are deleted idlePodReapTime time.Duration // pods unused for idlePodReapTime are deleted
fsCache *fscache.FunctionServiceCache // cache funcSvc's by function, address and podname fsCache *fscache.FunctionServiceCache // cache funcSvc's by function, address and podname
useSvc bool // create k8s service for specialized pods useSvc bool // create k8s service for specialized pods
poolInstanceId string // small random string to uniquify pod names useIstio bool
poolInstanceId string // small random string to uniquify pod names
fetcherImage string fetcherImage string
fetcherImagePullPolicy apiv1.PullPolicy fetcherImagePullPolicy apiv1.PullPolicy
runtimeImagePullPolicy apiv1.PullPolicy // pull policy for generic pool to created env deployment runtimeImagePullPolicy apiv1.PullPolicy // pull policy for generic pool to created env deployment
@@ -120,6 +122,16 @@ func MakeGenericPool(
runtimeImagePullPolicy = "IfNotPresent" runtimeImagePullPolicy = "IfNotPresent"
} }
enableIstio := false
if len(os.Getenv("ENABLE_ISTIO")) > 0 {
istio, err := strconv.ParseBool(os.Getenv("ENABLE_ISTIO"))
if err != nil {
log.Println("Failed to parse ENABLE_ISTIO")
}
enableIstio = istio
}
// TODO: in general we need to provide the user a way to configure pools. Initial // TODO: in general we need to provide the user a way to configure pools. Initial
// replicas, autoscaling params, various timeouts, etc. // replicas, autoscaling params, various timeouts, etc.
gp := &GenericPool{ gp := &GenericPool{
@@ -136,6 +148,7 @@ func MakeGenericPool(
instanceId: instanceId, instanceId: instanceId,
fetcherImage: fetcherImage, fetcherImage: fetcherImage,
useSvc: false, // defaults off -- svc takes a second or more to become routable, slowing cold start useSvc: false, // defaults off -- svc takes a second or more to become routable, slowing cold start
useIstio: enableIstio, // defaults off -- istio integration requires pod relabeling and it takes a second or more to become routable, slowing cold start
sharedMountPath: "/userfunc", // change this may break v1 compatibility, since most of the v1 environments have hard-coded "/userfunc" in loading path sharedMountPath: "/userfunc", // change this may break v1 compatibility, since most of the v1 environments have hard-coded "/userfunc" in loading path
sharedSecretPath: "/secrets", sharedSecretPath: "/secrets",
sharedCfgMapPath: "/configs", sharedCfgMapPath: "/configs",
@@ -280,7 +293,11 @@ func (gp *GenericPool) scheduleDeletePod(name string) {
// cleaned up. (We need a better solutions for both those things; log // cleaned up. (We need a better solutions for both those things; log
// aggregation and storage will help.) // aggregation and storage will help.)
log.Printf("Error in pod '%v', scheduling cleanup", name) log.Printf("Error in pod '%v', scheduling cleanup", name)
time.Sleep(5 * time.Minute) // Ignore sleep here if istio feature is enabled, function pod
// will be deleted after 6 mins (terminationGracePeriodSeconds).
if !gp.useIstio {
time.Sleep(5 * time.Minute)
}
gp.kubernetesClient.CoreV1().Pods(gp.namespace).Delete(name, nil) gp.kubernetesClient.CoreV1().Pods(gp.namespace).Delete(name, nil)
}() }()
} }
@@ -339,10 +356,15 @@ func (gp *GenericPool) specializePod(pod *apiv1.Pod, metadata *metav1.ObjectMeta
if len(podIP) == 0 { if len(podIP) == 0 {
return errors.New("Pod has no IP") return errors.New("Pod has no IP")
} }
// specialize pod with service
if gp.useIstio {
svc := fission.GetFunctionIstioServiceName(metadata.Name, metadata.Namespace)
podIP = fmt.Sprintf("%v.%v", svc, gp.namespace)
}
// tell fetcher to get the function. // tell fetcher to get the function.
fetcherUrl := gp.getFetcherUrl(podIP) fetcherUrl := gp.getFetcherUrl(podIP)
log.Printf("[%v] calling fetcher to copy function", metadata.Name) log.Printf("[%v] calling fetcher to copy function with fetcher url: %v", metadata.Name, fetcherUrl)
fn, err := gp.fissionClient. fn, err := gp.fissionClient.
Functions(metadata.Namespace). Functions(metadata.Namespace).
@@ -394,33 +416,48 @@ func (gp *GenericPool) specializePod(pod *apiv1.Pod, metadata *metav1.ObjectMeta
var resp2 *http.Response var resp2 *http.Response
if gp.env.Spec.Version == 2 { if gp.env.Spec.Version == 2 {
specializeUrl := gp.getSpecializeUrl(podIP, 2) specializeUrl := gp.getSpecializeUrl(podIP, 2)
log.Printf("specialize url: %v", specializeUrl)
resp2, err = http.Post(specializeUrl, "application/json", bytes.NewReader(body)) resp2, err = http.Post(specializeUrl, "application/json", bytes.NewReader(body))
} else { } else {
specializeUrl := gp.getSpecializeUrl(podIP, 1) specializeUrl := gp.getSpecializeUrl(podIP, 1)
resp2, err = http.Post(specializeUrl, "text/plain", bytes.NewReader([]byte{})) resp2, err = http.Post(specializeUrl, "text/plain", bytes.NewReader([]byte{}))
} }
if err == nil && resp2.StatusCode < 300 { if err == nil && resp2.StatusCode < 300 {
// Success // Success
resp2.Body.Close() resp2.Body.Close()
return nil return nil
} }
retry := false
// Only retry for the specific case of a connection error. // Only retry for the specific case of a connection error.
if urlErr, ok := err.(*url.Error); ok { if urlErr, ok := err.(*url.Error); ok {
if netErr, ok := urlErr.Err.(*net.OpError); ok { if netErr, ok := urlErr.Err.(*net.OpError); ok {
if netErr.Op == "dial" { if netErr.Op == "dial" {
if i < maxRetries-1 { if i < maxRetries-1 {
time.Sleep(500 * time.Duration(2*i) * time.Millisecond) retry = true
log.Printf("Error connecting to pod (%v), retrying", netErr)
continue
} }
} }
} }
} }
// Receive response with non-200 http code
if err == nil { if err == nil {
err = fission.MakeErrorFromHTTP(resp2) err = fission.MakeErrorFromHTTP(resp2)
// The istio-proxy block all http requests until it's ready
// to serve traffic. Retry if istio feature is enabled.
if gp.useIstio {
retry = true
}
} }
if retry {
time.Sleep(500 * time.Duration(2*i) * time.Millisecond)
log.Printf("Error connecting to pod (%v), retrying", err)
continue
}
log.Printf("Failed to specialize pod: %v", err) log.Printf("Failed to specialize pod: %v", err)
return err return err
} }
@@ -439,6 +476,16 @@ func (gp *GenericPool) createPool() error {
return err return err
} }
// Use long terminationGracePeriodSeconds for connection draining in case that
// pod still runs user functions.
var gracePeriodSeconds int64 = 6 * 60
podAnnotation := make(map[string]string)
if gp.useIstio && gp.env.Spec.AllowAccessToExternalNetwork {
podAnnotation["sidecar.istio.io/inject"] = "false"
}
deployment := &v1beta1.Deployment{ deployment := &v1beta1.Deployment{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: poolDeploymentName, Name: poolDeploymentName,
@@ -451,7 +498,8 @@ func (gp *GenericPool) createPool() error {
}, },
Template: apiv1.PodTemplateSpec{ Template: apiv1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Labels: gp.labelsForPool, Labels: gp.labelsForPool,
Annotations: podAnnotation,
}, },
Spec: apiv1.PodSpec{ Spec: apiv1.PodSpec{
Volumes: []apiv1.Volume{ Volumes: []apiv1.Volume{
@@ -461,14 +509,12 @@ func (gp *GenericPool) createPool() error {
EmptyDir: &apiv1.EmptyDirVolumeSource{}, EmptyDir: &apiv1.EmptyDirVolumeSource{},
}, },
}, },
{ {
Name: "secrets", Name: "secrets",
VolumeSource: apiv1.VolumeSource{ VolumeSource: apiv1.VolumeSource{
EmptyDir: &apiv1.EmptyDirVolumeSource{}, EmptyDir: &apiv1.EmptyDirVolumeSource{},
}, },
}, },
{ {
Name: "config", Name: "config",
VolumeSource: apiv1.VolumeSource{ VolumeSource: apiv1.VolumeSource{
@@ -487,18 +533,32 @@ func (gp *GenericPool) createPool() error {
Name: "userfunc", Name: "userfunc",
MountPath: gp.sharedMountPath, MountPath: gp.sharedMountPath,
}, },
{ {
Name: "secrets", Name: "secrets",
MountPath: gp.sharedSecretPath, MountPath: gp.sharedSecretPath,
}, },
{ {
Name: "config", Name: "config",
MountPath: gp.sharedCfgMapPath, MountPath: gp.sharedCfgMapPath,
}, },
}, },
Resources: gp.env.Spec.Resources, Resources: gp.env.Spec.Resources,
// Pod is removed from endpoints list for service when it's
// state became "Termination". We used preStop hook as the
// workaround for connection draining since pod maybe shutdown
// before grace period expires.
// https://kubernetes.io/docs/concepts/workloads/pods/pod/#termination-of-pods
// https://github.com/kubernetes/kubernetes/issues/47576#issuecomment-308900172
Lifecycle: &apiv1.Lifecycle{
PreStop: &apiv1.Handler{
Exec: &apiv1.ExecAction{
Command: []string{
"sleep",
fmt.Sprintf("%v", gracePeriodSeconds),
},
},
},
},
}, },
{ {
Name: "fetcher", Name: "fetcher",
@@ -510,12 +570,10 @@ func (gp *GenericPool) createPool() error {
Name: "userfunc", Name: "userfunc",
MountPath: gp.sharedMountPath, MountPath: gp.sharedMountPath,
}, },
{ {
Name: "secrets", Name: "secrets",
MountPath: gp.sharedSecretPath, MountPath: gp.sharedSecretPath,
}, },
{ {
Name: "config", Name: "config",
MountPath: gp.sharedCfgMapPath, MountPath: gp.sharedCfgMapPath,
@@ -526,6 +584,22 @@ func (gp *GenericPool) createPool() error {
"-secret-dir", gp.sharedSecretPath, "-secret-dir", gp.sharedSecretPath,
"-cfgmap-dir", gp.sharedCfgMapPath, "-cfgmap-dir", gp.sharedCfgMapPath,
gp.sharedMountPath}, gp.sharedMountPath},
// Pod is removed from endpoints list for service when it's
// state became "Termination". We used preStop hook as the
// workaround for connection draining since pod maybe shutdown
// before grace period expires.
// https://kubernetes.io/docs/concepts/workloads/pods/pod/#termination-of-pods
// https://github.com/kubernetes/kubernetes/issues/47576#issuecomment-308900172
Lifecycle: &apiv1.Lifecycle{
PreStop: &apiv1.Handler{
Exec: &apiv1.ExecAction{
Command: []string{
"sleep",
fmt.Sprintf("%v", gracePeriodSeconds),
},
},
},
},
ReadinessProbe: &apiv1.Probe{ ReadinessProbe: &apiv1.Probe{
InitialDelaySeconds: 1, InitialDelaySeconds: 1,
PeriodSeconds: 1, PeriodSeconds: 1,
@@ -556,10 +630,15 @@ func (gp *GenericPool) createPool() error {
}, },
}, },
ServiceAccountName: "fission-fetcher", ServiceAccountName: "fission-fetcher",
// TerminationGracePeriodSeconds should be equal to the
// sleep time of preStop to make sure that SIGTERM is sent
// to pod after 6 mins.
TerminationGracePeriodSeconds: &gracePeriodSeconds,
}, },
}, },
}, },
} }
depl, err := gp.kubernetesClient.ExtensionsV1beta1().Deployments(gp.namespace).Create(deployment) depl, err := gp.kubernetesClient.ExtensionsV1beta1().Deployments(gp.namespace).Create(deployment)
if err != nil { if err != nil {
log.Printf("Error creating deployment for %s in kubernetes, err: %v", deployment.Name, err) log.Printf("Error creating deployment for %s in kubernetes, err: %v", deployment.Name, err)
@@ -613,9 +692,50 @@ func (gp *GenericPool) createSvc(name string, labels map[string]string) (*apiv1.
} }
func (gp *GenericPool) GetFuncSvc(m *metav1.ObjectMeta) (*fscache.FuncSvc, error) { func (gp *GenericPool) GetFuncSvc(m *metav1.ObjectMeta) (*fscache.FuncSvc, error) {
log.Printf("[%v] Choosing pod from pool", m.Name) log.Printf("[%v] Choosing pod from pool", m.Name)
newLabels := gp.labelsForFunction(m) newLabels := gp.labelsForFunction(m)
if gp.useIstio {
// Istio only allows accessing pod through k8s service, and requests come to
// service are not always being routed to the same pod. For example:
// If there is only one pod (podA) behind the service svcX.
// svcX -> podA
// All requests (specialize request & function access requests)
// will be routed to podA without any problem.
// If podA and podB are behind svcX.
// svcX -> podA (specialized)
// -> podB (non-specialized)
// The specialize request may be routed to podA and the function access
// requests may go to podB. In this case, the function cannot be served
// properly.
// To prevent such problem, we need to delete old versions function pods
// and make sure that there is only one pod behind the service
sel := map[string]string{
"functionName": m.Name,
"functionUid": string(m.UID),
}
podList, err := gp.kubernetesClient.CoreV1().Pods(gp.namespace).List(metav1.ListOptions{
LabelSelector: labels.Set(sel).AsSelector().String(),
})
if err != nil {
return nil, err
}
// Remove old versions function pods
for _, pod := range podList.Items {
// Delete pod no matter what status it is
gp.kubernetesClient.CoreV1().Pods(gp.namespace).Delete(pod.ObjectMeta.Name, nil)
}
}
pod, err := gp.choosePod(newLabels) pod, err := gp.choosePod(newLabels)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -649,6 +769,9 @@ func (gp *GenericPool) GetFuncSvc(m *metav1.ObjectMeta) (*fscache.FuncSvc, error
// the fission router isn't in the same namespace, so return a // the fission router isn't in the same namespace, so return a
// namespace-qualified hostname // namespace-qualified hostname
svcHost = fmt.Sprintf("%v.%v", svcName, gp.namespace) svcHost = fmt.Sprintf("%v.%v", svcName, gp.namespace)
} else if gp.useIstio {
svc := fission.GetFunctionIstioServiceName(m.Name, m.Namespace)
svcHost = fmt.Sprintf("%v.%v:8888", svc, gp.namespace)
} else { } else {
log.Printf("Using pod IP for specialized pod") log.Printf("Using pod IP for specialized pod")
svcHost = fmt.Sprintf("%v:8888", pod.Status.PodIP) svcHost = fmt.Sprintf("%v:8888", pod.Status.PodIP)
+6 -2
View File
@@ -151,11 +151,14 @@ func (gpm *GenericPoolManager) CleanupPools(envs []crd.Environment) {
func (gpm *GenericPoolManager) eagerPoolCreator() { func (gpm *GenericPoolManager) eagerPoolCreator() {
pollSleep := time.Duration(2 * time.Second) pollSleep := time.Duration(2 * time.Second)
for { for {
time.Sleep(pollSleep)
// get list of envs from controller // get list of envs from controller
envs, err := gpm.fissionClient.Environments(metav1.NamespaceAll).List(metav1.ListOptions{}) envs, err := gpm.fissionClient.Environments(metav1.NamespaceAll).List(metav1.ListOptions{})
if err != nil { if err != nil {
if fission.IsNetworkError(err) {
log.Printf("Encountered network error, retrying: %v", err)
time.Sleep(5 * time.Second)
continue
}
log.Fatalf("Failed to get environment list: %v", err) log.Fatalf("Failed to get environment list: %v", err)
} }
@@ -176,6 +179,7 @@ func (gpm *GenericPoolManager) eagerPoolCreator() {
// Clean up pools whose env was deleted // Clean up pools whose env was deleted
gpm.CleanupPools(envs.Items) gpm.CleanupPools(envs.Items)
time.Sleep(pollSleep)
} }
} }
+7 -2
View File
@@ -54,6 +54,7 @@ func envCreate(c *cli.Context) error {
envVersion := c.Int("version") envVersion := c.Int("version")
envBuilderImg := c.String("builder") envBuilderImg := c.String("builder")
envBuildCmd := c.String("buildcmd") envBuildCmd := c.String("buildcmd")
envExternalNetwork := c.Bool("externalnetwork")
if len(envBuilderImg) > 0 { if len(envBuilderImg) > 0 {
envVersion = 2 envVersion = 2
@@ -84,8 +85,9 @@ func envCreate(c *cli.Context) error {
Image: envBuilderImg, Image: envBuilderImg,
Command: envBuildCmd, Command: envBuildCmd,
}, },
Poolsize: poolsize, Poolsize: poolsize,
Resources: resourceReq, Resources: resourceReq,
AllowAccessToExternalNetwork: envExternalNetwork,
}, },
} }
@@ -130,6 +132,7 @@ func envUpdate(c *cli.Context) error {
envImg := c.String("image") envImg := c.String("image")
envBuilderImg := c.String("builder") envBuilderImg := c.String("builder")
envBuildCmd := c.String("buildcmd") envBuildCmd := c.String("buildcmd")
envExternalNetwork := c.Bool("externalnetwork")
if len(envImg) == 0 && len(envBuilderImg) == 0 && len(envBuildCmd) == 0 { if len(envImg) == 0 && len(envBuilderImg) == 0 && len(envBuildCmd) == 0 {
fatal("Need --image to specify env image, or use --builder to specify env builder, or use --buildcmd to specify new build command.") fatal("Need --image to specify env image, or use --builder to specify env builder, or use --buildcmd to specify new build command.")
@@ -160,6 +163,8 @@ func envUpdate(c *cli.Context) error {
env.Spec.Poolsize = c.Int("poolsize") env.Spec.Poolsize = c.Int("poolsize")
} }
env.Spec.AllowAccessToExternalNetwork = envExternalNetwork
_, err = client.EnvironmentUpdate(env) _, err = client.EnvironmentUpdate(env)
checkErr(err, "update environment") checkErr(err, "update environment")
+3 -2
View File
@@ -142,12 +142,13 @@ func main() {
envImageFlag := cli.StringFlag{Name: "image", Usage: "Environment image URL"} envImageFlag := cli.StringFlag{Name: "image", Usage: "Environment image URL"}
envBuilderImageFlag := cli.StringFlag{Name: "builder", Usage: "Environment builder image URL (optional)"} envBuilderImageFlag := cli.StringFlag{Name: "builder", Usage: "Environment builder image URL (optional)"}
envBuildCmdFlag := cli.StringFlag{Name: "buildcmd", Usage: "Build command for environment builder to build source package (optional)"} envBuildCmdFlag := cli.StringFlag{Name: "buildcmd", Usage: "Build command for environment builder to build source package (optional)"}
envExternalNetworkFlag := cli.BoolFlag{Name: "externalnetwork", Usage: "Allow environment access external network when istio feature enabled (optional, defaults to false)"}
envVersionFlag := cli.IntFlag{Name: "version", Usage: "Environment API version: defaults to 1 (means v1 interface)"} envVersionFlag := cli.IntFlag{Name: "version", Usage: "Environment API version: defaults to 1 (means v1 interface)"}
envSubcommands := []cli.Command{ envSubcommands := []cli.Command{
{Name: "create", Aliases: []string{"add"}, Usage: "Add an environment", Flags: []cli.Flag{envNameFlag, envPoolsizeFlag, envImageFlag, envBuilderImageFlag, envBuildCmdFlag, minCpu, maxCpu, minMem, maxMem, envVersionFlag}, Action: envCreate}, {Name: "create", Aliases: []string{"add"}, Usage: "Add an environment", Flags: []cli.Flag{envNameFlag, envPoolsizeFlag, envImageFlag, envBuilderImageFlag, envBuildCmdFlag, minCpu, maxCpu, minMem, maxMem, envVersionFlag, envExternalNetworkFlag}, Action: envCreate},
{Name: "get", Usage: "Get environment details", Flags: []cli.Flag{envNameFlag}, Action: envGet}, {Name: "get", Usage: "Get environment details", Flags: []cli.Flag{envNameFlag}, Action: envGet},
{Name: "update", Usage: "Update environment", Flags: []cli.Flag{envNameFlag, envPoolsizeFlag, envImageFlag, envBuilderImageFlag, envBuildCmdFlag, minCpu, maxCpu, minMem, maxMem}, Action: envUpdate}, {Name: "update", Usage: "Update environment", Flags: []cli.Flag{envNameFlag, envPoolsizeFlag, envImageFlag, envBuilderImageFlag, envBuildCmdFlag, minCpu, maxCpu, minMem, maxMem, envExternalNetworkFlag}, Action: envUpdate},
{Name: "delete", Usage: "Delete environment", Flags: []cli.Flag{envNameFlag}, Action: envDelete}, {Name: "delete", Usage: "Delete environment", Flags: []cli.Flag{envNameFlag}, Action: envDelete},
{Name: "list", Usage: "List all environments", Flags: []cli.Flag{}, Action: envList}, {Name: "list", Usage: "List all environments", Flags: []cli.Flag{}, Action: envList},
} }
+6
View File
@@ -24,6 +24,7 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/fission/fission"
"github.com/fission/fission/crd" "github.com/fission/fission/crd"
) )
@@ -169,6 +170,11 @@ func (mqt *MessageQueueTriggerManager) syncTriggers() {
// get new set of triggers // get new set of triggers
newTriggers, err := mqt.fissionClient.MessageQueueTriggers(metav1.NamespaceAll).List(metav1.ListOptions{}) newTriggers, err := mqt.fissionClient.MessageQueueTriggers(metav1.NamespaceAll).List(metav1.ListOptions{})
if err != nil { if err != nil {
if fission.IsNetworkError(err) {
log.Printf("Encounter network error, retry again: %v", err)
time.Sleep(5 * time.Second)
continue
}
log.Fatalf("Failed to read message queue trigger list: %v", err) log.Fatalf("Failed to read message queue trigger list: %v", err)
} }
newTriggerMap := make(map[string]*crd.MessageQueueTrigger) newTriggerMap := make(map[string]*crd.MessageQueueTrigger)
+2 -1
View File
@@ -17,10 +17,11 @@ limitations under the License.
package router package router
import ( import (
"github.com/gorilla/mux"
"log" "log"
"net/http" "net/http"
"sync/atomic" "sync/atomic"
"github.com/gorilla/mux"
) )
// //
+6
View File
@@ -22,6 +22,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/fission/fission"
"github.com/fission/fission/crd" "github.com/fission/fission/crd"
) )
@@ -45,6 +46,11 @@ func (ws *TimerSync) syncSvc() {
for { for {
triggers, err := ws.fissionClient.TimeTriggers(metav1.NamespaceAll).List(metav1.ListOptions{}) triggers, err := ws.fissionClient.TimeTriggers(metav1.NamespaceAll).List(metav1.ListOptions{})
if err != nil { if err != nil {
if fission.IsNetworkError(err) {
log.Printf("Encounter network error, retry again: %v", err)
time.Sleep(5 * time.Second)
continue
}
log.Fatalf("Failed get time trigger list: %v", err) log.Fatalf("Failed get time trigger list: %v", err)
} }
ws.timer.Sync(triggers.Items) ws.timer.Sync(triggers.Items)
+3
View File
@@ -233,6 +233,9 @@ type (
// Optional, defaults to 'AllowedFunctionsPerContainerSingle' // Optional, defaults to 'AllowedFunctionsPerContainerSingle'
AllowedFunctionsPerContainer AllowedFunctionsPerContainer `json:"allowedFunctionsPerContainer,omitempty"` AllowedFunctionsPerContainer AllowedFunctionsPerContainer `json:"allowedFunctionsPerContainer,omitempty"`
// Optional, defaults to 'false'
AllowAccessToExternalNetwork bool `json:"allowAccessToExternalNetwork,omitempty"`
// Request and limit resources for the environment // Request and limit resources for the environment
Resources v1.ResourceRequirements `json:"resources"` Resources v1.ResourceRequirements `json:"resources"`