From f3778463d3e5488a332de468dbf06de338c8d67f Mon Sep 17 00:00:00 2001 From: Liang Huang Date: Mon, 4 Mar 2024 20:05:47 +0800 Subject: [PATCH] add client Signed-off-by: Liang Huang --- client/cluster.go | 94 ++++++++++++++++++ client/cluster_test.go | 1 + client/error.go | 14 +++ client/error_test.go | 32 +++++++ client/project.go | 12 +++ client/project_test.go | 91 ++++++++++++++++++ client/zilliz.go | 209 +++++++++++++++++++++++++++++++++++++++++ client/zilliz_test.go | 49 ++++++++++ 8 files changed, 502 insertions(+) create mode 100644 client/cluster.go create mode 100644 client/cluster_test.go create mode 100644 client/error.go create mode 100644 client/error_test.go create mode 100644 client/project.go create mode 100644 client/project_test.go create mode 100644 client/zilliz.go create mode 100644 client/zilliz_test.go diff --git a/client/cluster.go b/client/cluster.go new file mode 100644 index 0000000..ed49093 --- /dev/null +++ b/client/cluster.go @@ -0,0 +1,94 @@ +package client +type ModifyClusterParams struct { + CuSize int `json:"cuSize"` +} + +type ModifyClusterResponse struct { + ClusterId string `json:"clusterId"` +} + +func (c *Client) ModifyCluster(clusterId string, params *ModifyClusterParams) (*string, error) { + var response zillizResponse[ModifyClusterResponse] + err := c.do("POST", "clusters/"+clusterId+"/modify", params, &response) + if err != nil { + return nil, err + } + return &response.Data.ClusterId, err +} + +type DropClusterResponse struct { + ClusterId string `json:"clusterId"` +} + +func (c *Client) DropCluster(clusterId string) (*string, error) { + var response zillizResponse[DropClusterResponse] + err := c.do("DELETE", "clusters/"+clusterId+"/drop", nil, &response) + if err != nil { + return nil, err + } + return &response.Data.ClusterId, err +} + + + +type Clusters struct { + zillizPage + Clusters []Cluster `json:"clusters"` +} + +type Cluster struct { + ClusterId string `json:"clusterId"` + ClusterName string `json:"clusterName"` + Description string `json:"description"` + RegionId string `json:"regionId"` + ClusterType string `json:"clusterType"` + CuSize int64 `json:"cuSize"` + Status string `json:"status"` + ConnectAddress string `json:"connectAddress"` + PrivateLinkAddress string `json:"privateLinkAddress"` + CreateTime string `json:"createTime"` +} + +func (c *Client) ListClusters() (Clusters, error) { + var clusters zillizResponse[Clusters] + err := c.do("GET", "clusters", nil, &clusters) + return clusters.Data, err +} + +func (c *Client) DescribeCluster(clusterId string) (Cluster, error) { + var cluster zillizResponse[Cluster] + err := c.do("GET", "clusters/"+clusterId, nil, &cluster) + return cluster.Data, err +} + +type CreateClusterParams struct { + Plan string `json:"plan"` + ClusterName string `json:"clusterName"` + CUSize int `json:"cuSize"` + CUType string `json:"cuType"` + ProjectId string `json:"projectId"` +} + +type CreateServerlessClusterParams struct { + ClusterName string `json:"clusterName"` + ProjectId string `json:"projectId"` +} + +type CreateClusterResponse struct { + ClusterId string `json:"clusterId"` + Username string `json:"username"` + Password string `json:"password"` + Prompt string `json:"prompt"` +} + +func (c *Client) CreateCluster(params CreateClusterParams) (*CreateClusterResponse, error) { + var clusterResponse zillizResponse[CreateClusterResponse] + err := c.do("POST", "clusters/create", params, &clusterResponse) + return &clusterResponse.Data, err +} + +func (c *Client) CreateServerlessCluster(params CreateServerlessClusterParams) (*CreateClusterResponse, error) { + var clusterResponse zillizResponse[CreateClusterResponse] + err := c.do("POST", "clusters/createServerless", params, &clusterResponse) + return &clusterResponse.Data, err +} \ No newline at end of file diff --git a/client/cluster_test.go b/client/cluster_test.go new file mode 100644 index 0000000..da13c8e --- /dev/null +++ b/client/cluster_test.go @@ -0,0 +1 @@ +package client diff --git a/client/error.go b/client/error.go new file mode 100644 index 0000000..f34f43e --- /dev/null +++ b/client/error.go @@ -0,0 +1,14 @@ +package client + +import ( + "fmt" +) + +type Error struct { + Code int `json:"code"` + Message string `json:"message"` +} + +func (err Error) Error() string { + return fmt.Sprintf("code:%d,Message:%s", err.Code, err.Message) +} diff --git a/client/error_test.go b/client/error_test.go new file mode 100644 index 0000000..80d36d7 --- /dev/null +++ b/client/error_test.go @@ -0,0 +1,32 @@ +package client + +import ( + "encoding/json" + "strings" + "testing" +) + +var errJson = []byte(` +{ + "code":80001, + "message":"Invalid token: Your token is not valid. Please double-check your request parameters and ensure that you have provided a valid token." +} +`) + +func TestError_UnmarshalJSON(t *testing.T) { + var e Error + err := json.Unmarshal(errJson, &e) + if err != nil { + t.Errorf("Error.UnmarshalJSON() error = %v", err) + } + wantCode := 80001 + if e.Code != wantCode { + t.Errorf("Error.UnmarshalJSON() = %v, want %v", e.Code, wantCode) + } + + wantMessage := "Invalid token" + if !strings.Contains(e.Message, wantMessage) { + t.Errorf("Error.UnmarshalJSON() = %v, want %v", e.Message, wantMessage) + } + +} diff --git a/client/project.go b/client/project.go new file mode 100644 index 0000000..be7406c --- /dev/null +++ b/client/project.go @@ -0,0 +1,12 @@ +package client + +type Project struct { + ProjectId string `json:"projectId"` + ProjectName string `json:"projectName"` +} + +func (c *Client) ListProjects() ([]Project, error) { + var response zillizResponse[[]Project] + err := c.do("GET", "projects", nil, &response) + return response.Data, err +} diff --git a/client/project_test.go b/client/project_test.go new file mode 100644 index 0000000..55ec012 --- /dev/null +++ b/client/project_test.go @@ -0,0 +1,91 @@ +package client + +import ( + "flag" + "testing" +) + +var ( + apiKey string +) + +func init() { + flag.StringVar(&apiKey, "key", "", "Your TEST secret key for the zilliz cloud API. If present, integration tests will be run using this key.") +} + + +func TestClient_ListProjects(t *testing.T) { + if apiKey == "" { + t.Skip("No API key provided") + } + + type checkFn func(*testing.T, []Project, error) + check := func(fns ...checkFn) []checkFn { return fns } + + hasNoErr := func() checkFn { + return func(t *testing.T, _ []Project, err error) { + if err != nil { + t.Fatalf("err = %v; want nil", err) + } + } + } + + hasErrCode := func(code int) checkFn { + return func(t *testing.T, _ []Project, err error) { + se, ok := err.(Error) + if !ok { + t.Fatalf("err isn't a Error") + } + if se.Code != code { + t.Errorf("err.Code = %d; want %d", se.Code, code) + } + } + } + + hasProject := func(Name string) checkFn { + return func(t *testing.T, p []Project, err error) { + for _, project := range p { + if project.ProjectName == Name { + return + } + } + t.Errorf("project not found: %s", Name) + } + } + + type fields struct { + CloudRegionId string + apiKey string + } + tests := []struct { + name string + fields fields + checks []checkFn + }{ + { + "postive 1", + fields{CloudRegionId: "gcp-us-west1", apiKey: apiKey}, + check( + hasNoErr(), + hasProject("Default Project")), + }, + { + "none exist region", + fields{CloudRegionId: "gcp-us-west1", apiKey: "fake"}, + check(hasErrCode(80001)), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := NewClient( + tt.fields.apiKey, + tt.fields.CloudRegionId, + ) + + got, err := c.ListProjects() + for _, check := range tt.checks { + check(t, got, err) + } + }) + } +} diff --git a/client/zilliz.go b/client/zilliz.go new file mode 100644 index 0000000..fd3f7d1 --- /dev/null +++ b/client/zilliz.go @@ -0,0 +1,209 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "path" + "time" +) + +const ( + apiTemplateUrl string = "https://controller.api.%s.zillizcloud.com/v1/" +) + +type Client struct { + CloudRegionId string + HTTPClient *http.Client + baseUrl string + apiKey string + userAgent string +} + +// NewClient - creates new Pinecone client. +func NewClient(apiKey string, cloudRegionId string) *Client { + c := &Client{ + CloudRegionId: cloudRegionId, + HTTPClient: &http.Client{Timeout: 30 * time.Second}, + baseUrl: apiTemplateUrl, + apiKey: apiKey, + userAgent: "zilliztech/terraform-provider-zillizcloud", + } + return c +} + +type zillizResponse[T any] struct { + Code int `json:"code"` + Data T `json:"data"` + Message string `json:"message"` +} + +type ZillizAPIError struct { + Code int `json:"code"` + Message string `json:"message"` +} + +func (r *ZillizAPIError) Error() string { + return fmt.Sprintf("error, code: %d, message: %s", r.Code, r.Message) +} + +type zillizPage struct { + Count int `json:"count"` + CurrentPage int `json:"currentPage"` + PageSize int `json:"pageSize"` +} + +type CloudProvider struct { + CloudId string `json:"cloudId"` + Description string `json:"description"` +} + +func (c *Client) ListCloudProviders() ([]CloudProvider, error) { + var cloudProviders zillizResponse[[]CloudProvider] + err := c.do("GET", "clouds", nil, &cloudProviders) + return cloudProviders.Data, err +} + +type CloudRegion struct { + ApiBaseUrl string `json:"apiBaseUrl"` + CloudId string `json:"cloudId"` + RegionId string `json:"regionId"` +} + +func (c *Client) ListCloudRegions(cloudId string) ([]CloudRegion, error) { + var cloudRegions zillizResponse[[]CloudRegion] + err := c.do("GET", "regions?cloudId="+cloudId, nil, &cloudRegions) + return cloudRegions.Data, err +} + + + + +func (c *Client) do(method string, path string, body interface{}, result interface{}) error { + u, err := c.buildURL(path) + if err != nil { + return err + } + req, err := c.newRequest(method, u, body) + if err != nil { + return err + } + return c.doRequest(req, result) +} + +func (c *Client) newRequest(method string, u *url.URL, body interface{}) (*http.Request, error) { + var buf io.ReadWriter + if body != nil { + buf = new(bytes.Buffer) + err := json.NewEncoder(buf).Encode(body) + if err != nil { + return nil, err + } + } + req, err := http.NewRequest(method, u.String(), buf) + if err != nil { + return nil, err + } + // req.Header.Set("User-Agent", c.UserAgent) + req.Header.Set("Accept", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiKey)) + if body != nil { + req.Header.Set("Content-Type", "application/json") + } + + return req, nil +} + +func (c *Client) doRequest(req *http.Request, v any) error { + res, err := c.HTTPClient.Do(req) + if err != nil { + return err + } + + defer res.Body.Close() + + if res.StatusCode >= http.StatusBadRequest { + return parseError(res.Body) + } + + return decodeResponse(res.Body, v) +} + +func parseError(body io.Reader) error { + + b, err := io.ReadAll(body) + if err != nil { + return err + + } + var e Error + err = json.Unmarshal(b, &e) + if err != nil { + return err + } + + return e +} + +func decodeResponse(body io.Reader, v any) error { + if v == nil { + return nil + } + b, err := io.ReadAll(body) + if err != nil { + return err + } + + var apierr ZillizAPIError + err = json.Unmarshal(b, &apierr) + if err == nil && apierr.Code != 200 { + return &apierr + } + err = json.Unmarshal(b, v) + return err + // return json.NewDecoder(body).Decode(v) +} + +func (c *Client) buildURL(endpointPath string) (*url.URL, error) { + u, err := url.Parse(endpointPath) + if err != nil { + return nil, err + } + sBaseUrl := c.baseUrl + if c.CloudRegionId != "" { + sBaseUrl = fmt.Sprintf(apiTemplateUrl, c.CloudRegionId) + } + baseUrl, err := url.Parse(sBaseUrl) + if err != nil { + return nil, err + } + u.Path = path.Join(baseUrl.Path, u.Path) + return baseUrl.ResolveReference(u), err +} + +func (c *Client) handleHTTPErrorResp(resp *http.Response) error { + data, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + reqErr := &HTTPError{ + StatusCode: resp.StatusCode, + Status: resp.Status, + Message: string(data), + } + return reqErr +} + +// HTTPError provides informations about generic HTTP errors. +type HTTPError struct { + StatusCode int + Status string + Message string +} + +func (e HTTPError) Error() string { + return fmt.Sprintf("error, status code: %d, message: %s", e.StatusCode, e.Message) +} diff --git a/client/zilliz_test.go b/client/zilliz_test.go new file mode 100644 index 0000000..3f6a101 --- /dev/null +++ b/client/zilliz_test.go @@ -0,0 +1,49 @@ +package client + +import ( + "net/http" + "reflect" + "testing" +) + +func TestClient_CreateCluster(t *testing.T) { + type fields struct { + CloudRegionId string + HTTPClient *http.Client + baseUrl string + apiKey string + userAgent string + } + type args struct { + params CreateClusterParams + } + tests := []struct { + name string + fields fields + args args + want *CreateClusterResponse + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Client{ + CloudRegionId: tt.fields.CloudRegionId, + HTTPClient: tt.fields.HTTPClient, + baseUrl: tt.fields.baseUrl, + apiKey: tt.fields.apiKey, + userAgent: tt.fields.userAgent, + } + got, err := c.CreateCluster(tt.args.params) + if (err != nil) != tt.wantErr { + t.Errorf("Client.CreateCluster() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Client.CreateCluster() = %v, want %v", got, tt.want) + } + }) + } +} +