diff --git a/README-OPERATOR.md b/README-OPERATOR.md index 31c650f3e..fab9617ed 100644 --- a/README-OPERATOR.md +++ b/README-OPERATOR.md @@ -129,6 +129,8 @@ helm upgrade openfaas --install openfaas/openfaas \ > Note: If you are switching from the OpenFaaS `faas-netes` controller, then you will need to remove all functions and redeploy them after switching to the operator. +If you want to enable multiple namespaces feature which enables you to create functions across namespaces in the cluster, set `clusterRole=true`. + #### Deploy a function with kubectl: ```bash diff --git a/README.md b/README.md index 62481b1bd..1d571237a 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ The rest of this document is dedicated to technical and operational information There are two modes available for faas-netes, the classic mode is the default. * Classic mode (aka faas-netes) - includes a REST API, multiple-namespace support but no Function CRD -* Operator mode (aka "The OpenFaaS Operator") - includes a REST API, with a "Function" CRD, but you must use a single namespace per installation +* Operator mode (aka "The OpenFaaS Operator") - includes a REST API, with a "Function" CRD and multiple-namespace support See also: [README for "The OpenFaaS Operator"](README-OPERATOR.md) @@ -59,21 +59,21 @@ The single faas-netes image and binary contains both modes, switch between one o faas-netes can be configured with environment variables, but for a full set of options see the [helm chart](./chart/openfaas/). -| Option | Usage | -|---------------------|-------------------------------------------------------------------------------------------------| -| `httpProbe` | Boolean - use http probe type for function readiness and liveness. Default: `false` | -| `write_timeout` | HTTP timeout for writing a response body from your function (in seconds). Default: `60s` | -| `read_timeout` | HTTP timeout for reading the payload from the client caller (in seconds). Default: `60s` | -| `image_pull_policy` | Image pull policy for deployed functions (`Always`, `IfNotPresent`, `Never`). Default: `Always` | -| `gateway.resources` | CPU/Memory resources requests/limits (memory: `120Mi`, cpu: `50m`) | -| `faasnetes.resources` | CPU/Memory resources requests/limits (memory: `120Mi`, cpu: `50m`) | -| `operator.resources` | CPU/Memory resources requests/limits (memory: `120Mi`, cpu: `50m`) | -| `queueWorker.resources` | CPU/Memory resources requests/limits (memory: `120Mi`, cpu: `50m`) | -| `prometheus.resources` | CPU/Memory resources requests/limits (memory: `512Mi`) | -| `alertmanager.resources` | CPU/Memory resources requests/limits (memory: `25Mi`) | -| `nats.resources` | CPU/Memory resources requests/limits (memory: `120Mi`) | -| `faasIdler.resources` | CPU/Memory resources requests/limits (memory: `64Mi`) | -| `basicAuthPlugin.resources`| CPU/Memory resources requests/limits (memory: `50Mi`, cpu: `20m`) | +| Option | Usage | +| --------------------------- | ------------------------------------------------------------------------------------------------ | +| `httpProbe` | Boolean - use http probe type for function readiness and liveness. Default: `false` | +| `write_timeout` | HTTP timeout for writing a response body from your function (in seconds). Default: `60s` | +| `read_timeout` | HTTP timeout for reading the payload from the client caller (in seconds). Default: `60s` | +| `image_pull_policy` | Image pull policy for deployed functions (`Always`, `IfNotPresent`, `Never`). Default: `Always` | +| `gateway.resources` | CPU/Memory resources requests/limits (memory: `120Mi`, cpu: `50m`) | +| `faasnetes.resources` | CPU/Memory resources requests/limits (memory: `120Mi`, cpu: `50m`) | +| `operator.resources` | CPU/Memory resources requests/limits (memory: `120Mi`, cpu: `50m`) | +| `queueWorker.resources` | CPU/Memory resources requests/limits (memory: `120Mi`, cpu: `50m`) | +| `prometheus.resources` | CPU/Memory resources requests/limits (memory: `512Mi`) | +| `alertmanager.resources` | CPU/Memory resources requests/limits (memory: `25Mi`) | +| `nats.resources` | CPU/Memory resources requests/limits (memory: `120Mi`) | +| `faasIdler.resources` | CPU/Memory resources requests/limits (memory: `64Mi`) | +| `basicAuthPlugin.resources` | CPU/Memory resources requests/limits (memory: `50Mi`, cpu: `20m`) | ### Readiness checking diff --git a/main.go b/main.go index 874e2d9f3..c55c0b5e3 100644 --- a/main.go +++ b/main.go @@ -104,7 +104,7 @@ func main() { defaultResync := time.Minute * 5 namespaceScope := config.DefaultFunctionNamespace - if operator && config.ClusterRole { + if config.ClusterRole { namespaceScope = "" } diff --git a/pkg/handlers/namespaces.go b/pkg/handlers/namespaces.go index 885dc7a03..4b04dc100 100644 --- a/pkg/handlers/namespaces.go +++ b/pkg/handlers/namespaces.go @@ -23,6 +23,10 @@ func MakeNamespacesLister(defaultNamespace string, clientset kubernetes.Interfac return func(w http.ResponseWriter, r *http.Request) { log.Println("Query namespaces") + if r.Body != nil { + defer r.Body.Close() + } + res := ListNamespaces(defaultNamespace, clientset) out, err := json.Marshal(res) diff --git a/pkg/server/apply.go b/pkg/server/apply.go index be54c3149..997db5f3e 100644 --- a/pkg/server/apply.go +++ b/pkg/server/apply.go @@ -1,7 +1,6 @@ package server import ( - "context" "encoding/json" "fmt" "io/ioutil" @@ -39,7 +38,7 @@ func makeApplyHandler(defaultNamespace string, client clientset.Interface) http. } opts := metav1.GetOptions{} - got, err := client.OpenfaasV1().Functions(namespace).Get(context.TODO(), req.Service, opts) + got, err := client.OpenfaasV1().Functions(namespace).Get(r.Context(), req.Service, opts) miss := false if err != nil { if errors.IsNotFound(err) { @@ -60,7 +59,7 @@ func makeApplyHandler(defaultNamespace string, client clientset.Interface) http. updated.Spec = toFunctionSpec(req) if _, err = client.OpenfaasV1().Functions(namespace). - Update(context.TODO(), updated, metav1.UpdateOptions{}); err != nil { + Update(r.Context(), updated, metav1.UpdateOptions{}); err != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(fmt.Sprintf("Error updating function: %s", err.Error()))) return @@ -76,7 +75,7 @@ func makeApplyHandler(defaultNamespace string, client clientset.Interface) http. } if _, err = client.OpenfaasV1().Functions(namespace). - Create(context.TODO(), newFunc, metav1.CreateOptions{}); err != nil { + Create(r.Context(), newFunc, metav1.CreateOptions{}); err != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(fmt.Sprintf("Error creating function: %s", err.Error()))) return diff --git a/pkg/server/delete.go b/pkg/server/delete.go index f5168032d..679766283 100644 --- a/pkg/server/delete.go +++ b/pkg/server/delete.go @@ -1,7 +1,6 @@ package server import ( - "context" "encoding/json" "io" "io/ioutil" @@ -49,7 +48,7 @@ func makeDeleteHandler(defaultNamespace string, client clientset.Interface) http } err = client.OpenfaasV1().Functions(lookupNamespace). - Delete(context.TODO(), request.FunctionName, metav1.DeleteOptions{}) + Delete(r.Context(), request.FunctionName, metav1.DeleteOptions{}) if err != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(err.Error())) diff --git a/pkg/server/list.go b/pkg/server/list.go index b317ed05b..22161a7fb 100644 --- a/pkg/server/list.go +++ b/pkg/server/list.go @@ -1,7 +1,6 @@ package server import ( - "context" "encoding/json" "net/http" @@ -39,7 +38,7 @@ func makeListHandler(defaultNamespace string, functions := []types.FunctionStatus{} opts := metav1.ListOptions{} - res, err := client.OpenfaasV1().Functions(lookupNamespace).List(context.TODO(), opts) + res, err := client.OpenfaasV1().Functions(lookupNamespace).List(r.Context(), opts) if err != nil { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(err.Error())) diff --git a/pkg/server/namespace.go b/pkg/server/namespace.go deleted file mode 100644 index 7b31d9a0a..000000000 --- a/pkg/server/namespace.go +++ /dev/null @@ -1,29 +0,0 @@ -package server - -import ( - "encoding/json" - "net/http" - - "github.com/openfaas/faas-netes/pkg/handlers" - "k8s.io/client-go/kubernetes" - glog "k8s.io/klog" -) - -func makeListNamespaceHandler(defaultNamespace string, clientset kubernetes.Interface) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - res := handlers.ListNamespaces(defaultNamespace, clientset) - - out, err := json.Marshal(res) - if err != nil { - glog.Errorf("Failed to marshal namespaces: %s", err.Error()) - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte("Failed to marshal namespaces")) - return - } - - w.Header().Set("Content-Type", "application/json") - - w.WriteHeader(http.StatusOK) - w.Write(out) - } -} diff --git a/pkg/server/replicas.go b/pkg/server/replicas.go index a69799c57..52926dc2c 100644 --- a/pkg/server/replicas.go +++ b/pkg/server/replicas.go @@ -1,7 +1,6 @@ package server import ( - "context" "encoding/json" "io/ioutil" "net/http" @@ -31,7 +30,7 @@ func makeReplicaReader(defaultNamespace string, client clientset.Interface, list opts := metav1.GetOptions{} k8sfunc, err := client.OpenfaasV1().Functions(lookupNamespace). - Get(context.TODO(), functionName, opts) + Get(r.Context(), functionName, opts) if err != nil { w.WriteHeader(http.StatusNotFound) w.Write([]byte(err.Error())) @@ -110,7 +109,7 @@ func makeReplicaHandler(defaultNamespace string, kube kubernetes.Interface) http } opts := metav1.GetOptions{} - dep, err := kube.AppsV1().Deployments(lookupNamespace).Get(context.TODO(), functionName, opts) + dep, err := kube.AppsV1().Deployments(lookupNamespace).Get(r.Context(), functionName, opts) if err != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(err.Error())) @@ -119,7 +118,7 @@ func makeReplicaHandler(defaultNamespace string, kube kubernetes.Interface) http } dep.Spec.Replicas = int32p(int32(req.Replicas)) - _, err = kube.AppsV1().Deployments(lookupNamespace).Update(context.TODO(), dep, metav1.UpdateOptions{}) + _, err = kube.AppsV1().Deployments(lookupNamespace).Update(r.Context(), dep, metav1.UpdateOptions{}) if err != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(err.Error())) diff --git a/pkg/server/secret.go b/pkg/server/secret.go deleted file mode 100644 index a5b5e24fc..000000000 --- a/pkg/server/secret.go +++ /dev/null @@ -1,24 +0,0 @@ -package server - -import ( - "net/http" - - "github.com/openfaas/faas-netes/pkg/k8s" - - "github.com/openfaas/faas-netes/pkg/handlers" - "k8s.io/client-go/kubernetes" -) - -const ( - secretLabel = "app.kubernetes.io/managed-by" - secretLabelValue = "openfaas" -) - -// makeSecretHandler provides the secrets CRUD endpoint -func makeSecretHandler(namespace string, kube kubernetes.Interface) http.HandlerFunc { - handler := handlers.SecretsHandler{ - LookupNamespace: handlers.NewNamespaceResolver(namespace, kube), - Secrets: k8s.NewSecretsClient(kube), - } - return handler.ServeHTTP -} diff --git a/pkg/server/secret_test.go b/pkg/server/secret_test.go deleted file mode 100644 index 1b6c9a8bd..000000000 --- a/pkg/server/secret_test.go +++ /dev/null @@ -1,160 +0,0 @@ -package server - -import ( - "context" - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "net/http/httptest" - "strings" - "testing" - - faastypes "github.com/openfaas/faas-provider/types" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - testclient "k8s.io/client-go/kubernetes/fake" -) - -func Test_makeSecretHandler(t *testing.T) { - namespace := "openfaas-fn" - kube := testclient.NewSimpleClientset() - secretsHandler := makeSecretHandler(namespace, kube).ServeHTTP - - secretName := "testsecret" - - t.Run("create managed secrets", func(t *testing.T) { - secretValue := "testsecretvalue" - payload := fmt.Sprintf(`{"name": "%s", "value": "%s"}`, secretName, secretValue) - req := httptest.NewRequest("POST", "http://system/secrets", strings.NewReader(payload)) - w := httptest.NewRecorder() - - secretsHandler(w, req) - - resp := w.Result() - if resp.StatusCode != http.StatusAccepted { - t.Errorf("expected status code '%d', got '%d'", http.StatusAccepted, resp.StatusCode) - } - - actualSecret, err := kube.CoreV1().Secrets(namespace).Get(context.TODO(), "testsecret", metav1.GetOptions{}) - if err != nil { - t.Errorf("error validating secret: %s", err) - } - - if actualSecret.Name != "testsecret" { - t.Errorf("expected secret with name: 'testsecret', got: '%s'", actualSecret.Name) - } - - managedBy := actualSecret.Labels[secretLabel] - if managedBy != secretLabelValue { - t.Errorf("expected secret to be managed by '%s', got: '%s'", secretLabelValue, managedBy) - } - - actualValue := actualSecret.StringData[secretName] - if actualValue != secretValue { - t.Errorf("expected secret value: '%s', got: '%s'", secretValue, actualValue) - } - }) - - t.Run("update managed secrets", func(t *testing.T) { - newSecretValue := "newtestsecretvalue" - payload := fmt.Sprintf(`{"name": "%s", "value": "%s"}`, secretName, newSecretValue) - req := httptest.NewRequest("PUT", "http://system/secrets", strings.NewReader(payload)) - w := httptest.NewRecorder() - - secretsHandler(w, req) - - resp := w.Result() - if resp.StatusCode != http.StatusAccepted { - t.Errorf("expected status code '%d', got '%d'", http.StatusAccepted, resp.StatusCode) - } - - actualSecret, err := kube.CoreV1().Secrets(namespace).Get(context.TODO(), "testsecret", metav1.GetOptions{}) - if err != nil { - t.Errorf("error validting secret: %s", err) - } - - if actualSecret.Name != "testsecret" { - t.Errorf("expected secret with name: 'testsecret', got: '%s'", actualSecret.Name) - } - - managedBy := actualSecret.Labels[secretLabel] - if managedBy != secretLabelValue { - t.Errorf("expected secret to be managed by '%s', got: '%s'", secretLabelValue, managedBy) - } - - actualValue := actualSecret.StringData[secretName] - if actualValue != newSecretValue { - t.Errorf("expected secret value: '%s', got: '%s'", newSecretValue, actualValue) - } - }) - - t.Run("list managed secrets only", func(t *testing.T) { - req := httptest.NewRequest("GET", "http://system/secrets", nil) - w := httptest.NewRecorder() - - secretsHandler(w, req) - - resp := w.Result() - if resp.StatusCode != http.StatusOK { - t.Errorf("expected status code '%d', got '%d'", http.StatusOK, resp.StatusCode) - } - - decoder := json.NewDecoder(resp.Body) - - secretList := []faastypes.Secret{} - err := decoder.Decode(&secretList) - if err != nil { - t.Error(err) - } - - if len(secretList) != 1 { - t.Errorf("expected 1 secret, got %d", len(secretList)) - } - - actualSecret := secretList[0] - if actualSecret.Name != secretName { - t.Errorf("expected secret name: '%s', got: '%s'", secretName, actualSecret.Name) - } - }) - - t.Run("delete managed secrets", func(t *testing.T) { - secretName := "testsecret" - payload := fmt.Sprintf(`{"name": "%s"}`, secretName) - req := httptest.NewRequest("DELETE", "http://system/secrets", strings.NewReader(payload)) - w := httptest.NewRecorder() - - secretsHandler(w, req) - - resp := w.Result() - if resp.StatusCode != http.StatusAccepted { - t.Errorf("expected status code '%d', got '%d'", http.StatusAccepted, resp.StatusCode) - } - - actualSecret, err := kube.CoreV1().Secrets(namespace).Get(context.TODO(), "testsecret", metav1.GetOptions{}) - if err == nil { - t.Errorf("expected not found error, got secret payload '%s'", actualSecret) - } - }) -} - -func Test_makeSecretHandler_WithEmptyList(t *testing.T) { - namespace := "openfaas-fn" - kube := testclient.NewSimpleClientset() - secretsHandler := makeSecretHandler(namespace, kube).ServeHTTP - - req := httptest.NewRequest("GET", "http://system/secrets", nil) - w := httptest.NewRecorder() - - secretsHandler(w, req) - - resp := w.Result() - if resp.StatusCode != http.StatusOK { - t.Errorf("expected status code '%d', got '%d'", http.StatusOK, resp.StatusCode) - } - - body, _ := ioutil.ReadAll(resp.Body) - - if string(body) == "null" { - t.Errorf(`want empty list to be valid json i.e. "[]", but was %q`, string(body)) - } -} diff --git a/pkg/server/server.go b/pkg/server/server.go index 278bfb0bf..16e846faf 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -8,6 +8,7 @@ import ( "time" clientset "github.com/openfaas/faas-netes/pkg/client/clientset/versioned" + "github.com/openfaas/faas-netes/pkg/handlers" "github.com/openfaas/faas-netes/pkg/k8s" faasnetesk8s "github.com/openfaas/faas-netes/pkg/k8s" bootstrap "github.com/openfaas/faas-provider" @@ -90,8 +91,8 @@ func New(client clientset.Interface, UpdateHandler: makeApplyHandler(functionNamespace, client), HealthHandler: makeHealthHandler(), InfoHandler: makeInfoHandler(), - SecretHandler: makeSecretHandler(functionNamespace, kube), - ListNamespaceHandler: makeListNamespaceHandler(functionNamespace, kube), + SecretHandler: handlers.MakeSecretHandler(functionNamespace, kube), + ListNamespaceHandler: handlers.MakeNamespacesLister(functionNamespace, kube), LogHandler: logs.NewLogHandlerFunc(faasnetesk8s.NewLogRequestor(kube, functionNamespace), bootstrapConfig.WriteTimeout), }