From b9dcf117cc815ab336b63c503bdb8e0276fbbe26 Mon Sep 17 00:00:00 2001 From: disha-wani Date: Fri, 20 Jul 2018 20:30:48 +0530 Subject: [PATCH 1/2] add cce cluster interfaces --- openstack/cce/v3/clusters/doc.go | 55 +++++ openstack/cce/v3/clusters/requests.go | 192 ++++++++++++++++++ openstack/cce/v3/clusters/results.go | 161 +++++++++++++++ openstack/cce/v3/clusters/testing/doc.go | 2 + openstack/cce/v3/clusters/testing/fixtures.go | 118 +++++++++++ .../cce/v3/clusters/testing/requests_test.go | 156 ++++++++++++++ openstack/cce/v3/clusters/urls.go | 15 ++ openstack/cce/v3/common/common_tests.go | 14 ++ openstack/client.go | 10 + 9 files changed, 723 insertions(+) create mode 100644 openstack/cce/v3/clusters/doc.go create mode 100644 openstack/cce/v3/clusters/requests.go create mode 100644 openstack/cce/v3/clusters/results.go create mode 100644 openstack/cce/v3/clusters/testing/doc.go create mode 100644 openstack/cce/v3/clusters/testing/fixtures.go create mode 100644 openstack/cce/v3/clusters/testing/requests_test.go create mode 100644 openstack/cce/v3/clusters/urls.go create mode 100644 openstack/cce/v3/common/common_tests.go diff --git a/openstack/cce/v3/clusters/doc.go b/openstack/cce/v3/clusters/doc.go new file mode 100644 index 000000000..504375363 --- /dev/null +++ b/openstack/cce/v3/clusters/doc.go @@ -0,0 +1,55 @@ +/* +Package Clusters enables management and retrieval of Clusters +CCE service. + +Example to List Clusters + + listOpts:=clusters.ListOpts{} + allClusters,err:=clusters.List(client,listOpts) + if err != nil { + panic(err) + } + + for _, cluster := range allClusters { + fmt.Printf("%+v\n", cluster) + } + +Example to Create a cluster + + createOpts:=clusters.CreateOpts{Kind:"Cluster", + ApiVersion:"v3", + Metadata:clusters.CreateMetaData{Name:"test-cluster"}, + Spec:clusters.Spec{Type: "VirtualMachine", + Flavor: "cce.s1.small", + Version:"v1.7.3-r10", + HostNetwork:clusters.HostNetworkSpec{VpcId:"3b9740a0-b44d-48f0-84ee-42eb166e54f7", + SubnetId:"3e8e5957-649f-477b-9e5b-f1f75b21c045",}, + ContainerNetwork:clusters.ContainerNetworkSpec{Mode:"overlay_l2"}, + }, + } + cluster,err := clusters.Create(client,createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a cluster + + updateOpts := clusters.UpdateOpts{Spec:clusters.UpdateSpec{Description:"test"}} + + clusterID := "4e8e5957-649f-477b-9e5b-f1f75b21c03c" + + cluster,err := clusters.Update(client,clusterID,updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a cluster + + clusterID := "4e8e5957-649f-477b-9e5b-f1f75b21c03c" + + err := clusters.Delete(client,clusterID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package clusters diff --git a/openstack/cce/v3/clusters/requests.go b/openstack/cce/v3/clusters/requests.go new file mode 100644 index 000000000..9d89a53cd --- /dev/null +++ b/openstack/cce/v3/clusters/requests.go @@ -0,0 +1,192 @@ +package clusters + +import ( + "reflect" + + "github.com/huaweicloud/golangsdk" +) + +var RequestOpts golangsdk.RequestOpts = golangsdk.RequestOpts{ + MoreHeaders: map[string]string{"Content-Type": "application/json"}, +} + +// ListOpts allows the filtering of list data using given parameters. +type ListOpts struct { + Name string `json:"name"` + ID string `json:"uuid"` + Type string `json:"type"` + VpcID string `json:"vpc"` + Phase string `json:"phase"` +} + +// List returns collection of clusters. +func List(client *golangsdk.ServiceClient, opts ListOpts) ([]Clusters, error) { + var r ListResult + _, r.Err = client.Get(rootURL(client), &r.Body, &golangsdk.RequestOpts{ + OkCodes: []int{200}, + MoreHeaders: RequestOpts.MoreHeaders, JSONBody: nil, + }) + + allClusters, err := r.ExtractClusters() + if err != nil { + return nil, err + } + + return FilterClusters(allClusters, opts), nil +} + +func FilterClusters(clusters []Clusters, opts ListOpts) []Clusters { + + var refinedClusters []Clusters + var matched bool + m := map[string]FilterStruct{} + + if opts.Name != "" { + m["Name"] = FilterStruct{Value: opts.Name, Driller: []string{"Metadata"}} + } + if opts.ID != "" { + m["Id"] = FilterStruct{Value: opts.ID, Driller: []string{"Metadata"}} + } + if opts.Type != "" { + m["Type"] = FilterStruct{Value: opts.Type, Driller: []string{"Spec"}} + } + if opts.VpcID != "" { + m["VpcId"] = FilterStruct{Value: opts.VpcID, Driller: []string{"Spec", "HostNetwork"}} + } + if opts.Phase != "" { + m["Phase"] = FilterStruct{Value: opts.Phase, Driller: []string{"Status"}} + } + + if len(m) > 0 && len(clusters) > 0 { + for _, cluster := range clusters { + matched = true + + for key, value := range m { + if sVal := GetStructNestedField(&cluster, key, value.Driller); !(sVal == value.Value) { + matched = false + } + } + if matched { + refinedClusters = append(refinedClusters, cluster) + } + } + + } else { + refinedClusters = clusters + } + + return refinedClusters +} + +type FilterStruct struct { + Value string + Driller []string +} + +func GetStructNestedField(v *Clusters, field string, structDriller []string) string { + r := reflect.ValueOf(v) + for _, drillField := range structDriller { + f := reflect.Indirect(r).FieldByName(drillField).Interface() + r = reflect.ValueOf(f) + } + f1 := reflect.Indirect(r).FieldByName(field) + return string(f1.String()) +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToClusterCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains all the values needed to create a new cluster +type CreateOpts struct { + // API type, fixed value Cluster + Kind string `json:"kind" required:"true"` + // API version, fixed value v3 + ApiVersion string `json:"apiversion" required:"true"` + // Metadata required to create a cluster + Metadata CreateMetaData `json:"metadata" required:"true"` + // specifications to create a cluster + Spec Spec `json:"spec" required:"true"` +} + +// Metadata required to create a cluster +type CreateMetaData struct { + // Cluster unique name + Name string `json:"name" required:"true"` + // Cluster tag, key/value pair format + Labels map[string]string `json:"labels,omitempty"` + // Cluster annotation, key/value pair format + Annotations map[string]string `json:"annotations,omitempty"` +} + +// ToClusterCreateMap builds a create request body from CreateOpts. +func (opts CreateOpts) ToClusterCreateMap() (map[string]interface{}, error) { + return golangsdk.BuildRequestBody(opts, "") +} + +// Create accepts a CreateOpts struct and uses the values to create a new +// logical cluster. +func Create(c *golangsdk.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToClusterCreateMap() + if err != nil { + r.Err = err + return + } + reqOpt := &golangsdk.RequestOpts{OkCodes: []int{201}} + _, r.Err = c.Post(rootURL(c), b, &r.Body, reqOpt) + return +} + +// Get retrieves a particular cluster based on its unique ID. +func Get(c *golangsdk.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(resourceURL(c, id), &r.Body, &golangsdk.RequestOpts{ + OkCodes: []int{200}, + MoreHeaders: RequestOpts.MoreHeaders, JSONBody: nil, + }) + return +} + +// UpdateOpts contains all the values needed to update a new cluster +type UpdateOpts struct { + Spec UpdateSpec `json:"spec" required:"true"` +} + +type UpdateSpec struct { + // Cluster description + Description string `json:"description,omitempty"` +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToClusterUpdateMap() (map[string]interface{}, error) +} + +// ToClusterUpdateMap builds an update body based on UpdateOpts. +func (opts UpdateOpts) ToClusterUpdateMap() (map[string]interface{}, error) { + return golangsdk.BuildRequestBody(opts, "") +} + +// Update allows clusters to update description. +func Update(c *golangsdk.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToClusterUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &golangsdk.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete will permanently delete a particular cluster based on its unique ID. +func Delete(c *golangsdk.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(resourceURL(c, id), &golangsdk.RequestOpts{ + OkCodes: []int{200}, + MoreHeaders: RequestOpts.MoreHeaders, JSONBody: nil, + }) + return +} diff --git a/openstack/cce/v3/clusters/results.go b/openstack/cce/v3/clusters/results.go new file mode 100644 index 000000000..09b2d6625 --- /dev/null +++ b/openstack/cce/v3/clusters/results.go @@ -0,0 +1,161 @@ +package clusters + +import ( + "github.com/huaweicloud/golangsdk" +) + +type ListCluster struct { + // API type, fixed value Cluster + Kind string `json:"kind"` + //API version, fixed value v3 + ApiVersion string `json:"apiVersion"` + //all Clusters + Clusters []Clusters `json:"items"` +} + +type Clusters struct { + // API type, fixed value Cluster + Kind string `json:"kind" required:"true"` + //API version, fixed value v3 + ApiVersion string `json:"apiversion" required:"true"` + //Metadata of a Cluster + Metadata MetaData `json:"metadata" required:"true"` + //specifications of a Cluster + Spec Spec `json:"spec" required:"true"` + //status of a Cluster + Status Status `json:"status"` +} + +//Metadata required to create a cluster +type MetaData struct { + //Cluster unique name + Name string `json:"name"` + //Cluster unique Id + Id string `json:"uid"` + // Cluster tag, key/value pair format + Labels map[string]string `json:"labels,omitempty"` + //Cluster annotation, key/value pair format + Annotations map[string]string `json:"annotations,omitempty"` +} + +//Specifications to create a cluster +type Spec struct { + //Cluster Type: VirtualMachine, BareMetal, or Windows + Type string `json:"type" required:"true"` + // Cluster specifications + Flavor string `json:"flavor" required:"true"` + // For the cluster version, please fill in v1.7.3-r10 or v1.9.2-r1. Currently only Kubernetes 1.7 and 1.9 clusters are supported. + Version string `json:"version,omitempty"` + //Cluster description + Description string `json:"description,omitempty"` + // Node network parameters + HostNetwork HostNetworkSpec `json:"hostNetwork" required:"true"` + //Container network parameters + ContainerNetwork ContainerNetworkSpec `json:"containerNetwork" required:"true"` + // Charging mode of the cluster, which is 0 (on demand) + BillingMode int `json:"billingMode,omitempty"` + //Extended parameter for a cluster + ExtendParam map[string]string `json:"extendParam,omitempty"` +} + +// Node network parameters +type HostNetworkSpec struct { + //The ID of the VPC used to create the node + VpcId string `json:"vpc" required:"true"` + //The ID of the subnet used to create the node + SubnetId string `json:"subnet" required:"true"` + // The ID of the high speed network used to create bare metal nodes. + // This parameter is required when creating a bare metal cluster. + HighwaySubnet string `json:"highwaySubnet,omitempty"` +} + +//Container network parameters +type ContainerNetworkSpec struct { + //Container network type: overlay_l2 , underlay_ipvlan or vpc-router + Mode string `json:"mode" required:"true"` + //Container network segment: 172.16.0.0/16 ~ 172.31.0.0/16. If there is a network segment conflict, it will be automatically reselected. + Cidr string `json:"cidr,omitempty"` +} + +type Status struct { + //The state of the cluster + Phase string `json:"phase"` + //The ID of the Job that is operating asynchronously in the cluster + JobID string `json:"jobID"` + //Reasons for the cluster to become current + Reason string `json:"reason"` + //The status of each component in the cluster + Conditions Conditions `json:"conditions"` + //Kube-apiserver access address in the cluster + Endpoints []Endpoints `json:"endpoints"` +} + +type Conditions struct { + //The type of component + Type string `json:"type"` + //The state of the component + Status string `json:"status"` + //The reason that the component becomes current + Reason string `json:"reason"` +} + +type Endpoints struct { + //The address accessed within the user's subnet + Url string `json:"url"` + //Public network access address + Type string `json:"type"` +} + +type commonResult struct { + golangsdk.Result +} + +// Extract is a function that accepts a result and extracts a cluster. +func (r commonResult) Extract() (*Clusters, error) { + var s Clusters + err := r.ExtractInto(&s) + return &s, err +} + +// ExtractCluster is a function that accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +func (r commonResult) ExtractClusters() ([]Clusters, error) { + var s ListCluster + err := r.ExtractInto(&s) + if err != nil { + return nil, err + } + + return s.Clusters, nil + +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a Cluster. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Cluster. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Cluster. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its ExtractErr +// method to determine if the request succeeded or failed. +type DeleteResult struct { + golangsdk.ErrResult +} + +// ListResult represents the result of a list operation. Call its ExtractCluster +// method to interpret it as a Cluster. +type ListResult struct { + commonResult +} diff --git a/openstack/cce/v3/clusters/testing/doc.go b/openstack/cce/v3/clusters/testing/doc.go new file mode 100644 index 000000000..d859a1992 --- /dev/null +++ b/openstack/cce/v3/clusters/testing/doc.go @@ -0,0 +1,2 @@ +// cluster unit tests +package testing diff --git a/openstack/cce/v3/clusters/testing/fixtures.go b/openstack/cce/v3/clusters/testing/fixtures.go new file mode 100644 index 000000000..037533a62 --- /dev/null +++ b/openstack/cce/v3/clusters/testing/fixtures.go @@ -0,0 +1,118 @@ +package testing + +import ( + "github.com/huaweicloud/golangsdk/openstack/cce/v3/clusters" +) + +const Output = ` +{ + "kind": "Cluster", + "apiVersion": "v3", + "metadata": { + "name": "test-cluster", + "uid": "daa97872-59d7-11e8-a787-0255ac101f54" + }, + "spec": { + "type": "VirtualMachine", + "flavor": "cce.s1.small", + "version": "v1.7.3-r10", + "hostNetwork": { + "vpc": "3305eb40-2707-4940-921c-9f335f84a2ca", + "subnet": "00e41db7-e56b-4946-bf91-27bb9effd664" + }, + "containerNetwork": { + "mode": "overlay_l2" + }, + "billingMode": 0 + }, + "status": { + "phase": "Available", + "endpoints": [ + { + "url": "https://192.168.0.68:5443", + "type": "Internal" + } + ] + } +}` + +var Expected = &clusters.Clusters{ + Kind: "Cluster", + ApiVersion: "v3", + Metadata: clusters.MetaData{ + Name: "test-cluster", + Id: "daa97872-59d7-11e8-a787-0255ac101f54", + }, + Spec: clusters.Spec{ + Type: "VirtualMachine", + Flavor: "cce.s1.small", + Version: "v1.7.3-r10", + HostNetwork: clusters.HostNetworkSpec{ + VpcId: "3305eb40-2707-4940-921c-9f335f84a2ca", + SubnetId: "00e41db7-e56b-4946-bf91-27bb9effd664", + }, + ContainerNetwork: clusters.ContainerNetworkSpec{ + Mode: "overlay_l2", + }, + BillingMode: 0, + }, + Status: clusters.Status{ + Phase: "Available", + Endpoints: []clusters.Endpoints{ + {Url: "https://192.168.0.68:5443", Type: "Internal"}, + }, + }, +} + +const ListOutput = ` +{ + "items": [ + { + "kind": "Cluster", + "apiVersion": "v3", + "metadata": { + "name": "test123", + "uid": "daa97872-59d7-11e8-a787-0255ac101f54" + }, + "spec": { + "type": "VirtualMachine", + "flavor": "cce.s1.small", + "version": "v1.7.3-r10", + "hostNetwork": { + "vpc": "3305eb40-2707-4940-921c-9f335f84a2ca", + "subnet": "00e41db7-e56b-4946-bf91-27bb9effd664" + }, + "containerNetwork": { + "mode": "overlay_l2" + }, + "billingMode": 0 + }, + "status": { + "phase": "Available", + "endpoints": [ + { + "url": "https://192.168.0.68:5443", + "type": "Internal" + } + ] + } + } + ] +} +` + +var ListExpected = []clusters.Clusters{ + { + Kind: "Cluster", + ApiVersion: "v3", + Metadata: clusters.MetaData{Name: "test123", Id: "daa97872-59d7-11e8-a787-0255ac101f54"}, + Spec: clusters.Spec{Type: "VirtualMachine", + Flavor: "cce.s1.small", + HostNetwork: clusters.HostNetworkSpec{VpcId: "3305eb40-2707-4940-921c-9f335f84a2ca", SubnetId: "00e41db7-e56b-4946-bf91-27bb9effd664"}, + ContainerNetwork: clusters.ContainerNetworkSpec{Mode: "overlay_l2"}, + BillingMode: 0, + Version: "v1.7.3-r10", + }, + Status: clusters.Status{Phase: "Available", Endpoints: []clusters.Endpoints{{Url: "https://192.168.0.68:5443", Type: "Internal"}}}, + }, +} diff --git a/openstack/cce/v3/clusters/testing/requests_test.go b/openstack/cce/v3/clusters/testing/requests_test.go new file mode 100644 index 000000000..e4f2a3d87 --- /dev/null +++ b/openstack/cce/v3/clusters/testing/requests_test.go @@ -0,0 +1,156 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/huaweicloud/golangsdk/openstack/cce/v3/clusters" + fake "github.com/huaweicloud/golangsdk/openstack/cce/v3/common" + th "github.com/huaweicloud/golangsdk/testhelper" +) + +func TestGetV3Cluster(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/api/v3/projects/c59fd21fd2a94963b822d8985b884673/clusters/daa97872-59d7-11e8-a787-0255ac101f54", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, Output) + }) + + actual, err := clusters.Get(fake.ServiceClient(), "daa97872-59d7-11e8-a787-0255ac101f54").Extract() + th.AssertNoErr(t, err) + expected := Expected + th.AssertDeepEquals(t, expected, actual) + +} + +func TestListV3Cluster(t *testing.T) { + + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/api/v3/projects/c59fd21fd2a94963b822d8985b884673/clusters", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ListOutput) + }) + + //count := 0 + + actual, err := clusters.List(fake.ServiceClient(), clusters.ListOpts{}) + if err != nil { + t.Errorf("Failed to extract clusters: %v", err) + } + + expected := ListExpected + + th.AssertDeepEquals(t, expected, actual) +} +func TestCreateV3Cluster(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/api/v3/projects/c59fd21fd2a94963b822d8985b884673/clusters", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + + th.TestJSONRequest(t, r, ` +{ + "kind": "Cluster", + "apiversion": "v3", + "metadata": { + "name": "test-cluster" + }, + "spec": { + "type": "VirtualMachine", + "flavor": "cce.s1.small", + "version": "v1.7.3-r10", + "hostNetwork": { + "vpc": "3305eb40-2707-4940-921c-9f335f84a2ca", + "subnet": "00e41db7-e56b-4946-bf91-27bb9effd664" + }, + "containerNetwork": { + "mode": "overlay_l2" + } + } + +} +`) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, Output) + }) + options := clusters.CreateOpts{Kind: "Cluster", + ApiVersion: "v3", + Metadata: clusters.CreateMetaData{Name: "test-cluster"}, + Spec: clusters.Spec{Type: "VirtualMachine", + Flavor: "cce.s1.small", + Version: "v1.7.3-r10", + HostNetwork: clusters.HostNetworkSpec{ + VpcId: "3305eb40-2707-4940-921c-9f335f84a2ca", + SubnetId: "00e41db7-e56b-4946-bf91-27bb9effd664"}, + ContainerNetwork: clusters.ContainerNetworkSpec{Mode: "overlay_l2"}, + }, + } + actual, err := clusters.Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + expected := Expected + th.AssertDeepEquals(t, expected, actual) + +} + +func TestUpdateV3Cluster(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/api/v3/projects/c59fd21fd2a94963b822d8985b884673/clusters/daa97872-59d7-11e8-a787-0255ac101f54", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "spec": { + "description": "new description" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, Output) + }) + options := clusters.UpdateOpts{Spec: clusters.UpdateSpec{Description: "new description"}} + actual, err := clusters.Update(fake.ServiceClient(), "daa97872-59d7-11e8-a787-0255ac101f54", options).Extract() + th.AssertNoErr(t, err) + expected := Expected + th.AssertDeepEquals(t, expected, actual) +} + +func TestDeleteV3Cluster(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/api/v3/projects/c59fd21fd2a94963b822d8985b884673/clusters/daa97872-59d7-11e8-a787-0255ac101f54", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusOK) + }) + + err := clusters.Delete(fake.ServiceClient(), "daa97872-59d7-11e8-a787-0255ac101f54").ExtractErr() + th.AssertNoErr(t, err) + +} diff --git a/openstack/cce/v3/clusters/urls.go b/openstack/cce/v3/clusters/urls.go new file mode 100644 index 000000000..3d483e90e --- /dev/null +++ b/openstack/cce/v3/clusters/urls.go @@ -0,0 +1,15 @@ +package clusters + +import "github.com/huaweicloud/golangsdk" + +const ( + rootPath = "clusters" +) + +func rootURL(client *golangsdk.ServiceClient) string { + return client.ServiceURL(rootPath) +} + +func resourceURL(c *golangsdk.ServiceClient, id string) string { + return c.ServiceURL(rootPath, id) +} diff --git a/openstack/cce/v3/common/common_tests.go b/openstack/cce/v3/common/common_tests.go new file mode 100644 index 000000000..598418f20 --- /dev/null +++ b/openstack/cce/v3/common/common_tests.go @@ -0,0 +1,14 @@ +package common + +import ( + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/testhelper/client" +) + +const TokenID = client.TokenID + +func ServiceClient() *golangsdk.ServiceClient { + sc := client.ServiceClient() + sc.ResourceBase = sc.Endpoint + "api/" + "v3/" + "projects/" + "c59fd21fd2a94963b822d8985b884673/" + return sc +} diff --git a/openstack/client.go b/openstack/client.go index a509f508f..f659719cf 100644 --- a/openstack/client.go +++ b/openstack/client.go @@ -511,6 +511,16 @@ func NewAntiDDoSV2(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) return sc, err } +func NewCCEV3(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "compute") + sc.Endpoint = strings.Replace(sc.Endpoint, "ecs", "cce", 1) + sc.Endpoint = strings.Replace(sc.Endpoint, "v2", "api/v3/projects", 1) + sc.Endpoint = strings.Replace(sc.Endpoint, "myhwclouds", "myhuaweicloud", 1) + sc.ResourceBase = sc.Endpoint + sc.Type = "cce" + return sc, err +} + // NewDMSServiceV1 creates a ServiceClient that may be used to access the v1 Distributed Message Service. func NewDMSServiceV1(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { sc, err := initClientOpts(client, eo, "network") From fdea87e5a2d61c3072101509ebd93f755d9cbc4f Mon Sep 17 00:00:00 2001 From: disha-wani Date: Fri, 20 Jul 2018 20:34:31 +0530 Subject: [PATCH 2/2] add cce node interfaces --- errors.go | 14 + openstack/cce/v3/nodes/doc.go | 62 +++++ openstack/cce/v3/nodes/requests.go | 195 ++++++++++++++ openstack/cce/v3/nodes/results.go | 230 +++++++++++++++++ openstack/cce/v3/nodes/testing/doc.go | 2 + openstack/cce/v3/nodes/testing/fixtures.go | 143 ++++++++++ .../cce/v3/nodes/testing/requests_test.go | 244 ++++++++++++++++++ openstack/cce/v3/nodes/urls.go | 20 ++ provider_client.go | 5 + 9 files changed, 915 insertions(+) create mode 100644 openstack/cce/v3/nodes/doc.go create mode 100644 openstack/cce/v3/nodes/requests.go create mode 100644 openstack/cce/v3/nodes/results.go create mode 100644 openstack/cce/v3/nodes/testing/doc.go create mode 100644 openstack/cce/v3/nodes/testing/fixtures.go create mode 100644 openstack/cce/v3/nodes/testing/requests_test.go create mode 100644 openstack/cce/v3/nodes/urls.go diff --git a/errors.go b/errors.go index 9975c2b56..d358c68eb 100644 --- a/errors.go +++ b/errors.go @@ -72,6 +72,11 @@ type ErrDefault401 struct { ErrUnexpectedResponseCode } +// ErrDefault403 is the default error type returned on a 403 HTTP response code. +type ErrDefault403 struct { + ErrUnexpectedResponseCode +} + // ErrDefault404 is the default error type returned on a 404 HTTP response code. type ErrDefault404 struct { ErrUnexpectedResponseCode @@ -108,6 +113,9 @@ func (e ErrDefault400) Error() string { func (e ErrDefault401) Error() string { return "Authentication failed" } +func (e ErrDefault403) Error() string { + return "Action Forbidden" +} func (e ErrDefault404) Error() string { return "Resource not found" } @@ -141,6 +149,12 @@ type Err401er interface { Error401(ErrUnexpectedResponseCode) error } +// Err403er is the interface resource error types implement to override the error message +// from a 403 error. +type Err403er interface { + Error403(ErrUnexpectedResponseCode) error +} + // Err404er is the interface resource error types implement to override the error message // from a 404 error. type Err404er interface { diff --git a/openstack/cce/v3/nodes/doc.go b/openstack/cce/v3/nodes/doc.go new file mode 100644 index 000000000..bd782158d --- /dev/null +++ b/openstack/cce/v3/nodes/doc.go @@ -0,0 +1,62 @@ +/* +Package nodes enables management and retrieval of nodes +CCE service. + +Example to List nodes + + clusterID := "4e8e5957-649f-477b-9e5b-f1f75b21c03c" + + listNodes := nodes.ListOpts{} + allNodes, err := nodes.List(client,clusterID).ExtractNode(listNodes) + + if err != nil { + panic(err) + } + + for _, node := range allNodes { + fmt.Printf("%+v\n", node) + } + +Example to Create a node + + createOpts := nodes.CreateOpts{Kind:"Node", + ApiVersion:"v3", + Metadata:nodes.CreateMetaData{Name:"node_1"}, + Spec:nodes.Spec{Flavor:"s1.medium", + Az:"az1.dc1", + Login:nodes.LoginSpec{"myKeypair"}, + Count:1, + RootVolume:nodes.VolumeSpec{Size:10,VolumeType:"SATA"}, + DataVolumes:[]nodes.VolumeSpec{{Size:10,VolumeType:"SATA"}}, + }, + } + node,err := nodes.Create(client,clusterID,createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a cluster + + clusterID := "4e8e5957-649f-477b-9e5b-f1f75b21c03c" + + nodeID := "3c8e5957-649f-477b-9e5b-f1f75b21c011" + + updateOpts := nodes.UpdateOpts{Metadata:nodes.UpdateMetadata{Name:"node_1"}} + node,err := nodes.Update(client,clusterID,nodeID,updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a cluster + + clusterID := "4e8e5957-649f-477b-9e5b-f1f75b21c03c" + + nodeID := "3c8e5957-649f-477b-9e5b-f1f75b21c011" + + err := nodes.Delete(client,clusterID,nodeID).Extract() + if err != nil { + panic(err) + } +*/ + +package nodes diff --git a/openstack/cce/v3/nodes/requests.go b/openstack/cce/v3/nodes/requests.go new file mode 100644 index 000000000..71caa2642 --- /dev/null +++ b/openstack/cce/v3/nodes/requests.go @@ -0,0 +1,195 @@ +package nodes + +import ( + "reflect" + + "github.com/huaweicloud/golangsdk" +) + +var RequestOpts golangsdk.RequestOpts = golangsdk.RequestOpts{ + MoreHeaders: map[string]string{"Content-Type": "application/json"}, +} + +// ListOpts allows the filtering of list data using given parameters. +type ListOpts struct { + Name string `json:"name"` + Uid string `json:"uid"` + Phase string `json:"phase"` +} + +// List returns collection of nodes. +func List(client *golangsdk.ServiceClient, clusterID string, opts ListOpts) ([]Nodes, error) { + var r ListResult + _, r.Err = client.Get(rootURL(client, clusterID), &r.Body, &golangsdk.RequestOpts{ + OkCodes: []int{200}, + MoreHeaders: RequestOpts.MoreHeaders, JSONBody: nil, + }) + + allNodes, err := r.ExtractNode() + + if err != nil { + return nil, err + } + + return FilterNodes(allNodes, opts), nil +} + +func FilterNodes(nodes []Nodes, opts ListOpts) []Nodes { + + var refinedNodes []Nodes + var matched bool + + m := map[string]FilterStruct{} + + if opts.Name != "" { + m["Name"] = FilterStruct{Value: opts.Name, Driller: []string{"Metadata"}} + } + if opts.Uid != "" { + m["Uid"] = FilterStruct{Value: opts.Uid, Driller: []string{"Metadata"}} + } + + if opts.Phase != "" { + m["Phase"] = FilterStruct{Value: opts.Phase, Driller: []string{"Status"}} + } + + if len(m) > 0 && len(nodes) > 0 { + for _, nodes := range nodes { + matched = true + + for key, value := range m { + if sVal := GetStructNestedField(&nodes, key, value.Driller); !(sVal == value.Value) { + matched = false + } + } + if matched { + refinedNodes = append(refinedNodes, nodes) + } + } + } else { + refinedNodes = nodes + } + return refinedNodes +} + +func GetStructNestedField(v *Nodes, field string, structDriller []string) string { + r := reflect.ValueOf(v) + for _, drillField := range structDriller { + f := reflect.Indirect(r).FieldByName(drillField).Interface() + r = reflect.ValueOf(f) + } + f1 := reflect.Indirect(r).FieldByName(field) + return string(f1.String()) +} + +type FilterStruct struct { + Value string + Driller []string +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOpts struct { + // API type, fixed value Node + Kind string `json:"kind" required:"true"` + // API version, fixed value v3 + ApiVersion string `json:"apiversion" required:"true"` + // Metadata required to create a Node + Metadata CreateMetaData `json:"metadata"` + // specifications to create a Node + Spec Spec `json:"spec" required:"true"` +} + +// Metadata required to create a Node +type CreateMetaData struct { + // Node name + Name string `json:"name,omitempty"` + // Node tag, key value pair format + Labels map[string]string `json:"labels,omitempty"` + // Node annotation, key value pair format + Annotations map[string]string `json:"annotations,omitempty"` +} + +// Create accepts a CreateOpts struct and uses the values to create a new +// logical Node. When it is created, the Node does not have an internal +// interface +type CreateOptsBuilder interface { + ToNodeCreateMap() (map[string]interface{}, error) +} + +// ToNodeCreateMap builds a create request body from CreateOpts. +func (opts CreateOpts) ToNodeCreateMap() (map[string]interface{}, error) { + return golangsdk.BuildRequestBody(opts, "") +} + +// Create accepts a CreateOpts struct and uses the values to create a new +// logical node. +func Create(c *golangsdk.ServiceClient, clusterid string, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToNodeCreateMap() + if err != nil { + r.Err = err + return + } + reqOpt := &golangsdk.RequestOpts{OkCodes: []int{201}} + _, r.Err = c.Post(rootURL(c, clusterid), b, &r.Body, reqOpt) + return +} + +// Get retrieves a particular nodes based on its unique ID and cluster ID. +func Get(c *golangsdk.ServiceClient, clusterid, nodeid string) (r GetResult) { + _, r.Err = c.Get(resourceURL(c, clusterid, nodeid), &r.Body, &golangsdk.RequestOpts{ + OkCodes: []int{200}, + MoreHeaders: RequestOpts.MoreHeaders, JSONBody: nil, + }) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToNodeUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts contains all the values needed to update a new node +type UpdateOpts struct { + Metadata UpdateMetadata `json:"metadata,omitempty"` +} + +type UpdateMetadata struct { + Name string `json:"name,omitempty"` +} + +// ToNodeUpdateMap builds an update body based on UpdateOpts. +func (opts UpdateOpts) ToNodeUpdateMap() (map[string]interface{}, error) { + return golangsdk.BuildRequestBody(opts, "") +} + +// Update allows nodes to be updated. +func Update(c *golangsdk.ServiceClient, clusterid, nodeid string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToNodeUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(resourceURL(c, clusterid, nodeid), b, &r.Body, &golangsdk.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete will permanently delete a particular node based on its unique ID and cluster ID. +func Delete(c *golangsdk.ServiceClient, clusterid, nodeid string) (r DeleteResult) { + _, r.Err = c.Delete(resourceURL(c, clusterid, nodeid), &golangsdk.RequestOpts{ + OkCodes: []int{200}, + MoreHeaders: RequestOpts.MoreHeaders, JSONBody: nil, + }) + return +} + +// GetJobDetails retrieves a particular job based on its unique ID +func GetJobDetails(c *golangsdk.ServiceClient, jobid string) (r GetResult) { + _, r.Err = c.Get(getJobURL(c, jobid), &r.Body, &golangsdk.RequestOpts{ + OkCodes: []int{200}, + MoreHeaders: RequestOpts.MoreHeaders, JSONBody: nil, + }) + return +} diff --git a/openstack/cce/v3/nodes/results.go b/openstack/cce/v3/nodes/results.go new file mode 100644 index 000000000..c9101d80d --- /dev/null +++ b/openstack/cce/v3/nodes/results.go @@ -0,0 +1,230 @@ +package nodes + +import ( + "github.com/huaweicloud/golangsdk" +) + +//Describes the Node Structure of cluster +type ListNode struct { + // API type, fixed value "List" + Kind string `json:"kind"` + // API version, fixed value "v3" + Apiversion string `json:"apiVersion"` + // all Clusters + Nodes []Nodes `json:"items"` +} + +// Individual nodes of the cluster +type Nodes struct { + // API type, fixed value " Host " + Kind string `json:"kind"` + // API version, fixed value v3 + Apiversion string `json:"apiVersion"` + // Node metadata + Metadata Metadata `json:"metadata"` + // Node detailed parameters + Spec Spec `json:"spec"` + // Node status information + Status Status `json:"status"` +} + +// Metadata required to create a node +type Metadata struct { + //Node name + Name string `json:"name"` + //Node ID + Id string `json:"uid"` + // Node tag, key value pair format + Labels map[string]string `json:"labels,omitempty"` + //Node annotation, keyvalue pair format + Annotations map[string]string `json:"annotations,omitempty"` +} + +// Describes Nodes specification +type Spec struct { + // Node specifications + Flavor string `json:"flavor" required:"true"` + // The value of the available partition name + Az string `json:"az" required:"true"` + // Node login parameters + Login LoginSpec `json:"login" required:"true"` + // System disk parameter of the node + RootVolume VolumeSpec `json:"rootVolume" required:"true"` + // The data disk parameter of the node must currently be a disk + DataVolumes []VolumeSpec `json:"dataVolumes" required:"true"` + // Elastic IP parameters of the node + PublicIP PublicIPSpec `json:"publicIP,omitempty"` + // The billing mode of the node: the value is 0 (on demand) + BillingMode int `json:"billingMode,omitempty"` + // Number of nodes when creating in batch + Count int `json:"count" required:"true"` + // Extended parameter + ExtendParam string `json:"extendParam,omitempty"` +} + +// Gives the current status of the node +type Status struct { + // The state of the Node + Phase string `json:"phase"` + // The virtual machine ID of the node in the ECS + ServerID string `json:"ServerID"` + // Elastic IP of the node + PublicIP string `json:"PublicIP"` + //Private IP of the node + PrivateIP string `json:"privateIP"` + // The ID of the Job that is operating asynchronously in the Node + JobID string `json:"jobID"` + // Reasons for the Node to become current + Reason string `json:"reason"` + // Details of the node transitioning to the current state + Message string `json:"message"` + //The status of each component in the Node + Conditions Conditions `json:"conditions"` +} + +type LoginSpec struct { + // Select the key pair name when logging in by key pair mode + SshKey string `json:"sshKey" required:"true"` +} + +type VolumeSpec struct { + // Disk size in GB + Size int `json:"size" required:"true"` + // Disk type + VolumeType string `json:"volumetype" required:"true"` + // Disk extension parameter + ExtendParam string `json:"extendParam,omitempty"` +} + +type PublicIPSpec struct { + // List of existing elastic IP IDs + Ids []string `json:"ids,omitempty"` + // The number of elastic IPs to be dynamically created + Count int `json:"count,omitempty"` + // Elastic IP parameters + Eip EipSpec `json:"eip,omitempty"` +} + +type EipSpec struct { + // The value of the iptype keyword + IpType string `json:"iptype,omitempty"` + // Elastic IP bandwidth parameters + Bandwidth BandwidthOpts `json:"bandwidth,omitempty"` +} + +type BandwidthOpts struct { + ChargeMode string `json:"chargemode,omitempty"` + Size int `json:"size,omitempty"` + ShareType string `json:"sharetype,omitempty"` +} + +type Conditions struct { + //The type of component + Type string `json:"type"` + //The state of the component + Status string `json:"status"` + //The reason that the component becomes current + Reason string `json:"reason"` +} + +// Describes the Job Structure +type Job struct { + // API type, fixed value "Job" + Kind string `json:"kind"` + // API version, fixed value "v3" + Apiversion string `json:"apiVersion"` + // Node metadata + Metadata JobMetadata `json:"metadata"` + // Node detailed parameters + Spec JobSpec `json:"spec"` + //Node status information + Status JobStatus `json:"status"` +} + +type JobMetadata struct { + // ID of the job + ID string `json:"uid"` +} + +type JobSpec struct { + // Type of job + Type string `json:"type"` + // ID of the cluster where the job is located + ClusterID string `json:"clusterUID"` + // ID of the IaaS resource for the job operation + ResourceID string `json:"resourceID"` + // The name of the IaaS resource for the job operation + ResourceName string `json:"resourceName"` + // List of child jobs + SubJobs []Job `json:"subJobs"` + // ID of the parent job + OwnerJob string `json:"ownerJob"` +} + +type JobStatus struct { + // Job status + Phase string `json:"phase"` + // The reason why the job becomes the current state + Reason string `json:"reason"` + // The job becomes the current state details + Message string `json:"message"` +} + +type commonResult struct { + golangsdk.Result +} + +// Extract is a function that accepts a result and extracts a node. +func (r commonResult) Extract() (*Nodes, error) { + var s Nodes + err := r.ExtractInto(&s) + return &s, err +} + +// ExtractNode is a function that accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +func (r commonResult) ExtractNode() ([]Nodes, error) { + var s ListNode + err := r.ExtractInto(&s) + if err != nil { + return nil, err + } + return s.Nodes, nil +} + +// ExtractJob is a function that accepts a result and extracts a job. +func (r commonResult) ExtractJob() (*Job, error) { + var s Job + err := r.ExtractInto(&s) + return &s, err +} + +// ListResult represents the result of a list operation. Call its ExtractNode +// method to interpret it as a Nodes. +type ListResult struct { + commonResult +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a Node. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Node. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Node. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its ExtractErr +// method to determine if the request succeeded or failed. +type DeleteResult struct { + golangsdk.ErrResult +} diff --git a/openstack/cce/v3/nodes/testing/doc.go b/openstack/cce/v3/nodes/testing/doc.go new file mode 100644 index 000000000..58f36ac97 --- /dev/null +++ b/openstack/cce/v3/nodes/testing/doc.go @@ -0,0 +1,2 @@ +// node unit tests +package testing diff --git a/openstack/cce/v3/nodes/testing/fixtures.go b/openstack/cce/v3/nodes/testing/fixtures.go new file mode 100644 index 000000000..6de5c5f78 --- /dev/null +++ b/openstack/cce/v3/nodes/testing/fixtures.go @@ -0,0 +1,143 @@ +package testing + +import "github.com/huaweicloud/golangsdk/openstack/cce/v3/nodes" + +const Output = `{ + "kind": "Host", + "apiVersion": "v3", + "metadata": { + "name": "test-node" + }, + "spec": { + "flavor": "s3.large.2", + "az": "cn-east-2a", + "login": { + "sshKey": "test-keypair" + }, + "rootVolume": { + "volumetype": "SATA", + "size": 40 + }, + "publicIP": { + "eip": { + "bandwidth": {} + } + }, + "dataVolumes": [ + { + "volumetype": "SATA", + "size": 100 + } + ] +} +}` + +var Expected = &nodes.Nodes{ + Kind: "Host", + Apiversion: "v3", + Metadata: nodes.Metadata{Name: "test-node"}, + Spec: nodes.Spec{ + Flavor: "s3.large.2", + Az: "cn-east-2a", + Login: nodes.LoginSpec{ + SshKey: "test-keypair", + }, + PublicIP: nodes.PublicIPSpec{Eip: nodes.EipSpec{Bandwidth: nodes.BandwidthOpts{}, IpType: ""}}, + RootVolume: nodes.VolumeSpec{ + VolumeType: "SATA", + Size: 40, + }, + DataVolumes: []nodes.VolumeSpec{ + { + VolumeType: "SATA", + Size: 100, + }, + }, + }, +} + +const JobOutput = `{ + "kind": "Job", + "metadata": { + "uid": "73ce052c-8b1b-11e8-8f9d-0255ac10193f" + }, + "spec": { + "type": "ScaleupCluster", + "clusterUID": "6951aa4d-88ef-11e8-b196-0255ac101c43", + "resourceName": "cluster-test", + "subJobs": [ + { + "kind": "Job", + "metadata": { + "uid": "73cc28df-8b1b-11e8-8f9d-0255ac10193f" + }, + "spec": { + "type": "CreateNode", + "clusterUID": "6951aa4d-88ef-11e8-b196-0255ac101c43", + "resourceName": "myhost", + "subJobs": [ + { + "kind": "Job", + "metadata": { + "uid": "73ce03fd-8b1b-11e8-8f9d-0255ac10193f" + }, + "spec": { + "type": "GetPSMCert", + "clusterUID": "6951aa4d-88ef-11e8-b196-0255ac101c43" + }, + "status": { + "phase": "Success" + } + }, + { + "kind": "Job", + "metadata": { + "uid": "73ce0473-8b1b-11e8-8f9d-0255ac10193f" + }, + "spec": { + "type": "InstallNode", + "clusterUID": "6951aa4d-88ef-11e8-b196-0255ac101c43", + "resourceID": "73bd7e31-8b1b-11e8-8f9d-0255ac10193f" + }, + "status": { + "phase": "Success" + } + } + ] + } + } + ] + }, + "status": { + "phase": "Success" + } +}` + +var ExpectedJob = &nodes.Job{ + Kind: "Job", + Status: nodes.JobStatus{Phase: "Success"}, + Metadata: nodes.JobMetadata{ID: "73ce052c-8b1b-11e8-8f9d-0255ac10193f"}, + Spec: nodes.JobSpec{Type: "ScaleupCluster", + ClusterID: "6951aa4d-88ef-11e8-b196-0255ac101c43", + ResourceName: "cluster-test", + SubJobs: []nodes.Job{{Kind: "Job", + Metadata: nodes.JobMetadata{ID: "73cc28df-8b1b-11e8-8f9d-0255ac10193f"}, + Spec: nodes.JobSpec{Type: "CreateNode", + ClusterID: "6951aa4d-88ef-11e8-b196-0255ac101c43", + ResourceName: "myhost", + SubJobs: []nodes.Job{{Kind: "Job", + Metadata: nodes.JobMetadata{ID: "73ce03fd-8b1b-11e8-8f9d-0255ac10193f"}, + Spec: nodes.JobSpec{Type: "GetPSMCert", + ClusterID: "6951aa4d-88ef-11e8-b196-0255ac101c43"}, + Status: nodes.JobStatus{Phase: "Success"}}, + {Kind: "Job", + Metadata: nodes.JobMetadata{ID: "73ce0473-8b1b-11e8-8f9d-0255ac10193f"}, + Spec: nodes.JobSpec{Type: "InstallNode", + ClusterID: "6951aa4d-88ef-11e8-b196-0255ac101c43", + ResourceID: "73bd7e31-8b1b-11e8-8f9d-0255ac10193f"}, + Status: nodes.JobStatus{Phase: "Success"}}, + }, + }, + }}, + }, +} diff --git a/openstack/cce/v3/nodes/testing/requests_test.go b/openstack/cce/v3/nodes/testing/requests_test.go new file mode 100644 index 000000000..dfae3d4cc --- /dev/null +++ b/openstack/cce/v3/nodes/testing/requests_test.go @@ -0,0 +1,244 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/huaweicloud/golangsdk/openstack/cce/v3/common" + "github.com/huaweicloud/golangsdk/openstack/cce/v3/nodes" + th "github.com/huaweicloud/golangsdk/testhelper" +) + +func TestListNode(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/api/v3/projects/c59fd21fd2a94963b822d8985b884673/clusters/cec124c2-58f1-11e8-ad73-0255ac101926/nodes", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "kind": "List", + "apiVersion": "v3", + "items": + [ + { + "kind": "Host", + "apiVersion": "v3", + "metadata": { + "name": "test-node-1234", + "uid": "b99acd73-5d7c-11e8-8e76-0255ac101929" + }, + "spec": { + "flavor": "s1.medium", + "az": "cn-east-2a", + "login": { + "sshKey": "test-keypair", + "userPassword": {} + }, + "rootVolume": { + "volumetype": "SATA", + "size": 40 + }, + "dataVolumes": [ + { + "volumetype": "SATA", + "size": 100 + } + ], + "publicIP": { + "eip": { + "bandwidth": {} + } + }, + "billingMode": 0 + }, + "status": { + "phase": "Active", + "serverId": "41748e56-33d4-46a1-aa57-2c8c29907995", + "privateIP": "192.168.0.3" + } + } + ] +} + `) + }) + + listNodes := nodes.ListOpts{Name: "test-node-1234"} + actual, err := nodes.List(fake.ServiceClient(), "cec124c2-58f1-11e8-ad73-0255ac101926", listNodes) + + if err != nil { + t.Errorf("Failed to extract nodes: %v", err) + } + + expected := []nodes.Nodes{ + { + Kind: "Host", + Apiversion: "v3", + Metadata: nodes.Metadata{Name: "test-node-1234", + Id: "b99acd73-5d7c-11e8-8e76-0255ac101929"}, + Spec: nodes.Spec{Az: "cn-east-2a", + Login: nodes.LoginSpec{SshKey: "test-keypair"}, + RootVolume: nodes.VolumeSpec{Size: 40, VolumeType: "SATA"}, + BillingMode: 0, + DataVolumes: []nodes.VolumeSpec{ + { + VolumeType: "SATA", + Size: 100, + }}, + Flavor: "s1.medium", + }, + Status: nodes.Status{Phase: "Active", ServerID: "41748e56-33d4-46a1-aa57-2c8c29907995", PrivateIP: "192.168.0.3"}, + }, + } + + th.AssertDeepEquals(t, expected, actual) +} + +func TestGetV3Node(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/api/v3/projects/c59fd21fd2a94963b822d8985b884673/clusters/cec124c2-58f1-11e8-ad73-0255ac101926/nodes/cf4bc001-58f1-11e8-ad73-0255ac101926", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, Output) + }) + + actual, err := nodes.Get(fake.ServiceClient(), "cec124c2-58f1-11e8-ad73-0255ac101926", "cf4bc001-58f1-11e8-ad73-0255ac101926").Extract() + th.AssertNoErr(t, err) + expected := Expected + th.AssertDeepEquals(t, expected, actual) + +} + +func TestCreateV3Node(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/api/v3/projects/c59fd21fd2a94963b822d8985b884673/clusters/cec124c2-58f1-11e8-ad73-0255ac101926/nodes", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + + th.TestJSONRequest(t, r, ` + { + "apiversion": "v3", + "kind": "Node", + "metadata": { + "name": "test-node" + }, + "spec": { + "az": "cn-east-2a", + "count": 1, + "dataVolumes": [ + { + "size": 100, + "volumetype": "SATA" + } + ], + "flavor": "s3.large.2", + "login": { + "sshKey": "test-keypair" + }, + "publicIP": { + "eip": { + "bandwidth": {} + } + }, + "rootVolume": { + "size": 40, + "volumetype": "SATA" + } + } + } +`) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, Output) + }) + options := nodes.CreateOpts{Kind: "Node", + ApiVersion: "v3", + Metadata: nodes.CreateMetaData{Name: "test-node"}, + Spec: nodes.Spec{Flavor: "s3.large.2", Az: "cn-east-2a", + Login: nodes.LoginSpec{SshKey: "test-keypair"}, + RootVolume: nodes.VolumeSpec{Size: 40, VolumeType: "SATA"}, + DataVolumes: []nodes.VolumeSpec{{Size: 100, VolumeType: "SATA"}}, + Count: 1}, + } + actual, err := nodes.Create(fake.ServiceClient(), "cec124c2-58f1-11e8-ad73-0255ac101926", options).Extract() + th.AssertNoErr(t, err) + expected := Expected + th.AssertDeepEquals(t, expected, actual) + +} + +func TestUpdateV3Node(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/api/v3/projects/c59fd21fd2a94963b822d8985b884673/clusters/cec124c2-58f1-11e8-ad73-0255ac101926/nodes/cf4bc001-58f1-11e8-ad73-0255ac101926", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "metadata": { + "name": "test-node" + } +} `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, Output) + }) + options := nodes.UpdateOpts{Metadata: nodes.UpdateMetadata{Name: "test-node"}} + actual, err := nodes.Update(fake.ServiceClient(), "cec124c2-58f1-11e8-ad73-0255ac101926", "cf4bc001-58f1-11e8-ad73-0255ac101926", options).Extract() + th.AssertNoErr(t, err) + expected := Expected + th.AssertDeepEquals(t, expected, actual) +} + +func TestDeleteNode(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/api/v3/projects/c59fd21fd2a94963b822d8985b884673/clusters/cec124c2-58f1-11e8-ad73-0255ac101926/nodes/cf4bc001-58f1-11e8-ad73-0255ac101926", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusOK) + }) + + err := nodes.Delete(fake.ServiceClient(), "cec124c2-58f1-11e8-ad73-0255ac101926", "cf4bc001-58f1-11e8-ad73-0255ac101926").ExtractErr() + th.AssertNoErr(t, err) + +} + +func TestGetV3Job(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/api/v3/projects/c59fd21fd2a94963b822d8985b884673/jobs/73ce03fd-8b1b-11e8-8f9d-0255ac10193f", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, JobOutput) + }) + + actual, err := nodes.GetJobDetails(fake.ServiceClient(), "73ce03fd-8b1b-11e8-8f9d-0255ac10193f").ExtractJob() + th.AssertNoErr(t, err) + expected := ExpectedJob + th.AssertDeepEquals(t, expected, actual) + +} diff --git a/openstack/cce/v3/nodes/urls.go b/openstack/cce/v3/nodes/urls.go new file mode 100644 index 000000000..229214112 --- /dev/null +++ b/openstack/cce/v3/nodes/urls.go @@ -0,0 +1,20 @@ +package nodes + +import "github.com/huaweicloud/golangsdk" + +const ( + rootPath = "clusters" + resourcePath = "nodes" +) + +func rootURL(c *golangsdk.ServiceClient, clusterid string) string { + return c.ServiceURL(rootPath, clusterid, resourcePath) +} + +func resourceURL(c *golangsdk.ServiceClient, clusterid, nodeid string) string { + return c.ServiceURL(rootPath, clusterid, resourcePath, nodeid) +} + +func getJobURL(c *golangsdk.ServiceClient, jobid string) string { + return c.ServiceURL("jobs", jobid) +} diff --git a/provider_client.go b/provider_client.go index c2bacf5c6..11904f00b 100644 --- a/provider_client.go +++ b/provider_client.go @@ -301,6 +301,11 @@ func (client *ProviderClient) Request(method, url string, options *RequestOpts) if error401er, ok := errType.(Err401er); ok { err = error401er.Error401(respErr) } + case http.StatusForbidden: + err = ErrDefault403{respErr} + if error403er, ok := errType.(Err403er); ok { + err = error403er.Error403(respErr) + } case http.StatusNotFound: err = ErrDefault404{respErr} if error404er, ok := errType.(Err404er); ok {