mirror of
https://github.com/kevin-DL/services.git
synced 2026-01-11 19:04:35 +00:00
App API (#291)
* app deployment * add source to deployment * support without https prefix * add image * minor comment * fix deploy * further fixes * fix the error output * add account name * set limits * fix error handling * fix app handler * fix unmarshalling * remove http2 * set status * fix bug * . * add ability to specify branch * . * . * add better error handling * add app limit * add function limit * update app readme * log status error * update app to use the store * remove double e limit * switch to created/updated * update app handler * 5 bucks * 10 bucks * unique deployments * fix the sid * unique name handling * allow running where reservation expired * check for reservations * update readme * fix update check * update proto comment * add resolve endpoint * ship with domain * fix url * create unprivileged service account * add service account to update * . * . * update comment * . * update public api json
This commit is contained in:
@@ -2,5 +2,6 @@ Global app deployment
|
|||||||
|
|
||||||
# App Service
|
# App Service
|
||||||
|
|
||||||
Deploy apps and services quickly and easily from a source url or container image.
|
Deploy apps and services quickly and easily from a source url.
|
||||||
Get a globally unique URL ([name].m3o.app) and share with others. Reserve your app name now.
|
|
||||||
|
Run 10 apps for free. Pay to reserve more instances beyond it.
|
||||||
|
|||||||
6
app/config.md
Normal file
6
app/config.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Config
|
||||||
|
|
||||||
|
The app service currently defaults to Google Cloud Run as a provider. In future it will support more.
|
||||||
|
This requires setting up a config key `app` with service accounts and related info. See handler/google.go
|
||||||
|
to understand what is required. Additionally we require a dummy service account with no permissions
|
||||||
|
for the deployed apps.
|
||||||
@@ -17,16 +17,127 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"vote": [
|
"list": [
|
||||||
{
|
{
|
||||||
"title": "Vote for the API",
|
"title": "List the apps",
|
||||||
|
"run_check": false,
|
||||||
|
"request": {},
|
||||||
|
"response": {
|
||||||
|
"services": [{
|
||||||
|
"id": "helloworld",
|
||||||
|
"name": "helloworld",
|
||||||
|
"repo": "github.com/asim/helloworld",
|
||||||
|
"branch": "master",
|
||||||
|
"region": "europe-west1",
|
||||||
|
"port": 8080,
|
||||||
|
"status": "Running",
|
||||||
|
"url": "https://helloworld.m3o.app",
|
||||||
|
"created": "2021-12-15T09:48:50.864124234Z",
|
||||||
|
"updated": "2021-12-15T09:50:13.722008Z",
|
||||||
|
"env_vars": {},
|
||||||
|
"custom_domains": []
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"run": [
|
||||||
|
{
|
||||||
|
"title": "Run an app",
|
||||||
"run_check": false,
|
"run_check": false,
|
||||||
"request": {
|
"request": {
|
||||||
"message": "Launch it!"
|
"name": "helloworld",
|
||||||
|
"repo": "github.com/asim/helloworld",
|
||||||
|
"branch": "master",
|
||||||
|
"port": 8080,
|
||||||
|
"region": "eu-west-1"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
"message": "Thanks for the vote!"
|
"service": {
|
||||||
|
"id": "helloworld",
|
||||||
|
"name": "helloworld",
|
||||||
|
"repo": "github.com/asim/helloworld",
|
||||||
|
"branch": "master",
|
||||||
|
"region": "europe-west1",
|
||||||
|
"port": 8080,
|
||||||
|
"status": "Deploying",
|
||||||
|
"url": "https://helloworld.m3o.app",
|
||||||
|
"created": "2021-12-15T09:48:50.864124234Z",
|
||||||
|
"updated": "",
|
||||||
|
"env_vars": {},
|
||||||
|
"custom_domains": []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"regions": [
|
||||||
|
{
|
||||||
|
"title": "List regions",
|
||||||
|
"run_check": false,
|
||||||
|
"request": {},
|
||||||
|
"response": {
|
||||||
|
"regions": [
|
||||||
|
"europe-west1",
|
||||||
|
"us-east1",
|
||||||
|
"us-west1"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"status": [
|
||||||
|
{
|
||||||
|
"title": "Get the status of an app",
|
||||||
|
"run_check": false,
|
||||||
|
"request": {
|
||||||
|
"name": "helloworld"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"service": {
|
||||||
|
"id": "helloworld",
|
||||||
|
"name": "helloworld",
|
||||||
|
"repo": "github.com/asim/helloworld",
|
||||||
|
"branch": "master",
|
||||||
|
"region": "europe-west1",
|
||||||
|
"port": 8080,
|
||||||
|
"status": "Deploying",
|
||||||
|
"url": "https://helloworld.m3o.app",
|
||||||
|
"created": "2021-12-15T09:48:50.864124234Z",
|
||||||
|
"updated": "",
|
||||||
|
"env_vars": {},
|
||||||
|
"custom_domains": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resolve": [
|
||||||
|
{
|
||||||
|
"title": "Resolve app by id",
|
||||||
|
"run_check": false,
|
||||||
|
"request": {
|
||||||
|
"id": "helloworld"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"url": "https://helloworld-jn5gitv2pa-ew.a.run.app"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"update": [
|
||||||
|
{
|
||||||
|
"title": "Update an app",
|
||||||
|
"run_check": false,
|
||||||
|
"request": {
|
||||||
|
"name": "helloworld"
|
||||||
|
},
|
||||||
|
"response": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"delete": [
|
||||||
|
{
|
||||||
|
"title": "Delete an app",
|
||||||
|
"run_check": false,
|
||||||
|
"request": {
|
||||||
|
"name": "helloworld"
|
||||||
|
},
|
||||||
|
"response": {}
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ type App struct{}
|
|||||||
var (
|
var (
|
||||||
mtx sync.Mutex
|
mtx sync.Mutex
|
||||||
|
|
||||||
ReservationKey = "reservedApp/"
|
OwnerKey = "app/owner/"
|
||||||
|
ServiceKey = "app/service/"
|
||||||
|
ReservationKey = "app/reservation/"
|
||||||
NameFormat = regexp.MustCompilePOSIX("[a-z0-9]+")
|
NameFormat = regexp.MustCompilePOSIX("[a-z0-9]+")
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -83,14 +85,29 @@ func (a *App) Reserve(ctx context.Context, req *pb.ReserveRequest, rsp *pb.Reser
|
|||||||
return errors.BadRequest("app.reserve", "name already reserved")
|
return errors.BadRequest("app.reserve", "name already reserved")
|
||||||
}
|
}
|
||||||
|
|
||||||
// check the owner matches
|
// check the owner matches or if the reservation expired
|
||||||
if rsrv.Owner != id {
|
if rsrv.Owner != id && rsrv.Expires.After(time.Now()) {
|
||||||
return errors.BadRequest("app.reserve", "name already reserved")
|
return errors.BadRequest("app.reserve", "name already reserved")
|
||||||
}
|
}
|
||||||
|
|
||||||
// update the reservation
|
// update the owner
|
||||||
|
rsrv.Owner = id
|
||||||
|
|
||||||
|
// update the reservation expiry
|
||||||
rsrv.Expires = time.Now().AddDate(1, 0, 0)
|
rsrv.Expires = time.Now().AddDate(1, 0, 0)
|
||||||
} else {
|
} else {
|
||||||
|
// check if its already running
|
||||||
|
key := ServiceKey + req.Name
|
||||||
|
recs, err := store.Read(key, store.ReadLimit(1))
|
||||||
|
if err != nil && err != store.ErrNotFound {
|
||||||
|
return errors.InternalServerError("app.reserve", "failed to reserve name")
|
||||||
|
}
|
||||||
|
|
||||||
|
// existing service is running by that name
|
||||||
|
if len(recs) > 0 {
|
||||||
|
return errors.BadRequest("app.reserve", "service already exists")
|
||||||
|
}
|
||||||
|
|
||||||
// not reserved
|
// not reserved
|
||||||
rsrv = &Reservation{
|
rsrv = &Reservation{
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
|
|||||||
732
app/handler/google.go
Normal file
732
app/handler/google.go
Normal file
@@ -0,0 +1,732 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"math/rand"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/micro/micro/v3/service/auth"
|
||||||
|
"github.com/micro/micro/v3/service/config"
|
||||||
|
"github.com/micro/micro/v3/service/context/metadata"
|
||||||
|
"github.com/micro/micro/v3/service/errors"
|
||||||
|
log "github.com/micro/micro/v3/service/logger"
|
||||||
|
"github.com/micro/micro/v3/service/runtime/source/git"
|
||||||
|
"github.com/micro/micro/v3/service/store"
|
||||||
|
pb "github.com/micro/services/app/proto"
|
||||||
|
"github.com/micro/services/pkg/tenant"
|
||||||
|
"github.com/teris-io/shortid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GoogleApp struct {
|
||||||
|
// the associated google project
|
||||||
|
project string
|
||||||
|
// eg. https://us-central1-m3o-apis.cloudfunctions.net/
|
||||||
|
address string
|
||||||
|
// max number of apps per user
|
||||||
|
limit int
|
||||||
|
// custom domain for apps
|
||||||
|
domain string
|
||||||
|
// the service account for the app
|
||||||
|
identity string
|
||||||
|
// Embed the app handler
|
||||||
|
*App
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// hardcoded list of supported regions
|
||||||
|
GoogleRegions = []string{"europe-west1", "us-east1", "us-west1"}
|
||||||
|
|
||||||
|
// hardcoded list of valid repos
|
||||||
|
GitRepos = []string{"github.com", "gitlab.org", "bitbucket.org"}
|
||||||
|
)
|
||||||
|
|
||||||
|
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 New() *GoogleApp {
|
||||||
|
v, err := config.Get("app.service_account_json")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("app.service_account_json: %v", err)
|
||||||
|
}
|
||||||
|
keyfile := v.Bytes()
|
||||||
|
if len(keyfile) == 0 {
|
||||||
|
log.Fatalf("empty keyfile")
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err = config.Get("app.address")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("app.address: %v", err)
|
||||||
|
}
|
||||||
|
address := v.String("")
|
||||||
|
if len(address) == 0 {
|
||||||
|
log.Fatalf("empty address")
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err = config.Get("app.domain")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("app.domain: %v", err)
|
||||||
|
}
|
||||||
|
domain := v.String("")
|
||||||
|
|
||||||
|
v, err = config.Get("app.project")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("app.project: %v", err)
|
||||||
|
}
|
||||||
|
project := v.String("")
|
||||||
|
if len(project) == 0 {
|
||||||
|
log.Fatalf("empty project")
|
||||||
|
}
|
||||||
|
v, err = config.Get("app.limit")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("app.limit: %v", err)
|
||||||
|
}
|
||||||
|
limit := v.Int(0)
|
||||||
|
if limit == 0 {
|
||||||
|
log.Infof("App limit is %d", limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err = config.Get("app.service_account")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("app.service_account: %v", err)
|
||||||
|
}
|
||||||
|
accName := v.String("")
|
||||||
|
|
||||||
|
v, err = config.Get("app.service_identity")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("app.service_identity: %v", err)
|
||||||
|
}
|
||||||
|
identity := v.String("")
|
||||||
|
|
||||||
|
m := map[string]interface{}{}
|
||||||
|
err = json.Unmarshal(keyfile, &m)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("invalid json: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// only root
|
||||||
|
err = ioutil.WriteFile("account.json", keyfile, 0700)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("app.service_account: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DO THIS STEP
|
||||||
|
// https://cloud.google.com/functions/docs/reference/iam/roles#additional-configuration
|
||||||
|
|
||||||
|
// https://cloud.google.com/sdk/docs/authorizing#authorizing_with_a_service_account
|
||||||
|
outp, err := exec.Command("gcloud", "auth", "activate-service-account", accName, "--key-file", "account.json").CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(string(outp), err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
outp, err = exec.Command("gcloud", "auth", "list").CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(string(outp), err.Error())
|
||||||
|
}
|
||||||
|
log.Info(string(outp))
|
||||||
|
|
||||||
|
return &GoogleApp{
|
||||||
|
domain: domain,
|
||||||
|
project: project,
|
||||||
|
address: address,
|
||||||
|
limit: limit,
|
||||||
|
identity: identity,
|
||||||
|
App: new(App),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *GoogleApp) Regions(ctx context.Context, req *pb.RegionsRequest, rsp *pb.RegionsResponse) error {
|
||||||
|
rsp.Regions = GoogleRegions
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *GoogleApp) Run(ctx context.Context, req *pb.RunRequest, rsp *pb.RunResponse) error {
|
||||||
|
log.Info("Received App.Run request")
|
||||||
|
|
||||||
|
if len(req.Name) == 0 {
|
||||||
|
return errors.BadRequest("app.run", "missing name")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Repo) == 0 {
|
||||||
|
return errors.BadRequest("app.run", "missing repo")
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Port <= 0 {
|
||||||
|
req.Port = 8080
|
||||||
|
}
|
||||||
|
|
||||||
|
// validations
|
||||||
|
if !NameFormat.MatchString(req.Name) {
|
||||||
|
return errors.BadRequest("app.run", "invalidate name format")
|
||||||
|
}
|
||||||
|
|
||||||
|
id, ok := tenant.FromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
id = "micro"
|
||||||
|
}
|
||||||
|
|
||||||
|
var reservedApp bool
|
||||||
|
|
||||||
|
// check if the name is reserved by a different owner
|
||||||
|
reservedKey := ReservationKey + req.Name
|
||||||
|
|
||||||
|
recs, err := store.Read(reservedKey, store.ReadLimit(1))
|
||||||
|
if err == nil && len(recs) > 0 {
|
||||||
|
res := new(Reservation)
|
||||||
|
recs[0].Decode(res)
|
||||||
|
if res.Owner != id && res.Expires.After(time.Now()) {
|
||||||
|
return errors.BadRequest("app.run", "name %s is reserved", req.Name)
|
||||||
|
}
|
||||||
|
reservedApp = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var validRepo bool
|
||||||
|
|
||||||
|
// only support github and gitlab
|
||||||
|
for _, repo := range GitRepos {
|
||||||
|
rp := repo
|
||||||
|
|
||||||
|
if strings.HasPrefix(req.Repo, "https://") {
|
||||||
|
rp = "https://" + repo
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(req.Repo, rp) {
|
||||||
|
validRepo = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !validRepo {
|
||||||
|
return errors.BadRequest("app.run", "invalid git repo")
|
||||||
|
}
|
||||||
|
|
||||||
|
var supportedRegion bool
|
||||||
|
|
||||||
|
if len(req.Region) == 0 {
|
||||||
|
// set to default region
|
||||||
|
req.Region = GoogleRegions[0]
|
||||||
|
supportedRegion = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if its in the supported regions
|
||||||
|
for _, reg := range GoogleRegions {
|
||||||
|
if req.Region == reg {
|
||||||
|
supportedRegion = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// unsupported region requested
|
||||||
|
if !supportedRegion {
|
||||||
|
return errors.BadRequest("app.run", "Unsupported region")
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkout the source code
|
||||||
|
if len(req.Branch) == 0 {
|
||||||
|
req.Branch = "master"
|
||||||
|
}
|
||||||
|
|
||||||
|
// look for the existing service
|
||||||
|
key := OwnerKey + id + "/" + req.Name
|
||||||
|
|
||||||
|
// check the owner isn't already running it
|
||||||
|
recs, err = store.Read(key, store.ReadLimit(1))
|
||||||
|
if err == nil && len(recs) > 0 {
|
||||||
|
return errors.BadRequest("app.run", "%s already running", req.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check the global namespace
|
||||||
|
// look for the existing service
|
||||||
|
key = ServiceKey + req.Name
|
||||||
|
|
||||||
|
// set the id
|
||||||
|
appId := req.Name
|
||||||
|
|
||||||
|
// check the owner isn't already running it
|
||||||
|
recs, err = store.Read(key, store.ReadLimit(1))
|
||||||
|
|
||||||
|
// if there's an existing service 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)
|
||||||
|
appId = req.Name + "-" + sid
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for app limit
|
||||||
|
if e.limit > 0 && !reservedApp {
|
||||||
|
ownerKey := OwnerKey + id + "/"
|
||||||
|
recs, err := store.Read(ownerKey, store.ReadPrefix())
|
||||||
|
if err == nil && len(recs) >= e.limit {
|
||||||
|
return errors.BadRequest("app.run", "deployment limit reached")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkout the code
|
||||||
|
gitter := git.NewGitter(map[string]string{})
|
||||||
|
if err := gitter.Checkout(req.Repo, req.Branch); err != nil {
|
||||||
|
log.Errorf("Failed to download %s@%s\n", req.Repo, req.Branch)
|
||||||
|
return errors.InternalServerError("app.run", "Failed to download source")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO validate name and use custom domain name
|
||||||
|
|
||||||
|
// process the env vars to the required format
|
||||||
|
var envVars []string
|
||||||
|
|
||||||
|
for k, v := range req.EnvVars {
|
||||||
|
envVars = append(envVars, k+"="+v)
|
||||||
|
}
|
||||||
|
|
||||||
|
service := &pb.Service{
|
||||||
|
Name: req.Name,
|
||||||
|
Id: appId,
|
||||||
|
Repo: req.Repo,
|
||||||
|
Branch: req.Branch,
|
||||||
|
Region: req.Region,
|
||||||
|
Port: req.Port,
|
||||||
|
Status: "Deploying",
|
||||||
|
EnvVars: req.EnvVars,
|
||||||
|
Created: time.Now().Format(time.RFC3339Nano),
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := []string{
|
||||||
|
// service key
|
||||||
|
ServiceKey + service.Id,
|
||||||
|
// owner key
|
||||||
|
OwnerKey + id + "/" + req.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the keys for the service
|
||||||
|
for _, key := range keys {
|
||||||
|
rec := store.NewRecord(key, service)
|
||||||
|
|
||||||
|
if err := store.Write(rec); err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go func(service *pb.Service) {
|
||||||
|
// generate a unique service account for the app
|
||||||
|
// https://jsoverson.medium.com/how-to-deploy-node-js-functions-to-google-cloud-8bba05e9c10a
|
||||||
|
cmd := exec.Command("gcloud", "--project", e.project, "--format", "json", "run", "deploy", service.Id, "--region", req.Region,
|
||||||
|
"--service-account", e.identity,
|
||||||
|
"--cpu", "1", "--memory", "256Mi", "--port", fmt.Sprintf("%d", req.Port),
|
||||||
|
"--allow-unauthenticated", "--max-instances", "1", "--source", ".",
|
||||||
|
)
|
||||||
|
|
||||||
|
// if env vars exist then set them
|
||||||
|
if len(envVars) > 0 {
|
||||||
|
cmd.Args = append(cmd.Args, "--set-env-vars", strings.Join(envVars, ","))
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the command dir
|
||||||
|
cmd.Dir = gitter.RepoDir()
|
||||||
|
|
||||||
|
// execute the command
|
||||||
|
outp, err := cmd.CombinedOutput()
|
||||||
|
|
||||||
|
// by this point the context may have been cancelled
|
||||||
|
acc, _ := auth.AccountFromContext(ctx)
|
||||||
|
md, _ := metadata.FromContext(ctx)
|
||||||
|
|
||||||
|
ctx = metadata.NewContext(context.Background(), md)
|
||||||
|
ctx = auth.ContextWithAccount(ctx, acc)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
// populate the app status
|
||||||
|
err = e.Status(ctx, &pb.StatusRequest{Name: req.Name}, &pb.StatusResponse{})
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
errString := string(outp)
|
||||||
|
|
||||||
|
log.Error(fmt.Errorf(errString))
|
||||||
|
|
||||||
|
// set the error status
|
||||||
|
service.Status = "DeploymentError"
|
||||||
|
|
||||||
|
if strings.Contains(errString, "Failed to start and then listen on the port defined by the PORT environment variable") {
|
||||||
|
service.Status += ": Failed to start and listen on port " + fmt.Sprintf("%d", req.Port)
|
||||||
|
} else if strings.Contains(errString, "Build failed") {
|
||||||
|
service.Status += ": Build failed"
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := []string{
|
||||||
|
// service key
|
||||||
|
ServiceKey + service.Id,
|
||||||
|
// owner key
|
||||||
|
OwnerKey + id + "/" + req.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the keys for the service
|
||||||
|
for _, key := range keys {
|
||||||
|
rec := store.NewRecord(key, service)
|
||||||
|
|
||||||
|
if err := store.Write(rec); err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(service)
|
||||||
|
|
||||||
|
// set the service in the response
|
||||||
|
rsp.Service = service
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *GoogleApp) Update(ctx context.Context, req *pb.UpdateRequest, rsp *pb.UpdateResponse) error {
|
||||||
|
log.Info("Received App.Update request")
|
||||||
|
|
||||||
|
if len(req.Name) == 0 {
|
||||||
|
return errors.BadRequest("app.update", "missing name")
|
||||||
|
}
|
||||||
|
|
||||||
|
// validations
|
||||||
|
if !NameFormat.MatchString(req.Name) {
|
||||||
|
return errors.BadRequest("app.run", "invalidate name format")
|
||||||
|
}
|
||||||
|
|
||||||
|
id, ok := tenant.FromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
id = "micro"
|
||||||
|
}
|
||||||
|
|
||||||
|
key := OwnerKey + id + "/" + req.Name
|
||||||
|
|
||||||
|
// look for the existing service
|
||||||
|
recs, err := store.Read(key, store.ReadLimit(1))
|
||||||
|
if err != nil && err == store.ErrNotFound {
|
||||||
|
return errors.BadRequest("app.update", "%s does not exist", req.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(recs) == 0 {
|
||||||
|
return errors.BadRequest("app.update", "%s does not exist", req.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
srv := new(pb.Service)
|
||||||
|
|
||||||
|
if err := recs[0].Decode(srv); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkout the code
|
||||||
|
gitter := git.NewGitter(map[string]string{})
|
||||||
|
if err := gitter.Checkout(srv.Repo, srv.Branch); err != nil {
|
||||||
|
log.Errorf("Failed to download %s@%s\n", srv.Repo, srv.Branch)
|
||||||
|
return errors.InternalServerError("app.run", "Failed to download source")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO validate name and use custom domain name
|
||||||
|
|
||||||
|
// process the env vars to the required format
|
||||||
|
var envVars []string
|
||||||
|
|
||||||
|
for k, v := range srv.EnvVars {
|
||||||
|
envVars = append(envVars, k+"="+v)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func(service *pb.Service) {
|
||||||
|
// https://jsoverson.medium.com/how-to-deploy-node-js-functions-to-google-cloud-8bba05e9c10a
|
||||||
|
cmd := exec.Command("gcloud", "--project", e.project, "--format", "json", "run", "deploy", service.Id, "--region", service.Region,
|
||||||
|
"--service-account", e.identity,
|
||||||
|
"--cpu", "1", "--memory", "256Mi", "--port", fmt.Sprintf("%d", service.Port),
|
||||||
|
"--allow-unauthenticated", "--max-instances", "1", "--source", ".",
|
||||||
|
)
|
||||||
|
|
||||||
|
// if env vars exist then set them
|
||||||
|
if len(envVars) > 0 {
|
||||||
|
cmd.Args = append(cmd.Args, "--set-env-vars", strings.Join(envVars, ","))
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the command dir
|
||||||
|
cmd.Dir = gitter.RepoDir()
|
||||||
|
|
||||||
|
// execute the command
|
||||||
|
outp, err := cmd.CombinedOutput()
|
||||||
|
|
||||||
|
// by this point the context may have been cancelled
|
||||||
|
acc, _ := auth.AccountFromContext(ctx)
|
||||||
|
md, _ := metadata.FromContext(ctx)
|
||||||
|
|
||||||
|
ctx = metadata.NewContext(context.Background(), md)
|
||||||
|
ctx = auth.ContextWithAccount(ctx, acc)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
// populate the app status
|
||||||
|
err = e.Status(ctx, &pb.StatusRequest{Name: service.Name}, &pb.StatusResponse{})
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
errString := string(outp)
|
||||||
|
|
||||||
|
log.Error(fmt.Errorf(errString))
|
||||||
|
|
||||||
|
// set the error status
|
||||||
|
service.Status = "DeploymentError"
|
||||||
|
|
||||||
|
if strings.Contains(errString, "Failed to start and then listen on the port defined by the PORT environment variable") {
|
||||||
|
service.Status += ": Failed to start and listen on port " + fmt.Sprintf("%d", service.Port)
|
||||||
|
} else if strings.Contains(errString, "Build failed") {
|
||||||
|
service.Status += ": Build failed"
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := []string{
|
||||||
|
// service key
|
||||||
|
ServiceKey + service.Id,
|
||||||
|
// owner key
|
||||||
|
OwnerKey + id + "/" + req.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the keys for the service
|
||||||
|
for _, key := range keys {
|
||||||
|
rec := store.NewRecord(key, service)
|
||||||
|
|
||||||
|
if err := store.Write(rec); err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(srv)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
func (e *GoogleApp) Delete(ctx context.Context, req *pb.DeleteRequest, rsp *pb.DeleteResponse) error {
|
||||||
|
log.Info("Received App.Delete request")
|
||||||
|
|
||||||
|
if len(req.Name) == 0 {
|
||||||
|
return errors.BadRequest("app.delete", "missing name")
|
||||||
|
}
|
||||||
|
|
||||||
|
id, ok := tenant.FromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
id = "micro"
|
||||||
|
}
|
||||||
|
|
||||||
|
// read the app for the owner
|
||||||
|
key := OwnerKey + id + "/" + req.Name
|
||||||
|
recs, err := store.Read(key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// not running
|
||||||
|
if len(recs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// decode the service
|
||||||
|
srv := new(pb.Service)
|
||||||
|
|
||||||
|
if err := recs[0].Decode(srv); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// execute the delete async
|
||||||
|
go func(srv *pb.Service) {
|
||||||
|
cmd := exec.Command("gcloud", "--quiet", "--project", e.project, "run", "services", "delete", "--region", srv.Region, srv.Id)
|
||||||
|
outp, err := cmd.CombinedOutput()
|
||||||
|
if err != nil && !strings.Contains(string(outp), "could not be found") {
|
||||||
|
log.Error(fmt.Errorf(string(outp)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete from the db
|
||||||
|
keys := []string{
|
||||||
|
// service key
|
||||||
|
ServiceKey + srv.Id,
|
||||||
|
// owner key
|
||||||
|
OwnerKey + id + "/" + req.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete the keys for the service
|
||||||
|
for _, key := range keys {
|
||||||
|
if err := store.Delete(key); err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}(srv)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *GoogleApp) List(ctx context.Context, req *pb.ListRequest, rsp *pb.ListResponse) error {
|
||||||
|
log.Info("Received App.List request")
|
||||||
|
|
||||||
|
id, ok := tenant.FromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
id = "micro"
|
||||||
|
}
|
||||||
|
|
||||||
|
key := OwnerKey + id + "/"
|
||||||
|
|
||||||
|
recs, err := store.Read(key, store.ReadPrefix())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rsp.Services = []*pb.Service{}
|
||||||
|
|
||||||
|
for _, rec := range recs {
|
||||||
|
srv := new(pb.Service)
|
||||||
|
if err := rec.Decode(srv); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the custom domain
|
||||||
|
if len(e.domain) > 0 {
|
||||||
|
srv.Url = fmt.Sprintf("https://%s.%s", srv.Id, e.domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
rsp.Services = append(rsp.Services, srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *GoogleApp) Status(ctx context.Context, req *pb.StatusRequest, rsp *pb.StatusResponse) error {
|
||||||
|
id, ok := tenant.FromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
id = "micro"
|
||||||
|
}
|
||||||
|
|
||||||
|
key := OwnerKey + id + "/" + req.Name
|
||||||
|
|
||||||
|
recs, err := store.Read(key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(recs) == 0 {
|
||||||
|
return errors.NotFound("app.status", "app not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
srv := new(pb.Service)
|
||||||
|
if err := recs[0].Decode(srv); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the current app status
|
||||||
|
cmd := exec.Command("gcloud", "--project", e.project, "--format", "json", "run", "services", "describe", "--region", srv.Region, srv.Id)
|
||||||
|
outp, err := cmd.CombinedOutput()
|
||||||
|
if err != nil && srv.Status == "Deploying" {
|
||||||
|
log.Error(fmt.Errorf(string(outp)))
|
||||||
|
rsp.Service = srv
|
||||||
|
return nil
|
||||||
|
} else if err != nil {
|
||||||
|
log.Error(fmt.Errorf(string(outp)))
|
||||||
|
return errors.BadRequest("app.status", "service does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
var output map[string]interface{}
|
||||||
|
if err = json.Unmarshal(outp, &output); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
currentStatus := srv.Status
|
||||||
|
currentUrl := srv.Url
|
||||||
|
updatedAt := srv.Updated
|
||||||
|
|
||||||
|
// get the service status
|
||||||
|
status := output["status"].(map[string]interface{})
|
||||||
|
deployed := status["conditions"].([]interface{})[0].(map[string]interface{})
|
||||||
|
srv.Updated = deployed["lastTransitionTime"].(string)
|
||||||
|
|
||||||
|
switch deployed["status"].(string) {
|
||||||
|
case "True":
|
||||||
|
srv.Status = "Running"
|
||||||
|
srv.Url = status["url"].(string)
|
||||||
|
default:
|
||||||
|
srv.Status = deployed["status"].(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// set response
|
||||||
|
rsp.Service = srv
|
||||||
|
|
||||||
|
// no change in status and we have a pre-existing url
|
||||||
|
if srv.Status == currentStatus && srv.Url == currentUrl && srv.Updated == updatedAt {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// update built in status
|
||||||
|
// delete from the db
|
||||||
|
keys := []string{
|
||||||
|
// global key
|
||||||
|
ServiceKey + srv.Id,
|
||||||
|
// owner key
|
||||||
|
OwnerKey + id + "/" + req.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete the keys for the service
|
||||||
|
for _, key := range keys {
|
||||||
|
rec := store.NewRecord(key, srv)
|
||||||
|
// write the app to the db
|
||||||
|
if err := store.Write(rec); err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the custom domain
|
||||||
|
if len(e.domain) > 0 {
|
||||||
|
rsp.Service.Url = fmt.Sprintf("https://%s.%s", srv.Id, e.domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) Resolve(ctx context.Context, req *pb.ResolveRequest, rsp *pb.ResolveResponse) error {
|
||||||
|
if len(req.Id) == 0 {
|
||||||
|
return errors.BadRequest("app.resolve", "missing id")
|
||||||
|
}
|
||||||
|
|
||||||
|
key := ServiceKey + req.Id
|
||||||
|
|
||||||
|
recs, err := store.Read(key, store.ReadLimit(1))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(recs) == 0 {
|
||||||
|
return errors.BadRequest("app.resolve", "app does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
srv := new(pb.Service)
|
||||||
|
recs[0].Decode(srv)
|
||||||
|
|
||||||
|
rsp.Url = srv.Url
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
|
|
||||||
"github.com/micro/services/pkg/tenant"
|
|
||||||
"github.com/micro/micro/v3/service/store"
|
|
||||||
pb "github.com/micro/services/app/proto"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
voteKey = "votes/"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Vote struct {
|
|
||||||
Id string `json:"id"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
VotedAt time.Time `json:"voted_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *App) Vote(ctx context.Context, req *pb.VoteRequest, rsp *pb.VoteResponse) error {
|
|
||||||
mtx.Lock()
|
|
||||||
defer mtx.Unlock()
|
|
||||||
|
|
||||||
id, ok := tenant.FromContext(ctx)
|
|
||||||
if !ok {
|
|
||||||
id = "micro"
|
|
||||||
}
|
|
||||||
|
|
||||||
rec := store.NewRecord(voteKey + id, &Vote{
|
|
||||||
Id: id,
|
|
||||||
Message: req.Message,
|
|
||||||
VotedAt: time.Now(),
|
|
||||||
})
|
|
||||||
|
|
||||||
// we don't need to check the error
|
|
||||||
store.Write(rec)
|
|
||||||
|
|
||||||
rsp.Message = "Thanks for the vote!"
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
24
app/image/Dockerfile
Normal file
24
app/image/Dockerfile
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
FROM micro/cells:v3 as builder
|
||||||
|
|
||||||
|
RUN apk add curl
|
||||||
|
|
||||||
|
# Install python/pip
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
RUN apk add --update --no-cache python3 && ln -sf python3 /usr/bin/python
|
||||||
|
RUN python3 -m ensurepip
|
||||||
|
RUN pip3 install --no-cache --upgrade pip setuptools
|
||||||
|
|
||||||
|
# https://stackoverflow.com/questions/28372328/how-to-install-the-google-cloud-sdk-in-a-docker-image
|
||||||
|
|
||||||
|
# Downloading gcloud package
|
||||||
|
RUN curl https://dl.google.com/dl/cloudsdk/release/google-cloud-sdk.tar.gz > /tmp/google-cloud-sdk.tar.gz
|
||||||
|
|
||||||
|
# Installing the package
|
||||||
|
RUN mkdir -p /usr/local/gcloud \
|
||||||
|
&& tar -C /usr/local/gcloud -xvf /tmp/google-cloud-sdk.tar.gz \
|
||||||
|
&& /usr/local/gcloud/google-cloud-sdk/install.sh
|
||||||
|
|
||||||
|
# Adding the package path to local
|
||||||
|
ENV PATH $PATH:/usr/local/gcloud/google-cloud-sdk/bin
|
||||||
|
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ func main() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Register handler
|
// Register handler
|
||||||
pb.RegisterAppHandler(srv.Server(), new(handler.App))
|
pb.RegisterAppHandler(srv.Server(), handler.New())
|
||||||
|
|
||||||
// Run service
|
// Run service
|
||||||
if err := srv.Run(); err != nil {
|
if err := srv.Run(); err != nil {
|
||||||
|
|||||||
1265
app/proto/app.pb.go
1265
app/proto/app.pb.go
File diff suppressed because it is too large
Load Diff
@@ -43,7 +43,13 @@ func NewAppEndpoints() []*api.Endpoint {
|
|||||||
|
|
||||||
type AppService interface {
|
type AppService interface {
|
||||||
Reserve(ctx context.Context, in *ReserveRequest, opts ...client.CallOption) (*ReserveResponse, error)
|
Reserve(ctx context.Context, in *ReserveRequest, opts ...client.CallOption) (*ReserveResponse, error)
|
||||||
Vote(ctx context.Context, in *VoteRequest, opts ...client.CallOption) (*VoteResponse, error)
|
Regions(ctx context.Context, in *RegionsRequest, opts ...client.CallOption) (*RegionsResponse, error)
|
||||||
|
Run(ctx context.Context, in *RunRequest, opts ...client.CallOption) (*RunResponse, error)
|
||||||
|
Update(ctx context.Context, in *UpdateRequest, opts ...client.CallOption) (*UpdateResponse, error)
|
||||||
|
Delete(ctx context.Context, in *DeleteRequest, opts ...client.CallOption) (*DeleteResponse, error)
|
||||||
|
Status(ctx context.Context, in *StatusRequest, opts ...client.CallOption) (*StatusResponse, error)
|
||||||
|
List(ctx context.Context, in *ListRequest, opts ...client.CallOption) (*ListResponse, error)
|
||||||
|
Resolve(ctx context.Context, in *ResolveRequest, opts ...client.CallOption) (*ResolveResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type appService struct {
|
type appService struct {
|
||||||
@@ -68,9 +74,69 @@ func (c *appService) Reserve(ctx context.Context, in *ReserveRequest, opts ...cl
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *appService) Vote(ctx context.Context, in *VoteRequest, opts ...client.CallOption) (*VoteResponse, error) {
|
func (c *appService) Regions(ctx context.Context, in *RegionsRequest, opts ...client.CallOption) (*RegionsResponse, error) {
|
||||||
req := c.c.NewRequest(c.name, "App.Vote", in)
|
req := c.c.NewRequest(c.name, "App.Regions", in)
|
||||||
out := new(VoteResponse)
|
out := new(RegionsResponse)
|
||||||
|
err := c.c.Call(ctx, req, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *appService) Run(ctx context.Context, in *RunRequest, opts ...client.CallOption) (*RunResponse, error) {
|
||||||
|
req := c.c.NewRequest(c.name, "App.Run", in)
|
||||||
|
out := new(RunResponse)
|
||||||
|
err := c.c.Call(ctx, req, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *appService) Update(ctx context.Context, in *UpdateRequest, opts ...client.CallOption) (*UpdateResponse, error) {
|
||||||
|
req := c.c.NewRequest(c.name, "App.Update", in)
|
||||||
|
out := new(UpdateResponse)
|
||||||
|
err := c.c.Call(ctx, req, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *appService) Delete(ctx context.Context, in *DeleteRequest, opts ...client.CallOption) (*DeleteResponse, error) {
|
||||||
|
req := c.c.NewRequest(c.name, "App.Delete", in)
|
||||||
|
out := new(DeleteResponse)
|
||||||
|
err := c.c.Call(ctx, req, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *appService) Status(ctx context.Context, in *StatusRequest, opts ...client.CallOption) (*StatusResponse, error) {
|
||||||
|
req := c.c.NewRequest(c.name, "App.Status", in)
|
||||||
|
out := new(StatusResponse)
|
||||||
|
err := c.c.Call(ctx, req, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *appService) List(ctx context.Context, in *ListRequest, opts ...client.CallOption) (*ListResponse, error) {
|
||||||
|
req := c.c.NewRequest(c.name, "App.List", in)
|
||||||
|
out := new(ListResponse)
|
||||||
|
err := c.c.Call(ctx, req, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *appService) Resolve(ctx context.Context, in *ResolveRequest, opts ...client.CallOption) (*ResolveResponse, error) {
|
||||||
|
req := c.c.NewRequest(c.name, "App.Resolve", in)
|
||||||
|
out := new(ResolveResponse)
|
||||||
err := c.c.Call(ctx, req, out, opts...)
|
err := c.c.Call(ctx, req, out, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -82,13 +148,25 @@ func (c *appService) Vote(ctx context.Context, in *VoteRequest, opts ...client.C
|
|||||||
|
|
||||||
type AppHandler interface {
|
type AppHandler interface {
|
||||||
Reserve(context.Context, *ReserveRequest, *ReserveResponse) error
|
Reserve(context.Context, *ReserveRequest, *ReserveResponse) error
|
||||||
Vote(context.Context, *VoteRequest, *VoteResponse) error
|
Regions(context.Context, *RegionsRequest, *RegionsResponse) error
|
||||||
|
Run(context.Context, *RunRequest, *RunResponse) error
|
||||||
|
Update(context.Context, *UpdateRequest, *UpdateResponse) error
|
||||||
|
Delete(context.Context, *DeleteRequest, *DeleteResponse) error
|
||||||
|
Status(context.Context, *StatusRequest, *StatusResponse) error
|
||||||
|
List(context.Context, *ListRequest, *ListResponse) error
|
||||||
|
Resolve(context.Context, *ResolveRequest, *ResolveResponse) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func RegisterAppHandler(s server.Server, hdlr AppHandler, opts ...server.HandlerOption) error {
|
func RegisterAppHandler(s server.Server, hdlr AppHandler, opts ...server.HandlerOption) error {
|
||||||
type app interface {
|
type app interface {
|
||||||
Reserve(ctx context.Context, in *ReserveRequest, out *ReserveResponse) error
|
Reserve(ctx context.Context, in *ReserveRequest, out *ReserveResponse) error
|
||||||
Vote(ctx context.Context, in *VoteRequest, out *VoteResponse) error
|
Regions(ctx context.Context, in *RegionsRequest, out *RegionsResponse) error
|
||||||
|
Run(ctx context.Context, in *RunRequest, out *RunResponse) error
|
||||||
|
Update(ctx context.Context, in *UpdateRequest, out *UpdateResponse) error
|
||||||
|
Delete(ctx context.Context, in *DeleteRequest, out *DeleteResponse) error
|
||||||
|
Status(ctx context.Context, in *StatusRequest, out *StatusResponse) error
|
||||||
|
List(ctx context.Context, in *ListRequest, out *ListResponse) error
|
||||||
|
Resolve(ctx context.Context, in *ResolveRequest, out *ResolveResponse) error
|
||||||
}
|
}
|
||||||
type App struct {
|
type App struct {
|
||||||
app
|
app
|
||||||
@@ -105,6 +183,30 @@ func (h *appHandler) Reserve(ctx context.Context, in *ReserveRequest, out *Reser
|
|||||||
return h.AppHandler.Reserve(ctx, in, out)
|
return h.AppHandler.Reserve(ctx, in, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *appHandler) Vote(ctx context.Context, in *VoteRequest, out *VoteResponse) error {
|
func (h *appHandler) Regions(ctx context.Context, in *RegionsRequest, out *RegionsResponse) error {
|
||||||
return h.AppHandler.Vote(ctx, in, out)
|
return h.AppHandler.Regions(ctx, in, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *appHandler) Run(ctx context.Context, in *RunRequest, out *RunResponse) error {
|
||||||
|
return h.AppHandler.Run(ctx, in, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *appHandler) Update(ctx context.Context, in *UpdateRequest, out *UpdateResponse) error {
|
||||||
|
return h.AppHandler.Update(ctx, in, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *appHandler) Delete(ctx context.Context, in *DeleteRequest, out *DeleteResponse) error {
|
||||||
|
return h.AppHandler.Delete(ctx, in, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *appHandler) Status(ctx context.Context, in *StatusRequest, out *StatusResponse) error {
|
||||||
|
return h.AppHandler.Status(ctx, in, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *appHandler) List(ctx context.Context, in *ListRequest, out *ListResponse) error {
|
||||||
|
return h.AppHandler.List(ctx, in, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *appHandler) Resolve(ctx context.Context, in *ResolveRequest, out *ResolveResponse) error {
|
||||||
|
return h.AppHandler.Resolve(ctx, in, out)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,20 +6,98 @@ option go_package = "./proto;app";
|
|||||||
|
|
||||||
service App {
|
service App {
|
||||||
rpc Reserve(ReserveRequest) returns (ReserveResponse) {}
|
rpc Reserve(ReserveRequest) returns (ReserveResponse) {}
|
||||||
rpc Vote(VoteRequest) returns (VoteResponse) {}
|
rpc Regions(RegionsRequest) returns (RegionsResponse) {}
|
||||||
|
rpc Run(RunRequest) returns (RunResponse) {}
|
||||||
|
rpc Update(UpdateRequest) returns (UpdateResponse) {}
|
||||||
|
rpc Delete(DeleteRequest) returns (DeleteResponse) {}
|
||||||
|
rpc Status(StatusRequest) returns (StatusResponse) {}
|
||||||
|
rpc List(ListRequest) returns (ListResponse) {}
|
||||||
|
rpc Resolve(ResolveRequest) returns (ResolveResponse) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vote to have the App api launched faster!
|
message Service {
|
||||||
message VoteRequest {
|
// unique id
|
||||||
// optional message
|
string id = 1;
|
||||||
string message = 1;
|
// name of the app
|
||||||
|
string name = 2;
|
||||||
|
// source repository
|
||||||
|
string repo = 3;
|
||||||
|
// branch of code
|
||||||
|
string branch = 4;
|
||||||
|
// region running in
|
||||||
|
string region = 5;
|
||||||
|
// port running on
|
||||||
|
int32 port = 6;
|
||||||
|
// status of the app
|
||||||
|
string status = 7;
|
||||||
|
// app url
|
||||||
|
string url = 8;
|
||||||
|
// time of creation
|
||||||
|
string created = 9;
|
||||||
|
// last updated
|
||||||
|
string updated = 10;
|
||||||
|
// associated env vars
|
||||||
|
map<string,string> env_vars = 11;
|
||||||
|
// custom domains
|
||||||
|
repeated string custom_domains = 12;
|
||||||
}
|
}
|
||||||
|
|
||||||
message VoteResponse {
|
// Delete an app
|
||||||
// response message
|
message DeleteRequest {
|
||||||
string message = 2;
|
// name of the app
|
||||||
|
string name = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message DeleteResponse {}
|
||||||
|
|
||||||
|
// List all the apps
|
||||||
|
message ListRequest {
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListResponse {
|
||||||
|
// all the apps
|
||||||
|
repeated Service services = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run an app
|
||||||
|
message RunRequest {
|
||||||
|
// name of the app
|
||||||
|
string name = 1;
|
||||||
|
// source repository
|
||||||
|
string repo = 2;
|
||||||
|
// branch. defaults to master
|
||||||
|
string branch = 3;
|
||||||
|
// region to run in
|
||||||
|
string region = 4;
|
||||||
|
// port to run on
|
||||||
|
int32 port = 5;
|
||||||
|
// associatede env vars to pass in
|
||||||
|
map<string,string> env_vars = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RunResponse {
|
||||||
|
// The running service
|
||||||
|
Service service = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the app
|
||||||
|
message UpdateRequest {
|
||||||
|
// name of the app
|
||||||
|
string name = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdateResponse {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Return the support regions
|
||||||
|
message RegionsRequest {}
|
||||||
|
|
||||||
|
message RegionsResponse {
|
||||||
|
repeated string regions = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reservation represents a reserved app instance
|
||||||
message Reservation {
|
message Reservation {
|
||||||
// name of the app
|
// name of the app
|
||||||
string name = 1;
|
string name = 1;
|
||||||
@@ -33,12 +111,35 @@ message Reservation {
|
|||||||
string expires = 5;
|
string expires = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reserve your app name
|
// Reserve apps beyond the free quota. Call Run after.
|
||||||
message ReserveRequest {
|
message ReserveRequest {
|
||||||
// name of your app e.g helloworld
|
// name of your app e.g helloworld
|
||||||
string name = 1;
|
string name = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ReserveResponse {
|
message ReserveResponse {
|
||||||
|
// The app reservation
|
||||||
Reservation reservation = 1;
|
Reservation reservation = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the status of an app
|
||||||
|
message StatusRequest {
|
||||||
|
// name of the app
|
||||||
|
string name = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message StatusResponse {
|
||||||
|
// running service info
|
||||||
|
Service service = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve an app by id to its raw backend endpoint
|
||||||
|
message ResolveRequest {
|
||||||
|
// the service id
|
||||||
|
string id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ResolveResponse {
|
||||||
|
// the end provider url
|
||||||
|
string url = 1;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "app",
|
"name": "app",
|
||||||
"icon": "☁️",
|
"icon": "☁️",
|
||||||
"category": "coming soon",
|
"category": "hosting",
|
||||||
"display_name": "Apps (Coming Soon)",
|
"display_name": "Apps",
|
||||||
"pricing": {
|
"pricing": {
|
||||||
"App.Reserve": 1000000
|
"App.Reserve": 10000000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user