diff --git a/README.md b/README.md index 6253643d..100e2113 100644 --- a/README.md +++ b/README.md @@ -265,8 +265,8 @@ $ echo "Nic" | faas-cli --gateway http://192.168.1.113:8080/ invoke gofunction That is all there is to it, checkout the OpenFaaS community page for some inspiration and other demos. [faas/community.md at master · openfaas/faas · GitHub](https://github.com/openfaas/faas/blob/master/community.md) -### Datacenters and Constraints -By default, the Nomad provider will use the datacenter of the Nomad agent or `dc1`. This can be overridden by setting one or more constraints `datacenter == value`. Constraints for limiting CPU and memory can also be set `memory` is an integer representing Megabytes, `cpu` is an integer representing MHz of CPU where 1024 equals one core. +### Datacenters and Limits +By default, the Nomad provider will use the datacenter of the Nomad agent or `dc1`. This can be overridden by setting one or more constraints `datacenter == value`. Limits for CPU and memory can also be set `memory` is an integer representing Megabytes, `cpu` is an integer representing MHz of CPU where 1024 equals one core. i.e. ```bash @@ -287,6 +287,19 @@ functions: "datacenter == test1" ``` +### Nomad Job Constraints +Additionally to the `datacenter` constraint [Nomad job constraints](https://www.nomadproject.io/docs/job-specification/constraint.html) are supported. + +i.e. +```bash +$ faas-cli deploy --constraint '${attr.cpu.arch} = arm' +``` + +For compatibility and convenience the interpolation notation (`${}`) can be left out and `==` instead of `=` is supported. + +All provided constraints are applied to the job (not the group or the task). +Leaving out the a field (.e.g. `${meta.foo} is_set`) or using more than one operator (e.g. `${meta.foo} is_set = bar`) is currently __not supported__. + ### Annotations Metadata can be added to the Nomad job definition through the use of the OpenFaaS annotation config. The below example would add the key `git` to the `Meta` section of nomad job definition which can be accessed through the API. diff --git a/handlers/deploy.go b/handlers/deploy.go index 19d1341b..8bcee75c 100644 --- a/handlers/deploy.go +++ b/handlers/deploy.go @@ -7,6 +7,7 @@ import ( "io/ioutil" "net/http" "net/url" + "regexp" "strconv" "strings" "time" @@ -29,8 +30,7 @@ var ( ephemeralDiskSize = 20 // Constraints - constraintCPUArch = "amd64" - taskMemory = 128 + taskMemory = 128 // Update Strategy updateAutoRevert = true @@ -84,13 +84,12 @@ func createJob(r requests.CreateFunctionRequest, providerConfig types.ProviderCo job.Update = createUpdateStrategy() // add constraints - job.Constraints = append(job.Constraints, - &api.Constraint{ - LTarget: "${attr.cpu.arch}", - Operand: "=", - RTarget: constraintCPUArch, - }, - ) + job.Constraints = append(job.Constraints, createConstraints(r)...) + + cpuArchConstraint := createMissingCPUArchConstraint(job.Constraints, providerConfig.CPUArchConstraint) + if cpuArchConstraint != nil { + job.Constraints = append(job.Constraints, cpuArchConstraint) + } job.TaskGroups = createTaskGroup(r, providerConfig) @@ -241,9 +240,13 @@ func createDataCenters(r requests.CreateFunctionRequest, defaultDC string) []str dcs := []string{} for _, constr := range r.Constraints { - if strings.Contains(constr, "datacenter") { - dcs = append(dcs, strings.Trim(strings.Split(constr, "==")[1], " ")) + fields := strings.Fields(constr) + + if len(fields) != 3 || !strings.Contains(fields[0], "datacenter") || fields[1] != "==" { + continue } + + dcs = append(dcs, fields[2]) } return dcs @@ -253,6 +256,57 @@ func createDataCenters(r requests.CreateFunctionRequest, defaultDC string) []str return []string{defaultDC} } +func createConstraints(r requests.CreateFunctionRequest) []*api.Constraint { + constraints := make([]*api.Constraint, 0, len(r.Constraints)) + + if r.Constraints == nil { + return constraints + } + + for _, requestConstraint := range r.Constraints { + fields := strings.Fields(requestConstraint) + + if len(fields) < 3 || strings.Contains(fields[0], "datacenter") { + continue + } + + attribute := fields[0] + operator := fields[1] + value := strings.Join(fields[2:], " ") + + match, _ := regexp.MatchString("^\\${.*}$", attribute) + if !match { + attribute = fmt.Sprintf("${%v}", attribute) + } + + if operator == "==" { + operator = "=" + } + + constraints = append(constraints, &api.Constraint{ + LTarget: attribute, + Operand: operator, + RTarget: value, + }) + } + + return constraints +} + +func createMissingCPUArchConstraint(constraints []*api.Constraint, defaultCPUArch string) *api.Constraint { + for _, constraint := range constraints { + if constraint.LTarget == "${attr.cpu.arch}" { + return nil + } + } + + return &api.Constraint{ + LTarget: "${attr.cpu.arch}", + Operand: "=", + RTarget: defaultCPUArch, + } +} + func createEnvVars(r requests.CreateFunctionRequest) map[string]string { envVars := map[string]string{} diff --git a/handlers/deploy_test.go b/handlers/deploy_test.go index 6ab9ee1e..e8f98374 100644 --- a/handlers/deploy_test.go +++ b/handlers/deploy_test.go @@ -27,7 +27,7 @@ func setupDeploy(body string) (http.HandlerFunc, *httptest.ResponseRecorder, *ht logger := hclog.Default() - return MakeDeploy(mockJob, fntypes.ProviderConfig{Vault: fntypes.VaultConfig{DefaultPolicy: "openfaas", SecretPathPrefix: "secret/openfaas"}, Datacenter: "dc1", ConsulAddress: "http://localhost:8500", ConsulDNSEnabled: true}, logger, mockStats), + return MakeDeploy(mockJob, fntypes.ProviderConfig{Vault: fntypes.VaultConfig{DefaultPolicy: "openfaas", SecretPathPrefix: "secret/openfaas"}, Datacenter: "dc1", ConsulAddress: "http://localhost:8500", ConsulDNSEnabled: true, CPUArchConstraint: "amd64"}, logger, mockStats), httptest.NewRecorder(), httptest.NewRequest("GET", "/system/functions", bytes.NewReader([]byte(body))) } @@ -224,3 +224,134 @@ func TestHandleDeployWithRegistryAuth(t *testing.T) { assert.Equal(t, "username", auth[0]["username"]) assert.Equal(t, "password", auth[0]["password"]) } + +func TestHandlesRequestUsingDefaultCPUArchConstraint(t *testing.T) { + fr := createRequest() + expectedCpuArchConstraint := api.Constraint{ + LTarget: "${attr.cpu.arch}", + Operand: "=", + RTarget: "amd64", + } + + h, rw, r := setupDeploy(fr.String()) + + h(rw, r) + + args := mockJob.Calls[0].Arguments + job := args.Get(0).(*api.Job) + constraints := job.Constraints + + assert.Equal(t, expectedCpuArchConstraint, *constraints[0]) +} + +func TestHandlesCpuArchConstraintFromRequest(t *testing.T) { + fr := createRequest() + fr.Constraints = []string{"${attr.cpu.arch} = arm"} + expectedCpuArchConstraint := api.Constraint{ + LTarget: "${attr.cpu.arch}", + Operand: "=", + RTarget: "arm", + } + + h, rw, r := setupDeploy(fr.String()) + + h(rw, r) + + args := mockJob.Calls[0].Arguments + job := args.Get(0).(*api.Job) + constraints := job.Constraints + + assert.Equal(t, expectedCpuArchConstraint, *constraints[0]) +} + +func TestHandlesConstraintsWithSpaces(t *testing.T) { + fr := createRequest() + fr.Constraints = []string{"${attr.cpu.arch} = not a real architecture"} + expectedCpuArchConstraint := api.Constraint{ + LTarget: "${attr.cpu.arch}", + Operand: "=", + RTarget: "not a real architecture", + } + + h, rw, r := setupDeploy(fr.String()) + + h(rw, r) + + args := mockJob.Calls[0].Arguments + job := args.Get(0).(*api.Job) + constraints := job.Constraints + + assert.Equal(t, expectedCpuArchConstraint, *constraints[0]) +} + +func TestHandlesConstraintsWithoutInterpolationNotation(t *testing.T) { + fr := createRequest() + fr.Constraints = []string{"attr.cpu.arch = arm"} + expectedCpuArchConstraint := api.Constraint{ + LTarget: "${attr.cpu.arch}", + Operand: "=", + RTarget: "arm", + } + + h, rw, r := setupDeploy(fr.String()) + + h(rw, r) + + args := mockJob.Calls[0].Arguments + job := args.Get(0).(*api.Job) + constraints := job.Constraints + + assert.Equal(t, expectedCpuArchConstraint, *constraints[0]) +} + +func TestHandlesConstraintsWithTwoEqualSigns(t *testing.T) { + fr := createRequest() + fr.Constraints = []string{"attr.cpu.arch == arm"} + expectedCpuArchConstraint := api.Constraint{ + LTarget: "${attr.cpu.arch}", + Operand: "=", + RTarget: "arm", + } + + h, rw, r := setupDeploy(fr.String()) + + h(rw, r) + + args := mockJob.Calls[0].Arguments + job := args.Get(0).(*api.Job) + constraints := job.Constraints + + assert.Equal(t, expectedCpuArchConstraint, *constraints[0]) +} + +func TestConstraintsForDataCenterDoNotCreateAJobConstraint(t *testing.T) { + fr := createRequest() + fr.Constraints = []string{"something.datacenter == dc1"} + + h, rw, r := setupDeploy(fr.String()) + + h(rw, r) + + args := mockJob.Calls[0].Arguments + job := args.Get(0).(*api.Job) + constraints := job.Constraints + + assert.Equal(t, 1, len(constraints)) + assert.NotContains(t, "datacenter", constraints[0].RTarget) +} + +func TestIncompleteConstraintsAreIgnored(t *testing.T) { + fr := createRequest() + fr.Constraints = []string{"something", "something ="} + + h, rw, r := setupDeploy(fr.String()) + + h(rw, r) + + args := mockJob.Calls[0].Arguments + job := args.Get(0).(*api.Job) + constraints := job.Constraints + + assert.Equal(t, 1, len(constraints)) + assert.NotContains(t, "something", constraints[0].RTarget) +} diff --git a/main.go b/main.go index 0de691dc..17dd440c 100644 --- a/main.go +++ b/main.go @@ -49,6 +49,7 @@ var ( vaultSecretPathPrefix = flag.String("vault_secret_path_prefix", "secret/openfaas", "The Vault k/v path prefix used when secrets are deployed with a function") vaultAppRoleID = flag.String("vault_app_role_id", "", "A valid Vault AppRole role_id") vaultAppRoleSecretID = flag.String("vault_app_secret_id", "", "A valid Vault AppRole secret_id derived from the role") + cpuArchConstraint = flag.String("cpu_arch_constraint", "amd64", "CPU architecture to constraint deployed functions to") ) var functionTimeout = flag.Duration("function_timeout", 30*time.Second, "Timeout for function execution") @@ -127,10 +128,11 @@ func createFaaSHandlers(nomadClient *api.Client, consulResolver *consul.Resolver vaultConfig.TLSSkipVerify = *vaultTLSSkipVerify providerConfig := &fntypes.ProviderConfig{ - Vault: vaultConfig, - Datacenter: datacenter, - ConsulAddress: *consulAddr, - ConsulDNSEnabled: *enableConsulDNS, + Vault: vaultConfig, + Datacenter: datacenter, + ConsulAddress: *consulAddr, + ConsulDNSEnabled: *enableConsulDNS, + CPUArchConstraint: *cpuArchConstraint, } vs := vault.NewVaultService(&vaultConfig, logger) diff --git a/types/provider_config.go b/types/provider_config.go index 6838385e..b6b31c5b 100644 --- a/types/provider_config.go +++ b/types/provider_config.go @@ -1,8 +1,9 @@ package types type ProviderConfig struct { - Vault VaultConfig - Datacenter string - ConsulAddress string - ConsulDNSEnabled bool + Vault VaultConfig + Datacenter string + ConsulAddress string + ConsulDNSEnabled bool + CPUArchConstraint string }