Compare commits

...

13 Commits

Author SHA1 Message Date
bbd3b4ff07 Add comment to explain how method works
Signed-off-by: Alex Ellis (OpenFaaS Ltd) <alexellis2@gmail.com>
2021-11-01 11:04:38 +00:00
1d07fda0a4 Wait for a function to become healthy in scale-up event
Prior to this change, after scaling a function up and
returning the API call, a function may still not be ready to
serve traffic. This resulted in HTTP errors, for a percentage
of the time, especially if the task was deleted instead of
being just paused.

Pausing was instant, but during re-creation the function needs
some time to start up.

This change puts a health check into the hot path for the
scale event. It is blocking, so scaling up will have some
additional latency, but will return with a ready endpoint
much more of the time than previously.

This approach means that faasd doesn't have to run a set of
exec or HTTP healthchecks continually, and use CPU for
each of them, even when a function is idle.

Tested with the nodeinfo function, by killing the task
and then invoking the function. Prior to this, the
function may give an error code some of the time.

Signed-off-by: Alex Ellis (OpenFaaS Ltd) <alexellis2@gmail.com>
2021-11-01 11:00:39 +00:00
b42066d1a1 Fixed bad memory display and refactor test cases in functions_test.go
Signed-off-by: Shikachuu <zcmate@gmail.com>
2021-11-01 10:07:25 +00:00
17188b8de9 Added unit tests for readMemoryLimitFromSpec
Signed-off-by: Shikachuu <zcmate@gmail.com>
2021-11-01 10:07:25 +00:00
0c0088e8b0 Change readMemoryLimitFromSpec, to a more clear implementation, edited error message.
Signed-off-by: Shikachuu <zcmate@gmail.com>
2021-11-01 10:07:25 +00:00
c5f167df21 Change plain number response to Decimal String.
Signed-off-by: Shikachuu <zcmate@gmail.com>
2021-11-01 10:07:25 +00:00
d5fcc7b2ab Fixed nil pointer dereference while parsing memory limit
Signed-off-by: Shikachuu <zcmate@gmail.com>
2021-11-01 10:07:25 +00:00
cbfefb6fa5 Extend the Function type with a memoryLimit field, create a conversion to k8s resource value and return it through the REST API.
Signed-off-by: Shikachuu <zcmate@gmail.com>
2021-11-01 10:07:25 +00:00
ea62c1b12d feat: add support for raw secret values
Load the secret value from the RawValue field, if it is empty, use the
string value. Add unit tests for the creation handler.

Refactor secret parser tests.

Resolves #208

Signed-off-by: Lucas Roesler <roesler.lucas@gmail.com>
2021-10-17 18:04:06 +01:00
8f40618a5c Update README.md 2021-10-17 15:49:25 +01:00
3fe0d8d8d3 Update messages to want/got for unit tests
This is the style used in the openfaas project.

Signed-off-by: Alex Ellis (OpenFaaS Ltd) <alexellis2@gmail.com>
2021-09-16 10:44:36 +01:00
5aa4c69e03 Inline namespace check and create const for label
* Inlines the namespace check for valid faasd namespaces
* Creates a const for the namespace label applied to faasd
namespaces

Tested with go build and go test.

Signed-off-by: Alex Ellis (OpenFaaS Ltd) <alexellis2@gmail.com>
2021-09-16 10:43:21 +01:00
12b5e8ca7f Add check for namespace label openfaas=true
This commit adds the checks that the namespace supplied by the user has
the `openfaas=true` label. Without this check the user can
deploy/update/read functions in any namespace  using the CLI.

The UI is not effected because it calls the listnamesaces endpoint,
which has the check for the label

Signed-off-by: Alistair Hey <alistair@heyal.co.uk>
2021-09-16 10:37:32 +01:00
18 changed files with 482 additions and 132 deletions

View File

@ -1,5 +1,6 @@
# faasd - a lightweight & portable faas engine # faasd - a lightweight & portable faas engine
[![Sponsor this](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&link=https://github.com/sponsors/openfaas)](https://github.com/sponsors/openfaas)
[![Build Status](https://github.com/openfaas/faasd/workflows/build/badge.svg?branch=master)](https://github.com/openfaas/faasd/actions) [![Build Status](https://github.com/openfaas/faasd/workflows/build/badge.svg?branch=master)](https://github.com/openfaas/faasd/actions)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![OpenFaaS](https://img.shields.io/badge/openfaas-serverless-blue.svg)](https://www.openfaas.com) [![OpenFaaS](https://img.shields.io/badge/openfaas-serverless-blue.svg)](https://www.openfaas.com)

View File

@ -88,7 +88,7 @@ func makeProviderCmd() *cobra.Command {
baseUserSecretsPath := path.Join(wd, "secrets") baseUserSecretsPath := path.Join(wd, "secrets")
if err := moveSecretsToDefaultNamespaceSecrets( if err := moveSecretsToDefaultNamespaceSecrets(
baseUserSecretsPath, baseUserSecretsPath,
faasd.FunctionNamespace); err != nil { faasd.DefaultFunctionNamespace); err != nil {
return err return err
} }
@ -98,7 +98,7 @@ func makeProviderCmd() *cobra.Command {
DeployHandler: handlers.MakeDeployHandler(client, cni, baseUserSecretsPath, alwaysPull), DeployHandler: handlers.MakeDeployHandler(client, cni, baseUserSecretsPath, alwaysPull),
FunctionReader: handlers.MakeReadHandler(client), FunctionReader: handlers.MakeReadHandler(client),
ReplicaReader: handlers.MakeReplicaReaderHandler(client), ReplicaReader: handlers.MakeReplicaReaderHandler(client),
ReplicaUpdater: handlers.MakeReplicaUpdateHandler(client, cni), ReplicaUpdater: handlers.MakeReplicaUpdateHandler(client, cni, invokeResolver),
UpdateHandler: handlers.MakeUpdateHandler(client, cni, baseUserSecretsPath, alwaysPull), UpdateHandler: handlers.MakeUpdateHandler(client, cni, baseUserSecretsPath, alwaysPull),
HealthHandler: func(w http.ResponseWriter, r *http.Request) {}, HealthHandler: func(w http.ResponseWriter, r *http.Request) {},
InfoHandler: handlers.MakeInfoHandler(Version, GitCommit), InfoHandler: handlers.MakeInfoHandler(Version, GitCommit),

View File

@ -1,8 +1,11 @@
package pkg package pkg
const ( const (
// FunctionNamespace is the default containerd namespace functions are created // DefaultFunctionNamespace is the default containerd namespace functions are created
FunctionNamespace = "openfaas-fn" DefaultFunctionNamespace = "openfaas-fn"
// NamespaceLabel indicates that a namespace is managed by faasd
NamespaceLabel = "openfaas"
// FaasdNamespace is the containerd namespace services are created // FaasdNamespace is the containerd namespace services are created
FaasdNamespace = "openfaas" FaasdNamespace = "openfaas"

View File

@ -71,7 +71,7 @@ func buildCmd(ctx context.Context, req logs.Request) *exec.Cmd {
namespace := req.Namespace namespace := req.Namespace
if namespace == "" { if namespace == "" {
namespace = faasd.FunctionNamespace namespace = faasd.DefaultFunctionNamespace
} }
// find the description of the fields here // find the description of the fields here

View File

@ -42,6 +42,18 @@ func MakeDeleteHandler(client *containerd.Client, cni gocni.CNI) func(w http.Res
lookupNamespace := getRequestNamespace(readNamespaceFromQuery(r)) lookupNamespace := getRequestNamespace(readNamespaceFromQuery(r))
// Check if namespace exists, and it has the openfaas label
valid, err := validNamespace(client, lookupNamespace)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if !valid {
http.Error(w, "namespace not valid", http.StatusBadRequest)
return
}
name := req.FunctionName name := req.FunctionName
function, err := GetFunction(client, name, lookupNamespace) function, err := GetFunction(client, name, lookupNamespace)

View File

@ -52,10 +52,25 @@ func MakeDeployHandler(client *containerd.Client, cni gocni.CNI, secretMountPath
} }
namespace := getRequestNamespace(req.Namespace) namespace := getRequestNamespace(req.Namespace)
// Check if namespace exists, and it has the openfaas label
valid, err := validNamespace(client, namespace)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if !valid {
http.Error(w, "namespace not valid", http.StatusBadRequest)
return
}
namespaceSecretMountPath := getNamespaceSecretMountPath(secretMountPath, namespace) namespaceSecretMountPath := getNamespaceSecretMountPath(secretMountPath, namespace)
err = validateSecrets(namespaceSecretMountPath, req.Secrets) err = validateSecrets(namespaceSecretMountPath, req.Secrets)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return
} }
name := req.Service name := req.Service

View File

@ -53,7 +53,7 @@ func Test_BuildLabels_WithAnnotations(t *testing.T) {
} }
if !reflect.DeepEqual(val, tc.result) { if !reflect.DeepEqual(val, tc.result) {
t.Errorf("Got: %s, expected %s", val, tc.result) t.Errorf("Want: %s, got: %s", val, tc.result)
} }
}) })
} }

View File

@ -2,6 +2,7 @@ package handlers
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"log" "log"
"strings" "strings"
@ -11,6 +12,7 @@ import (
"github.com/containerd/containerd" "github.com/containerd/containerd"
"github.com/containerd/containerd/namespaces" "github.com/containerd/containerd/namespaces"
"github.com/openfaas/faasd/pkg"
faasd "github.com/openfaas/faasd/pkg" faasd "github.com/openfaas/faasd/pkg"
"github.com/openfaas/faasd/pkg/cninetwork" "github.com/openfaas/faasd/pkg/cninetwork"
) )
@ -27,11 +29,23 @@ type Function struct {
secrets []string secrets []string
envVars map[string]string envVars map[string]string
envProcess string envProcess string
memoryLimit int64
createdAt time.Time createdAt time.Time
} }
// ListFunctions returns a map of all functions with running tasks on namespace // ListFunctions returns a map of all functions with running tasks on namespace
func ListFunctions(client *containerd.Client, namespace string) (map[string]*Function, error) { func ListFunctions(client *containerd.Client, namespace string) (map[string]*Function, error) {
// Check if namespace exists, and it has the openfaas label
valid, err := validNamespace(client, namespace)
if err != nil {
return nil, err
}
if !valid {
return nil, errors.New("namespace not valid")
}
ctx := namespaces.WithNamespace(context.Background(), namespace) ctx := namespaces.WithNamespace(context.Background(), namespace)
functions := make(map[string]*Function) functions := make(map[string]*Function)
@ -79,7 +93,7 @@ func GetFunction(client *containerd.Client, name string, namespace string) (Func
spec, err := c.Spec(ctx) spec, err := c.Spec(ctx)
if err != nil { if err != nil {
return Function{}, fmt.Errorf("unable to load function spec for reading secrets: %s, error %w", name, err) return Function{}, fmt.Errorf("unable to load function %s error: %w", name, err)
} }
info, err := c.Info(ctx) info, err := c.Info(ctx)
@ -99,6 +113,7 @@ func GetFunction(client *containerd.Client, name string, namespace string) (Func
fn.envVars = envVars fn.envVars = envVars
fn.envProcess = envProcess fn.envProcess = envProcess
fn.createdAt = info.CreatedAt fn.createdAt = info.CreatedAt
fn.memoryLimit = readMemoryLimitFromSpec(spec)
replicas := 0 replicas := 0
task, err := c.Task(ctx, nil) task, err := c.Task(ctx, nil)
@ -187,7 +202,7 @@ func ListNamespaces(client *containerd.Client) []string {
namespaces, err := store.List(context.Background()) namespaces, err := store.List(context.Background())
if err != nil { if err != nil {
log.Printf("Error listing namespaces: %s", err.Error()) log.Printf("Error listing namespaces: %s", err.Error())
set = append(set, faasd.FunctionNamespace) set = append(set, faasd.DefaultFunctionNamespace)
return set return set
} }
@ -198,12 +213,12 @@ func ListNamespaces(client *containerd.Client) []string {
continue continue
} }
if _, found := labels["openfaas"]; found { if _, found := labels[pkg.NamespaceLabel]; found {
set = append(set, namespace) set = append(set, namespace)
} }
if !findNamespace(faasd.FunctionNamespace, set) { if !findNamespace(faasd.DefaultFunctionNamespace, set) {
set = append(set, faasd.FunctionNamespace) set = append(set, faasd.DefaultFunctionNamespace)
} }
} }
@ -218,3 +233,10 @@ func findNamespace(target string, items []string) bool {
} }
return false return false
} }
func readMemoryLimitFromSpec(spec *specs.Spec) int64 {
if spec.Linux == nil || spec.Linux.Resources == nil || spec.Linux.Resources.Memory == nil || spec.Linux.Resources.Memory.Limit == nil {
return 0
}
return *spec.Linux.Resources.Memory.Limit
}

View File

@ -32,54 +32,54 @@ func Test_BuildLabelsAndAnnotationsFromServiceSpec_Annotations(t *testing.T) {
} }
func Test_SplitMountToSecrets(t *testing.T) { func Test_SplitMountToSecrets(t *testing.T) {
type test struct { type testCase struct {
Name string Name string
Input []specs.Mount Input []specs.Mount
Expected []string Want []string
} }
tests := []test{ tests := []testCase{
{Name: "No matching openfaas secrets", Input: []specs.Mount{{Destination: "/foo/"}}, Expected: []string{}}, {Name: "No matching openfaas secrets", Input: []specs.Mount{{Destination: "/foo/"}}, Want: []string{}},
{Name: "Nil mounts", Input: nil, Expected: []string{}}, {Name: "Nil mounts", Input: nil, Want: []string{}},
{Name: "No Mounts", Input: []specs.Mount{{Destination: "/foo/"}}, Expected: []string{}}, {Name: "No Mounts", Input: []specs.Mount{{Destination: "/foo/"}}, Want: []string{}},
{Name: "One Mounts IS secret", Input: []specs.Mount{{Destination: "/var/openfaas/secrets/secret1"}}, Expected: []string{"secret1"}}, {Name: "One Mounts IS secret", Input: []specs.Mount{{Destination: "/var/openfaas/secrets/secret1"}}, Want: []string{"secret1"}},
{Name: "Multiple Mounts 1 secret", Input: []specs.Mount{{Destination: "/var/openfaas/secrets/secret1"}, {Destination: "/some/other/path"}}, Expected: []string{"secret1"}}, {Name: "Multiple Mounts 1 secret", Input: []specs.Mount{{Destination: "/var/openfaas/secrets/secret1"}, {Destination: "/some/other/path"}}, Want: []string{"secret1"}},
{Name: "Multiple Mounts all secrets", Input: []specs.Mount{{Destination: "/var/openfaas/secrets/secret1"}, {Destination: "/var/openfaas/secrets/secret2"}}, Expected: []string{"secret1", "secret2"}}, {Name: "Multiple Mounts all secrets", Input: []specs.Mount{{Destination: "/var/openfaas/secrets/secret1"}, {Destination: "/var/openfaas/secrets/secret2"}}, Want: []string{"secret1", "secret2"}},
} }
for _, tc := range tests { for _, tc := range tests {
t.Run(tc.Name, func(t *testing.T) { t.Run(tc.Name, func(t *testing.T) {
got := readSecretsFromMounts(tc.Input) got := readSecretsFromMounts(tc.Input)
if !reflect.DeepEqual(got, tc.Expected) { if !reflect.DeepEqual(got, tc.Want) {
t.Fatalf("expected %s, got %s", tc.Expected, got) t.Fatalf("Want %s, got %s", tc.Want, got)
} }
}) })
} }
} }
func Test_ProcessEnvToEnvVars(t *testing.T) { func Test_ProcessEnvToEnvVars(t *testing.T) {
type test struct { type testCase struct {
Name string Name string
Input []string Input []string
Expected map[string]string Want map[string]string
fprocess string fprocess string
} }
tests := []test{ tests := []testCase{
{Name: "No matching EnvVars", Input: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "fprocess=python index.py"}, Expected: make(map[string]string), fprocess: "python index.py"}, {Name: "No matching EnvVars", Input: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "fprocess=python index.py"}, Want: make(map[string]string), fprocess: "python index.py"},
{Name: "No EnvVars", Input: []string{}, Expected: make(map[string]string), fprocess: ""}, {Name: "No EnvVars", Input: []string{}, Want: make(map[string]string), fprocess: ""},
{Name: "One EnvVar", Input: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "fprocess=python index.py", "env=this"}, Expected: map[string]string{"env": "this"}, fprocess: "python index.py"}, {Name: "One EnvVar", Input: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "fprocess=python index.py", "env=this"}, Want: map[string]string{"env": "this"}, fprocess: "python index.py"},
{Name: "Multiple EnvVars", Input: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "this=that", "env=var", "fprocess=python index.py"}, Expected: map[string]string{"this": "that", "env": "var"}, fprocess: "python index.py"}, {Name: "Multiple EnvVars", Input: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "this=that", "env=var", "fprocess=python index.py"}, Want: map[string]string{"this": "that", "env": "var"}, fprocess: "python index.py"},
{Name: "Nil EnvVars", Input: nil, Expected: make(map[string]string)}, {Name: "Nil EnvVars", Input: nil, Want: make(map[string]string)},
} }
for _, tc := range tests { for _, tc := range tests {
t.Run(tc.Name, func(t *testing.T) { t.Run(tc.Name, func(t *testing.T) {
got, fprocess := readEnvFromProcessEnv(tc.Input) got, fprocess := readEnvFromProcessEnv(tc.Input)
if !reflect.DeepEqual(got, tc.Expected) { if !reflect.DeepEqual(got, tc.Want) {
t.Fatalf("expected: %s, got: %s", tc.Expected, got) t.Fatalf("Want: %s, got: %s", tc.Want, got)
} }
if fprocess != tc.fprocess { if fprocess != tc.fprocess {
t.Fatalf("expected fprocess: %s, got: %s", tc.fprocess, got) t.Fatalf("Want fprocess: %s, got: %s", tc.fprocess, got)
} }
}) })
@ -87,22 +87,46 @@ func Test_ProcessEnvToEnvVars(t *testing.T) {
} }
func Test_findNamespace(t *testing.T) { func Test_findNamespace(t *testing.T) {
type test struct { type testCase struct {
Name string Name string
foundNamespaces []string foundNamespaces []string
namespace string namespace string
Expected bool Want bool
} }
tests := []test{ tests := []testCase{
{Name: "Namespace Found", namespace: "fn", foundNamespaces: []string{"fn", "openfaas-fn"}, Expected: true}, {Name: "Namespace Found", namespace: "fn", foundNamespaces: []string{"fn", "openfaas-fn"}, Want: true},
{Name: "namespace Not Found", namespace: "fn", foundNamespaces: []string{"openfaas-fn"}, Expected: false}, {Name: "namespace Not Found", namespace: "fn", foundNamespaces: []string{"openfaas-fn"}, Want: false},
} }
for _, tc := range tests { for _, tc := range tests {
t.Run(tc.Name, func(t *testing.T) { t.Run(tc.Name, func(t *testing.T) {
got := findNamespace(tc.namespace, tc.foundNamespaces) got := findNamespace(tc.namespace, tc.foundNamespaces)
if got != tc.Expected { if got != tc.Want {
t.Fatalf("expected %t, got %t", tc.Expected, got) t.Fatalf("Want %t, got %t", tc.Want, got)
}
})
}
}
func Test_readMemoryLimitFromSpec(t *testing.T) {
type testCase struct {
Name string
Spec *specs.Spec
Want int64
}
testLimit := int64(64)
tests := []testCase{
{Name: "specs.Linux not found", Spec: &specs.Spec{Linux: nil}, Want: int64(0)},
{Name: "specs.LinuxResource not found", Spec: &specs.Spec{Linux: &specs.Linux{Resources: nil}}, Want: int64(0)},
{Name: "specs.LinuxMemory not found", Spec: &specs.Spec{Linux: &specs.Linux{Resources: &specs.LinuxResources{Memory: nil}}}, Want: int64(0)},
{Name: "specs.LinuxMemory.Limit not found", Spec: &specs.Spec{Linux: &specs.Linux{Resources: &specs.LinuxResources{Memory: &specs.LinuxMemory{Limit: nil}}}}, Want: int64(0)},
{Name: "Memory limit set as Want", Spec: &specs.Spec{Linux: &specs.Linux{Resources: &specs.LinuxResources{Memory: &specs.LinuxMemory{Limit: &testLimit}}}}, Want: int64(64)},
}
for _, tc := range tests {
t.Run(tc.Name, func(t *testing.T) {
got := readMemoryLimitFromSpec(tc.Spec)
if got != tc.Want {
t.Fatalf("Want %d, got %d", tc.Want, got)
} }
}) })
} }

View File

@ -24,7 +24,7 @@ func (i *InvokeResolver) Resolve(functionName string) (url.URL, error) {
actualFunctionName := functionName actualFunctionName := functionName
log.Printf("Resolve: %q\n", actualFunctionName) log.Printf("Resolve: %q\n", actualFunctionName)
namespace := getNamespace(functionName, faasd.FunctionNamespace) namespace := getNamespace(functionName, faasd.DefaultFunctionNamespace)
if strings.Contains(functionName, ".") { if strings.Contains(functionName, ".") {
actualFunctionName = strings.TrimSuffix(functionName, "."+namespace) actualFunctionName = strings.TrimSuffix(functionName, "."+namespace)

View File

@ -2,6 +2,7 @@ package handlers
import ( import (
"encoding/json" "encoding/json"
"k8s.io/apimachinery/pkg/api/resource"
"log" "log"
"net/http" "net/http"
@ -14,6 +15,17 @@ func MakeReadHandler(client *containerd.Client) func(w http.ResponseWriter, r *h
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
lookupNamespace := getRequestNamespace(readNamespaceFromQuery(r)) lookupNamespace := getRequestNamespace(readNamespaceFromQuery(r))
// Check if namespace exists, and it has the openfaas label
valid, err := validNamespace(client, lookupNamespace)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if !valid {
http.Error(w, "namespace not valid", http.StatusBadRequest)
return
}
res := []types.FunctionStatus{} res := []types.FunctionStatus{}
fns, err := ListFunctions(client, lookupNamespace) fns, err := ListFunctions(client, lookupNamespace)
@ -26,6 +38,7 @@ func MakeReadHandler(client *containerd.Client) func(w http.ResponseWriter, r *h
for _, fn := range fns { for _, fn := range fns {
annotations := &fn.annotations annotations := &fn.annotations
labels := &fn.labels labels := &fn.labels
memory := resource.NewQuantity(fn.memoryLimit, resource.BinarySI)
res = append(res, types.FunctionStatus{ res = append(res, types.FunctionStatus{
Name: fn.name, Name: fn.name,
Image: fn.image, Image: fn.image,
@ -36,6 +49,7 @@ func MakeReadHandler(client *containerd.Client) func(w http.ResponseWriter, r *h
Secrets: fn.secrets, Secrets: fn.secrets,
EnvVars: fn.envVars, EnvVars: fn.envVars,
EnvProcess: fn.envProcess, EnvProcess: fn.envProcess,
Limits: &types.FunctionResources{Memory: memory.String()},
CreatedAt: fn.createdAt, CreatedAt: fn.createdAt,
}) })
} }

View File

@ -16,6 +16,18 @@ func MakeReplicaReaderHandler(client *containerd.Client) func(w http.ResponseWri
functionName := vars["name"] functionName := vars["name"]
lookupNamespace := getRequestNamespace(readNamespaceFromQuery(r)) lookupNamespace := getRequestNamespace(readNamespaceFromQuery(r))
// Check if namespace exists, and it has the openfaas label
valid, err := validNamespace(client, lookupNamespace)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if !valid {
http.Error(w, "namespace not valid", http.StatusBadRequest)
return
}
if f, err := GetFunction(client, functionName, lookupNamespace); err == nil { if f, err := GetFunction(client, functionName, lookupNamespace); err == nil {
found := types.FunctionStatus{ found := types.FunctionStatus{
Name: functionName, Name: functionName,

View File

@ -6,16 +6,20 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log" "log"
"net"
"net/http" "net/http"
"net/url"
"time"
"github.com/containerd/containerd" "github.com/containerd/containerd"
"github.com/containerd/containerd/namespaces" "github.com/containerd/containerd/namespaces"
gocni "github.com/containerd/go-cni" gocni "github.com/containerd/go-cni"
"github.com/openfaas/faas-provider/proxy"
"github.com/openfaas/faas-provider/types" "github.com/openfaas/faas-provider/types"
) )
func MakeReplicaUpdateHandler(client *containerd.Client, cni gocni.CNI) func(w http.ResponseWriter, r *http.Request) { func MakeReplicaUpdateHandler(client *containerd.Client, cni gocni.CNI, resolver proxy.BaseURLResolver) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
@ -30,31 +34,45 @@ func MakeReplicaUpdateHandler(client *containerd.Client, cni gocni.CNI) func(w h
log.Printf("[Scale] request: %s\n", string(body)) log.Printf("[Scale] request: %s\n", string(body))
req := types.ScaleServiceRequest{} req := types.ScaleServiceRequest{}
err := json.Unmarshal(body, &req) if err := json.Unmarshal(body, &req); err != nil {
if err != nil {
log.Printf("[Scale] error parsing input: %s\n", err) log.Printf("[Scale] error parsing input: %s\n", err)
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
namespace := getRequestNamespace(readNamespaceFromQuery(r)) namespace := getRequestNamespace(readNamespaceFromQuery(r))
// Check if namespace exists, and it has the openfaas label
valid, err := validNamespace(client, namespace)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if !valid {
http.Error(w, "namespace not valid", http.StatusBadRequest)
return
}
name := req.ServiceName name := req.ServiceName
if _, err := GetFunction(client, name, namespace); err != nil { fn, err := GetFunction(client, name, namespace)
if err != nil {
msg := fmt.Sprintf("service %s not found", name) msg := fmt.Sprintf("service %s not found", name)
log.Printf("[Scale] %s\n", msg) log.Printf("[Scale] %s\n", msg)
http.Error(w, msg, http.StatusNotFound) http.Error(w, msg, http.StatusNotFound)
return return
} }
ctx := namespaces.WithNamespace(context.Background(), namespace) healthPath := "/_/healthz"
if v := fn.annotations["com.openfaas.health.http.path"]; len(v) > 0 {
healthPath = v
}
ctr, ctrErr := client.LoadContainer(ctx, name) ctx := namespaces.WithNamespace(context.Background(), namespace)
if ctrErr != nil { ctr, err := client.LoadContainer(ctx, name)
msg := fmt.Sprintf("cannot load service %s, error: %s", name, ctrErr) if err != nil {
msg := fmt.Sprintf("cannot load service %s, error: %s", name, err)
log.Printf("[Scale] %s\n", msg) log.Printf("[Scale] %s\n", msg)
http.Error(w, msg, http.StatusNotFound) http.Error(w, msg, http.StatusNotFound)
return return
@ -63,16 +81,16 @@ func MakeReplicaUpdateHandler(client *containerd.Client, cni gocni.CNI) func(w h
var taskExists bool var taskExists bool
var taskStatus *containerd.Status var taskStatus *containerd.Status
task, taskErr := ctr.Task(ctx, nil) task, err := ctr.Task(ctx, nil)
if taskErr != nil { if err != nil {
msg := fmt.Sprintf("cannot load task for service %s, error: %s", name, taskErr) msg := fmt.Sprintf("cannot load task for service %s, error: %s", name, err)
log.Printf("[Scale] %s\n", msg) log.Printf("[Scale] %s\n", msg)
taskExists = false taskExists = false
} else { } else {
taskExists = true taskExists = true
status, statusErr := task.Status(ctx) status, err := task.Status(ctx)
if statusErr != nil { if err != nil {
msg := fmt.Sprintf("cannot load task status for %s, error: %s", name, statusErr) msg := fmt.Sprintf("cannot load task status for %s, error: %s", name, err)
log.Printf("[Scale] %s\n", msg) log.Printf("[Scale] %s\n", msg)
http.Error(w, msg, http.StatusInternalServerError) http.Error(w, msg, http.StatusInternalServerError)
return return
@ -87,28 +105,31 @@ func MakeReplicaUpdateHandler(client *containerd.Client, cni gocni.CNI) func(w h
if req.Replicas == 0 { if req.Replicas == 0 {
// If a task is running, pause it // If a task is running, pause it
if taskExists && taskStatus.Status == containerd.Running { if taskExists && taskStatus.Status == containerd.Running {
if pauseErr := task.Pause(ctx); pauseErr != nil { if err := task.Pause(ctx); err != nil {
wrappedPauseErr := fmt.Errorf("error pausing task %s, error: %s", name, pauseErr) werr := fmt.Errorf("error pausing task %s, error: %s", name, err)
log.Printf("[Scale] %s\n", wrappedPauseErr.Error()) log.Printf("[Scale] %s\n", werr.Error())
http.Error(w, wrappedPauseErr.Error(), http.StatusNotFound) http.Error(w, werr.Error(), http.StatusNotFound)
return return
} }
} }
// Otherwise, no action is required
return
} }
if taskExists { if taskExists {
if taskStatus != nil { if taskStatus != nil {
if taskStatus.Status == containerd.Paused { if taskStatus.Status == containerd.Paused {
if resumeErr := task.Resume(ctx); resumeErr != nil { if err := task.Resume(ctx); err != nil {
log.Printf("[Scale] error resuming task %s, error: %s\n", name, resumeErr) log.Printf("[Scale] error resuming task %s, error: %s\n", name, err)
http.Error(w, resumeErr.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
} else if taskStatus.Status == containerd.Stopped { } else if taskStatus.Status == containerd.Stopped {
// Stopped tasks cannot be restarted, must be removed, and created again // Stopped tasks cannot be restarted, must be removed, and created again
if _, delErr := task.Delete(ctx); delErr != nil { if _, err := task.Delete(ctx); err != nil {
log.Printf("[Scale] error deleting stopped task %s, error: %s\n", name, delErr) log.Printf("[Scale] error deleting stopped task %s, error: %s\n", name, err)
http.Error(w, delErr.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
createNewTask = true createNewTask = true
@ -119,12 +140,70 @@ func MakeReplicaUpdateHandler(client *containerd.Client, cni gocni.CNI) func(w h
} }
if createNewTask { if createNewTask {
deployErr := createTask(ctx, client, ctr, cni) err := createTask(ctx, client, ctr, cni)
if deployErr != nil { if err != nil {
log.Printf("[Scale] error deploying %s, error: %s\n", name, deployErr) log.Printf("[Scale] error deploying %s, error: %s\n", name, err)
http.Error(w, deployErr.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
} }
if err := waitUntilHealthy(name, resolver, healthPath); err != nil {
log.Printf("[Scale] error waiting for function %s to become ready, error: %s\n", name, err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
} }
} }
// waitUntilHealthy blocks until the healthPath returns a HTTP 200 for the
// IP address resolved for the given function.
// Maximum retries: 100
// Delay between each attempt: 20ms
// A custom path can be set via an annotation in the function's spec:
// com.openfaas.health.http.path: /handlers/ready
//
func waitUntilHealthy(name string, resolver proxy.BaseURLResolver, healthPath string) error {
endpoint, err := resolver.Resolve(name)
if err != nil {
return err
}
host, port, _ := net.SplitHostPort(endpoint.Host)
u, err := url.Parse(fmt.Sprintf("http://%s:%s%s", host, port, healthPath))
if err != nil {
return err
}
// Try to hit the health endpoint and block until
// ready.
attempts := 100
pause := time.Millisecond * 20
for i := 0; i < attempts; i++ {
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
if err != nil {
return err
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
if res.Body != nil {
res.Body.Close()
}
if res.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected health status: %d", res.StatusCode)
}
if err == nil {
break
}
time.Sleep(pause)
}
return nil
}

View File

@ -49,6 +49,18 @@ func MakeSecretHandler(c *containerd.Client, mountPath string) func(w http.Respo
func listSecrets(c *containerd.Client, w http.ResponseWriter, r *http.Request, mountPath string) { func listSecrets(c *containerd.Client, w http.ResponseWriter, r *http.Request, mountPath string) {
lookupNamespace := getRequestNamespace(readNamespaceFromQuery(r)) lookupNamespace := getRequestNamespace(readNamespaceFromQuery(r))
// Check if namespace exists, and it has the openfaas label
valid, err := validNamespace(c, lookupNamespace)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if !valid {
http.Error(w, "namespace not valid", http.StatusBadRequest)
return
}
mountPath = getNamespaceSecretMountPath(mountPath, lookupNamespace) mountPath = getNamespaceSecretMountPath(mountPath, lookupNamespace)
files, err := ioutil.ReadDir(mountPath) files, err := ioutil.ReadDir(mountPath)
@ -74,6 +86,14 @@ func createSecret(c *containerd.Client, w http.ResponseWriter, r *http.Request,
return return
} }
err = validateSecret(secret)
if err != nil {
log.Printf("[secret] error %s", err.Error())
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
log.Printf("[secret] is valid: %q", secret.Name)
namespace := getRequestNamespace(secret.Namespace) namespace := getRequestNamespace(secret.Namespace)
mountPath = getNamespaceSecretMountPath(mountPath, namespace) mountPath = getNamespaceSecretMountPath(mountPath, namespace)
@ -84,7 +104,12 @@ func createSecret(c *containerd.Client, w http.ResponseWriter, r *http.Request,
return return
} }
err = ioutil.WriteFile(path.Join(mountPath, secret.Name), []byte(secret.Value), secretFilePermission) data := secret.RawValue
if len(data) == 0 {
data = []byte(secret.Value)
}
err = ioutil.WriteFile(path.Join(mountPath, secret.Name), data, secretFilePermission)
if err != nil { if err != nil {
log.Printf("[secret] error %s", err.Error()) log.Printf("[secret] error %s", err.Error())
@ -125,10 +150,6 @@ func parseSecret(r *http.Request) (types.Secret, error) {
return secret, err return secret, err
} }
if isTraversal(secret.Name) {
return secret, fmt.Errorf(traverseErrorSt)
}
return secret, err return secret, err
} }
@ -138,3 +159,13 @@ func isTraversal(name string) bool {
return strings.Contains(name, fmt.Sprintf("%s", string(os.PathSeparator))) || return strings.Contains(name, fmt.Sprintf("%s", string(os.PathSeparator))) ||
strings.Contains(name, "..") strings.Contains(name, "..")
} }
func validateSecret(secret types.Secret) error {
if strings.TrimSpace(secret.Name) == "" {
return fmt.Errorf("non-empty name is required")
}
if isTraversal(secret.Name) {
return fmt.Errorf(traverseErrorSt)
}
return nil
}

View File

@ -1,63 +1,163 @@
package handlers package handlers
import ( import (
"bytes"
"encoding/json"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os"
"path/filepath"
"reflect"
"strings"
"testing" "testing"
"github.com/openfaas/faas-provider/types" "github.com/openfaas/faas-provider/types"
) )
func Test_parseSecretValidName(t *testing.T) { func Test_parseSecret(t *testing.T) {
cases := []struct {
name string
payload string
expError string
expSecret types.Secret
}{
{
name: "no error when name is valid without extention and with no traversal",
payload: `{"name": "authorized_keys", "value": "foo"}`,
expSecret: types.Secret{Name: "authorized_keys", Value: "foo"},
},
{
name: "no error when name is valid and parses RawValue correctly",
payload: `{"name": "authorized_keys", "rawValue": "YmFy"}`,
expSecret: types.Secret{Name: "authorized_keys", RawValue: []byte("bar")},
},
{
name: "no error when name is valid with dot and with no traversal",
payload: `{"name": "authorized.keys", "value": "foo"}`,
expSecret: types.Secret{Name: "authorized.keys", Value: "foo"},
},
}
s := types.Secret{Name: "authorized_keys"} for _, tc := range cases {
body, _ := json.Marshal(s) t.Run(tc.name, func(t *testing.T) {
reader := bytes.NewReader(body) reader := strings.NewReader(tc.payload)
r := httptest.NewRequest(http.MethodPost, "/", reader) r := httptest.NewRequest(http.MethodPost, "/", reader)
_, err := parseSecret(r) secret, err := parseSecret(r)
if err != nil && tc.expError == "" {
t.Fatalf("unexpected error: %s", err)
return
}
if tc.expError != "" {
if err == nil {
t.Fatalf("expected error: %s, got nil", tc.expError)
}
if err.Error() != tc.expError {
t.Fatalf("expected error: %s, got: %s", tc.expError, err)
}
return
}
if !reflect.DeepEqual(secret, tc.expSecret) {
t.Fatalf("expected secret: %+v, got: %+v", tc.expSecret, secret)
}
})
}
}
func TestSecretCreation(t *testing.T) {
mountPath, err := os.MkdirTemp("", "test_secret_creation")
if err != nil { if err != nil {
t.Fatalf("secret name is valid with no traversal characters") t.Fatalf("unexpected error while creating temp directory: %s", err)
} }
}
defer os.RemoveAll(mountPath)
func Test_parseSecretValidNameWithDot(t *testing.T) {
handler := MakeSecretHandler(nil, mountPath)
s := types.Secret{Name: "authorized.keys"}
body, _ := json.Marshal(s) cases := []struct {
reader := bytes.NewReader(body) name string
r := httptest.NewRequest(http.MethodPost, "/", reader) verb string
_, err := parseSecret(r) payload string
status int
if err != nil { secretPath string
t.Fatalf("secret name is valid with no traversal characters") secret string
} err string
} }{
{
func Test_parseSecretWithTraversalWithSlash(t *testing.T) { name: "returns error when the name contains a traversal",
verb: http.MethodPost,
s := types.Secret{Name: "/root/.ssh/authorized_keys"} payload: `{"name": "/root/.ssh/authorized_keys", "value": "foo"}`,
body, _ := json.Marshal(s) status: http.StatusBadRequest,
reader := bytes.NewReader(body) err: "directory traversal found in name\n",
r := httptest.NewRequest(http.MethodPost, "/", reader) },
_, err := parseSecret(r) {
name: "returns error when the name contains a traversal",
if err == nil { verb: http.MethodPost,
t.Fatalf("secret name should fail due to path traversal") payload: `{"name": "..", "value": "foo"}`,
} status: http.StatusBadRequest,
} err: "directory traversal found in name\n",
},
func Test_parseSecretWithTraversalWithDoubleDot(t *testing.T) { {
name: "empty request returns a validation error",
s := types.Secret{Name: ".."} verb: http.MethodPost,
body, _ := json.Marshal(s) payload: `{}`,
reader := bytes.NewReader(body) status: http.StatusBadRequest,
r := httptest.NewRequest(http.MethodPost, "/", reader) err: "non-empty name is required\n",
_, err := parseSecret(r) },
{
if err == nil { name: "can create secret from string",
t.Fatalf("secret name should fail due to path traversal") verb: http.MethodPost,
payload: `{"name": "foo", "value": "bar"}`,
status: http.StatusOK,
secretPath: "/openfaas-fn/foo",
secret: "bar",
},
{
name: "can create secret from raw value",
verb: http.MethodPost,
payload: `{"name": "foo", "rawValue": "YmFy"}`,
status: http.StatusOK,
secretPath: "/openfaas-fn/foo",
secret: "bar",
},
{
name: "can create secret in non-default namespace from raw value",
verb: http.MethodPost,
payload: `{"name": "pity", "rawValue": "dGhlIGZvbw==", "namespace": "a-team"}`,
status: http.StatusOK,
secretPath: "/a-team/pity",
secret: "the foo",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest(tc.verb, "http://example.com/foo", strings.NewReader(tc.payload))
w := httptest.NewRecorder()
handler(w, req)
resp := w.Result()
if resp.StatusCode != tc.status {
t.Logf("response body: %s", w.Body.String())
t.Fatalf("expected status: %d, got: %d", tc.status, resp.StatusCode)
}
if resp.StatusCode != http.StatusOK && w.Body.String() != tc.err {
t.Fatalf("expected error message: %q, got %q", tc.err, w.Body.String())
}
if tc.secretPath != "" {
data, err := os.ReadFile(filepath.Join(mountPath, tc.secretPath))
if err != nil {
t.Fatalf("can not read the secret from disk: %s", err)
}
if string(data) != tc.secret {
t.Fatalf("expected secret value: %s, got %s", tc.secret, string(data))
}
}
})
} }
} }

View File

@ -41,6 +41,19 @@ func MakeUpdateHandler(client *containerd.Client, cni gocni.CNI, secretMountPath
} }
name := req.Service name := req.Service
namespace := getRequestNamespace(req.Namespace) namespace := getRequestNamespace(req.Namespace)
// Check if namespace exists, and it has the openfaas label
valid, err := validNamespace(client, namespace)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if !valid {
http.Error(w, "namespace not valid", http.StatusBadRequest)
return
}
namespaceSecretMountPath := getNamespaceSecretMountPath(secretMountPath, namespace) namespaceSecretMountPath := getNamespaceSecretMountPath(secretMountPath, namespace)
function, err := GetFunction(client, name, namespace) function, err := GetFunction(client, name, namespace)

View File

@ -1,9 +1,13 @@
package handlers package handlers
import ( import (
"context"
"net/http" "net/http"
"path" "path"
"github.com/containerd/containerd"
"github.com/openfaas/faasd/pkg"
faasd "github.com/openfaas/faasd/pkg" faasd "github.com/openfaas/faasd/pkg"
) )
@ -12,7 +16,7 @@ func getRequestNamespace(namespace string) string {
if len(namespace) > 0 { if len(namespace) > 0 {
return namespace return namespace
} }
return faasd.FunctionNamespace return faasd.DefaultFunctionNamespace
} }
func readNamespaceFromQuery(r *http.Request) string { func readNamespaceFromQuery(r *http.Request) string {
@ -23,3 +27,23 @@ func readNamespaceFromQuery(r *http.Request) string {
func getNamespaceSecretMountPath(userSecretPath string, namespace string) string { func getNamespaceSecretMountPath(userSecretPath string, namespace string) string {
return path.Join(userSecretPath, namespace) return path.Join(userSecretPath, namespace)
} }
// validNamespace indicates whether the namespace is eligable to be
// used for OpenFaaS functions.
func validNamespace(client *containerd.Client, namespace string) (bool, error) {
if namespace == faasd.DefaultFunctionNamespace {
return true, nil
}
store := client.NamespaceService()
labels, err := store.Labels(context.Background(), namespace)
if err != nil {
return false, err
}
if value, found := labels[pkg.NamespaceLabel]; found && value == "true" {
return true, nil
}
return false, nil
}

View File

@ -15,7 +15,7 @@ func Test_getRequestNamespace(t *testing.T) {
requestNamespace string requestNamespace string
expectedNamespace string expectedNamespace string
}{ }{
{name: "RequestNamespace is not provided", requestNamespace: "", expectedNamespace: faasd.FunctionNamespace}, {name: "RequestNamespace is not provided", requestNamespace: "", expectedNamespace: faasd.DefaultFunctionNamespace},
{name: "RequestNamespace is provided", requestNamespace: "user-namespace", expectedNamespace: "user-namespace"}, {name: "RequestNamespace is provided", requestNamespace: "user-namespace", expectedNamespace: "user-namespace"},
} }
@ -23,7 +23,7 @@ func Test_getRequestNamespace(t *testing.T) {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
actualNamespace := getRequestNamespace(tc.requestNamespace) actualNamespace := getRequestNamespace(tc.requestNamespace)
if actualNamespace != tc.expectedNamespace { if actualNamespace != tc.expectedNamespace {
t.Errorf("Got: %s, expected %s", actualNamespace, tc.expectedNamespace) t.Errorf("Want: %s, got: %s", actualNamespace, tc.expectedNamespace)
} }
}) })
} }
@ -36,7 +36,7 @@ func Test_getNamespaceSecretMountPath(t *testing.T) {
requestNamespace string requestNamespace string
expectedSecretPath string expectedSecretPath string
}{ }{
{name: "Default Namespace is provided", requestNamespace: faasd.FunctionNamespace, expectedSecretPath: "/var/openfaas/secrets/" + faasd.FunctionNamespace}, {name: "Default Namespace is provided", requestNamespace: faasd.DefaultFunctionNamespace, expectedSecretPath: "/var/openfaas/secrets/" + faasd.DefaultFunctionNamespace},
{name: "User Namespace is provided", requestNamespace: "user-namespace", expectedSecretPath: "/var/openfaas/secrets/user-namespace"}, {name: "User Namespace is provided", requestNamespace: "user-namespace", expectedSecretPath: "/var/openfaas/secrets/user-namespace"},
} }
@ -44,7 +44,7 @@ func Test_getNamespaceSecretMountPath(t *testing.T) {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
actualNamespace := getNamespaceSecretMountPath(userSecretPath, tc.requestNamespace) actualNamespace := getNamespaceSecretMountPath(userSecretPath, tc.requestNamespace)
if actualNamespace != tc.expectedSecretPath { if actualNamespace != tc.expectedSecretPath {
t.Errorf("Got: %s, expected %s", actualNamespace, tc.expectedSecretPath) t.Errorf("Want: %s, got: %s", actualNamespace, tc.expectedSecretPath)
} }
}) })
} }
@ -68,7 +68,7 @@ func Test_readNamespaceFromQuery(t *testing.T) {
actualNamespace := readNamespaceFromQuery(r) actualNamespace := readNamespaceFromQuery(r)
if actualNamespace != tc.expectedNamespace { if actualNamespace != tc.expectedNamespace {
t.Errorf("Got: %s, expected %s", actualNamespace, tc.expectedNamespace) t.Errorf("Want: %s, got: %s", actualNamespace, tc.expectedNamespace)
} }
}) })
} }