diff --git a/api-service/controller/env_instance.go b/api-service/controller/env_instance.go index 8e23a005..0478b5e4 100644 --- a/api-service/controller/env_instance.go +++ b/api-service/controller/env_instance.go @@ -81,10 +81,10 @@ func (ctrl *EnvInstanceController) CreateEnvInstance(c *gin.Context) { backendmodels.JSONErrorWithMessage(c, 404, "Environment not found: "+req.EnvName) return } + if backendEnv.DeployConfig == nil { + backendEnv.DeployConfig = make(map[string]interface{}) + } if req.Datasource != "" { - if backendEnv.DeployConfig == nil { - backendEnv.DeployConfig = make(map[string]interface{}) - } // Prefer imagePrefix from DeployConfig, default to empty string imagePrefix := "docker.io/library/aenv" if value, ok := backendEnv.DeployConfig["imagePrefix"]; ok { @@ -102,7 +102,9 @@ func (ctrl *EnvInstanceController) CreateEnvInstance(c *gin.Context) { backendEnv.DeployConfig["arguments"] = req.Arguments } // Set TTL for environment - backendEnv.DeployConfig["ttl"] = req.TTL + if req.TTL != "" { + backendEnv.DeployConfig["ttl"] = req.TTL + } // Set owner for controller to store in pod label if req.Owner != "" { backendEnv.DeployConfig["owner"] = req.Owner diff --git a/api-service/go.mod b/api-service/go.mod index 5269bb7a..f5c6a3b4 100644 --- a/api-service/go.mod +++ b/api-service/go.mod @@ -15,7 +15,9 @@ require ( gopkg.in/natefinch/lumberjack.v2 v2.2.1 ) -replace envhub => ../envhub +replace ( + envhub => ../envhub +) require ( github.com/beorn7/perks v1.0.1 // indirect diff --git a/api-service/main.go b/api-service/main.go index 4ee75530..225979fe 100644 --- a/api-service/main.go +++ b/api-service/main.go @@ -96,17 +96,21 @@ func main() { } var scheduleClient service.EnvInstanceService + var envServiceController *controller.EnvServiceController switch scheduleType { case "k8s": scheduleClient = service.NewScheduleClient(scheduleAddr) + envServiceController = controller.NewEnvServiceController(scheduleClient, backendClient, redisClient) case "standard": scheduleClient = service.NewEnvInstanceClient(scheduleAddr) + case "faas": + scheduleClient = service.NewFaaSClient(scheduleAddr) default: log.Fatalf("unsupported schedule type: %v", scheduleType) } envInstanceController := controller.NewEnvInstanceController(scheduleClient, backendClient, redisClient) - envServiceController := controller.NewEnvServiceController(scheduleClient, backendClient, redisClient) + // Main route configuration mainRouter.POST("/env-instance", middleware.AuthTokenMiddleware(tokenEnabled, backendClient), @@ -118,14 +122,16 @@ func main() { mainRouter.DELETE("/env-instance/:id", middleware.AuthTokenMiddleware(tokenEnabled, backendClient), envInstanceController.DeleteEnvInstance) // Service routes - mainRouter.POST("/env-service", - middleware.AuthTokenMiddleware(tokenEnabled, backendClient), - middleware.RateLimit(qps), - envServiceController.CreateEnvService) - mainRouter.GET("/env-service/:id/list", middleware.AuthTokenMiddleware(tokenEnabled, backendClient), envServiceController.ListEnvServices) - mainRouter.GET("/env-service/:id", middleware.AuthTokenMiddleware(tokenEnabled, backendClient), envServiceController.GetEnvService) - mainRouter.DELETE("/env-service/:id", middleware.AuthTokenMiddleware(tokenEnabled, backendClient), envServiceController.DeleteEnvService) - mainRouter.PUT("/env-service/:id", middleware.AuthTokenMiddleware(tokenEnabled, backendClient), envServiceController.UpdateEnvService) + if envServiceController != nil { + mainRouter.POST("/env-service", + middleware.AuthTokenMiddleware(tokenEnabled, backendClient), + middleware.RateLimit(qps), + envServiceController.CreateEnvService) + mainRouter.GET("/env-service/:id/list", middleware.AuthTokenMiddleware(tokenEnabled, backendClient), envServiceController.ListEnvServices) + mainRouter.GET("/env-service/:id", middleware.AuthTokenMiddleware(tokenEnabled, backendClient), envServiceController.GetEnvService) + mainRouter.DELETE("/env-service/:id", middleware.AuthTokenMiddleware(tokenEnabled, backendClient), envServiceController.DeleteEnvService) + mainRouter.PUT("/env-service/:id", middleware.AuthTokenMiddleware(tokenEnabled, backendClient), envServiceController.UpdateEnvService) + } mainRouter.GET("/health", healthChecker) mainRouter.GET("/metrics", gin.WrapH(promhttp.Handler())) diff --git a/api-service/service/faas_client.go b/api-service/service/faas_client.go new file mode 100644 index 00000000..cb185d61 --- /dev/null +++ b/api-service/service/faas_client.go @@ -0,0 +1,476 @@ +package service + +import ( + "fmt" + "math" + "strconv" + "time" + + "k8s.io/apimachinery/pkg/api/resource" + + "api-service/models" + "api-service/service/faas_model" + backend "envhub/models" +) + +var _ EnvInstanceService = (*FaaSClient)(nil) + +type FaaSClient struct { + client *faas_model.HTTPClient +} + +func NewFaaSClient(endpoint string) *FaaSClient { + client := faas_model.NewHTTPClient(endpoint) + return &FaaSClient{client} +} + +// CreateEnvInstance creates an environment instance by triggering the create-env function +func (c *FaaSClient) CreateEnvInstance(req *backend.Env) (*models.EnvInstance, error) { + functionName := fmt.Sprintf("%s-%s", req.Name, req.Version) + // use datasource as runtime name + dynamicRuntimeName := "" + if name, ok := req.DeployConfig["dataSource"]; ok { + s, ok := name.(string) + if !ok { + return nil, fmt.Errorf("value for 'dataSource' in DeployConfig must be a string, but got %T", name) + } + dynamicRuntimeName = s + } + //if err := c.PrepareFunction(functionName, req); err != nil { + // return nil, fmt.Errorf("prepare function failed: %v", err.Error()) + //} + // Synchronously call the function + instanceId, err := c.CreateInstanceByFunction(functionName, dynamicRuntimeName) + if err != nil { + return nil, fmt.Errorf("failed to create env instance %s: %v", functionName, err) + } + return models.NewEnvInstance(instanceId, req, ""), nil +} + +func (c *FaaSClient) PrepareFunction(functionName string, req *backend.Env) error { + runtimeName := fmt.Sprintf("runtime-%s", functionName) + // Create runtime + runtime, err := c.GetRuntime(runtimeName) + if runtime == nil || err != nil { + runtimeReq := faas_model.RuntimeCreateOrUpdateRequest{ + Name: runtimeName, + Description: req.Description, + Content: &faas_model.RuntimeContent{ + OssURL: req.GetImage(), + }, + Labels: map[string]string{ + "huse.alipay.com/runsc-oci": "true", + }, + } + if err := c.CreateRuntime(&runtimeReq); err != nil { + return fmt.Errorf("failed to create runtime: %v", err.Error()) + } + } else if runtime.Status != string(faas_model.RuntimeStatusActive) { + return fmt.Errorf("runtime %s is not active: %v", runtime.Name, runtime.Status) + } + // Create function + function, err := c.GetFunction(functionName) + if function == nil || err != nil { + memoryQuntity, err := resource.ParseQuantity(req.GetMemory()) + if err != nil { + return fmt.Errorf("failed to parse memory value: %v", err.Error()) + } + functionReq := faas_model.FunctionCreateOrUpdateRequest{ + Name: functionName, + PackageType: "zip", + // FIXME: swe hard code here, should use specified env source code as function code + Code: &faas_model.FunctionCode{ + OSSURL: "", + }, + Runtime: runtimeName, + Labels: map[string]string{ + faas_model.LabelStatefulFunction: "true", + //faas-api-service receiver uses strconv.Atoi, using int here to prevent overflow + "custom.hcsfaas.hcs.io/idle-timeout": strconv.FormatInt(math.MaxInt32, 10), + }, + Description: req.Description, + Memory: memoryQuntity.ScaledValue(resource.Mega), + Timeout: 3600, + } + if err := c.CreateFunction(&functionReq); err != nil { + return fmt.Errorf("failed to create function: %v", err.Error()) + } + } + return nil +} + +func (c *FaaSClient) CreateInstanceByFunction(name string, dynamicRuntimeName string) (string, error) { + f, err := c.GetFunction(name) + if err != nil { + return "", err + } + + instanceId, err := c.InitializeFunction(f.Name, dynamicRuntimeName, faas_model.FunctionInvocationTypeSync, []byte("{}")) + if err != nil { + return "", fmt.Errorf("failed to create functions instance from faas server: %v", err.Error()) + } + return instanceId, nil +} + +// GetEnvInstance gets the details of the specified environment instance +func (c *FaaSClient) GetEnvInstance(id string) (*models.EnvInstance, error) { + // Reuse HCSFaaSClient's GetInstance + instance, err := c.GetInstance(id) + if err != nil { + return nil, fmt.Errorf("get instance %s failed: %w", id, err) + } + + // Map model.Instance -> models.EnvInstance + envInst := &models.EnvInstance{ + ID: instance.InstanceID, + IP: instance.IP, + TTL: "", // No TTL field source available yet, can be added later + // CreatedAt / UpdatedAt use current time or default values (should actually be returned by backend) + CreatedAt: time.Unix(instance.CreateTimestamp, 0).Format(time.RFC3339), + UpdatedAt: time.Now().Format("2006-01-02 15:04:05"), + Status: convertStatus(instance.Status), + // Env field cannot be directly obtained from Instance, needs to rely on Create return or additional queries + // Can only be empty here, recommend maintaining through Create/CreateFromRecord + Env: nil, + } + + return envInst, nil +} + +// DeleteEnvInstance deletes the specified environment instance +func (c *FaaSClient) DeleteEnvInstance(id string) error { + return c.DeleteInstance(id) // Direct proxy +} + +// ListEnvInstances lists all environment instances, supporting filtering by env name +func (c *FaaSClient) ListEnvInstances(envName string) ([]*models.EnvInstance, error) { + labels := make(map[string]string) + if envName != "" { + labels["env"] = envName + } + + resp, err := c.ListInstances(labels) + if err != nil { + return nil, fmt.Errorf("list instances failed: %w", err) + } + + var result []*models.EnvInstance + for _, inst := range resp.Instances { + result = append(result, &models.EnvInstance{ + ID: inst.InstanceID, + IP: inst.IP, + Status: convertStatus(inst.Status), + CreatedAt: time.Now().Format("2006-01-02 15:04:05"), // Could consider constructing from CreateTimestamp + UpdatedAt: time.Now().Format("2006-01-02 15:04:05"), + TTL: "", + Env: nil, // Cannot obtain full Env information from Instance + }) + } + + return result, nil +} + +// Warmup warms up the specified environment: polling PrepareFunction calls until success or timeout +func (c *FaaSClient) Warmup(req *backend.Env) error { + errCh := c.WarmupAsyncChan(req) + select { + case err := <-errCh: + if err != nil { + return err + } else { + return nil + } + case <-time.After(300 * time.Second): + return fmt.Errorf("timed out waiting for env instance to become ready") + } +} + +// WarmupAsyncChan async warmup, returns result channel +func (c *FaaSClient) WarmupAsyncChan(req *backend.Env) <-chan error { + resultCh := make(chan error, 1) // Buffer of 1 to prevent goroutine leak + + go func() { + defer close(resultCh) + + const ( + timeout = 300 * time.Second + interval = 10 * time.Second + ) + + deadline := time.Now().Add(timeout) + functionName := fmt.Sprintf("%s-%s", req.Name, req.Version) + + var lastErr error + for time.Now().Before(deadline) { + lastErr = c.PrepareFunction(functionName, req) + if lastErr == nil { + return // Success, don't send error + } + + fmt.Printf("Warmup retry: %v\n", lastErr) + time.Sleep(interval) + } + + // Timeout, send error + resultCh <- fmt.Errorf("warmup timeout: function %s not ready after %v", functionName, timeout) + }() + + return resultCh +} + +func (c *FaaSClient) Cleanup() error { + return fmt.Errorf("cleanup not implemented in faas") +} + +// --- Newly added local method implementations --- + +func (c *FaaSClient) CreateFunction(in *faas_model.FunctionCreateOrUpdateRequest) error { + uri := "/hapis/faas.hcs.io/v1/functions/" + + funcResp := &faas_model.APIResponse{} + err := c.client.Post(uri).Body(*in).Do().Into(funcResp) + if err != nil { + return err + } + + return nil +} + +func (c *FaaSClient) GetFunction(name string) (*faas_model.Function, error) { + uri := "/hapis/faas.hcs.io/v1/functions/" + name + + funcResp := &faas_model.APIResponse{} + err := c.client.Get(uri).Do().Into(&funcResp) + if err != nil { + return nil, fmt.Errorf("get function failed with err: %s", err) + } + + if !funcResp.Success { + return nil, fmt.Errorf("failed to get function from faas server with message: %s", funcResp.ErrorMessage) + } + + data, ok := funcResp.Data.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("invalid response type for Function") + } + + // Convert map to Function struct + function := &faas_model.Function{} + if name, ok := data["name"].(string); ok { + function.Name = name + } + if packageType, ok := data["packageType"].(string); ok { + function.PackageType = packageType + } + if description, ok := data["description"].(string); ok { + function.Description = description + } + if runtime, ok := data["runtime"].(string); ok { + function.Runtime = runtime + } + if memory, ok := data["memory"].(float64); ok { + function.Memory = int64(memory) + } + if timeout, ok := data["timeout"].(float64); ok { + function.Timeout = int64(timeout) + } + + return function, nil +} + +func (c *FaaSClient) CreateRuntime(in *faas_model.RuntimeCreateOrUpdateRequest) error { + uri := "/hapis/faas.hcs.io/v1/runtimes/" + + runtimeResp := &faas_model.APIResponse{} + err := c.client.Post(uri).Body(*in).Do().Into(runtimeResp) + if err != nil { + return err + } + + return nil +} + +func (c *FaaSClient) GetRuntime(name string) (*faas_model.Runtime, error) { + uri := "/hapis/faas.hcs.io/v1/runtimes/" + name + + runtimeResp := &faas_model.APIResponse{} + err := c.client.Get(uri).Do().Into(&runtimeResp) + if err != nil { + return nil, fmt.Errorf("get runtime failed with err: %s", err) + } + + if !runtimeResp.Success { + return nil, fmt.Errorf("failed to get runtime from faas server with message: %s", runtimeResp.ErrorMessage) + } + + data, ok := runtimeResp.Data.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("invalid response type for Runtime") + } + + // Convert map to Runtime struct + runtime := &faas_model.Runtime{} + if name, ok := data["name"].(string); ok { + runtime.Name = name + } + if description, ok := data["description"].(string); ok { + runtime.Description = description + } + if status, ok := data["status"].(string); ok { + runtime.Status = status + } + + return runtime, nil +} + +func (c *FaaSClient) InitializeFunction(name string, dynamicRuntimeName string, invocationType string, invocationBody []byte) (string, error) { + uri := fmt.Sprintf("/hapis/faas.hcs.io/v1/functions/%s/initialize", name) + + f, err := c.GetFunction(name) + if err != nil { + return "", err + } + + if invocationType == faas_model.FunctionInvocationTypeAsync { + invocationType = faas_model.FunctionInvocationTypeAsync + } else { + invocationType = faas_model.FunctionInvocationTypeSync + } + + req := c.client.Post(uri).BodyData(invocationBody).Timeout(time.Duration(f.Timeout)*time.Second).Query("invocationType", invocationType) + + // If dynamicRuntimeName is provided, add it to the query parameters + if dynamicRuntimeName != "" { + req = req.Query("dynamicRuntimeName", dynamicRuntimeName) + } + + resp, err := req.Do().Response() + if err != nil { + return "", err + } + + if resp.StatusCode != 200 { + return "", fmt.Errorf("failed to initialize function from faas server with status code %d", resp.StatusCode) + } + + instanceId := resp.Header.Get(faas_model.HttpHeaderInstanceID) + return instanceId, nil +} + +func (c *FaaSClient) ListInstances(labels map[string]string) (*faas_model.InstanceListResp, error) { + uri := "/hapis/faas.hcs.io/v1/instances" + + req := &faas_model.InstanceListRequest{Labels: labels} + resp := &faas_model.APIResponse{ + Data: &faas_model.InstanceListResp{}, + } + err := c.client.Post(uri).Body(*req).Do().Into(resp) + if err != nil { + return nil, fmt.Errorf("failed to list instances: %w", err) + } + + if !resp.Success { + return nil, fmt.Errorf("failed to list instances: %s", resp.ErrorMessage) + } + + data, ok := resp.Data.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("invalid response type for InstanceListResp") + } + + // Convert map to InstanceListResp struct + instances := []*faas_model.Instance{} + if insts, ok := data["instances"].([]interface{}); ok { + for _, inst := range insts { + if instMap, ok := inst.(map[string]interface{}); ok { + instance := &faas_model.Instance{} + if instanceID, ok := instMap["instanceID"].(string); ok { + instance.InstanceID = instanceID + } + if ip, ok := instMap["ip"].(string); ok { + instance.IP = ip + } + if status, ok := instMap["status"].(string); ok { + instance.Status = faas_model.InstanceStatus(status) + } + instances = append(instances, instance) + } + } + } + + return &faas_model.InstanceListResp{Instances: instances}, nil +} + +func (c *FaaSClient) GetInstance(name string) (*faas_model.Instance, error) { + uri := fmt.Sprintf("/hapis/faas.hcs.io/v1/instances/%s", name) + + resp := &faas_model.APIResponse{ + Data: &faas_model.Instance{}, + } + err := c.client.Get(uri).Do().Into(resp) + if err != nil { + return nil, fmt.Errorf("failed to get instance %s: %w", name, err) + } + + if !resp.Success { + return nil, fmt.Errorf("failed to get instance %s: %s", name, resp.ErrorMessage) + } + + data, ok := resp.Data.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("invalid response type for Instance") + } + + // Convert map to Instance struct + instance := &faas_model.Instance{} + if instanceID, ok := data["instanceID"].(string); ok { + instance.InstanceID = instanceID + } + if ip, ok := data["ip"].(string); ok { + instance.IP = ip + } + if status, ok := data["status"].(string); ok { + instance.Status = faas_model.InstanceStatus(status) + } + if createTimestamp, ok := data["createTimestamp"].(float64); ok { + instance.CreateTimestamp = int64(createTimestamp) + } + + return instance, nil +} + +func (c *FaaSClient) DeleteInstance(name string) error { + uri := fmt.Sprintf("/hapis/faas.hcs.io/v1/instances/%s", name) + + resp := &faas_model.APIResponse{} + err := c.client.Delete(uri).Do().Into(resp) + if err != nil { + return fmt.Errorf("failed to delete instance %s: %w", name, err) + } + + if !resp.Success { + return fmt.Errorf("failed to delete instance %s: %s", name, resp.ErrorMessage) + } + + return nil +} + +// --- Utility functions --- + +// convertStatus converts model.InstanceStatus to models.EnvInstanceStatus.String() +func convertStatus(s faas_model.InstanceStatus) string { + switch s { + case "Pending": + return models.EnvInstanceStatusPending.String() + case "Creating": + return models.EnvInstanceStatusCreating.String() + case "Running": + return models.EnvInstanceStatusRunning.String() + case "Failed": + return models.EnvInstanceStatusFailed.String() + case "Terminated": + return models.EnvInstanceStatusTerminated.String() + default: + return models.EnvInstanceStatusRunning.String() + } +} diff --git a/api-service/service/faas_model/function.go b/api-service/service/faas_model/function.go new file mode 100644 index 00000000..a75dae57 --- /dev/null +++ b/api-service/service/faas_model/function.go @@ -0,0 +1,97 @@ +package faas_model + +const ( + FunctionInvocationTypeSync = "RequestResponse" + FunctionInvocationTypeAsync = "Event" +) + +// InstanceStatus represents the status of an instance +type InstanceStatus string + +const ( + InstanceStatusPending InstanceStatus = "Pending" + InstanceStatusCreating InstanceStatus = "Creating" + InstanceStatusRunning InstanceStatus = "Running" + InstanceStatusFailed InstanceStatus = "Failed" + InstanceStatusTerminated InstanceStatus = "Terminated" +) + +type FunctionCreateOrUpdateRequest struct { + Name string `json:"name"` + PackageType string `json:"packageType"` + Code *FunctionCode `json:"code"` + Runtime string `json:"runtime"` + Labels map[string]string `json:"labels,omitempty"` + Envs map[string]string `json:"envs,omitempty"` + Handler string `json:"handler"` + Description string `json:"description"` + Memory int64 `json:"memory"` + Timeout int64 `json:"timeout"` +} + +type FunctionCode struct { + OSSURL string `json:"ossURL"` + Image string `json:"image"` +} + +type Function struct { + Name string `json:"name"` + PackageType string `json:"packageType"` + Code *FunctionCode `json:"code"` + Runtime string `json:"runtime"` + Labels map[string]string `json:"labels,omitempty"` + Description string `json:"description"` + Memory int64 `json:"memory"` + Timeout int64 `json:"timeout"` +} + +type Instance struct { + InstanceID string `json:"instanceID"` + CreateTimestamp int64 `json:"createTimestamp"` + IP string `json:"ip"` + Labels map[string]string `json:"labels"` + Status InstanceStatus `json:"status"` +} + +type InstanceListResp struct { + Instances []*Instance `json:"instances"` +} + +type InstanceListRequest struct { + Labels map[string]string `json:"labels"` +} + +type APIResponse struct { + Success bool `json:"success"` + ErrorMessage string `json:"errorMessage,omitempty"` + Data interface{} `json:"data,omitempty"` +} + +type RuntimeCreateOrUpdateRequest struct { + Name string `json:"name"` + Description string `json:"description"` + Content *RuntimeContent `json:"content"` + Labels map[string]string `json:"labels"` +} + +type RuntimeContent struct { + OssURL string `json:"ossUrl,omitempty"` + Image string `json:"image,omitempty"` +} + +type Runtime struct { + Name string `json:"name"` + Description string `json:"description"` + Content *RuntimeContent `json:"content"` + Labels map[string]string `json:"labels"` + Status string `json:"status"` +} + +type RuntimeStatus string + +const ( + RuntimeStatusActive RuntimeStatus = "active" + RuntimeStatusDeleting RuntimeStatus = "deleting" + RuntimeStatusPreparing RuntimeStatus = "preparing" + RuntimeStatusError RuntimeStatus = "error" +) diff --git a/api-service/service/faas_model/http_client.go b/api-service/service/faas_model/http_client.go new file mode 100644 index 00000000..49817827 --- /dev/null +++ b/api-service/service/faas_model/http_client.go @@ -0,0 +1,256 @@ +package faas_model + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "time" +) + +// HTTPClient a http client in RESTful style +// the http client can be used like: +// c := NewHTTPClient(baseURL) +// c.Get("/some/url").Headers().Do().Into(&MyStruct{}) +// c.Post("/some/url").Body("{}").Do().Into(&MyStruct{}) +type HTTPClient struct { + *http.Client + BaseURL string +} + +// NewHTTPClient creates a new http client +// baseURL is the base url for all requests +// returns an instance of HTTPClient +// Example: +// +// c := NewHTTPClient("https://example.com") +// resp, err := c.Get("/users").QueryParam("id", "123").Do() +func NewHTTPClient(baseURL string) *HTTPClient { + return &HTTPClient{ + Client: &http.Client{ + Timeout: 10 * time.Second, + Transport: &http.Transport{ + TLSHandshakeTimeout: 5 * time.Second, + }, + }, + BaseURL: baseURL, + } +} + +// Get creates a GET request +func (c *HTTPClient) Get(path string) *HTTPReq { + return c.do(http.MethodGet, path) +} + +// Delete creates a DELETE request +func (c *HTTPClient) Delete(path string) *HTTPReq { + return c.do(http.MethodDelete, path) +} + +// Post creates a POST request +func (c *HTTPClient) Post(path string) *HTTPReq { + return c.do(http.MethodPost, path) +} + +// Put creates a PUT request +func (c *HTTPClient) Put(path string) *HTTPReq { + return c.do(http.MethodPut, path) +} + +func (c *HTTPClient) do(method string, path string) *HTTPReq { + return NewHTTPReqWithMethodPath(c.Client, c.BaseURL, method, path) +} + +// HTTPReq represents a http request +type HTTPReq struct { + // client is the http client to send the real http request + client *http.Client + + // timeout is the timeout duration for this request + // default value is 10 seconds + timeout time.Duration + + // internal representations of the request + headers map[string]string + baseURL string + method string + path string + body []byte + q url.Values + + resp *http.Response + + errors []error +} + +// NewHTTPReq creates a new http req +func NewHTTPReq(c *http.Client) *HTTPReq { + return &HTTPReq{ + client: c, + + headers: map[string]string{}, + + errors: make([]error, 0), + } +} + +func NewHTTPReqWithMethodPath(c *http.Client, baseURL, method, path string) *HTTPReq { + return &HTTPReq{ + client: c, + baseURL: baseURL, + method: method, + path: path, + headers: map[string]string{}, + errors: make([]error, 0), + } +} + +// recordError records errors +// if there are any errors, they will be recorded here +func (r *HTTPReq) recordError(err error) { + if err != nil { + if r.errors == nil { + r.errors = make([]error, 0) + } + r.errors = append(r.errors, err) + } +} + +// Do will send the request +// returns the same instance of HTTPReq +// If http response is wanted, caller should call Response() method after +// Example usage: +// r := c.Get("/user").QueryParam("name", "john").Do() +// resp, err := r.Response() +func (r *HTTPReq) Do() *HTTPReq { + fullPath := r.baseURL + r.path + if r.q != nil { + fullPath = fullPath + "?" + r.q.Encode() + } + + req, err := http.NewRequest(r.method, fullPath, bytes.NewBuffer(r.body)) + if err != nil { + r.recordError(fmt.Errorf("failed to create http request: %v", err)) + return r + } + + if req.Header.Get("Content-Type") == "" { + req.Header.Add("Content-Type", "application/json") + } + for k, v := range r.headers { + req.Header.Add(k, v) + } + + if r.timeout != 0 { + r.client.Timeout = r.timeout + } + + resp, err := r.client.Do(req) + if err != nil { + r.recordError(fmt.Errorf("failed to send http request: %v", err)) + return r + } + + r.resp = resp + return r +} + +func (r *HTTPReq) Timeout(t time.Duration) *HTTPReq { + r.timeout = t + return r +} + +// Headers sets headers on this request +func (r *HTTPReq) Headers(headers map[string]string) *HTTPReq { + for k, v := range headers { + r.headers[k] = v + } + + return r +} + +// Body will set the body of this request +// the body should be a pointer to an empty struct +// it will be marshaled and sent as the body +func (r *HTTPReq) Body(in interface{}) *HTTPReq { + data, err := json.Marshal(in) + if err != nil { + r.recordError(fmt.Errorf("failed to marshal body: %v", err)) + return r + } + + r.body = data + return r +} + +func (r *HTTPReq) BodyData(data []byte) *HTTPReq { + r.body = data + return r +} + +func (r *HTTPReq) Query(key string, value string) *HTTPReq { + if r.q == nil { + r.q = url.Values{} + } + + r.q.Set(key, value) + return r +} + +// Into will unmarshal the response into the given object +// the obj should be a pointer to an empty struct +// if the response is not 200, the error object will be unmarshalled into e +func (r *HTTPReq) Into(obj interface{}, e ...interface{}) error { + if len(r.errors) > 0 { + return r.errors[0] + } + + if r.resp == nil { + return fmt.Errorf("response is not ready") + } + + data, err := io.ReadAll(r.resp.Body) + if err != nil { + return fmt.Errorf("failed to read response body: %v", err) + } + + // If execution is successful, parse the body content into the object; otherwise return an error + if r.resp.StatusCode >= http.StatusOK && r.resp.StatusCode < 300 { + if err := json.Unmarshal(data, obj); err != nil { + return fmt.Errorf("failed to unmarshal response data: %s. err: %v", data, err) + } + return nil + } else { + // try to unmarshal the error message into known struct + if len(e) > 0 { + if err := json.Unmarshal(data, e[0]); err != nil { + return fmt.Errorf("http request with non-200 status code: %d, body: %s", r.resp.StatusCode, string(data)) + } + } + + return fmt.Errorf("http request with non-200 status code: %d, body: %s", r.resp.StatusCode, string(data)) + } +} + +// Response will return the http response +// if there're any errors during sending or receiving, +// the first error will be returned +func (r *HTTPReq) Response() (*http.Response, error) { + if len(r.errors) > 0 { + return nil, r.errors[0] + } + + if r.resp == nil { + return nil, fmt.Errorf("response is not ready") + } + + return r.resp, nil +} + +// Constants for HTTP headers +const ( + HttpHeaderInstanceID = "Hcs-Faas-Instance-Id" + LabelStatefulFunction = "Hcs-Faas-Stateful-Function" +) diff --git a/envhub/models/env.go b/envhub/models/env.go index b78bae47..59739c61 100644 --- a/envhub/models/env.go +++ b/envhub/models/env.go @@ -18,6 +18,7 @@ package models import ( "encoding/json" + "fmt" "strings" "time" ) @@ -209,3 +210,36 @@ func (e *Env) FromJSON(data []byte) error { } return nil } + +// GetImage finds the artifact of type "docker-image" from Artifacts and returns its Content (the image address) +func (e *Env) GetImage() string { + for _, artifact := range e.Artifacts { + if strings.EqualFold(artifact.Type, "image") { + return artifact.Content + } + } + return "" +} + +// GetMemory retrieves the memory configuration from DeployConfig, such as "2G" +func (e *Env) GetMemory() string { + if val, exists := e.DeployConfig["memory"]; exists { + if s, ok := val.(string); ok { + return s + } + // If it's another type (such as float64), try to convert to string + return fmt.Sprintf("%v", val) + } + return "" +} + +// GetCPU retrieves the cpu configuration from DeployConfig, such as "1C" +func (e *Env) GetCPU() string { + if val, exists := e.DeployConfig["cpu"]; exists { + if s, ok := val.(string); ok { + return s + } + return fmt.Sprintf("%v", val) + } + return "" +}