diff --git a/cmd/provider.go b/cmd/provider.go index cf565b5..be3bffa 100644 --- a/cmd/provider.go +++ b/cmd/provider.go @@ -1,7 +1,6 @@ package cmd import ( - "encoding/json" "fmt" "io/ioutil" "log" @@ -14,6 +13,7 @@ import ( "github.com/openfaas/faas-provider/logs" "github.com/openfaas/faas-provider/proxy" "github.com/openfaas/faas-provider/types" + faasd "github.com/openfaas/faasd/pkg" "github.com/openfaas/faasd/pkg/cninetwork" faasdlogs "github.com/openfaas/faasd/pkg/logs" "github.com/openfaas/faasd/pkg/provider/config" @@ -84,6 +84,11 @@ func makeProviderCmd() *cobra.Command { userSecretPath := path.Join(wd, "secrets") + err = moveSecretsToDefaultNamespaceSecrets(userSecretPath, faasd.FunctionNamespace) + if err != nil { + return err + } + bootstrapHandlers := types.FaaSHandlers{ FunctionProxy: proxy.NewHandlerFunc(*config, invokeResolver), DeleteHandler: handlers.MakeDeleteHandler(client, cni), @@ -94,7 +99,7 @@ func makeProviderCmd() *cobra.Command { UpdateHandler: handlers.MakeUpdateHandler(client, cni, userSecretPath, alwaysPull), HealthHandler: func(w http.ResponseWriter, r *http.Request) {}, InfoHandler: handlers.MakeInfoHandler(Version, GitCommit), - ListNamespaceHandler: listNamespaces(), + ListNamespaceHandler: handlers.MakeNamespacesLister(client), SecretHandler: handlers.MakeSecretHandler(client, userSecretPath), LogHandler: logs.NewLogHandlerFunc(faasdlogs.New(), config.ReadTimeout), } @@ -107,10 +112,33 @@ func makeProviderCmd() *cobra.Command { return command } -func listNamespaces() func(w http.ResponseWriter, r *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - list := []string{""} - out, _ := json.Marshal(list) - w.Write(out) +/* +* Mutiple namespace support was added after release 0.13.0 +* Function will help users to migrate on multiple namespace support of faasd + */ +func moveSecretsToDefaultNamespaceSecrets(secretPath string, namespace string) error { + newSecretPath := path.Join(secretPath, namespace) + + err := ensureWorkingDir(newSecretPath) + if err != nil { + return err } + + files, err := ioutil.ReadDir(secretPath) + if err != nil { + return err + } + + for _, f := range files { + if !f.IsDir() { + oldPath := path.Join(secretPath, f.Name()) + newPath := path.Join(newSecretPath, f.Name()) + err = os.Rename(oldPath, newPath) + if err != nil { + return err + } + } + } + + return nil } diff --git a/pkg/constants.go b/pkg/constants.go index f92d484..45e7fee 100644 --- a/pkg/constants.go +++ b/pkg/constants.go @@ -4,8 +4,8 @@ const ( // FunctionNamespace is the default containerd namespace functions are created FunctionNamespace = "openfaas-fn" - // faasdNamespace is the containerd namespace services are created - faasdNamespace = "openfaas" + // FaasdNamespace is the containerd namespace services are created + FaasdNamespace = "openfaas" faasServicesPullAlways = false diff --git a/pkg/provider/handlers/delete.go b/pkg/provider/handlers/delete.go index 540ce10..b7cc740 100644 --- a/pkg/provider/handlers/delete.go +++ b/pkg/provider/handlers/delete.go @@ -13,7 +13,6 @@ import ( gocni "github.com/containerd/go-cni" "github.com/openfaas/faas/gateway/requests" - faasd "github.com/openfaas/faasd/pkg" cninetwork "github.com/openfaas/faasd/pkg/cninetwork" "github.com/openfaas/faasd/pkg/service" ) @@ -41,9 +40,11 @@ func MakeDeleteHandler(client *containerd.Client, cni gocni.CNI) func(w http.Res return } + lookupNamespace := getRequestNamespace(readNamespaceFromQuery(r)) + name := req.FunctionName - function, err := GetFunction(client, name) + function, err := GetFunction(client, name, lookupNamespace) if err != nil { msg := fmt.Sprintf("service %s not found", name) log.Printf("[Delete] %s\n", msg) @@ -51,7 +52,7 @@ func MakeDeleteHandler(client *containerd.Client, cni gocni.CNI) func(w http.Res return } - ctx := namespaces.WithNamespace(context.Background(), faasd.FunctionNamespace) + ctx := namespaces.WithNamespace(context.Background(), lookupNamespace) // TODO: this needs to still happen if the task is paused if function.replicas != 0 { diff --git a/pkg/provider/handlers/deploy.go b/pkg/provider/handlers/deploy.go index 36f1f29..2065436 100644 --- a/pkg/provider/handlers/deploy.go +++ b/pkg/provider/handlers/deploy.go @@ -20,7 +20,6 @@ import ( "github.com/docker/distribution/reference" "github.com/opencontainers/runtime-spec/specs-go" "github.com/openfaas/faas-provider/types" - faasd "github.com/openfaas/faasd/pkg" cninetwork "github.com/openfaas/faasd/pkg/cninetwork" "github.com/openfaas/faasd/pkg/service" "github.com/pkg/errors" @@ -52,15 +51,17 @@ func MakeDeployHandler(client *containerd.Client, cni gocni.CNI, secretMountPath return } - err = validateSecrets(secretMountPath, req.Secrets) + namespace := getRequestNamespace(req.Namespace) + namespaceSecretMountPath := getNamespaceSecretMountPath(secretMountPath, namespace) + err = validateSecrets(namespaceSecretMountPath, req.Secrets) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) } name := req.Service - ctx := namespaces.WithNamespace(context.Background(), faasd.FunctionNamespace) + ctx := namespaces.WithNamespace(context.Background(), namespace) - deployErr := deploy(ctx, req, client, cni, secretMountPath, alwaysPull) + deployErr := deploy(ctx, req, client, cni, namespaceSecretMountPath, alwaysPull) if deployErr != nil { log.Printf("[Deploy] error deploying %s, error: %s\n", name, deployErr) http.Error(w, deployErr.Error(), http.StatusBadRequest) diff --git a/pkg/provider/handlers/functions.go b/pkg/provider/handlers/functions.go index 7c0d999..232ec8b 100644 --- a/pkg/provider/handlers/functions.go +++ b/pkg/provider/handlers/functions.go @@ -11,9 +11,8 @@ import ( "github.com/containerd/containerd" "github.com/containerd/containerd/namespaces" - "github.com/openfaas/faasd/pkg/cninetwork" - faasd "github.com/openfaas/faasd/pkg" + "github.com/openfaas/faasd/pkg/cninetwork" ) type Function struct { @@ -32,8 +31,8 @@ type Function struct { } // ListFunctions returns a map of all functions with running tasks on namespace -func ListFunctions(client *containerd.Client) (map[string]*Function, error) { - ctx := namespaces.WithNamespace(context.Background(), faasd.FunctionNamespace) +func ListFunctions(client *containerd.Client, namespace string) (map[string]*Function, error) { + ctx := namespaces.WithNamespace(context.Background(), namespace) functions := make(map[string]*Function) containers, err := client.Containers(ctx) @@ -43,7 +42,7 @@ func ListFunctions(client *containerd.Client) (map[string]*Function, error) { for _, c := range containers { name := c.ID() - f, err := GetFunction(client, name) + f, err := GetFunction(client, name, namespace) if err != nil { log.Printf("error getting function %s: ", name) return functions, err @@ -55,8 +54,8 @@ func ListFunctions(client *containerd.Client) (map[string]*Function, error) { } // GetFunction returns a function that matches name -func GetFunction(client *containerd.Client, name string) (Function, error) { - ctx := namespaces.WithNamespace(context.Background(), faasd.FunctionNamespace) +func GetFunction(client *containerd.Client, name string, namespace string) (Function, error) { + ctx := namespaces.WithNamespace(context.Background(), namespace) fn := Function{} c, err := client.LoadContainer(ctx, name) @@ -92,7 +91,7 @@ func GetFunction(client *containerd.Client, name string) (Function, error) { secrets := readSecretsFromMounts(spec.Mounts) fn.name = containerName - fn.namespace = faasd.FunctionNamespace + fn.namespace = namespace fn.image = image.Name() fn.labels = labels fn.annotations = annotations @@ -181,3 +180,41 @@ func buildLabelsAndAnnotations(ctrLabels map[string]string) (map[string]string, return labels, annotations } + +func ListNamespaces(client *containerd.Client) []string { + set := []string{} + store := client.NamespaceService() + namespaces, err := store.List(context.Background()) + if err != nil { + log.Printf("Error listing namespaces: %s", err.Error()) + set = append(set, faasd.FunctionNamespace) + return set + } + + for _, namespace := range namespaces { + labels, err := store.Labels(context.Background(), namespace) + if err != nil { + log.Printf("Error listing label for namespace %s: %s", namespace, err.Error()) + continue + } + + if _, found := labels["openfaas"]; found { + set = append(set, namespace) + } + + if !findNamespace(faasd.FunctionNamespace, set) { + set = append(set, faasd.FunctionNamespace) + } + } + + return set +} + +func findNamespace(target string, items []string) bool { + for _, n := range items { + if n == target { + return true + } + } + return false +} diff --git a/pkg/provider/handlers/functions_test.go b/pkg/provider/handlers/functions_test.go index 8209b3e..9583cf4 100644 --- a/pkg/provider/handlers/functions_test.go +++ b/pkg/provider/handlers/functions_test.go @@ -2,9 +2,10 @@ package handlers import ( "fmt" - "github.com/opencontainers/runtime-spec/specs-go" "reflect" "testing" + + "github.com/opencontainers/runtime-spec/specs-go" ) func Test_BuildLabelsAndAnnotationsFromServiceSpec_Annotations(t *testing.T) { @@ -84,3 +85,25 @@ func Test_ProcessEnvToEnvVars(t *testing.T) { }) } } + +func Test_findNamespace(t *testing.T) { + type test struct { + Name string + foundNamespaces []string + namespace string + Expected bool + } + tests := []test{ + {Name: "Namespace Found", namespace: "fn", foundNamespaces: []string{"fn", "openfaas-fn"}, Expected: true}, + {Name: "namespace Not Found", namespace: "fn", foundNamespaces: []string{"openfaas-fn"}, Expected: false}, + } + + for _, tc := range tests { + t.Run(tc.Name, func(t *testing.T) { + got := findNamespace(tc.namespace, tc.foundNamespaces) + if got != tc.Expected { + t.Fatalf("expected %t, got %t", tc.Expected, got) + } + }) + } +} diff --git a/pkg/provider/handlers/invoke_resolver.go b/pkg/provider/handlers/invoke_resolver.go index 286d76f..2550740 100644 --- a/pkg/provider/handlers/invoke_resolver.go +++ b/pkg/provider/handlers/invoke_resolver.go @@ -4,8 +4,10 @@ import ( "fmt" "log" "net/url" + "strings" "github.com/containerd/containerd" + faasd "github.com/openfaas/faasd/pkg" ) const watchdogPort = 8080 @@ -19,11 +21,18 @@ func NewInvokeResolver(client *containerd.Client) *InvokeResolver { } func (i *InvokeResolver) Resolve(functionName string) (url.URL, error) { - log.Printf("Resolve: %q\n", functionName) + actualFunctionName := functionName + log.Printf("Resolve: %q\n", actualFunctionName) - function, err := GetFunction(i.client, functionName) + namespace := getNamespace(functionName, faasd.FunctionNamespace) + + if strings.Contains(functionName, ".") { + actualFunctionName = strings.TrimSuffix(functionName, "."+namespace) + } + + function, err := GetFunction(i.client, actualFunctionName, namespace) if err != nil { - return url.URL{}, fmt.Errorf("%s not found", functionName) + return url.URL{}, fmt.Errorf("%s not found", actualFunctionName) } serviceIP := function.IP @@ -37,3 +46,11 @@ func (i *InvokeResolver) Resolve(functionName string) (url.URL, error) { return *urlRes, nil } + +func getNamespace(name, defaultNamespace string) string { + namespace := defaultNamespace + if strings.Contains(name, ".") { + namespace = name[strings.LastIndexAny(name, ".")+1:] + } + return namespace +} diff --git a/pkg/provider/handlers/namespaces.go b/pkg/provider/handlers/namespaces.go new file mode 100644 index 0000000..77b73e3 --- /dev/null +++ b/pkg/provider/handlers/namespaces.go @@ -0,0 +1,18 @@ +package handlers + +import ( + "encoding/json" + "net/http" + + "github.com/containerd/containerd" +) + +func MakeNamespacesLister(client *containerd.Client) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + list := ListNamespaces(client) + body, _ := json.Marshal(list) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(body) + } +} diff --git a/pkg/provider/handlers/read.go b/pkg/provider/handlers/read.go index 7b76d5d..9707266 100644 --- a/pkg/provider/handlers/read.go +++ b/pkg/provider/handlers/read.go @@ -13,8 +13,10 @@ func MakeReadHandler(client *containerd.Client) func(w http.ResponseWriter, r *h return func(w http.ResponseWriter, r *http.Request) { + lookupNamespace := getRequestNamespace(readNamespaceFromQuery(r)) + res := []types.FunctionStatus{} - fns, err := ListFunctions(client) + fns, err := ListFunctions(client, lookupNamespace) if err != nil { log.Printf("[Read] error listing functions. Error: %s\n", err) http.Error(w, err.Error(), http.StatusBadRequest) diff --git a/pkg/provider/handlers/replicas.go b/pkg/provider/handlers/replicas.go index 721f6b3..0c7d143 100644 --- a/pkg/provider/handlers/replicas.go +++ b/pkg/provider/handlers/replicas.go @@ -14,8 +14,9 @@ func MakeReplicaReaderHandler(client *containerd.Client) func(w http.ResponseWri return func(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) functionName := vars["name"] + lookupNamespace := getRequestNamespace(readNamespaceFromQuery(r)) - if f, err := GetFunction(client, functionName); err == nil { + if f, err := GetFunction(client, functionName, lookupNamespace); err == nil { found := types.FunctionStatus{ Name: functionName, AvailableReplicas: uint64(f.replicas), diff --git a/pkg/provider/handlers/scale.go b/pkg/provider/handlers/scale.go index 6241889..e7dfa6a 100644 --- a/pkg/provider/handlers/scale.go +++ b/pkg/provider/handlers/scale.go @@ -13,7 +13,6 @@ import ( gocni "github.com/containerd/go-cni" "github.com/openfaas/faas-provider/types" - faasd "github.com/openfaas/faasd/pkg" ) func MakeReplicaUpdateHandler(client *containerd.Client, cni gocni.CNI) func(w http.ResponseWriter, r *http.Request) { @@ -40,16 +39,18 @@ func MakeReplicaUpdateHandler(client *containerd.Client, cni gocni.CNI) func(w h return } + namespace := getRequestNamespace(readNamespaceFromQuery(r)) + name := req.ServiceName - if _, err := GetFunction(client, name); err != nil { + if _, err := GetFunction(client, name, namespace); err != nil { msg := fmt.Sprintf("service %s not found", name) log.Printf("[Scale] %s\n", msg) http.Error(w, msg, http.StatusNotFound) return } - ctx := namespaces.WithNamespace(context.Background(), faasd.FunctionNamespace) + ctx := namespaces.WithNamespace(context.Background(), namespace) ctr, ctrErr := client.LoadContainer(ctx, name) if ctrErr != nil { diff --git a/pkg/provider/handlers/secret.go b/pkg/provider/handlers/secret.go index 9ea451e..3fc2f30 100644 --- a/pkg/provider/handlers/secret.go +++ b/pkg/provider/handlers/secret.go @@ -15,6 +15,7 @@ import ( ) const secretFilePermission = 0644 +const secretDirPermission = 0755 func MakeSecretHandler(c *containerd.Client, mountPath string) func(w http.ResponseWriter, r *http.Request) { @@ -46,6 +47,10 @@ func MakeSecretHandler(c *containerd.Client, mountPath string) func(w http.Respo } func listSecrets(c *containerd.Client, w http.ResponseWriter, r *http.Request, mountPath string) { + + lookupNamespace := getRequestNamespace(readNamespaceFromQuery(r)) + mountPath = getNamespaceSecretMountPath(mountPath, lookupNamespace) + files, err := ioutil.ReadDir(mountPath) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) @@ -54,7 +59,7 @@ func listSecrets(c *containerd.Client, w http.ResponseWriter, r *http.Request, m secrets := []types.Secret{} for _, f := range files { - secrets = append(secrets, types.Secret{Name: f.Name()}) + secrets = append(secrets, types.Secret{Name: f.Name(), Namespace: lookupNamespace}) } bytesOut, _ := json.Marshal(secrets) @@ -69,6 +74,16 @@ func createSecret(c *containerd.Client, w http.ResponseWriter, r *http.Request, return } + namespace := getRequestNamespace(secret.Namespace) + mountPath = getNamespaceSecretMountPath(mountPath, namespace) + + err = os.MkdirAll(mountPath, secretDirPermission) + if err != nil { + log.Printf("[secret] error %s", err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + err = ioutil.WriteFile(path.Join(mountPath, secret.Name), []byte(secret.Value), secretFilePermission) if err != nil { @@ -86,6 +101,9 @@ func deleteSecret(c *containerd.Client, w http.ResponseWriter, r *http.Request, return } + namespace := getRequestNamespace(readNamespaceFromQuery(r)) + mountPath = getNamespaceSecretMountPath(mountPath, namespace) + err = os.Remove(path.Join(mountPath, secret.Name)) if err != nil { diff --git a/pkg/provider/handlers/update.go b/pkg/provider/handlers/update.go index b32dea9..812be77 100644 --- a/pkg/provider/handlers/update.go +++ b/pkg/provider/handlers/update.go @@ -13,7 +13,6 @@ import ( gocni "github.com/containerd/go-cni" "github.com/openfaas/faas-provider/types" - faasd "github.com/openfaas/faasd/pkg" "github.com/openfaas/faasd/pkg/cninetwork" "github.com/openfaas/faasd/pkg/service" ) @@ -41,8 +40,10 @@ func MakeUpdateHandler(client *containerd.Client, cni gocni.CNI, secretMountPath return } name := req.Service + namespace := getRequestNamespace(req.Namespace) + namespaceSecretMountPath := getNamespaceSecretMountPath(secretMountPath, namespace) - function, err := GetFunction(client, name) + function, err := GetFunction(client, name, namespace) if err != nil { msg := fmt.Sprintf("service %s not found", name) log.Printf("[Update] %s\n", msg) @@ -50,12 +51,12 @@ func MakeUpdateHandler(client *containerd.Client, cni gocni.CNI, secretMountPath return } - err = validateSecrets(secretMountPath, req.Secrets) + err = validateSecrets(namespaceSecretMountPath, req.Secrets) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) } - ctx := namespaces.WithNamespace(context.Background(), faasd.FunctionNamespace) + ctx := namespaces.WithNamespace(context.Background(), namespace) if _, err := prepull(ctx, req, client, alwaysPull); err != nil { log.Printf("[Update] error with pre-pull: %s, %s\n", name, err) @@ -78,7 +79,7 @@ func MakeUpdateHandler(client *containerd.Client, cni gocni.CNI, secretMountPath // The pull has already been done in prepull, so we can force this pull to "false" pull := false - if err := deploy(ctx, req, client, cni, secretMountPath, pull); err != nil { + if err := deploy(ctx, req, client, cni, namespaceSecretMountPath, pull); err != nil { log.Printf("[Update] error deploying %s, error: %s\n", name, err) http.Error(w, err.Error(), http.StatusBadRequest) return diff --git a/pkg/provider/handlers/utils.go b/pkg/provider/handlers/utils.go new file mode 100644 index 0000000..7f8938a --- /dev/null +++ b/pkg/provider/handlers/utils.go @@ -0,0 +1,25 @@ +package handlers + +import ( + "net/http" + "path" + + faasd "github.com/openfaas/faasd/pkg" +) + +func getRequestNamespace(namespace string) string { + + if len(namespace) > 0 { + return namespace + } + return faasd.FunctionNamespace +} + +func readNamespaceFromQuery(r *http.Request) string { + q := r.URL.Query() + return q.Get("namespace") +} + +func getNamespaceSecretMountPath(userSecretPath string, namespace string) string { + return path.Join(userSecretPath, namespace) +} diff --git a/pkg/provider/handlers/utils_test.go b/pkg/provider/handlers/utils_test.go new file mode 100644 index 0000000..67c4cd5 --- /dev/null +++ b/pkg/provider/handlers/utils_test.go @@ -0,0 +1,75 @@ +package handlers + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + faasd "github.com/openfaas/faasd/pkg" +) + +func Test_getRequestNamespace(t *testing.T) { + tables := []struct { + name string + requestNamespace string + expectedNamespace string + }{ + {name: "RequestNamespace is not provided", requestNamespace: "", expectedNamespace: faasd.FunctionNamespace}, + {name: "RequestNamespace is provided", requestNamespace: "user-namespace", expectedNamespace: "user-namespace"}, + } + + for _, tc := range tables { + t.Run(tc.name, func(t *testing.T) { + actualNamespace := getRequestNamespace(tc.requestNamespace) + if actualNamespace != tc.expectedNamespace { + t.Errorf("Got: %s, expected %s", actualNamespace, tc.expectedNamespace) + } + }) + } +} + +func Test_getNamespaceSecretMountPath(t *testing.T) { + userSecretPath := "/var/openfaas/secrets" + tables := []struct { + name string + requestNamespace string + expectedSecretPath string + }{ + {name: "Default Namespace is provided", requestNamespace: faasd.FunctionNamespace, expectedSecretPath: "/var/openfaas/secrets/" + faasd.FunctionNamespace}, + {name: "User Namespace is provided", requestNamespace: "user-namespace", expectedSecretPath: "/var/openfaas/secrets/user-namespace"}, + } + + for _, tc := range tables { + t.Run(tc.name, func(t *testing.T) { + actualNamespace := getNamespaceSecretMountPath(userSecretPath, tc.requestNamespace) + if actualNamespace != tc.expectedSecretPath { + t.Errorf("Got: %s, expected %s", actualNamespace, tc.expectedSecretPath) + } + }) + } +} + +func Test_readNamespaceFromQuery(t *testing.T) { + tables := []struct { + name string + queryNamespace string + expectedNamespace string + }{ + {name: "No Namespace is provided", queryNamespace: "", expectedNamespace: ""}, + {name: "User Namespace is provided", queryNamespace: "user-namespace", expectedNamespace: "user-namespace"}, + } + + for _, tc := range tables { + t.Run(tc.name, func(t *testing.T) { + + url := fmt.Sprintf("/test?namespace=%s", tc.queryNamespace) + r := httptest.NewRequest(http.MethodGet, url, nil) + + actualNamespace := readNamespaceFromQuery(r) + if actualNamespace != tc.expectedNamespace { + t.Errorf("Got: %s, expected %s", actualNamespace, tc.expectedNamespace) + } + }) + } +} diff --git a/pkg/supervisor.go b/pkg/supervisor.go index 3d34ef9..092b678 100644 --- a/pkg/supervisor.go +++ b/pkg/supervisor.go @@ -79,7 +79,7 @@ func NewSupervisor(sock string) (*Supervisor, error) { } func (s *Supervisor) Start(svcs []Service) error { - ctx := namespaces.WithNamespace(context.Background(), faasdNamespace) + ctx := namespaces.WithNamespace(context.Background(), FaasdNamespace) wd, _ := os.Getwd() @@ -243,7 +243,7 @@ func (s *Supervisor) Close() { } func (s *Supervisor) Remove(svcs []Service) error { - ctx := namespaces.WithNamespace(context.Background(), faasdNamespace) + ctx := namespaces.WithNamespace(context.Background(), FaasdNamespace) for _, svc := range svcs { err := cninetwork.DeleteCNINetwork(ctx, s.cni, s.client, svc.Name)