diff --git a/acceptance/clients/clients.go b/acceptance/clients/clients.go index f82cf27ee..39a7f356f 100644 --- a/acceptance/clients/clients.go +++ b/acceptance/clients/clients.go @@ -1130,6 +1130,41 @@ func NewRMSClient() (client *golangsdk.ServiceClient, err error) { }) } +// NewEPSV1Client returns authenticated EPS v1 client. +// EPS requires domain-scoped authentication. The endpoint is discovered via +// NewEPSV1 (project-scoped catalog), then re-authenticated with domain scope. +func NewEPSV1Client() (client *golangsdk.ServiceClient, err error) { + cc, err := CloudAndClient() + if err != nil { + return nil, err + } + sc, err := openstack.NewEPSV1(cc.ProviderClient, golangsdk.EndpointOpts{ + Region: cc.RegionName, + }) + if err != nil { + return nil, err + } + + // Re-authenticate with domain scope for EPS API + cloud, err := EnvOS.Cloud() + if err != nil { + return nil, err + } + domainOpts := golangsdk.AuthOptions{ + IdentityEndpoint: cloud.AuthInfo.AuthURL, + Username: cloud.AuthInfo.Username, + Password: cloud.AuthInfo.Password, + DomainName: cloud.AuthInfo.UserDomainName, + AllowReauth: true, + } + domainProvider, err := openstack.AuthenticatedClient(domainOpts) + if err != nil { + return nil, err + } + sc.ProviderClient = domainProvider + return sc, nil +} + // NewCCIClient returns authenticated CCI v2 client func NewCCIClient() (client *golangsdk.ServiceClient, err error) { cc, err := CloudAndClient() diff --git a/acceptance/openstack/eps/v1/projects_test.go b/acceptance/openstack/eps/v1/projects_test.go new file mode 100644 index 000000000..a992e6c1b --- /dev/null +++ b/acceptance/openstack/eps/v1/projects_test.go @@ -0,0 +1,149 @@ +package v1 + +import ( + "fmt" + "testing" + "time" + + "github.com/opentelekomcloud/gophertelekomcloud/acceptance/clients" + "github.com/opentelekomcloud/gophertelekomcloud/openstack/eps/v1/projects" + "github.com/opentelekomcloud/gophertelekomcloud/openstack/eps/v1/providers" + "github.com/opentelekomcloud/gophertelekomcloud/openstack/eps/v1/resources" + "github.com/opentelekomcloud/gophertelekomcloud/openstack/eps/v1/versions" + th "github.com/opentelekomcloud/gophertelekomcloud/testhelper" +) + +func TestEnterpriseProjectsListAndGet(t *testing.T) { + t.Skip("enterprise project tests not for CI") + client, err := clients.NewEPSV1Client() + th.AssertNoErr(t, err) + + allProjects, err := projects.List(client, projects.ListOpts{}) + th.AssertNoErr(t, err) + + enterpriseProjectID := clients.EnvOS.GetEnv("ENTERPRISE_PROJECT_ID") + if enterpriseProjectID == "" { + if len(allProjects.EnterpriseProjects) == 0 { + t.Skip("no enterprise projects are available") + } + enterpriseProjectID = allProjects.EnterpriseProjects[0].ID + } + + p, err := projects.Get(client, enterpriseProjectID) + th.AssertNoErr(t, err) + th.AssertEquals(t, enterpriseProjectID, p.ID) +} + +func TestEnterpriseProjectsLifecycle(t *testing.T) { + t.Skip("enterprise project tests not for CI") + client, err := clients.NewEPSV1Client() + th.AssertNoErr(t, err) + + suffix := fmt.Sprintf("%d", time.Now().UnixNano()%100000) + createName := "eps-test-" + suffix + updateName := "eps-test-upd-" + suffix + + // Create + created, err := projects.Create(client, projects.CreateOpts{ + Name: createName, + Description: "Created by gophertelekomcloud acceptance test", + }) + th.AssertNoErr(t, err) + th.AssertEquals(t, createName, created.Name) + t.Logf("Created enterprise project: %s (%s)", created.Name, created.ID) + + // Ensure cleanup + defer func() { + _ = projects.Action(client, created.ID, projects.ActionOpts{Action: "disable"}) + }() + + // Update + updated, err := projects.Update(client, created.ID, projects.UpdateOpts{ + Name: updateName, + Description: "Updated by acceptance test", + }) + th.AssertNoErr(t, err) + th.AssertEquals(t, updateName, updated.Name) + + // List with name filter + filtered, err := projects.List(client, projects.ListOpts{ + Name: updateName, + }) + th.AssertNoErr(t, err) + if filtered.TotalCount < 1 { + t.Fatal("expected at least 1 project in filtered list") + } + + // Disable (Action) + err = projects.Action(client, created.ID, projects.ActionOpts{Action: "disable"}) + th.AssertNoErr(t, err) + t.Log("Disabled enterprise project") + + // Verify disabled + disabled, err := projects.Get(client, created.ID) + th.AssertNoErr(t, err) + th.AssertEquals(t, 2, disabled.Status) + + // Enable (Action) + err = projects.Action(client, created.ID, projects.ActionOpts{Action: "enable"}) + th.AssertNoErr(t, err) + t.Log("Enabled enterprise project") + + // Verify enabled + enabled, err := projects.Get(client, created.ID) + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, enabled.Status) +} + +func TestVersions(t *testing.T) { + t.Skip("enterprise project tests not for CI") + client, err := clients.NewEPSV1Client() + th.AssertNoErr(t, err) + + allVersions, err := versions.List(client) + th.AssertNoErr(t, err) + + if len(allVersions) == 0 { + t.Fatal("expected at least one API version") + } + t.Logf("Found %d API version(s)", len(allVersions)) + for _, v := range allVersions { + t.Logf(" %s (status: %s)", v.ID, v.Status) + } +} + +func TestProviders(t *testing.T) { + t.Skip("enterprise project tests not for CI") + client, err := clients.NewEPSV1Client() + th.AssertNoErr(t, err) + + allProviders, err := providers.List(client, providers.ListOpts{}) + th.AssertNoErr(t, err) + + if len(allProviders) == 0 { + t.Fatal("expected at least one provider") + } + t.Logf("Found %d provider(s)", len(allProviders)) + for _, p := range allProviders[:3] { + t.Logf(" %s (%s)", p.Provider, p.ProviderI18nDisplay) + } +} + +func TestResourcesFilter(t *testing.T) { + t.Skip("enterprise project tests not for CI") + client, err := clients.NewEPSV1Client() + th.AssertNoErr(t, err) + + projectID := clients.EnvOS.GetEnv("PROJECT_ID") + if projectID == "" { + t.Skip("OS_PROJECT_ID is required for resource filtering") + } + + result, err := resources.Filter(client, "0", resources.FilterOpts{ + Projects: []string{projectID}, + ResourceTypes: []string{"ecs"}, + Limit: 5, + }) + th.AssertNoErr(t, err) + t.Logf("Found %d resource(s) in default project (total: %d)", len(result.Resources), result.TotalCount) +} diff --git a/openstack/client.go b/openstack/client.go index d285b1516..285cf1c3e 100644 --- a/openstack/client.go +++ b/openstack/client.go @@ -1105,6 +1105,20 @@ func NewRmsServiceV1(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts return initClientOpts(client, eo, "rms") } +// NewEPSV1 creates a ServiceClient that may be used to access the EPS service. +// EPS is a global service — the endpoint URL does not include project_id. +func NewEPSV1(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initCommonServiceClient(client, eo, "eps", "v1.0") + if err != nil { + return nil, err + } + if client.ProjectID != "" { + sc.Endpoint = strings.Replace(sc.Endpoint, client.ProjectID+"/", "", 1) + } + sc.ResourceBase = sc.Endpoint + return sc, nil +} + func NewCCIServiceV2(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { sc, err := initClientOpts(client, eo, "ccev2.0") if err != nil { diff --git a/openstack/eps/v1/projects/Action.go b/openstack/eps/v1/projects/Action.go new file mode 100644 index 000000000..92d7a7190 --- /dev/null +++ b/openstack/eps/v1/projects/Action.go @@ -0,0 +1,22 @@ +package projects + +import ( + golangsdk "github.com/opentelekomcloud/gophertelekomcloud" + "github.com/opentelekomcloud/gophertelekomcloud/internal/build" +) + +type ActionOpts struct { + Action string `json:"action" required:"true"` +} + +func Action(client *golangsdk.ServiceClient, id string, opts ActionOpts) error { + b, err := build.RequestBody(opts, "") + if err != nil { + return err + } + + _, err = client.Post(client.ServiceURL("enterprise-projects", id, "action"), b, nil, &golangsdk.RequestOpts{ + OkCodes: []int{200, 204}, + }) + return err +} diff --git a/openstack/eps/v1/projects/Create.go b/openstack/eps/v1/projects/Create.go new file mode 100644 index 000000000..e59c4cdd5 --- /dev/null +++ b/openstack/eps/v1/projects/Create.go @@ -0,0 +1,30 @@ +package projects + +import ( + golangsdk "github.com/opentelekomcloud/gophertelekomcloud" + "github.com/opentelekomcloud/gophertelekomcloud/internal/build" + "github.com/opentelekomcloud/gophertelekomcloud/internal/extract" +) + +type CreateOpts struct { + Name string `json:"name" required:"true"` + Description string `json:"description,omitempty"` +} + +func Create(client *golangsdk.ServiceClient, opts CreateOpts) (*EnterpriseProject, error) { + b, err := build.RequestBody(opts, "") + if err != nil { + return nil, err + } + + raw, err := client.Post(client.ServiceURL("enterprise-projects"), b, nil, &golangsdk.RequestOpts{ + OkCodes: []int{200, 201}, + }) + if err != nil { + return nil, err + } + + var res EnterpriseProject + err = extract.IntoStructPtr(raw.Body, &res, "enterprise_project") + return &res, err +} diff --git a/openstack/eps/v1/projects/EnterpriseProject.go b/openstack/eps/v1/projects/EnterpriseProject.go new file mode 100644 index 000000000..3c78e031b --- /dev/null +++ b/openstack/eps/v1/projects/EnterpriseProject.go @@ -0,0 +1,13 @@ +package projects + +// EnterpriseProject represents an EPS enterprise project. +type EnterpriseProject struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Type string `json:"type"` + Status int `json:"status"` + DeleteFlag bool `json:"delete_flag"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` +} diff --git a/openstack/eps/v1/projects/Get.go b/openstack/eps/v1/projects/Get.go new file mode 100644 index 000000000..68e5aef6a --- /dev/null +++ b/openstack/eps/v1/projects/Get.go @@ -0,0 +1,17 @@ +package projects + +import ( + golangsdk "github.com/opentelekomcloud/gophertelekomcloud" + "github.com/opentelekomcloud/gophertelekomcloud/internal/extract" +) + +func Get(client *golangsdk.ServiceClient, id string) (*EnterpriseProject, error) { + raw, err := client.Get(client.ServiceURL("enterprise-projects", id), nil, nil) + if err != nil { + return nil, err + } + + var res EnterpriseProject + err = extract.IntoStructPtr(raw.Body, &res, "enterprise_project") + return &res, err +} diff --git a/openstack/eps/v1/projects/List.go b/openstack/eps/v1/projects/List.go new file mode 100644 index 000000000..5aec77daf --- /dev/null +++ b/openstack/eps/v1/projects/List.go @@ -0,0 +1,37 @@ +package projects + +import ( + golangsdk "github.com/opentelekomcloud/gophertelekomcloud" + "github.com/opentelekomcloud/gophertelekomcloud/internal/extract" +) + +type ListOpts struct { + ID string `q:"id"` + Limit int `q:"limit"` + Name string `q:"name"` + Offset int `q:"offset"` + SortDir string `q:"sort_dir"` + SortKey string `q:"sort_key"` + Status int `q:"status"` +} + +type ListResult struct { + EnterpriseProjects []EnterpriseProject `json:"enterprise_projects"` + TotalCount int `json:"total_count"` +} + +func List(client *golangsdk.ServiceClient, opts ListOpts) (*ListResult, error) { + q, err := golangsdk.BuildQueryString(&opts) + if err != nil { + return nil, err + } + + raw, err := client.Get(client.ServiceURL("enterprise-projects")+q.String(), nil, nil) + if err != nil { + return nil, err + } + + var res ListResult + err = extract.Into(raw.Body, &res) + return &res, err +} diff --git a/openstack/eps/v1/projects/Update.go b/openstack/eps/v1/projects/Update.go new file mode 100644 index 000000000..64e7dac58 --- /dev/null +++ b/openstack/eps/v1/projects/Update.go @@ -0,0 +1,31 @@ +package projects + +import ( + golangsdk "github.com/opentelekomcloud/gophertelekomcloud" + "github.com/opentelekomcloud/gophertelekomcloud/internal/build" + "github.com/opentelekomcloud/gophertelekomcloud/internal/extract" +) + +type UpdateOpts struct { + Name string `json:"name" required:"true"` + Description string `json:"description,omitempty"` + Type string `json:"type,omitempty"` +} + +func Update(client *golangsdk.ServiceClient, id string, opts UpdateOpts) (*EnterpriseProject, error) { + b, err := build.RequestBody(opts, "") + if err != nil { + return nil, err + } + + raw, err := client.Put(client.ServiceURL("enterprise-projects", id), b, nil, &golangsdk.RequestOpts{ + OkCodes: []int{200}, + }) + if err != nil { + return nil, err + } + + var res EnterpriseProject + err = extract.IntoStructPtr(raw.Body, &res, "enterprise_project") + return &res, err +} diff --git a/openstack/eps/v1/projects/testing/requests_test.go b/openstack/eps/v1/projects/testing/requests_test.go new file mode 100644 index 000000000..d3daf0eac --- /dev/null +++ b/openstack/eps/v1/projects/testing/requests_test.go @@ -0,0 +1,98 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/opentelekomcloud/gophertelekomcloud/openstack/eps/v1/projects" + th "github.com/opentelekomcloud/gophertelekomcloud/testhelper" + "github.com/opentelekomcloud/gophertelekomcloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/enterprise-projects", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, http.MethodGet) + w.Header().Add("Content-Type", "application/json") + _, _ = fmt.Fprint(w, `{"enterprise_projects":[{"id":"0","name":"default","description":"Default project","status":1,"created_at":"2020-01-01T00:00:00Z","updated_at":"2020-01-02T00:00:00Z","type":"prod","delete_flag":false}],"total_count":1}`) + }) + + actual, err := projects.List(client.ServiceClient(), projects.ListOpts{}) + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, actual.TotalCount) + th.AssertEquals(t, 1, len(actual.EnterpriseProjects)) + th.AssertEquals(t, "0", actual.EnterpriseProjects[0].ID) + th.AssertEquals(t, "default", actual.EnterpriseProjects[0].Name) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/enterprise-projects/0", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, http.MethodGet) + w.Header().Add("Content-Type", "application/json") + _, _ = fmt.Fprint(w, `{"enterprise_project":{"id":"0","name":"default","description":"Default project","status":1,"created_at":"2020-01-01T00:00:00Z","updated_at":"2020-01-02T00:00:00Z","type":"prod","delete_flag":false}}`) + }) + + actual, err := projects.Get(client.ServiceClient(), "0") + th.AssertNoErr(t, err) + th.AssertEquals(t, "0", actual.ID) + th.AssertEquals(t, "default", actual.Name) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/enterprise-projects", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, http.MethodPost) + th.TestJSONRequest(t, r, `{"name":"project-a","description":"created by test"}`) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + _, _ = fmt.Fprint(w, `{"enterprise_project":{"id":"0","name":"project-a","description":"created by test","status":1,"type":"prod","delete_flag":false}}`) + }) + + actual, err := projects.Create(client.ServiceClient(), projects.CreateOpts{ + Name: "project-a", + Description: "created by test", + }) + th.AssertNoErr(t, err) + th.AssertEquals(t, "project-a", actual.Name) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/enterprise-projects/0", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, http.MethodPut) + th.TestJSONRequest(t, r, `{"name":"default","description":"updated by test"}`) + w.Header().Add("Content-Type", "application/json") + _, _ = fmt.Fprint(w, `{"enterprise_project":{"id":"0","name":"default","description":"updated by test","status":1,"type":"prod","delete_flag":false}}`) + }) + + actual, err := projects.Update(client.ServiceClient(), "0", projects.UpdateOpts{ + Name: "default", + Description: "updated by test", + }) + th.AssertNoErr(t, err) + th.AssertEquals(t, "updated by test", actual.Description) +} + +func TestAction(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/enterprise-projects/0/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, http.MethodPost) + th.TestJSONRequest(t, r, `{"action":"enable"}`) + w.WriteHeader(http.StatusNoContent) + }) + + err := projects.Action(client.ServiceClient(), "0", projects.ActionOpts{Action: "enable"}) + th.AssertNoErr(t, err) +} diff --git a/openstack/eps/v1/providers/list.go b/openstack/eps/v1/providers/list.go new file mode 100644 index 000000000..f0182d163 --- /dev/null +++ b/openstack/eps/v1/providers/list.go @@ -0,0 +1,44 @@ +package providers + +import ( + golangsdk "github.com/opentelekomcloud/gophertelekomcloud" + "github.com/opentelekomcloud/gophertelekomcloud/internal/extract" +) + +type Provider struct { + Provider string `json:"provider"` + ProviderI18nDisplay string `json:"provider_i18n_display_name"` +} + +type ListOpts struct { + Locale string `q:"locale"` + Limit int `q:"limit"` + Offset int `q:"offset"` +} + +func (opts ListOpts) ToQuery() (string, error) { + q, err := golangsdk.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// List retrieves the services (providers) that support enterprise projects. +func List(client *golangsdk.ServiceClient, opts ListOpts) ([]Provider, error) { + url := client.ServiceURL("enterprise-projects", "providers") + query, err := opts.ToQuery() + if err != nil { + return nil, err + } + url += query + + raw, err := client.Get(url, nil, nil) + if err != nil { + return nil, err + } + + var res []Provider + err = extract.IntoSlicePtr(raw.Body, &res, "providers") + return res, err +} diff --git a/openstack/eps/v1/providers/testing/list_test.go b/openstack/eps/v1/providers/testing/list_test.go new file mode 100644 index 000000000..e409a0957 --- /dev/null +++ b/openstack/eps/v1/providers/testing/list_test.go @@ -0,0 +1,49 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/opentelekomcloud/gophertelekomcloud/openstack/eps/v1/providers" + th "github.com/opentelekomcloud/gophertelekomcloud/testhelper" + "github.com/opentelekomcloud/gophertelekomcloud/testhelper/client" +) + +func TestListProviders(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/enterprise-projects/providers", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, http.MethodGet) + w.Header().Add("Content-Type", "application/json") + _, _ = fmt.Fprint(w, `{"providers":[{"provider":"ecs","provider_i18n_display_name":"Elastic Cloud Server"},{"provider":"evs","provider_i18n_display_name":"Elastic Volume Service"}]}`) + }) + + actual, err := providers.List(client.ServiceClient(), providers.ListOpts{}) + th.AssertNoErr(t, err) + th.AssertEquals(t, 2, len(actual)) + th.AssertEquals(t, "ecs", actual[0].Provider) + th.AssertEquals(t, "Elastic Cloud Server", actual[0].ProviderI18nDisplay) + th.AssertEquals(t, "evs", actual[1].Provider) +} + +func TestListProvidersWithOpts(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/enterprise-projects/providers", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, http.MethodGet) + th.AssertEquals(t, "en-us", r.URL.Query().Get("locale")) + th.AssertEquals(t, "10", r.URL.Query().Get("limit")) + w.Header().Add("Content-Type", "application/json") + _, _ = fmt.Fprint(w, `{"providers":[{"provider":"ecs","provider_i18n_display_name":"Elastic Cloud Server"}]}`) + }) + + actual, err := providers.List(client.ServiceClient(), providers.ListOpts{ + Locale: "en-us", + Limit: 10, + }) + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, len(actual)) +} diff --git a/openstack/eps/v1/resources/Filter.go b/openstack/eps/v1/resources/Filter.go new file mode 100644 index 000000000..6232a63f3 --- /dev/null +++ b/openstack/eps/v1/resources/Filter.go @@ -0,0 +1,58 @@ +package resources + +import ( + golangsdk "github.com/opentelekomcloud/gophertelekomcloud" + "github.com/opentelekomcloud/gophertelekomcloud/internal/build" + "github.com/opentelekomcloud/gophertelekomcloud/internal/extract" +) + +type Resource struct { + ProjectID string `json:"project_id"` + ProjectName string `json:"project_name"` + ResourceID string `json:"resource_id"` + ResourceName string `json:"resource_name"` + ResourceType string `json:"resource_type"` +} + +type ResourceError struct { + ProjectID string `json:"project_id"` + ErrorCode string `json:"error_code"` + ErrorMessage string `json:"error_msg"` +} + +type FilterResult struct { + Resources []Resource `json:"resources"` + Errors []ResourceError `json:"errors"` + TotalCount int `json:"total_count"` +} + +type Match struct { + Key string `json:"key"` + Value string `json:"value"` +} + +type FilterOpts struct { + Projects []string `json:"projects,omitempty"` + ResourceTypes []string `json:"resource_types"` + Offset int `json:"offset,omitempty"` + Limit int `json:"limit,omitempty"` + Matches []Match `json:"matches,omitempty"` +} + +func Filter(client *golangsdk.ServiceClient, projectID string, opts FilterOpts) (*FilterResult, error) { + b, err := build.RequestBody(opts, "") + if err != nil { + return nil, err + } + + raw, err := client.Post(client.ServiceURL("enterprise-projects", projectID, "resources", "filter"), b, nil, &golangsdk.RequestOpts{ + OkCodes: []int{200}, + }) + if err != nil { + return nil, err + } + + var res FilterResult + err = extract.Into(raw.Body, &res) + return &res, err +} diff --git a/openstack/eps/v1/resources/Migrate.go b/openstack/eps/v1/resources/Migrate.go new file mode 100644 index 000000000..6e4a8a5ad --- /dev/null +++ b/openstack/eps/v1/resources/Migrate.go @@ -0,0 +1,29 @@ +package resources + +import ( + golangsdk "github.com/opentelekomcloud/gophertelekomcloud" + "github.com/opentelekomcloud/gophertelekomcloud/internal/build" +) + +type MigrateResource struct { + ResourceID string `json:"resource_id"` + ResourceType string `json:"resource_type"` + RegionID string `json:"region_id,omitempty"` +} + +type MigrateOpts struct { + ProjectID string `json:"project_id"` + Resources []MigrateResource `json:"resources"` +} + +func Migrate(client *golangsdk.ServiceClient, projectID string, opts MigrateOpts) error { + b, err := build.RequestBody(opts, "") + if err != nil { + return err + } + + _, err = client.Post(client.ServiceURL("enterprise-projects", projectID, "resources-migrate"), b, nil, &golangsdk.RequestOpts{ + OkCodes: []int{204}, + }) + return err +} diff --git a/openstack/eps/v1/resources/testing/requests_test.go b/openstack/eps/v1/resources/testing/requests_test.go new file mode 100644 index 000000000..7300ad665 --- /dev/null +++ b/openstack/eps/v1/resources/testing/requests_test.go @@ -0,0 +1,54 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/opentelekomcloud/gophertelekomcloud/openstack/eps/v1/resources" + th "github.com/opentelekomcloud/gophertelekomcloud/testhelper" + "github.com/opentelekomcloud/gophertelekomcloud/testhelper/client" +) + +func TestFilter(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/enterprise-projects/project-id-1/resources/filter", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, http.MethodPost) + th.TestJSONRequest(t, r, `{"projects":["project-id-1"],"resource_types":["ecs"],"limit":10,"matches":[{"key":"resource_name","value":"my-server"}]}`) + w.Header().Add("Content-Type", "application/json") + _, _ = fmt.Fprint(w, `{"resources":[{"project_id":"project-id-1","project_name":"default","resource_id":"res-1","resource_name":"my-server","resource_type":"ecs"}],"errors":[],"total_count":1}`) + }) + + result, err := resources.Filter(client.ServiceClient(), "project-id-1", resources.FilterOpts{ + Projects: []string{"project-id-1"}, + ResourceTypes: []string{"ecs"}, + Limit: 10, + Matches: []resources.Match{{Key: "resource_name", Value: "my-server"}}, + }) + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, result.TotalCount) + th.AssertEquals(t, 1, len(result.Resources)) + th.AssertEquals(t, "res-1", result.Resources[0].ResourceID) + th.AssertEquals(t, "ecs", result.Resources[0].ResourceType) +} + +func TestMigrate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/enterprise-projects/source-project/resources-migrate", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, http.MethodPost) + th.TestJSONRequest(t, r, `{"project_id":"target-project","resources":[{"resource_id":"res-1","resource_type":"ecs"}]}`) + w.WriteHeader(http.StatusNoContent) + }) + + err := resources.Migrate(client.ServiceClient(), "source-project", resources.MigrateOpts{ + ProjectID: "target-project", + Resources: []resources.MigrateResource{ + {ResourceID: "res-1", ResourceType: "ecs"}, + }, + }) + th.AssertNoErr(t, err) +} diff --git a/openstack/eps/v1/versions/list.go b/openstack/eps/v1/versions/list.go new file mode 100644 index 000000000..b9a7d52d5 --- /dev/null +++ b/openstack/eps/v1/versions/list.go @@ -0,0 +1,38 @@ +package versions + +import ( + "strings" + + golangsdk "github.com/opentelekomcloud/gophertelekomcloud" + "github.com/opentelekomcloud/gophertelekomcloud/internal/extract" +) + +type Version struct { + ID string `json:"id"` + Links []Link `json:"links"` + Status string `json:"status"` + Update string `json:"updated"` +} + +type Link struct { + Href string `json:"href"` + Rel string `json:"rel"` +} + +// List queries available API versions at the EPS service root. +func List(client *golangsdk.ServiceClient) ([]Version, error) { + // Strip version path to query the root: https://eps.xxx/v1.0/ → https://eps.xxx/ + baseURL := client.Endpoint + if idx := strings.Index(baseURL, "/v"); idx > 0 { + baseURL = baseURL[:idx] + "/" + } + + raw, err := client.Get(baseURL, nil, nil) + if err != nil { + return nil, err + } + + var res []Version + err = extract.IntoSlicePtr(raw.Body, &res, "versions") + return res, err +} diff --git a/openstack/eps/v1/versions/testing/list_test.go b/openstack/eps/v1/versions/testing/list_test.go new file mode 100644 index 000000000..c5faed437 --- /dev/null +++ b/openstack/eps/v1/versions/testing/list_test.go @@ -0,0 +1,34 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/opentelekomcloud/gophertelekomcloud/openstack/eps/v1/versions" + th "github.com/opentelekomcloud/gophertelekomcloud/testhelper" + "github.com/opentelekomcloud/gophertelekomcloud/testhelper/client" +) + +func TestListVersions(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, http.MethodGet) + w.Header().Add("Content-Type", "application/json") + _, _ = fmt.Fprint(w, `{"versions":[{"id":"v1.0","links":[{"href":"https://eps.eu-de.otc.t-systems.com/v1.0/","rel":"self"}],"status":"CURRENT","updated":"2018-09-30T00:00:00Z"}]}`) + }) + + sc := client.ServiceClient() + // ServiceClient endpoint is like http://localhost:PORT/ + // versions.List strips /v... so it will hit "/" + sc.Endpoint = th.Endpoint() + + actual, err := versions.List(sc) + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, len(actual)) + th.AssertEquals(t, "v1.0", actual[0].ID) + th.AssertEquals(t, "CURRENT", actual[0].Status) + th.AssertEquals(t, "self", actual[0].Links[0].Rel) +} diff --git a/openstack/lts/v2/cloud-structuring/Delete.go b/openstack/lts/v2/cloud-structuring/Delete.go index 84da54d58..03d7ad11a 100644 --- a/openstack/lts/v2/cloud-structuring/Delete.go +++ b/openstack/lts/v2/cloud-structuring/Delete.go @@ -20,7 +20,7 @@ func Delete(client *golangsdk.ServiceClient, id string) (err error) { MoreHeaders: map[string]string{ "content-type": "application/json", }, - OkCodes: []int{200}, + OkCodes: []int{200, 204}, }) if err != nil { return err diff --git a/openstack/lts/v2/host-groups/Delete.go b/openstack/lts/v2/host-groups/Delete.go index f68b07eea..0b1b3c65d 100644 --- a/openstack/lts/v2/host-groups/Delete.go +++ b/openstack/lts/v2/host-groups/Delete.go @@ -22,7 +22,7 @@ func Delete(client *golangsdk.ServiceClient, opts DeleteOpts) (*DeleteResult, er MoreHeaders: map[string]string{ "content-type": "application/json", }, - OkCodes: []int{200}, + OkCodes: []int{200, 204}, }) if err != nil { return nil, err