package handler import ( "bytes" "context" "encoding/json" "fmt" "io/ioutil" "net/http" "os/exec" "path/filepath" "strings" "github.com/micro/micro/v3/service/config" log "github.com/micro/micro/v3/service/logger" "gopkg.in/yaml.v2" "github.com/micro/micro/v3/service/runtime/source/git" _struct "github.com/golang/protobuf/ptypes/struct" db "github.com/micro/services/db/proto" function "github.com/micro/services/function/proto" "github.com/micro/services/pkg/tenant" ) type Function struct { project string // eg. https://us-central1-m3o-apis.cloudfunctions.net/ address string db db.DbService } type Func struct { Name string `json:"name"` Tenant string `json:"tenant"` Project string `json:"project"` } func NewFunction(db db.DbService) *Function { v, err := config.Get("function.service_account_json") if err != nil { log.Fatalf("function.service_account_json: %v", err) } keyfile := v.Bytes() if len(keyfile) == 0 { log.Fatalf("empty keyfile") } v, err = config.Get("function.address") if err != nil { log.Fatalf("function.address: %v", err) } address := v.String("") if len(address) == 0 { log.Fatalf("empty address") } v, err = config.Get("function.project") if err != nil { log.Fatalf("function.project: %v", err) } project := v.String("") if len(project) == 0 { log.Fatalf("empty project") } v, err = config.Get("function.service_account") if err != nil { log.Fatalf("function.service_account: %v", err) } accName := 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("/acc.json", keyfile, 0700) if err != nil { log.Fatalf("function.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", "/acc.json").CombinedOutput() if err != nil { log.Fatalf(string(outp)) } outp, err = exec.Command("gcloud", "auth", "list").CombinedOutput() if err != nil { log.Fatalf(string(outp)) } log.Info(string(outp)) return &Function{project: project, address: address, db: db} } func (e *Function) Deploy(ctx context.Context, req *function.DeployRequest, rsp *function.DeployResponse) error { log.Info("Received Function.Deploy request") gitter := git.NewGitter(map[string]string{}) err := gitter.Checkout(req.Repo, "master") if err != nil { return err } tenantId, ok := tenant.FromContext(ctx) if !ok { tenantId = "micro" } multitenantPrefix := strings.Replace(tenantId, "/", "-", -1) if req.Entrypoint == "" { req.Entrypoint = req.Name } project := req.Project if project == "" { project = "default" } readRsp, err := e.db.Read(ctx, &db.ReadRequest{ Table: "functions", Query: fmt.Sprintf("tenantId == '%v' and project == '%v' and name == '%v'", tenantId, project, req.Name), }) if err != nil { return err } if req.Runtime == "" { return fmt.Errorf("missing runtime field, please specify nodejs14, go116 etc") } // process the env vars to the required format var envVars []string for k, v := range req.EnvVars { envVars = append(envVars, k+"="+v) } go func() { // https://jsoverson.medium.com/how-to-deploy-node-js-functions-to-google-cloud-8bba05e9c10a cmd := exec.Command("gcloud", "functions", "deploy", multitenantPrefix+"-"+req.Name, "--region", "europe-west1", "--allow-unauthenticated", "--entry-point", req.Entrypoint, "--trigger-http", "--project", e.project, "--runtime", req.Runtime) // if env vars exist then set them if len(envVars) > 0 { cmd.Args = append(cmd.Args, "--set-env-vars", strings.Join(envVars, ",")) } cmd.Dir = filepath.Join(gitter.RepoDir(), req.Subfolder) outp, err := cmd.CombinedOutput() if err != nil { log.Error(fmt.Errorf(string(outp))) } }() s := &_struct.Struct{} id := fmt.Sprintf("%v-%v-%v", tenantId, project, req.Name) jso, _ := json.Marshal(map[string]interface{}{ "id": id, "project": project, "name": req.Name, "tenantId": tenantId, "repo": req.Repo, "subfolder": req.Subfolder, "entrypoint": req.Entrypoint, "runtime": req.Runtime, }) err = s.UnmarshalJSON(jso) if err != nil { return err } if len(readRsp.Records) > 0 { _, err = e.db.Update(ctx, &db.UpdateRequest{ Table: "functions", Record: s, Id: id, }) if err != nil { log.Error(err) } return err } _, err = e.db.Create(ctx, &db.CreateRequest{ Table: "functions", Record: s, }) if err != nil { log.Error(err) } return err } func (e *Function) Call(ctx context.Context, req *function.CallRequest, rsp *function.CallResponse) error { log.Info("Received Function.Call request") tenantId, ok := tenant.FromContext(ctx) if !ok { tenantId = "micro" } multitenantPrefix := strings.Replace(tenantId, "/", "-", -1) url := e.address + multitenantPrefix + "-" + req.Name fmt.Println("URL:>", url) js, _ := json.Marshal(req.Request) if req.Request == nil || len(req.Request.Fields) == 0 { js = []byte("{}") } r, err := http.NewRequest("POST", url, bytes.NewBuffer(js)) if err != nil { return err } r.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(r) if err != nil { log.Errorf("error making request %v", err) return err } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { log.Errorf("error reading body %v", string(body)) return err } err = json.Unmarshal(body, &rsp.Response) if err != nil { log.Errorf("error unmarshaling %v", string(body)) return err } return nil } func (e *Function) Delete(ctx context.Context, req *function.DeleteRequest, rsp *function.DeleteResponse) error { log.Info("Received Function.Delete request") tenantId, ok := tenant.FromContext(ctx) if !ok { tenantId = "micro" } multitenantPrefix := strings.Replace(tenantId, "/", "-", -1) project := req.Project if project == "" { project = "default" } cmd := exec.Command("gcloud", "functions", "delete", "--project", e.project, "--region", "europe-west1", multitenantPrefix+"-"+req.Name) outp, err := cmd.CombinedOutput() if err != nil { log.Error(fmt.Errorf(string(outp))) return err } id := fmt.Sprintf("%v-%v-%v", tenantId, project, req.Name) _, err = e.db.Delete(ctx, &db.DeleteRequest{ Table: "functions", Id: id, }) return err } func (e *Function) List(ctx context.Context, req *function.ListRequest, rsp *function.ListResponse) error { log.Info("Received Function.List request") tenantId, ok := tenant.FromContext(ctx) if !ok { tenantId = "micro" } project := req.Project q := fmt.Sprintf(`tenantId == "%v"`, tenantId) if project != "" { q += fmt.Sprintf(` and project == "%v"`, project) } log.Infof("Making query %v", q) readRsp, err := e.db.Read(ctx, &db.ReadRequest{ Table: "functions", Query: q, }) if err != nil { return err } log.Info(readRsp.Records) multitenantPrefix := strings.Replace(tenantId, "/", "-", -1) cmd := exec.Command("gcloud", "functions", "list", "--project", e.project, "--filter", "name~"+multitenantPrefix+"*") outp, err := cmd.CombinedOutput() if err != nil { log.Error(fmt.Errorf(string(outp))) } lines := strings.Split(string(outp), "\n") statuses := map[string]string{} for _, line := range lines { fields := strings.Fields(line) if len(fields) < 2 { continue } statuses[fields[0]] = fields[1] } rsp.Functions = []*function.Func{} for _, record := range readRsp.Records { m := record.AsMap() bs, _ := json.Marshal(m) f := &function.Func{} err = json.Unmarshal(bs, f) if err != nil { return err } f.Status = statuses[multitenantPrefix+"-"+f.Name] rsp.Functions = append(rsp.Functions, f) } return nil } func (e *Function) Describe(ctx context.Context, req *function.DescribeRequest, rsp *function.DescribeResponse) error { tenantId, ok := tenant.FromContext(ctx) if !ok { tenantId = "micro" } project := req.Project if project == "" { project = "default" } multitenantPrefix := strings.Replace(tenantId, "/", "-", -1) id := fmt.Sprintf("%v-%v-%v", tenantId, project, req.Name) readRsp, err := e.db.Read(ctx, &db.ReadRequest{ Table: "functions", Id: id, }) if err != nil { return err } cmd := exec.Command("gcloud", "functions", "describe", "--region", "europe-west1", "--project", e.project, multitenantPrefix+"-"+req.Name) outp, err := cmd.CombinedOutput() if err != nil { log.Error(fmt.Errorf(string(outp))) return fmt.Errorf("function does not exist") } log.Info(string(outp)) m := map[string]interface{}{} err = yaml.Unmarshal(outp, m) if err != nil { return err } if len(readRsp.Records) > 0 { m := readRsp.Records[0].AsMap() bs, _ := json.Marshal(m) f := &function.Func{} err = json.Unmarshal(bs, f) if err != nil { return err } rsp.Function = f } else { rsp.Function = &function.Func{ Name: req.Name, Project: req.Project, } } // set describe info rsp.Function.Status = m["status"].(string) rsp.Timeout = m["timeout"].(string) rsp.UpdatedAt = m["updateTime"].(string) return nil }