From ac1cc16f0c5a472133dd3811402f9c4e3a1c7d59 Mon Sep 17 00:00:00 2001 From: Alex Tomic Date: Tue, 16 Jun 2020 22:32:08 -0400 Subject: [PATCH] Annotation support Provide support for annotations in faasd with namespaced container labels. Unit tested and confirmed with end to end test via faasd deployed to multipass VM Signed-off-by: Alex Tomic --- pkg/provider/handlers/deploy.go | 32 +++++++++++-- pkg/provider/handlers/deploy_test.go | 63 +++++++++++++++++++++++++ pkg/provider/handlers/functions.go | 50 +++++++++++++++----- pkg/provider/handlers/functions_test.go | 29 ++++++++++++ pkg/provider/handlers/read.go | 12 +++-- pkg/provider/handlers/replicas.go | 1 + 6 files changed, 167 insertions(+), 20 deletions(-) create mode 100644 pkg/provider/handlers/deploy_test.go create mode 100644 pkg/provider/handlers/functions_test.go diff --git a/pkg/provider/handlers/deploy.go b/pkg/provider/handlers/deploy.go index 9776850..63de3a5 100644 --- a/pkg/provider/handlers/deploy.go +++ b/pkg/provider/handlers/deploy.go @@ -26,6 +26,8 @@ import ( "k8s.io/apimachinery/pkg/api/resource" ) +const annotationLabelPrefix = "com.openfaas.annotations." + func MakeDeployHandler(client *containerd.Client, cni gocni.CNI, secretMountPath string, alwaysPull bool) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { @@ -101,9 +103,9 @@ func deploy(ctx context.Context, req types.FunctionDeployment, client *container name := req.Service - labels := map[string]string{} - if req.Labels != nil { - labels = *req.Labels + labels, err := buildLabels(&req) + if err != nil { + return fmt.Errorf("Unable to apply labels to conatiner: %s, error: %s", name, err) } var memory *specs.LinuxMemory @@ -140,6 +142,30 @@ func deploy(ctx context.Context, req types.FunctionDeployment, client *container } +func buildLabels(request *types.FunctionDeployment) (map[string]string, error) { + // Adapted from faas-swarm/handlers/deploy.go:buildLabels + labels := map[string]string{} + + if request.Labels != nil { + for k, v := range *request.Labels { + labels[k] = v + } + } + + if request.Annotations != nil { + for k, v := range *request.Annotations { + key := fmt.Sprintf("%s%s", annotationLabelPrefix, k) + if _, ok := labels[key]; !ok { + labels[key] = v + } else { + return nil, errors.New(fmt.Sprintf("Key %s cannot be used as a label due to a conflict with annotation prefix %s", k, annotationLabelPrefix)) + } + } + } + + return labels, nil +} + func createTask(ctx context.Context, client *containerd.Client, container containerd.Container, cni gocni.CNI) error { name := container.ID() diff --git a/pkg/provider/handlers/deploy_test.go b/pkg/provider/handlers/deploy_test.go new file mode 100644 index 0000000..3f4e22f --- /dev/null +++ b/pkg/provider/handlers/deploy_test.go @@ -0,0 +1,63 @@ +package handlers + +import ( + "fmt" + "reflect" + "testing" + + "github.com/openfaas/faas-provider/types" +) + +func Test_BuildLabels_WithAnnotations(t *testing.T) { + // Test each combination of nil/non-nil annotation + label + tables := []struct { + label map[string]string + annotation map[string]string + result map[string]string + }{ + {nil, nil, map[string]string{}}, + {map[string]string{"L1": "V1"}, nil, map[string]string{"L1": "V1"}}, + {nil, map[string]string{"A1": "V2"}, map[string]string{fmt.Sprintf("%sA1", annotationLabelPrefix): "V2"}}, + { + map[string]string{"L1": "V1"}, map[string]string{"A1": "V2"}, + map[string]string{"L1": "V1", fmt.Sprintf("%sA1", annotationLabelPrefix): "V2"}, + }, + } + + for _, pair := range tables { + + request := &types.FunctionDeployment{ + Labels: &pair.label, + Annotations: &pair.annotation, + } + + val, err := buildLabels(request) + + if err != nil { + t.Fatalf("want: no error got: %v", err) + } + + if !reflect.DeepEqual(val, pair.result) { + t.Errorf("Got: %s, expected %s", val, pair.result) + } + } + +} + +func Test_BuildLabels_WithAnnotationCollision(t *testing.T) { + request := &types.FunctionDeployment{ + Labels: &map[string]string{ + "function_name": "echo", + fmt.Sprintf("%scurrent-time", annotationLabelPrefix): "Wed 25 Jul 06:41:43 BST 2018", + }, + Annotations: &map[string]string{"current-time": "Wed 25 Jul 06:41:43 BST 2018"}, + } + + val, err := buildLabels(request) + + fmt.Printf("%s, %s\n", val, err) + if err == nil { + t.Errorf("Expected an error, got %d values", len(val)) + } + +} diff --git a/pkg/provider/handlers/functions.go b/pkg/provider/handlers/functions.go index 3f07cac..33b52f0 100644 --- a/pkg/provider/handlers/functions.go +++ b/pkg/provider/handlers/functions.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "strings" "github.com/containerd/containerd" "github.com/containerd/containerd/namespaces" @@ -13,13 +14,14 @@ import ( ) type Function struct { - name string - namespace string - image string - pid uint32 - replicas int - IP string - labels map[string]string + name string + namespace string + image string + pid uint32 + replicas int + IP string + labels map[string]string + annotations map[string]string } // ListFunctions returns a map of all functions with running tasks on namespace @@ -48,16 +50,20 @@ func GetFunction(client *containerd.Client, name string) (Function, error) { image, _ := c.Image(ctx) containerName := c.ID() - labels, labelErr := c.Labels(ctx) + allLabels, labelErr := c.Labels(ctx) + if labelErr != nil { log.Printf("cannot list container %s labels: %s", containerName, labelErr.Error()) } + labels, annotations := buildLabelsAndAnnotations(allLabels) + f := Function{ - name: containerName, - namespace: faasd.FunctionNamespace, - image: image.Name(), - labels: labels, + name: containerName, + namespace: faasd.FunctionNamespace, + image: image.Name(), + labels: labels, + annotations: annotations, } replicas := 0 @@ -90,3 +96,23 @@ func GetFunction(client *containerd.Client, name string) (Function, error) { return Function{}, fmt.Errorf("unable to find function: %s, error %s", name, err) } + +func buildLabelsAndAnnotations(ctrLabels map[string]string) (labels map[string]string, annotations map[string]string) { + for k, v := range ctrLabels { + if strings.HasPrefix(k, annotationLabelPrefix) { + if annotations == nil { + annotations = make(map[string]string) + } + + annotations[strings.TrimPrefix(k, annotationLabelPrefix)] = v + } else { + if labels == nil { + labels = make(map[string]string) + } + + labels[k] = v + } + } + + return labels, annotations +} diff --git a/pkg/provider/handlers/functions_test.go b/pkg/provider/handlers/functions_test.go new file mode 100644 index 0000000..bc54c9a --- /dev/null +++ b/pkg/provider/handlers/functions_test.go @@ -0,0 +1,29 @@ +package handlers + +import ( + "fmt" + "testing" +) + +func Test_BuildLabelsAndAnnotationsFromServiceSpec_Annotations(t *testing.T) { + container := map[string]string{ + "qwer": "ty", + "dvor": "ak", + fmt.Sprintf("%scurrent-time", annotationLabelPrefix): "5 Nov 20:10:20 PST 1955", + fmt.Sprintf("%sfuture-time", annotationLabelPrefix): "21 Oct 20:10:20 PST 2015", + } + + labels, annotation := buildLabelsAndAnnotations(container) + + if len(labels) != 2 { + t.Errorf("want: %d labels got: %d", 2, len(labels)) + } + + if len(annotation) != 2 { + t.Errorf("want: %d annotation got: %d", 1, len(annotation)) + } + + if _, ok := annotation["current-time"]; !ok { + t.Errorf("want: '%s' entry in annotation map got: key not found", "current-time") + } +} diff --git a/pkg/provider/handlers/read.go b/pkg/provider/handlers/read.go index e337165..de7ec54 100644 --- a/pkg/provider/handlers/read.go +++ b/pkg/provider/handlers/read.go @@ -21,12 +21,14 @@ func MakeReadHandler(client *containerd.Client) func(w http.ResponseWriter, r *h return } for _, function := range funcs { + res = append(res, types.FunctionStatus{ - Name: function.name, - Image: function.image, - Replicas: uint64(function.replicas), - Namespace: function.namespace, - Labels: &function.labels, + Name: function.name, + Image: function.image, + Replicas: uint64(function.replicas), + Namespace: function.namespace, + Labels: &function.labels, + Annotations: &function.annotations, }) } diff --git a/pkg/provider/handlers/replicas.go b/pkg/provider/handlers/replicas.go index e017c2e..934afd4 100644 --- a/pkg/provider/handlers/replicas.go +++ b/pkg/provider/handlers/replicas.go @@ -22,6 +22,7 @@ func MakeReplicaReaderHandler(client *containerd.Client) func(w http.ResponseWri Replicas: uint64(f.replicas), Namespace: f.namespace, Labels: &f.labels, + Annotations: &f.annotations, } functionBytes, _ := json.Marshal(found)