functions branch and region support (#330)

* branch and region support

* .

* .

* .

* .

* fix functions

* .

* break loop

* update example

* update examples
This commit is contained in:
Asim Aslam
2021-12-16 19:23:03 +00:00
committed by GitHub
parent c305ae5739
commit 609c4c9813
8 changed files with 1503 additions and 540 deletions

View File

@@ -2,23 +2,41 @@
"deploy": [ "deploy": [
{ {
"title": "Deploy a function", "title": "Deploy a function",
"run_check": false,
"request": { "request": {
"repo": "github.com/m3o/nodejs-function-example", "name": "helloworld",
"name": "helloworld", "repo": "https://github.com/m3o/m3o",
"entrypoint": "helloworld", "branch": "main",
"runtime": "nodejs14" "entrypoint": "Helloworld",
"subfolder": "examples/go-function",
"runtime": "go116",
"region": "europe-west1"
}, },
"response": {} "response": {
"function": {
"id": "helloworld",
"name": "helloworld",
"repo": "https://github.com/m3o/m3o",
"branch": "main",
"entrypoint": "Helloworld",
"subfolder": "examples/go-function",
"runtime": "go116",
"region": "europe-west1",
"env_vars": {},
"status": "Deploying",
"url": "https://helloworld.m3o.sh",
"created": "2021-12-16T17:27:09.230134479Z",
"updated": ""
}
}
} }
], ],
"update": [ "update": [
{ {
"title": "Update a function", "title": "Update a function",
"run_check": false,
"request": { "request": {
"repo": "github.com/m3o/nodejs-function-example", "name": "helloworld"
"name": "helloworld",
"entrypoint": "helloworld",
"runtime": "nodejs14"
}, },
"response": {} "response": {}
} }
@@ -26,13 +44,16 @@
"call": [ "call": [
{ {
"title": "Call a function", "title": "Call a function",
"run_check": false,
"request": { "request": {
"name": "helloworld", "name": "helloworld",
"request": {} "request": {
"name": "Alice"
}
}, },
"response": { "response": {
"response": { "response": {
"message": "Hello World!" "message": "Hello Alice!"
} }
} }
} }
@@ -40,24 +61,31 @@
"list": [ "list": [
{ {
"title": "List functions", "title": "List functions",
"run_check": false,
"request": {}, "request": {},
"response": { "response": {
"functions": [ "functions": [{
{ "id": "helloworld",
"name": "helloworld", "name": "helloworld",
"entrypoint": "helloworld", "repo": "https://github.com/m3o/m3o",
"repo": "github.com/m3o/nodejs-function-example", "branch": "main",
"subfolder": "", "entrypoint": "Helloworld",
"runtime": "nodejs14", "subfolder": "examples/go-function",
"status": "DEPLOY_IN_PROGRESS" "runtime": "go116",
} "region": "europe-west1",
] "env_vars": {},
"status": "Deploying",
"url": "https://helloworld.m3o.sh",
"created": "2021-12-16T17:27:09.230134479Z",
"updated": ""
}]
} }
} }
], ],
"delete": [ "delete": [
{ {
"title": "Delete a function", "title": "Delete a function",
"run_check": false,
"request": { "request": {
"name": "helloworld" "name": "helloworld"
}, },
@@ -67,20 +95,72 @@
"describe": [ "describe": [
{ {
"title": "Describe function status", "title": "Describe function status",
"run_check": false,
"request": { "request": {
"name": "helloworld" "name": "helloworld"
}, },
"response": { "response": {
"function": { "function": {
"name": "helloworld", "id": "helloworld",
"entrypoint": "helloworld", "name": "helloworld",
"repo": "github.com/m3o/nodejs-function-example", "repo": "https://github.com/m3o/m3o",
"subfolder": "", "branch": "main",
"runtime": "nodejs14", "entrypoint": "Helloworld",
"status": "ACTIVE" "subfolder": "examples/go-function",
}, "runtime": "go116",
"updated_at": "2021-10-08T10:17:34.914Z", "region": "europe-west1",
"timeout": "60s" "env_vars": {},
"status": "Deploying",
"url": "https://helloworld.m3o.sh",
"created": "2021-12-16T17:27:09.230134479Z",
"updated": ""
}
}
}
],
"regions": [
{
"title": "List regions",
"run_check": false,
"request": {},
"response": {
"regions": [
"asia-east1",
"europe-west1",
"us-central1",
"us-east1",
"us-west1"
]
}
}
],
"reserve": [
{
"title": "Reserve a function",
"run_check": false,
"request": {
"name": "helloworld"
},
"response": {
"reservation": {
"name": "helloworld",
"owner": "micro/40e5d9aa-1185-4add-b248-ce4d72ff7947",
"token": "c580be106204d103df461bb3a3075aefedda5f85",
"created": "2021-12-16T19:19:29.615737412Z",
"expires": "2022-12-16T19:19:29.615737502Z"
}
}
}
],
"proxy": [
{
"title": "Proxy URL",
"run_check": false,
"request": {
"id": "helloworld"
},
"response": {
"url": "https://europe-west1-m3o-apis.cloudfunctions.net/helloworld"
} }
} }
] ]

View File

@@ -0,0 +1,16 @@
package handler
import (
"regexp"
)
var (
IDFormat = regexp.MustCompilePOSIX("[a-z0-9-]+")
NameFormat = regexp.MustCompilePOSIX("[a-z0-9]+")
FunctionKey = "function/func/"
OwnerKey = "function/owner/"
ReservationKey = "function/reservation/"
)
type Function struct{}

View File

@@ -6,10 +6,12 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"math/rand"
"net/http" "net/http"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
"time"
"github.com/micro/micro/v3/service/config" "github.com/micro/micro/v3/service/config"
"github.com/micro/micro/v3/service/errors" "github.com/micro/micro/v3/service/errors"
@@ -18,7 +20,7 @@ import (
"github.com/micro/micro/v3/service/store" "github.com/micro/micro/v3/service/store"
function "github.com/micro/services/function/proto" function "github.com/micro/services/function/proto"
"github.com/micro/services/pkg/tenant" "github.com/micro/services/pkg/tenant"
"gopkg.in/yaml.v2" "github.com/teris-io/shortid"
) )
type GoogleFunction struct { type GoogleFunction struct {
@@ -29,6 +31,10 @@ type GoogleFunction struct {
limit int limit int
// function identity // function identity
identity string identity string
// custom domain
domain string
*Function
} }
var ( var (
@@ -51,8 +57,27 @@ var (
"ruby26", "ruby26",
"php74", "php74",
} }
// hardcoded list of supported regions
GoogleRegions = []string{"europe-west1", "us-central1", "us-east1", "us-west1", "asia-east1"}
) )
var (
alphanum = "abcdefghijklmnopqrstuvwxyz0123456789"
)
func random(i int) string {
bytes := make([]byte, i)
for {
rand.Read(bytes)
for i, b := range bytes {
bytes[i] = alphanum[b%byte(len(alphanum))]
}
return string(bytes)
}
return fmt.Sprintf("%d", time.Now().Unix())
}
func NewFunction() *GoogleFunction { func NewFunction() *GoogleFunction {
v, err := config.Get("function.service_account_json") v, err := config.Get("function.service_account_json")
if err != nil { if err != nil {
@@ -98,6 +123,11 @@ func NewFunction() *GoogleFunction {
log.Fatalf("function.service_identity: %v", err) log.Fatalf("function.service_identity: %v", err)
} }
identity := v.String("") identity := v.String("")
v, err = config.Get("function.domain")
if err != nil {
log.Fatalf("function.domain: %v", err)
}
domain := v.String("")
m := map[string]interface{}{} m := map[string]interface{}{}
err = json.Unmarshal(keyfile, &m) err = json.Unmarshal(keyfile, &m)
@@ -124,12 +154,16 @@ func NewFunction() *GoogleFunction {
if err != nil { if err != nil {
log.Fatalf(string(outp)) log.Fatalf(string(outp))
} }
log.Info(string(outp)) log.Info(string(outp))
return &GoogleFunction{ return &GoogleFunction{
project: project, project: project,
address: address, address: address,
limit: limit, limit: limit,
identity: identity, identity: identity,
domain: domain,
Function: new(Function),
} }
} }
@@ -159,17 +193,36 @@ func (e *GoogleFunction) Deploy(ctx context.Context, req *function.DeployRequest
return errors.BadRequest("function.deploy", "invalid runtime") return errors.BadRequest("function.deploy", "invalid runtime")
} }
gitter := git.NewGitter(map[string]string{}) var supportedRegion bool
var err error if len(req.Region) == 0 {
// set to default region
req.Region = GoogleRegions[0]
supportedRegion = true
}
for _, branch := range []string{"master", "main"} { // check if its in the supported regions
err = gitter.Checkout(req.Repo, branch) for _, reg := range GoogleRegions {
if err == nil { if req.Region == reg {
supportedRegion = true
break break
} }
} }
// unsupported region requested
if !supportedRegion {
return errors.BadRequest("function.deploy", "Unsupported region")
}
if len(req.Branch) == 0 {
req.Branch = "master"
}
gitter := git.NewGitter(map[string]string{})
var err error
err = gitter.Checkout(req.Repo, req.Branch)
if err != nil { if err != nil {
return errors.InternalServerError("function.deploy", err.Error()) return errors.InternalServerError("function.deploy", err.Error())
} }
@@ -179,18 +232,12 @@ func (e *GoogleFunction) Deploy(ctx context.Context, req *function.DeployRequest
tenantId = "micro" tenantId = "micro"
} }
multitenantPrefix := strings.Replace(tenantId, "/", "-", -1)
if req.Entrypoint == "" { if req.Entrypoint == "" {
req.Entrypoint = req.Name req.Entrypoint = req.Name
} }
project := req.Project // read the function by owner
if project == "" { key := fmt.Sprintf(OwnerKey+"%s/%s", tenantId, req.Name)
project = "default"
}
key := fmt.Sprintf("function/%s/%s/%s", tenantId, project, req.Name)
records, err := store.Read(key) records, err := store.Read(key)
if err != nil && err != store.ErrNotFound { if err != nil && err != store.ErrNotFound {
return err return err
@@ -203,7 +250,7 @@ func (e *GoogleFunction) Deploy(ctx context.Context, req *function.DeployRequest
// check for function limit // check for function limit
if e.limit > 0 { if e.limit > 0 {
// read all the records for the user // read all the records for the user
records, err := store.Read("function/"+tenantId+"/", store.ReadPrefix()) records, err := store.Read(OwnerKey+tenantId+"/", store.ReadPrefix())
if err != nil { if err != nil {
return err return err
} }
@@ -213,6 +260,26 @@ func (e *GoogleFunction) Deploy(ctx context.Context, req *function.DeployRequest
} }
} }
// set the id
id := req.Name
// check the owner isn't already running it
recs, err := store.Read(FunctionKey+req.Name, store.ReadLimit(1))
// if there's an existing function then generate a unique id
if err == nil && len(recs) > 0 {
// generate an id for the service
sid, err := shortid.Generate()
if err != nil || len(sid) == 0 {
sid = random(8)
}
sid = strings.ToLower(sid)
sid = strings.Replace(sid, "-", "", -1)
sid = strings.Replace(sid, "_", "", -1)
id = req.Name + "-" + sid
}
// process the env vars to the required format // process the env vars to the required format
var envVars []string var envVars []string
@@ -220,40 +287,122 @@ func (e *GoogleFunction) Deploy(ctx context.Context, req *function.DeployRequest
envVars = append(envVars, k+"="+v) envVars = append(envVars, k+"="+v)
} }
go func() { fn := &function.Func{
Id: id,
Name: req.Name,
Repo: req.Repo,
Subfolder: req.Subfolder,
Entrypoint: req.Entrypoint,
Runtime: req.Runtime,
EnvVars: req.EnvVars,
Region: req.Region,
Branch: req.Branch,
Created: time.Now().Format(time.RFC3339Nano),
Status: "Deploying",
Url: fmt.Sprintf("https://%s-%s.cloudfunctions.net/%s", req.Region, e.project, id),
}
// write the owner key
rec := store.NewRecord(key, fn)
if err := store.Write(rec); err != nil {
return err
}
// write the global key
rec = store.NewRecord(FunctionKey+fn.Id, fn)
if err := store.Write(rec); err != nil {
return err
}
// set the custom domain
if len(e.domain) > 0 {
fn.Url = fmt.Sprintf("https://%s.%s", fn.Id, e.domain)
}
// set the response
rsp.Function = fn
go func(fn *function.Func) {
// https://jsoverson.medium.com/how-to-deploy-node-js-functions-to-google-cloud-8bba05e9c10a // https://jsoverson.medium.com/how-to-deploy-node-js-functions-to-google-cloud-8bba05e9c10a
cmd := exec.Command("gcloud", "functions", "deploy", cmd := exec.Command("gcloud", "functions", "deploy", fn.Id, "--quiet",
multitenantPrefix+"-"+req.Name, "--region", "europe-west1", "--service-account", e.identity, "--region", fn.Region, "--service-account", e.identity,
"--allow-unauthenticated", "--entry-point", req.Entrypoint, "--allow-unauthenticated", "--entry-point", fn.Entrypoint,
"--trigger-http", "--project", e.project, "--runtime", req.Runtime) "--trigger-http", "--project", e.project, "--runtime", fn.Runtime)
// if env vars exist then set them // if env vars exist then set them
if len(envVars) > 0 { if len(envVars) > 0 {
cmd.Args = append(cmd.Args, "--set-env-vars", strings.Join(envVars, ",")) cmd.Args = append(cmd.Args, "--set-env-vars", strings.Join(envVars, ","))
} }
cmd.Dir = filepath.Join(gitter.RepoDir(), req.Subfolder) cmd.Dir = filepath.Join(gitter.RepoDir(), fn.Subfolder)
outp, err := cmd.CombinedOutput() outp, err := cmd.CombinedOutput()
if err != nil { if err != nil {
log.Error(fmt.Errorf(string(outp))) log.Error(fmt.Errorf(string(outp)))
fn.Status = "DeploymentError"
store.Write(store.NewRecord(key, fn))
return
} }
}()
id := fmt.Sprintf("%v-%v-%v", tenantId, project, req.Name) var status string
rec := store.NewRecord(key, map[string]interface{}{
"id": id,
"project": project,
"name": req.Name,
"tenantId": tenantId,
"repo": req.Repo,
"subfolder": req.Subfolder,
"entrypoint": req.Entrypoint,
"runtime": req.Runtime,
"env_vars": envVars,
})
// write the record LOOP:
return store.Write(rec) // wait for the deployment and status update
for i := 0; i < 120; i++ {
cmd = exec.Command("gcloud", "functions", "describe", "--format", "json",
"--region", fn.Region, "--project", e.project, fn.Id)
outp, err = cmd.CombinedOutput()
if err != nil {
log.Error(fmt.Errorf(string(outp)))
return
}
log.Info(string(outp))
var m map[string]interface{}
if err := json.Unmarshal(outp, &m); err != nil {
log.Error(err)
return
}
// write back the url
trigger := m["httpsTrigger"].(map[string]interface{})
if v := trigger["url"].(string); len(v) > 0 {
fn.Url = v
} else {
fn.Url = fmt.Sprintf("https://%s-%s.cloudfunctions.net/%s", fn.Region, e.project, fn.Id)
}
v := m["status"].(string)
switch v {
case "ACTIVE":
status = "Deployed"
break LOOP
case "DEPLOY_IN_PROGRESS":
status = "Deploying"
case "OFFLINE":
status = "DeploymentError"
break LOOP
}
// we need to try get the url again
time.Sleep(time.Second)
}
fn.Updated = time.Now().Format(time.RFC3339Nano)
fn.Status = status
// write the owners key
store.Write(store.NewRecord(key, fn))
// write the global key
rec = store.NewRecord(FunctionKey+fn.Id, fn)
store.Write(rec)
}(fn)
return nil
} }
func (e *GoogleFunction) Update(ctx context.Context, req *function.UpdateRequest, rsp *function.UpdateResponse) error { func (e *GoogleFunction) Update(ctx context.Context, req *function.UpdateRequest, rsp *function.UpdateResponse) error {
@@ -263,29 +412,12 @@ func (e *GoogleFunction) Update(ctx context.Context, req *function.UpdateRequest
return errors.BadRequest("function.update", "Missing name") return errors.BadRequest("function.update", "Missing name")
} }
if len(req.Repo) == 0 {
return errors.BadRequest("function.update", "Missing repo")
}
if req.Runtime == "" {
return fmt.Errorf("missing runtime field, please specify nodejs14, go116 etc")
}
tenantId, ok := tenant.FromContext(ctx) tenantId, ok := tenant.FromContext(ctx)
if !ok { if !ok {
tenantId = "micro" tenantId = "micro"
} }
multitenantPrefix := strings.Replace(tenantId, "/", "-", -1) key := fmt.Sprintf(OwnerKey+"%s/%s", tenantId, req.Name)
if req.Entrypoint == "" {
req.Entrypoint = req.Name
}
project := req.Project
if project == "" {
project = "default"
}
key := fmt.Sprintf("function/%s/%s/%s", tenantId, project, req.Name)
records, err := store.Read(key) records, err := store.Read(key)
if err != nil { if err != nil {
@@ -296,60 +428,87 @@ func (e *GoogleFunction) Update(ctx context.Context, req *function.UpdateRequest
return errors.BadRequest("function.deploy", "function does not exist") return errors.BadRequest("function.deploy", "function does not exist")
} }
gitter := git.NewGitter(map[string]string{}) fn := new(function.Func)
if err := records[0].Decode(fn); err != nil {
for _, branch := range []string{"master", "main"} { return err
err = gitter.Checkout(req.Repo, branch)
if err == nil {
break
}
} }
if err != nil { if len(fn.Region) == 0 {
fn.Region = GoogleRegions[0]
}
if len(fn.Branch) == 0 {
fn.Branch = "master"
}
gitter := git.NewGitter(map[string]string{})
if err := gitter.Checkout(fn.Repo, fn.Branch); err != nil {
return errors.InternalServerError("function.update", err.Error()) return errors.InternalServerError("function.update", err.Error())
} }
// process the env vars to the required format // process the env vars to the required format
var envVars []string var envVars []string
for k, v := range req.EnvVars { for k, v := range fn.EnvVars {
envVars = append(envVars, k+"="+v) envVars = append(envVars, k+"="+v)
} }
var status string
go func() { go func() {
// https://jsoverson.medium.com/how-to-deploy-node-js-functions-to-google-cloud-8bba05e9c10a
cmd := exec.Command("gcloud", "functions", "deploy",
multitenantPrefix+"-"+req.Name, "--region", "europe-west1", "--service-account", e.identity,
"--allow-unauthenticated", "--entry-point", req.Entrypoint,
"--trigger-http", "--project", e.project, "--runtime", req.Runtime)
// if env vars exist then set them LOOP:
if len(envVars) > 0 { // wait for the deployment and status update
cmd.Args = append(cmd.Args, "--set-env-vars", strings.Join(envVars, ",")) for i := 0; i < 120; i++ {
cmd := exec.Command("gcloud", "functions", "describe", "--quiet", "--format", "json",
"--region", fn.Region, "--project", e.project, fn.Id)
outp, err := cmd.CombinedOutput()
if err != nil {
log.Error(fmt.Errorf(string(outp)))
return
}
log.Info(string(outp))
var m map[string]interface{}
if err := json.Unmarshal(outp, &m); err != nil {
log.Error(err)
return
}
// write back the url
trigger := m["httpsTrigger"].(map[string]interface{})
if v := trigger["url"].(string); len(v) > 0 {
fn.Url = v
} else {
fn.Url = fmt.Sprintf("https://%s-%s.cloudfunctions.net/%s", fn.Region, e.project, fn.Id)
}
v := m["status"].(string)
switch v {
case "ACTIVE":
status = "Deployed"
break LOOP
case "DEPLOY_IN_PROGRESS":
status = "Deploying"
case "OFFLINE":
status = "DeploymentError"
break LOOP
}
// we need to try get the url again
time.Sleep(time.Second)
} }
cmd.Dir = filepath.Join(gitter.RepoDir(), req.Subfolder) fn.Status = status
outp, err := cmd.CombinedOutput() fn.Updated = time.Now().Format(time.RFC3339Nano)
if err != nil { store.Write(store.NewRecord(key, fn))
log.Error(fmt.Errorf(string(outp)))
}
}() }()
id := fmt.Sprintf("%v-%v-%v", tenantId, project, req.Name) // TODO: allow updating of branch and related?
rec := store.NewRecord(key, map[string]interface{}{ return nil
"id": id,
"project": project,
"name": req.Name,
"tenantId": tenantId,
"repo": req.Repo,
"subfolder": req.Subfolder,
"entrypoint": req.Entrypoint,
"runtime": req.Runtime,
"env_vars": envVars,
})
// write the record
return store.Write(rec)
} }
func (e *GoogleFunction) Call(ctx context.Context, req *function.CallRequest, rsp *function.CallResponse) error { func (e *GoogleFunction) Call(ctx context.Context, req *function.CallRequest, rsp *function.CallResponse) error {
@@ -363,10 +522,28 @@ func (e *GoogleFunction) Call(ctx context.Context, req *function.CallRequest, rs
if !ok { if !ok {
tenantId = "micro" tenantId = "micro"
} }
multitenantPrefix := strings.Replace(tenantId, "/", "-", -1)
url := e.address + multitenantPrefix + "-" + req.Name // get the function id based on the tenant
fmt.Println("URL:>", url) recs, err := store.Read(OwnerKey + tenantId + "/" + req.Name)
if err != nil {
return err
}
if len(recs) == 0 {
return errors.NotFound("function.call", "not found")
}
fn := new(function.Func)
recs[0].Decode(fn)
if len(fn.Id) == 0 {
return errors.NotFound("function.call", "not found")
}
url := fn.Url
if len(url) == 0 {
url = fmt.Sprintf("https://%s-%s.cloudfunctions.net/%s", fn.Region, e.project, fn.Id)
}
js, _ := json.Marshal(req.Request) js, _ := json.Marshal(req.Request)
if req.Request == nil || len(req.Request.Fields) == 0 { if req.Request == nil || len(req.Request.Fields) == 0 {
@@ -412,23 +589,40 @@ func (e *GoogleFunction) Delete(ctx context.Context, req *function.DeleteRequest
if !ok { if !ok {
tenantId = "micro" tenantId = "micro"
} }
multitenantPrefix := strings.Replace(tenantId, "/", "-", -1)
project := req.Project key := fmt.Sprintf(OwnerKey+"%v/%v", tenantId, req.Name)
if project == "" {
project = "default" records, err := store.Read(key)
if err != nil && err == store.ErrNotFound {
return nil
} }
cmd := exec.Command("gcloud", "functions", "delete", "--project", e.project, "--region", "europe-west1", multitenantPrefix+"-"+req.Name) if len(records) == 0 {
outp, err := cmd.CombinedOutput() return nil
if err != nil && !strings.Contains(string(outp), "does not exist") { }
log.Error(fmt.Errorf(string(outp)))
fn := new(function.Func)
if err := records[0].Decode(fn); err != nil {
return err return err
} }
key := fmt.Sprintf("function/%v/%v/%v", tenantId, project, req.Name) // async delete
go func() {
cmd := exec.Command("gcloud", "functions", "delete", "--quiet", "--project", e.project, "--region", fn.Region, fn.Id)
outp, err := cmd.CombinedOutput()
if err != nil && !strings.Contains(string(outp), "does not exist") {
log.Error(fmt.Errorf(string(outp)))
return
}
return store.Delete(key) // delete the owner key
store.Delete(key)
// delete the global key
store.Delete(FunctionKey + fn.Id)
}()
return nil
} }
func (e *GoogleFunction) List(ctx context.Context, req *function.ListRequest, rsp *function.ListResponse) error { func (e *GoogleFunction) List(ctx context.Context, req *function.ListRequest, rsp *function.ListResponse) error {
@@ -439,45 +633,31 @@ func (e *GoogleFunction) List(ctx context.Context, req *function.ListRequest, rs
tenantId = "micro" tenantId = "micro"
} }
key := "function/" + tenantId + "/" key := OwnerKey + tenantId + "/"
project := req.Project
if len(project) > 0 {
key = key + "/" + project + "/"
}
records, err := store.Read(key, store.ReadPrefix()) records, err := store.Read(key, store.ReadPrefix())
if err != nil { if err != nil {
return err return err
} }
multitenantPrefix := strings.Replace(tenantId, "/", "-", -1) for _, record := range records {
cmd := exec.Command("gcloud", "functions", "list", "--project", e.project, "--filter", "name~"+multitenantPrefix+"*") fn := new(function.Func)
outp, err := cmd.CombinedOutput() if err := record.Decode(fn); err != nil {
if err != nil {
log.Error(fmt.Errorf(string(outp)))
}
lines := strings.Split(string(outp), "\n")
statuses := map[string]string{}
for _, line := range lines {
fields := strings.Fields(line)
if len(fields) < 2 {
continue continue
} }
statuses[fields[0]] = fields[1] if len(fn.Region) == 0 {
} fn.Region = GoogleRegions[0]
rsp.Functions = []*function.Func{}
for _, record := range records {
f := new(function.Func)
if err := record.Decode(f); err != nil {
return err
} }
f.Status = statuses[multitenantPrefix+"-"+f.Name]
rsp.Functions = append(rsp.Functions, f) if len(fn.Branch) == 0 {
fn.Branch = "master"
}
// set the custom domain
if len(e.domain) > 0 {
fn.Url = fmt.Sprintf("https://%s.%s", fn.Id, e.domain)
}
rsp.Functions = append(rsp.Functions, fn)
} }
return nil return nil
@@ -493,20 +673,41 @@ func (e *GoogleFunction) Describe(ctx context.Context, req *function.DescribeReq
tenantId = "micro" tenantId = "micro"
} }
project := req.Project key := fmt.Sprintf(OwnerKey+"%v/%v", tenantId, req.Name)
if project == "" {
project = "default"
}
multitenantPrefix := strings.Replace(tenantId, "/", "-", -1)
key := fmt.Sprintf("function/%v/%v/%v", tenantId, project, req.Name)
records, err := store.Read(key) records, err := store.Read(key)
if err != nil { if err != nil {
return err return err
} }
cmd := exec.Command("gcloud", "functions", "describe", "--region", "europe-west1", "--project", e.project, multitenantPrefix+"-"+req.Name) if len(records) == 0 {
return errors.NotFound("function.describe", "function does not exist")
}
fn := new(function.Func)
if err := records[0].Decode(fn); err != nil {
return err
}
if len(fn.Region) == 0 {
fn.Region = GoogleRegions[0]
}
if len(fn.Branch) == 0 {
fn.Branch = "master"
}
// set the custom domain
if len(e.domain) > 0 {
fn.Url = fmt.Sprintf("https://%s.%s", fn.Id, e.domain)
}
// set the response function
rsp.Function = fn
// get the current status
cmd := exec.Command("gcloud", "functions", "describe", "--format", "json", "--region", fn.Region, "--project", e.project, fn.Id)
outp, err := cmd.CombinedOutput() outp, err := cmd.CombinedOutput()
if err != nil { if err != nil {
log.Error(fmt.Errorf(string(outp))) log.Error(fmt.Errorf(string(outp)))
@@ -515,28 +716,65 @@ func (e *GoogleFunction) Describe(ctx context.Context, req *function.DescribeReq
log.Info(string(outp)) log.Info(string(outp))
m := map[string]interface{}{} m := map[string]interface{}{}
err = yaml.Unmarshal(outp, m)
if err := json.Unmarshal(outp, &m); err != nil {
return err
}
// set describe info
status := m["status"].(string)
status = strings.Replace(status, "_", " ", -1)
status = strings.Title(strings.ToLower(status))
fn.Status = status
// set the url
if len(fn.Url) == 0 && status == "Active" {
v := m["httpsTrigger"].(map[string]interface{})
fn.Url = v["url"].(string)
}
// write it back
go store.Write(store.NewRecord(key, fn))
rsp.Function.Status = status
return nil
}
func (g *GoogleFunction) Proxy(ctx context.Context, req *function.ProxyRequest, rsp *function.ProxyResponse) error {
if len(req.Id) == 0 {
return errors.BadRequest("function.proxy", "missing id")
}
if !IDFormat.MatchString(req.Id) {
return errors.BadRequest("function.proxy", "invalid id")
}
recs, err := store.Read(FunctionKey+req.Id, store.ReadLimit(1))
if err != nil { if err != nil {
return err return err
} }
if len(records) > 0 { if len(recs) == 0 {
f := &function.Func{} return errors.BadRequest("function.proxy", "function does not exist")
if err := records[0].Decode(f); err != nil {
return err
}
rsp.Function = f
} else {
rsp.Function = &function.Func{
Name: req.Name,
Project: req.Project,
}
} }
// set describe info fn := new(function.Func)
rsp.Function.Status = m["status"].(string) recs[0].Decode(fn)
rsp.Timeout = m["timeout"].(string)
rsp.UpdatedAt = m["updateTime"].(string) url := fn.Url
// backup plan is to construct https://region-project.cloudfunctions.net/function-name
if len(url) == 0 {
url = fmt.Sprintf("https://%s-%s.cloudfunctions.net/%s", fn.Region, g.project, fn.Id)
}
rsp.Url = url
return nil return nil
} }
func (e *GoogleFunction) Regions(ctx context.Context, req *function.RegionsRequest, rsp *function.RegionsResponse) error {
rsp.Regions = GoogleRegions
return nil
}

128
function/handler/reserve.go Normal file
View File

@@ -0,0 +1,128 @@
package handler
import (
"context"
"crypto/sha1"
"fmt"
"io"
"sync"
"time"
"github.com/micro/micro/v3/service/errors"
"github.com/micro/micro/v3/service/store"
pb "github.com/micro/services/function/proto"
"github.com/micro/services/pkg/tenant"
)
var (
mtx sync.Mutex
)
type Reservation struct {
// The function name
Name string `json:"name"`
// The owner e.g tenant id
Owner string `json:"owner"`
// Uniq associated token
Token string `json:"token"`
// Time of creation
Created time.Time `json:"created"`
// The expiry time
Expires time.Time `json:"expires"`
}
func genToken(name, owner string) string {
h := sha1.New()
io.WriteString(h, name+owner)
return fmt.Sprintf("%x", h.Sum(nil))
}
// Call is a single request handler called via client.Call or the generated client code
func (f *Function) Reserve(ctx context.Context, req *pb.ReserveRequest, rsp *pb.ReserveResponse) error {
id, ok := tenant.FromContext(ctx)
if !ok {
id = "micro"
}
if len(req.Name) == 0 {
return errors.BadRequest("function.reserve", "missing function name")
}
if len(req.Name) < 3 || len(req.Name) > 32 {
return errors.BadRequest("function.reserve", "name must be longer than 3-32 chars in length")
}
if !NameFormat.MatchString(req.Name) {
return errors.BadRequest("function.reserve", "invalidate name format")
}
// to prevent race conditions in reservation lets global lock
mtx.Lock()
defer mtx.Unlock()
// check the store for reservation
recs, err := store.Read(ReservationKey + req.Name)
if err != nil && err != store.ErrNotFound {
return errors.InternalServerError("function.reserve", "failed to reserve name")
}
var rsrv *Reservation
// check if the record exists
if len(recs) > 0 {
// existing reservation exists
rec := recs[0]
if err := rec.Decode(&rsrv); err != nil {
return errors.BadRequest("function.reserve", "name already reserved")
}
// check the owner matches or if the reservation expired
if rsrv.Owner != id && rsrv.Expires.After(time.Now()) {
return errors.BadRequest("function.reserve", "name already reserved")
}
// update the owner
rsrv.Owner = id
// update the reservation expiry
rsrv.Expires = time.Now().AddDate(1, 0, 0)
} else {
// check if its already running
key := FunctionKey + req.Name
recs, err := store.Read(key, store.ReadLimit(1))
if err != nil && err != store.ErrNotFound {
return errors.InternalServerError("function.reserve", "failed to reserve name")
}
// existing function is running by that name
if len(recs) > 0 {
return errors.BadRequest("function.reserve", "function already exists")
}
// not reserved
rsrv = &Reservation{
Name: req.Name,
Owner: id,
Created: time.Now(),
Expires: time.Now().AddDate(1, 0, 0),
Token: genToken(req.Name, id),
}
}
rec := store.NewRecord(ReservationKey+req.Name, rsrv)
if err := store.Write(rec); err != nil {
return errors.InternalServerError("function.reserve", "error while reserving name")
}
rsp.Reservation = &pb.Reservation{
Name: rsrv.Name,
Owner: rsrv.Owner,
Created: rsrv.Created.Format(time.RFC3339Nano),
Expires: rsrv.Expires.Format(time.RFC3339Nano),
Token: rsrv.Token,
}
return nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -49,6 +49,9 @@ type FunctionService interface {
Delete(ctx context.Context, in *DeleteRequest, opts ...client.CallOption) (*DeleteResponse, error) Delete(ctx context.Context, in *DeleteRequest, opts ...client.CallOption) (*DeleteResponse, error)
Describe(ctx context.Context, in *DescribeRequest, opts ...client.CallOption) (*DescribeResponse, error) Describe(ctx context.Context, in *DescribeRequest, opts ...client.CallOption) (*DescribeResponse, error)
Update(ctx context.Context, in *UpdateRequest, opts ...client.CallOption) (*UpdateResponse, error) Update(ctx context.Context, in *UpdateRequest, opts ...client.CallOption) (*UpdateResponse, error)
Proxy(ctx context.Context, in *ProxyRequest, opts ...client.CallOption) (*ProxyResponse, error)
Regions(ctx context.Context, in *RegionsRequest, opts ...client.CallOption) (*RegionsResponse, error)
Reserve(ctx context.Context, in *ReserveRequest, opts ...client.CallOption) (*ReserveResponse, error)
} }
type functionService struct { type functionService struct {
@@ -123,6 +126,36 @@ func (c *functionService) Update(ctx context.Context, in *UpdateRequest, opts ..
return out, nil return out, nil
} }
func (c *functionService) Proxy(ctx context.Context, in *ProxyRequest, opts ...client.CallOption) (*ProxyResponse, error) {
req := c.c.NewRequest(c.name, "Function.Proxy", in)
out := new(ProxyResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *functionService) Regions(ctx context.Context, in *RegionsRequest, opts ...client.CallOption) (*RegionsResponse, error) {
req := c.c.NewRequest(c.name, "Function.Regions", in)
out := new(RegionsResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *functionService) Reserve(ctx context.Context, in *ReserveRequest, opts ...client.CallOption) (*ReserveResponse, error) {
req := c.c.NewRequest(c.name, "Function.Reserve", in)
out := new(ReserveResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// Server API for Function service // Server API for Function service
type FunctionHandler interface { type FunctionHandler interface {
@@ -132,6 +165,9 @@ type FunctionHandler interface {
Delete(context.Context, *DeleteRequest, *DeleteResponse) error Delete(context.Context, *DeleteRequest, *DeleteResponse) error
Describe(context.Context, *DescribeRequest, *DescribeResponse) error Describe(context.Context, *DescribeRequest, *DescribeResponse) error
Update(context.Context, *UpdateRequest, *UpdateResponse) error Update(context.Context, *UpdateRequest, *UpdateResponse) error
Proxy(context.Context, *ProxyRequest, *ProxyResponse) error
Regions(context.Context, *RegionsRequest, *RegionsResponse) error
Reserve(context.Context, *ReserveRequest, *ReserveResponse) error
} }
func RegisterFunctionHandler(s server.Server, hdlr FunctionHandler, opts ...server.HandlerOption) error { func RegisterFunctionHandler(s server.Server, hdlr FunctionHandler, opts ...server.HandlerOption) error {
@@ -142,6 +178,9 @@ func RegisterFunctionHandler(s server.Server, hdlr FunctionHandler, opts ...serv
Delete(ctx context.Context, in *DeleteRequest, out *DeleteResponse) error Delete(ctx context.Context, in *DeleteRequest, out *DeleteResponse) error
Describe(ctx context.Context, in *DescribeRequest, out *DescribeResponse) error Describe(ctx context.Context, in *DescribeRequest, out *DescribeResponse) error
Update(ctx context.Context, in *UpdateRequest, out *UpdateResponse) error Update(ctx context.Context, in *UpdateRequest, out *UpdateResponse) error
Proxy(ctx context.Context, in *ProxyRequest, out *ProxyResponse) error
Regions(ctx context.Context, in *RegionsRequest, out *RegionsResponse) error
Reserve(ctx context.Context, in *ReserveRequest, out *ReserveResponse) error
} }
type Function struct { type Function struct {
function function
@@ -177,3 +216,15 @@ func (h *functionHandler) Describe(ctx context.Context, in *DescribeRequest, out
func (h *functionHandler) Update(ctx context.Context, in *UpdateRequest, out *UpdateResponse) error { func (h *functionHandler) Update(ctx context.Context, in *UpdateRequest, out *UpdateResponse) error {
return h.FunctionHandler.Update(ctx, in, out) return h.FunctionHandler.Update(ctx, in, out)
} }
func (h *functionHandler) Proxy(ctx context.Context, in *ProxyRequest, out *ProxyResponse) error {
return h.FunctionHandler.Proxy(ctx, in, out)
}
func (h *functionHandler) Regions(ctx context.Context, in *RegionsRequest, out *RegionsResponse) error {
return h.FunctionHandler.Regions(ctx, in, out)
}
func (h *functionHandler) Reserve(ctx context.Context, in *ReserveRequest, out *ReserveResponse) error {
return h.FunctionHandler.Reserve(ctx, in, out)
}

View File

@@ -12,6 +12,9 @@ service Function {
rpc Delete(DeleteRequest) returns (DeleteResponse) {} rpc Delete(DeleteRequest) returns (DeleteResponse) {}
rpc Describe(DescribeRequest) returns (DescribeResponse) {} rpc Describe(DescribeRequest) returns (DescribeResponse) {}
rpc Update(UpdateRequest) returns (UpdateResponse) {} rpc Update(UpdateRequest) returns (UpdateResponse) {}
rpc Proxy(ProxyRequest) returns (ProxyResponse) {}
rpc Regions(RegionsRequest) returns (RegionsResponse) {}
rpc Reserve(ReserveRequest) returns (ReserveResponse) {}
} }
// Call a function by name // Call a function by name
@@ -33,63 +36,63 @@ message DeployRequest {
string name = 1; string name = 1;
// github url to repo // github url to repo
string repo = 2; string repo = 2;
// branch to deploy. defaults to master
string branch = 3;
// optional subfolder path // optional subfolder path
string subfolder = 3; string subfolder = 4;
// entry point, ie. handler name in the source code // entry point, ie. handler name in the source code
// if not provided, defaults to the name parameter // if not provided, defaults to the name parameter
string entrypoint = 4; string entrypoint = 5;
// project is used for namespacing your functions // runtime/lanaguage of the function e.g php74,
// optional. defaults to "default". // nodejs6, nodejs8, nodejs10, nodejs12, nodejs14, nodejs16,
string project = 5; // dotnet3, java11, ruby26, ruby27, go111, go113, go116,
// runtime/language of the function
// eg: php74,
// nodejs6, nodejs8, nodejs10, nodejs12, nodejs14, nodejs16
// dotnet3
// java11
// ruby26, ruby27
// go111, go113, go116
// python37, python38, python39 // python37, python38, python39
string runtime = 6; string runtime = 6;
// region to deploy in. defaults to europe-west1
string region = 7;
// environment variables to pass in at runtime // environment variables to pass in at runtime
map<string,string> env_vars = 7; map<string,string> env_vars = 8;
} }
message DeployResponse { message DeployResponse {
Func function = 1;
} }
// List all the deployed functions // List all the deployed functions
message ListRequest { message ListRequest {
// optional project name
string project = 1;
} }
message Func { message Func {
// project of function, optional // id of the function
// defaults to literal "default" string id = 1;
// used to namespace functions
string project = 1;
// function name // function name
// limitation: must be unique across projects // limitation: must be unique across projects
string name = 2; string name = 2;
// name of handler in source code
string entrypoint = 3;
// git repo address // git repo address
string repo = 4; string repo = 3;
// branch to deploy. defaults to master
string branch = 4;
// name of handler in source code
string entrypoint = 5;
// subfolder path to entrypoint // subfolder path to entrypoint
string subfolder = 5; string subfolder = 6;
// runtime/language of the function // runtime/language of the function e.g php74,
// eg: php74, // nodejs6, nodejs8, nodejs10, nodejs12, nodejs14, nodejs16,
// nodejs6, nodejs8, nodejs10, nodejs12, nodejs14, nodejs16 // dotnet3, java11, ruby26, ruby27, go111, go113, go116,
// dotnet3
// java11
// ruby26, ruby27
// go111, go113, go116
// python37, python38, python39 // python37, python38, python39
string runtime = 6; string runtime = 7;
// eg. ACTIVE, DEPLOY_IN_PROGRESS, OFFLINE etc // region to deploy in. defaults to europe-west1
string status = 7; string region = 8;
// associated env vars // associated env vars
map<string,string> env_vars = 8; map<string,string> env_vars = 9;
// eg. ACTIVE, DEPLOY_IN_PROGRESS, OFFLINE etc
string status = 10;
// unique url of the function
string url = 11;
// time of creation
string created = 12;
// time it was updated
string updated = 13;
} }
message ListResponse { message ListResponse {
@@ -101,8 +104,6 @@ message ListResponse {
message DeleteRequest { message DeleteRequest {
// The name of the function // The name of the function
string name = 1; string name = 1;
// Optional project name
string project = 2;
} }
message DeleteResponse { message DeleteResponse {
@@ -113,45 +114,63 @@ message DeleteResponse {
message DescribeRequest { message DescribeRequest {
// The name of the function // The name of the function
string name = 1; string name = 1;
// Optional project name
string project = 2;
} }
message DescribeResponse { message DescribeResponse {
// The function requested // The function requested
Func function = 1; Func function = 1;
// The time at which the function was updated
string updated_at = 2;
// The timeout for requests to the function
string timeout = 3;
} }
// Update a function // Update a function. Downloads the source, builds and redeploys
message UpdateRequest { message UpdateRequest {
// function name // function name
string name = 1; string name = 1;
// github url to repo
string repo = 2;
// optional subfolder path
string subfolder = 3;
// entry point, ie. handler name in the source code
// if not provided, defaults to the name parameter
string entrypoint = 4;
// project is used for namespacing your functions
// optional. defaults to "default".
string project = 5;
// runtime/language of the function
// eg: php74,
// nodejs6, nodejs8, nodejs10, nodejs12, nodejs14, nodejs16
// dotnet3
// java11
// ruby26, ruby27
// go111, go113, go116
// python37, python38, python39
string runtime = 6;
// environment variables to pass in at runtime
map<string,string> env_vars = 7;
} }
message UpdateResponse { message UpdateResponse {
} }
// Return the backend url for proxying
message ProxyRequest {
// id of the function
string id = 1;
}
message ProxyResponse {
// backend url
string url = 1;
}
// Return a list of supported regions
message RegionsRequest {
}
message RegionsResponse {
repeated string regions = 1;
}
// Reservation represents a reserved function
message Reservation {
// name of the app
string name = 1;
// owner id
string owner = 2;
// associated token
string token = 3;
// time of reservation
string created = 4;
// time reservation expires
string expires = 5;
}
// Reserve function names and resources beyond free quota
message ReserveRequest {
// name of your app e.g helloworld
string name = 1;
}
message ReserveResponse {
// The app reservation
Reservation reservation = 1;
}

View File

@@ -1,7 +1,10 @@
{ {
"name": "function", "name": "function",
"icon": "🔥", "icon": "🔥",
"category": "hosting", "category": "hosting",
"display_name": "Functions" "display_name": "Functions",
"pricing": {
"Function.Reserve": 10000000
} }
}