mirror of
https://github.com/kevin-DL/services.git
synced 2026-01-12 03:05:14 +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:
@@ -20,7 +20,9 @@ type App struct{}
|
||||
var (
|
||||
mtx sync.Mutex
|
||||
|
||||
ReservationKey = "reservedApp/"
|
||||
OwnerKey = "app/owner/"
|
||||
ServiceKey = "app/service/"
|
||||
ReservationKey = "app/reservation/"
|
||||
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")
|
||||
}
|
||||
|
||||
// check the owner matches
|
||||
if rsrv.Owner != id {
|
||||
// check the owner matches or if the reservation expired
|
||||
if rsrv.Owner != id && rsrv.Expires.After(time.Now()) {
|
||||
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)
|
||||
} 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
|
||||
rsrv = &Reservation{
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user