mirror of
https://github.com/kevin-DL/services.git
synced 2026-01-23 15:51:24 +00:00
functions branch and region support (#330)
* branch and region support * . * . * . * . * fix functions * . * break loop * update example * update examples
This commit is contained in:
@@ -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",
|
||||||
"entrypoint": "helloworld",
|
"repo": "https://github.com/m3o/m3o",
|
||||||
"runtime": "nodejs14"
|
"branch": "main",
|
||||||
|
"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": {
|
||||||
|
"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": "ACTIVE"
|
"runtime": "go116",
|
||||||
|
"region": "europe-west1",
|
||||||
|
"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"
|
||||||
},
|
},
|
||||||
"updated_at": "2021-10-08T10:17:34.914Z",
|
"response": {
|
||||||
"timeout": "60s"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
16
function/handler/function.go
Normal file
16
function/handler/function.go
Normal 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{}
|
||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
gitter := git.NewGitter(map[string]string{})
|
gitter := git.NewGitter(map[string]string{})
|
||||||
|
if err := gitter.Checkout(fn.Repo, fn.Branch); err != nil {
|
||||||
for _, branch := range []string{"master", "main"} {
|
|
||||||
err = gitter.Checkout(req.Repo, branch)
|
|
||||||
if err == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if 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)
|
||||||
|
|
||||||
cmd.Dir = filepath.Join(gitter.RepoDir(), req.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)))
|
||||||
|
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.Status = status
|
||||||
|
fn.Updated = time.Now().Format(time.RFC3339Nano)
|
||||||
|
store.Write(store.NewRecord(key, fn))
|
||||||
}()
|
}()
|
||||||
|
|
||||||
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{}
|
if len(fn.Branch) == 0 {
|
||||||
|
fn.Branch = "master"
|
||||||
for _, record := range records {
|
|
||||||
f := new(function.Func)
|
|
||||||
if err := record.Decode(f); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
f.Status = statuses[multitenantPrefix+"-"+f.Name]
|
// set the custom domain
|
||||||
rsp.Functions = append(rsp.Functions, f)
|
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
128
function/handler/reserve.go
Normal 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
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
"name": "function",
|
"name": "function",
|
||||||
"icon": "🔥",
|
"icon": "🔥",
|
||||||
"category": "hosting",
|
"category": "hosting",
|
||||||
"display_name": "Functions"
|
"display_name": "Functions",
|
||||||
|
"pricing": {
|
||||||
|
"Function.Reserve": 10000000
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user