* 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:
Asim Aslam
2021-12-15 11:11:58 +00:00
committed by GitHub
parent 8c6857f21f
commit ff51489278
12 changed files with 2324 additions and 141 deletions

View File

@@ -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
View 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.

View File

@@ -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": {}
}
] ]
} }

View File

@@ -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
View 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
}

View File

@@ -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
View 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

View File

@@ -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 {

File diff suppressed because it is too large Load Diff

View File

@@ -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)
} }

View File

@@ -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;
}

View File

@@ -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
} }
} }