From 827c03ee8d9e2658d26f749d68425789a05c3586 Mon Sep 17 00:00:00 2001 From: Pratik Mallya Date: Thu, 17 Sep 2015 00:10:47 -0500 Subject: [PATCH] Fix api interfaces for orchestration resources Some of the interfaces don't correspond well to the values expected by the requests and returned by api. --- .../v1/stackresources/fixtures.go | 94 ++++++-------- .../v1/stackresources/requests_test.go | 6 +- .../v1/stackresources/results.go | 118 +++++++++++------- openstack/orchestration/v1/stacks/fixtures.go | 52 ++++++-- openstack/orchestration/v1/stacks/requests.go | 18 ++- .../orchestration/v1/stacks/requests_test.go | 12 ++ openstack/orchestration/v1/stacks/results.go | 48 +++---- .../v1/stacktemplates/fixtures.go | 27 +--- .../v1/stacktemplates/requests.go | 4 +- .../v1/stacktemplates/requests_test.go | 48 +++---- .../v1/stacktemplates/results.go | 27 ++-- .../v1/stackresources/delegate_test.go | 2 +- .../v1/stacktemplates/delegate_test.go | 37 ++---- 13 files changed, 263 insertions(+), 230 deletions(-) diff --git a/openstack/orchestration/v1/stackresources/fixtures.go b/openstack/orchestration/v1/stackresources/fixtures.go index c3c3d3fb..2273ba75 100644 --- a/openstack/orchestration/v1/stackresources/fixtures.go +++ b/openstack/orchestration/v1/stackresources/fixtures.go @@ -28,10 +28,13 @@ var FindExpected = []Resource{ LogicalID: "hello_world", StatusReason: "state changed", UpdatedTime: time.Date(2015, 2, 5, 21, 33, 11, 0, time.UTC), + CreationTime: time.Date(2015, 2, 5, 21, 33, 10, 0, time.UTC), RequiredBy: []interface{}{}, Status: "CREATE_IN_PROGRESS", PhysicalID: "49181cd6-169a-4130-9455-31185bbfc5bf", Type: "OS::Nova::Server", + Attributes: map[string]interface{}{"SXSW": "atx"}, + Description: "Some resource", }, } @@ -40,6 +43,8 @@ const FindOutput = ` { "resources": [ { + "description": "Some resource", + "attributes": {"SXSW": "atx"}, "resource_name": "hello_world", "links": [ { @@ -54,6 +59,7 @@ const FindOutput = ` "logical_resource_id": "hello_world", "resource_status_reason": "state changed", "updated_time": "2015-02-05T21:33:11", + "creation_time": "2015-02-05T21:33:10", "required_by": [], "resource_status": "CREATE_IN_PROGRESS", "physical_resource_id": "49181cd6-169a-4130-9455-31185bbfc5bf", @@ -93,10 +99,13 @@ var ListExpected = []Resource{ LogicalID: "hello_world", StatusReason: "state changed", UpdatedTime: time.Date(2015, 2, 5, 21, 33, 11, 0, time.UTC), + CreationTime: time.Date(2015, 2, 5, 21, 33, 10, 0, time.UTC), RequiredBy: []interface{}{}, Status: "CREATE_IN_PROGRESS", PhysicalID: "49181cd6-169a-4130-9455-31185bbfc5bf", Type: "OS::Nova::Server", + Attributes: map[string]interface{}{"SXSW": "atx"}, + Description: "Some resource", }, } @@ -121,7 +130,10 @@ const ListOutput = `{ "required_by": [], "resource_status": "CREATE_IN_PROGRESS", "physical_resource_id": "49181cd6-169a-4130-9455-31185bbfc5bf", - "resource_type": "OS::Nova::Server" + "creation_time": "2015-02-05T21:33:10", + "resource_type": "OS::Nova::Server", + "attributes": {"SXSW": "atx"}, + "description": "Some resource" } ] }` @@ -162,6 +174,7 @@ var GetExpected = &Resource{ }, }, LogicalID: "wordpress_instance", + Attributes: map[string]interface{}{"SXSW": "atx"}, StatusReason: "state changed", UpdatedTime: time.Date(2014, 12, 10, 18, 34, 35, 0, time.UTC), RequiredBy: []interface{}{}, @@ -174,6 +187,8 @@ var GetExpected = &Resource{ const GetOutput = ` { "resource": { + "description": "Some resource", + "attributes": {"SXSW": "atx"}, "resource_name": "wordpress_instance", "description": "", "links": [ @@ -240,7 +255,7 @@ func HandleMetadataSuccessfully(t *testing.T, output string) { } // ListTypesExpected represents the expected object from a ListTypes request. -var ListTypesExpected = []string{ +var ListTypesExpected = resourceTypes{ "OS::Nova::Server", "OS::Heat::RandomString", "OS::Swift::Container", @@ -251,6 +266,18 @@ var ListTypesExpected = []string{ "OS::Nova::KeyPair", } +// same as above, but sorted +var SortedListTypesExpected = resourceTypes{ + "OS::Cinder::VolumeAttachment", + "OS::Heat::RandomString", + "OS::Nova::FloatingIP", + "OS::Nova::FloatingIPAssociation", + "OS::Nova::KeyPair", + "OS::Nova::Server", + "OS::Swift::Container", + "OS::Trove::Instance", +} + // ListTypesOutput represents the response body from a ListTypes request. const ListTypesOutput = ` { @@ -296,6 +323,11 @@ var GetSchemaExpected = &TypeSchema{ }, }, ResourceType: "OS::Heat::AResourceName", + SupportStatus: map[string]interface{}{ + "message": "A status message", + "status": "SUPPORTED", + "version": "2014.1", + }, } // GetSchemaOutput represents the response body from a Schema request. @@ -314,7 +346,12 @@ const GetSchemaOutput = ` "description": "A resource description." } }, - "resource_type": "OS::Heat::AResourceName" + "resource_type": "OS::Heat::AResourceName", + "support_status": { + "message": "A status message", + "status": "SUPPORTED", + "version": "2014.1" + } }` // HandleGetSchemaSuccessfully creates an HTTP handler at `/resource_types/OS::Heat::AResourceName` @@ -332,56 +369,7 @@ func HandleGetSchemaSuccessfully(t *testing.T, output string) { } // GetTemplateExpected represents the expected object from a Template request. -var GetTemplateExpected = &TypeTemplate{ - HeatTemplateFormatVersion: "2012-12-12", - Outputs: map[string]interface{}{ - "private_key": map[string]interface{}{ - "Description": "The private key if it has been saved.", - "Value": "{\"Fn::GetAtt\": [\"KeyPair\", \"private_key\"]}", - }, - "public_key": map[string]interface{}{ - "Description": "The public key.", - "Value": "{\"Fn::GetAtt\": [\"KeyPair\", \"public_key\"]}", - }, - }, - Parameters: map[string]interface{}{ - "name": map[string]interface{}{ - "Description": "The name of the key pair.", - "Type": "String", - }, - "public_key": map[string]interface{}{ - "Description": "The optional public key. This allows users to supply the public key from a pre-existing key pair. If not supplied, a new key pair will be generated.", - "Type": "String", - }, - "save_private_key": map[string]interface{}{ - "AllowedValues": []string{ - "True", - "true", - "False", - "false", - }, - "Default": false, - "Description": "True if the system should remember a generated private key; False otherwise.", - "Type": "String", - }, - }, - Resources: map[string]interface{}{ - "KeyPair": map[string]interface{}{ - "Properties": map[string]interface{}{ - "name": map[string]interface{}{ - "Ref": "name", - }, - "public_key": map[string]interface{}{ - "Ref": "public_key", - }, - "save_private_key": map[string]interface{}{ - "Ref": "save_private_key", - }, - }, - "Type": "OS::Nova::KeyPair", - }, - }, -} +var GetTemplateExpected = "{\n \"HeatTemplateFormatVersion\": \"2012-12-12\",\n \"Outputs\": {\n \"private_key\": {\n \"Description\": \"The private key if it has been saved.\",\n \"Value\": \"{\\\"Fn::GetAtt\\\": [\\\"KeyPair\\\", \\\"private_key\\\"]}\"\n },\n \"public_key\": {\n \"Description\": \"The public key.\",\n \"Value\": \"{\\\"Fn::GetAtt\\\": [\\\"KeyPair\\\", \\\"public_key\\\"]}\"\n }\n },\n \"Parameters\": {\n \"name\": {\n \"Description\": \"The name of the key pair.\",\n \"Type\": \"String\"\n },\n \"public_key\": {\n \"Description\": \"The optional public key. This allows users to supply the public key from a pre-existing key pair. If not supplied, a new key pair will be generated.\",\n \"Type\": \"String\"\n },\n \"save_private_key\": {\n \"AllowedValues\": [\n \"True\",\n \"true\",\n \"False\",\n \"false\"\n ],\n \"Default\": false,\n \"Description\": \"True if the system should remember a generated private key; False otherwise.\",\n \"Type\": \"String\"\n }\n },\n \"Resources\": {\n \"KeyPair\": {\n \"Properties\": {\n \"name\": {\n \"Ref\": \"name\"\n },\n \"public_key\": {\n \"Ref\": \"public_key\"\n },\n \"save_private_key\": {\n \"Ref\": \"save_private_key\"\n }\n },\n \"Type\": \"OS::Nova::KeyPair\"\n }\n }\n}" // GetTemplateOutput represents the response body from a Template request. const GetTemplateOutput = ` diff --git a/openstack/orchestration/v1/stackresources/requests_test.go b/openstack/orchestration/v1/stackresources/requests_test.go index f1378785..e5045a71 100644 --- a/openstack/orchestration/v1/stackresources/requests_test.go +++ b/openstack/orchestration/v1/stackresources/requests_test.go @@ -1,6 +1,7 @@ package stackresources import ( + "sort" "testing" "github.com/rackspace/gophercloud/pagination" @@ -75,6 +76,9 @@ func TestListResourceTypes(t *testing.T) { th.AssertNoErr(t, err) th.CheckDeepEquals(t, ListTypesExpected, actual) + // test if sorting works + sort.Sort(actual) + th.CheckDeepEquals(t, SortedListTypesExpected, actual) return true, nil }) @@ -103,5 +107,5 @@ func TestGetResourceTemplate(t *testing.T) { th.AssertNoErr(t, err) expected := GetTemplateExpected - th.AssertDeepEquals(t, expected, actual) + th.AssertDeepEquals(t, expected, string(actual)) } diff --git a/openstack/orchestration/v1/stackresources/results.go b/openstack/orchestration/v1/stackresources/results.go index df79d582..51c3c0cd 100644 --- a/openstack/orchestration/v1/stackresources/results.go +++ b/openstack/orchestration/v1/stackresources/results.go @@ -1,6 +1,7 @@ package stackresources import ( + "encoding/json" "fmt" "reflect" "time" @@ -12,15 +13,18 @@ import ( // Resource represents a stack resource. type Resource struct { - Links []gophercloud.Link `mapstructure:"links"` - LogicalID string `mapstructure:"logical_resource_id"` - Name string `mapstructure:"resource_name"` - PhysicalID string `mapstructure:"physical_resource_id"` - RequiredBy []interface{} `mapstructure:"required_by"` - Status string `mapstructure:"resource_status"` - StatusReason string `mapstructure:"resource_status_reason"` - Type string `mapstructure:"resource_type"` - UpdatedTime time.Time `mapstructure:"-"` + Attributes map[string]interface{} `mapstructure:"attributes"` + CreationTime time.Time `mapstructure:"-"` + Description string `mapstructure:"description"` + Links []gophercloud.Link `mapstructure:"links"` + LogicalID string `mapstructure:"logical_resource_id"` + Name string `mapstructure:"resource_name"` + PhysicalID string `mapstructure:"physical_resource_id"` + RequiredBy []interface{} `mapstructure:"required_by"` + Status string `mapstructure:"resource_status"` + StatusReason string `mapstructure:"resource_status_reason"` + Type string `mapstructure:"resource_type"` + UpdatedTime time.Time `mapstructure:"-"` } // FindResult represents the result of a Find operation. @@ -54,6 +58,13 @@ func (r FindResult) Extract() ([]Resource, error) { } res.Res[i].UpdatedTime = t } + if date, ok := resource["creation_time"]; ok && date != nil { + t, err := time.Parse(gophercloud.STACK_TIME_FMT, date.(string)) + if err != nil { + return nil, err + } + res.Res[i].CreationTime = t + } } return res.Res, nil @@ -75,18 +86,6 @@ func (r ResourcePage) IsEmpty() (bool, error) { return len(resources) == 0, nil } -// LastMarker returns the last container name in a ListResult. -func (r ResourcePage) LastMarker() (string, error) { - resources, err := ExtractResources(r) - if err != nil { - return "", err - } - if len(resources) == 0 { - return "", nil - } - return resources[len(resources)-1].PhysicalID, nil -} - // ExtractResources interprets the results of a single page from a List() call, producing a slice of Resource entities. func ExtractResources(page pagination.Page) ([]Resource, error) { casted := page.(ResourcePage).Body @@ -94,8 +93,9 @@ func ExtractResources(page pagination.Page) ([]Resource, error) { var response struct { Resources []Resource `mapstructure:"resources"` } - err := mapstructure.Decode(casted, &response) - + if err := mapstructure.Decode(casted, &response); err != nil { + return nil, err + } var resources []interface{} switch casted.(type) { case map[string]interface{}: @@ -115,9 +115,16 @@ func ExtractResources(page pagination.Page) ([]Resource, error) { } response.Resources[i].UpdatedTime = t } + if date, ok := resource["creation_time"]; ok && date != nil { + t, err := time.Parse(gophercloud.STACK_TIME_FMT, date.(string)) + if err != nil { + return nil, err + } + response.Resources[i].CreationTime = t + } } - return response.Resources, err + return response.Resources, nil } // GetResult represents the result of a Get operation. @@ -149,6 +156,13 @@ func (r GetResult) Extract() (*Resource, error) { } res.Res.UpdatedTime = t } + if date, ok := resource["creation_time"]; ok && date != nil { + t, err := time.Parse(gophercloud.STACK_TIME_FMT, date.(string)) + if err != nil { + return nil, err + } + res.Res.CreationTime = t + } return res.Res, nil } @@ -192,21 +206,42 @@ func (r ResourceTypePage) IsEmpty() (bool, error) { return len(rts) == 0, nil } +// resourceTypes represents the type that holds the result of ExtractResourceTypes. +// We define methods on this type to sort it before output +type resourceTypes []string + +func (r resourceTypes) Len() int { + return len(r) +} + +func (r resourceTypes) Swap(i, j int) { + r[i], r[j] = r[j], r[i] +} + +func (r resourceTypes) Less(i, j int) bool { + return r[i] < r[j] +} + // ExtractResourceTypes extracts and returns resource types. -func ExtractResourceTypes(page pagination.Page) ([]string, error) { +func ExtractResourceTypes(page pagination.Page) (resourceTypes, error) { + casted := page.(ResourceTypePage).Body + var response struct { - ResourceTypes []string `mapstructure:"resource_types"` + ResourceTypes resourceTypes `mapstructure:"resource_types"` } - err := mapstructure.Decode(page.(ResourceTypePage).Body, &response) - return response.ResourceTypes, err + if err := mapstructure.Decode(casted, &response); err != nil { + return nil, err + } + return response.ResourceTypes, nil } // TypeSchema represents a stack resource schema. type TypeSchema struct { - Attributes map[string]interface{} `mapstructure:"attributes"` - Properties map[string]interface{} `mapstrucutre:"properties"` - ResourceType string `mapstructure:"resource_type"` + Attributes map[string]interface{} `mapstructure:"attributes"` + Properties map[string]interface{} `mapstrucutre:"properties"` + ResourceType string `mapstructure:"resource_type"` + SupportStatus map[string]interface{} `mapstructure:"support_status"` } // SchemaResult represents the result of a Schema operation. @@ -230,31 +265,20 @@ func (r SchemaResult) Extract() (*TypeSchema, error) { return &res, nil } -// TypeTemplate represents a stack resource template. -type TypeTemplate struct { - HeatTemplateFormatVersion string - Outputs map[string]interface{} - Parameters map[string]interface{} - Resources map[string]interface{} -} - // TemplateResult represents the result of a Template operation. type TemplateResult struct { gophercloud.Result } -// Extract returns a pointer to a TypeTemplate object and is called after a +// Extract returns the template and is called after a // Template operation. -func (r TemplateResult) Extract() (*TypeTemplate, error) { +func (r TemplateResult) Extract() ([]byte, error) { if r.Err != nil { return nil, r.Err } - - var res TypeTemplate - - if err := mapstructure.Decode(r.Body, &res); err != nil { + template, err := json.MarshalIndent(r.Body, "", " ") + if err != nil { return nil, err } - - return &res, nil + return template, nil } diff --git a/openstack/orchestration/v1/stacks/fixtures.go b/openstack/orchestration/v1/stacks/fixtures.go index 3a621dab..f884d51c 100644 --- a/openstack/orchestration/v1/stacks/fixtures.go +++ b/openstack/orchestration/v1/stacks/fixtures.go @@ -63,6 +63,7 @@ var ListExpected = []ListedStack{ CreationTime: time.Date(2015, 2, 3, 20, 7, 39, 0, time.UTC), Status: "CREATE_COMPLETE", ID: "16ef0584-4458-41eb-87c8-0dc8d5f66c87", + Tags: []string{"rackspace", "atx"}, }, ListedStack{ Description: "Simple template to test heat commands", @@ -78,6 +79,7 @@ var ListExpected = []ListedStack{ UpdatedTime: time.Date(2014, 12, 11, 17, 40, 37, 0, time.UTC), Status: "UPDATE_COMPLETE", ID: "db6977b2-27aa-4775-9ae7-6213212d4ada", + Tags: []string{"sfo", "satx"}, }, } @@ -98,7 +100,8 @@ const FullListOutput = ` "creation_time": "2015-02-03T20:07:39", "updated_time": null, "stack_status": "CREATE_COMPLETE", - "id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87" + "id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87", + "tags": ["rackspace", "atx"] }, { "description": "Simple template to test heat commands", @@ -113,7 +116,8 @@ const FullListOutput = ` "creation_time": "2014-12-11T17:39:16", "updated_time": "2014-12-11T17:40:37", "stack_status": "UPDATE_COMPLETE", - "id": "db6977b2-27aa-4775-9ae7-6213212d4ada" + "id": "db6977b2-27aa-4775-9ae7-6213212d4ada", + "tags": ["sfo", "satx"] } ] } @@ -165,6 +169,7 @@ var GetExpected = &RetrievedStack{ Status: "CREATE_COMPLETE", ID: "16ef0584-4458-41eb-87c8-0dc8d5f66c87", TemplateDescription: "Simple template to test heat commands", + Tags: []string{"rackspace", "atx"}, } // GetOutput represents the response body from a Get request. @@ -194,7 +199,8 @@ const GetOutput = ` "stack_status": "CREATE_COMPLETE", "updated_time": null, "id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87", - "template_description": "Simple template to test heat commands" + "template_description": "Simple template to test heat commands", + "tags": ["rackspace", "atx"] } } ` @@ -248,7 +254,6 @@ var PreviewExpected = &PreviewedStack{ "OS::stack_name": "postman_stack", "OS::stack_id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87", }, - StatusReason: "Stack CREATE completed successfully", Name: "postman_stack", CreationTime: time.Date(2015, 2, 3, 20, 7, 39, 0, time.UTC), Links: []gophercloud.Link{ @@ -259,7 +264,6 @@ var PreviewExpected = &PreviewedStack{ }, Capabilities: []interface{}{}, NotificationTopics: []interface{}{}, - Status: "CREATE_COMPLETE", ID: "16ef0584-4458-41eb-87c8-0dc8d5f66c87", TemplateDescription: "Simple template to test heat commands", } @@ -316,6 +320,20 @@ var AbandonExpected = &AbandonedStack{ "type": "OS::Nova::Server", }, }, + Files: map[string]string{ + "file:///Users/prat8228/go/src/github.com/rackspace/rack/my_nova.yaml": "heat_template_version: 2014-10-16\nparameters:\n flavor:\n type: string\n description: Flavor for the server to be created\n default: 4353\n hidden: true\nresources:\n test_server:\n type: \"OS::Nova::Server\"\n properties:\n name: test-server\n flavor: 2 GB General Purpose v1\n image: Debian 7 (Wheezy) (PVHVM)\n", + }, + StackUserProjectID: "897686", + ProjectID: "897686", + Environment: map[string]interface{}{ + "encrypted_param_names": make([]map[string]interface{}, 0), + "parameter_defaults": make(map[string]interface{}), + "parameters": make(map[string]interface{}), + "resource_registry": map[string]interface{}{ + "file:///Users/prat8228/go/src/github.com/rackspace/rack/my_nova.yaml": "file:///Users/prat8228/go/src/github.com/rackspace/rack/my_nova.yaml", + "resources": make(map[string]interface{}), + }, + }, } // AbandonOutput represents the response body from an Abandon request. @@ -354,21 +372,35 @@ const AbandonOutput = ` "name": "hello_world", "resource_id": "8a310d36-46fc-436f-8be4-37a696b8ac63", "action": "CREATE", - "type": "OS::Nova::Server", + "type": "OS::Nova::Server" } - } + }, + "files": { + "file:///Users/prat8228/go/src/github.com/rackspace/rack/my_nova.yaml": "heat_template_version: 2014-10-16\nparameters:\n flavor:\n type: string\n description: Flavor for the server to be created\n default: 4353\n hidden: true\nresources:\n test_server:\n type: \"OS::Nova::Server\"\n properties:\n name: test-server\n flavor: 2 GB General Purpose v1\n image: Debian 7 (Wheezy) (PVHVM)\n" +}, + "environment": { + "encrypted_param_names": [], + "parameter_defaults": {}, + "parameters": {}, + "resource_registry": { + "file:///Users/prat8228/go/src/github.com/rackspace/rack/my_nova.yaml": "file:///Users/prat8228/go/src/github.com/rackspace/rack/my_nova.yaml", + "resources": {} + } + }, + "stack_user_project_id": "897686", + "project_id": "897686" }` // HandleAbandonSuccessfully creates an HTTP handler at `/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87/abandon` // on the test handler mux that responds with an `Abandon` response. -func HandleAbandonSuccessfully(t *testing.T) { - th.Mux.HandleFunc("/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87/abandon", func(w http.ResponseWriter, r *http.Request) { +func HandleAbandonSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c8/abandon", func(w http.ResponseWriter, r *http.Request) { th.TestMethod(t, r, "DELETE") th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) th.TestHeader(t, r, "Accept", "application/json") w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - fmt.Fprintf(w, AbandonOutput) + fmt.Fprintf(w, output) }) } diff --git a/openstack/orchestration/v1/stacks/requests.go b/openstack/orchestration/v1/stacks/requests.go index 0dd6af2c..bd8e59ef 100644 --- a/openstack/orchestration/v1/stacks/requests.go +++ b/openstack/orchestration/v1/stacks/requests.go @@ -2,6 +2,7 @@ package stacks import ( "errors" + "strings" "github.com/rackspace/gophercloud" "github.com/rackspace/gophercloud/pagination" @@ -60,6 +61,8 @@ type CreateOpts struct { Parameters map[string]string // (OPTIONAL) The timeout for stack creation in minutes. Timeout int + // (OPTIONAL) A list of tags to assosciate with the Stack + Tags []string } // ToStackCreateMap casts a CreateOpts struct to a map. @@ -97,6 +100,9 @@ func (opts CreateOpts) ToStackCreateMap() (map[string]interface{}, error) { s["timeout_mins"] = opts.Timeout } + if opts.Tags != nil { + s["tags"] = strings.Join(opts.Tags, ",") + } return s, nil } @@ -197,12 +203,12 @@ func (opts AdoptOpts) ToStackAdoptMap() (map[string]interface{}, error) { s["parameters"] = opts.Parameters } - if opts.Timeout == 0 { - return nil, errors.New("Required field 'Timeout' not provided.") + if opts.Timeout != 0 { + s["timeout"] = opts.Timeout } s["timeout_mins"] = opts.Timeout - return map[string]interface{}{"stack": s}, nil + return s, nil } // Adopt accepts an AdoptOpts struct and creates a new stack using the resources @@ -329,6 +335,8 @@ type UpdateOpts struct { Parameters map[string]string // (OPTIONAL) The timeout for stack creation in minutes. Timeout int + // (OPTIONAL) A list of tags to assosciate with the Stack + Tags []string } // ToStackUpdateMap casts a CreateOpts struct to a map. @@ -359,6 +367,10 @@ func (opts UpdateOpts) ToStackUpdateMap() (map[string]interface{}, error) { s["timeout_mins"] = opts.Timeout } + if opts.Tags != nil { + s["tags"] = strings.Join(opts.Tags, ",") + } + return s, nil } diff --git a/openstack/orchestration/v1/stacks/requests_test.go b/openstack/orchestration/v1/stacks/requests_test.go index 1e32ca2a..1606d982 100644 --- a/openstack/orchestration/v1/stacks/requests_test.go +++ b/openstack/orchestration/v1/stacks/requests_test.go @@ -215,3 +215,15 @@ func TestPreviewStack(t *testing.T) { expected := PreviewExpected th.AssertDeepEquals(t, expected, actual) } + +func TestAbandonStack(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleAbandonSuccessfully(t, AbandonOutput) + + actual, err := Abandon(fake.ServiceClient(), "postman_stack", "16ef0584-4458-41eb-87c8-0dc8d5f66c8").Extract() + th.AssertNoErr(t, err) + + expected := AbandonExpected + th.AssertDeepEquals(t, expected, actual) +} diff --git a/openstack/orchestration/v1/stacks/results.go b/openstack/orchestration/v1/stacks/results.go index dca06e4e..432bc8e0 100644 --- a/openstack/orchestration/v1/stacks/results.go +++ b/openstack/orchestration/v1/stacks/results.go @@ -69,6 +69,7 @@ type ListedStack struct { Name string `mapstructure:"stack_name"` Status string `mapstructure:"stack_status"` StatusReason string `mapstructure:"stack_status_reason"` + Tags []string `mapstructure:"tags"` UpdatedTime time.Time `mapstructure:"-"` } @@ -81,7 +82,7 @@ func ExtractStacks(page pagination.Page) ([]ListedStack, error) { Stacks []ListedStack `mapstructure:"stacks"` } - err := mapstructure.Decode(page.(StackPage).Body, &res) + err := mapstructure.Decode(casted, &res) if err != nil { return nil, err } @@ -133,6 +134,7 @@ type RetrievedStack struct { Name string `mapstructure:"stack_name"` Status string `mapstructure:"stack_status"` StatusReason string `mapstructure:"stack_status_reason"` + Tags []string `mapstructure:"tags"` TemplateDescription string `mapstructure:"template_description"` Timeout int `mapstructure:"timeout_mins"` UpdatedTime time.Time `mapstructure:"-"` @@ -200,21 +202,19 @@ type DeleteResult struct { // PreviewedStack represents the result of a Preview operation. type PreviewedStack struct { - Capabilities []interface{} `mapstructure:"capabilities"` - CreationTime time.Time `mapstructure:"-"` - Description string `mapstructure:"description"` - DisableRollback bool `mapstructure:"disable_rollback"` - ID string `mapstructure:"id"` - Links []gophercloud.Link `mapstructure:"links"` - Name string `mapstructure:"stack_name"` - NotificationTopics []interface{} `mapstructure:"notification_topics"` - Parameters map[string]string `mapstructure:"parameters"` - Resources []map[string]interface{} `mapstructure:"resources"` - Status string `mapstructure:"stack_status"` - StatusReason string `mapstructure:"stack_status_reason"` - TemplateDescription string `mapstructure:"template_description"` - Timeout int `mapstructure:"timeout_mins"` - UpdatedTime time.Time `mapstructure:"-"` + Capabilities []interface{} `mapstructure:"capabilities"` + CreationTime time.Time `mapstructure:"-"` + Description string `mapstructure:"description"` + DisableRollback bool `mapstructure:"disable_rollback"` + ID string `mapstructure:"id"` + Links []gophercloud.Link `mapstructure:"links"` + Name string `mapstructure:"stack_name"` + NotificationTopics []interface{} `mapstructure:"notification_topics"` + Parameters map[string]string `mapstructure:"parameters"` + Resources []interface{} `mapstructure:"resources"` + TemplateDescription string `mapstructure:"template_description"` + Timeout int `mapstructure:"timeout_mins"` + UpdatedTime time.Time `mapstructure:"-"` } // PreviewResult represents the result of a Preview operation. @@ -269,12 +269,16 @@ func (r PreviewResult) Extract() (*PreviewedStack, error) { // AbandonedStack represents the result of an Abandon operation. type AbandonedStack struct { - Status string `mapstructure:"status"` - Name string `mapstructure:"name"` - Template map[string]interface{} `mapstructure:"template"` - Action string `mapstructure:"action"` - ID string `mapstructure:"id"` - Resources map[string]interface{} `mapstructure:"resources"` + Status string `mapstructure:"status"` + Name string `mapstructure:"name"` + Template map[string]interface{} `mapstructure:"template"` + Action string `mapstructure:"action"` + ID string `mapstructure:"id"` + Resources map[string]interface{} `mapstructure:"resources"` + Files map[string]string `mapstructure:"files"` + StackUserProjectID string `mapstructure:"stack_user_project_id"` + ProjectID string `mapstructure:"project_id"` + Environment map[string]interface{} `mapstructure:"environment"` } // AbandonResult represents the result of an Abandon operation. diff --git a/openstack/orchestration/v1/stacktemplates/fixtures.go b/openstack/orchestration/v1/stacktemplates/fixtures.go index 71fa8089..fa9b3016 100644 --- a/openstack/orchestration/v1/stacktemplates/fixtures.go +++ b/openstack/orchestration/v1/stacktemplates/fixtures.go @@ -10,29 +10,7 @@ import ( ) // GetExpected represents the expected object from a Get request. -var GetExpected = &Template{ - Description: "Simple template to test heat commands", - HeatTemplateVersion: "2013-05-23", - Parameters: map[string]interface{}{ - "flavor": map[string]interface{}{ - "default": "m1.tiny", - "type": "string", - }, - }, - Resources: map[string]interface{}{ - "hello_world": map[string]interface{}{ - "type": "OS::Nova::Server", - "properties": map[string]interface{}{ - "key_name": "heat_key", - "flavor": map[string]interface{}{ - "get_param": "flavor", - }, - "image": "ad091b52-742f-469e-8f3c-fd81cadf0743", - "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n", - }, - }, - }, -} +var GetExpected = "{\n \"description\": \"Simple template to test heat commands\",\n \"heat_template_version\": \"2013-05-23\",\n \"parameters\": {\n \"flavor\": {\n \"default\": \"m1.tiny\",\n \"type\": \"string\"\n }\n },\n \"resources\": {\n \"hello_world\": {\n \"properties\": {\n \"flavor\": {\n \"get_param\": \"flavor\"\n },\n \"image\": \"ad091b52-742f-469e-8f3c-fd81cadf0743\",\n \"key_name\": \"heat_key\"\n },\n \"type\": \"OS::Nova::Server\"\n }\n }\n}" // GetOutput represents the response body from a Get request. const GetOutput = ` @@ -53,8 +31,7 @@ const GetOutput = ` "flavor": { "get_param": "flavor" }, - "image": "ad091b52-742f-469e-8f3c-fd81cadf0743", - "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n" + "image": "ad091b52-742f-469e-8f3c-fd81cadf0743" } } } diff --git a/openstack/orchestration/v1/stacktemplates/requests.go b/openstack/orchestration/v1/stacktemplates/requests.go index ad1e468d..c0cea357 100644 --- a/openstack/orchestration/v1/stacktemplates/requests.go +++ b/openstack/orchestration/v1/stacktemplates/requests.go @@ -23,14 +23,14 @@ type ValidateOptsBuilder interface { // ValidateOpts specifies the template validation parameters. type ValidateOpts struct { - Template map[string]interface{} + Template string TemplateURL string } // ToStackTemplateValidateMap assembles a request body based on the contents of a ValidateOpts. func (opts ValidateOpts) ToStackTemplateValidateMap() (map[string]interface{}, error) { vo := make(map[string]interface{}) - if opts.Template != nil { + if opts.Template != "" { vo["template"] = opts.Template return vo, nil } diff --git a/openstack/orchestration/v1/stacktemplates/requests_test.go b/openstack/orchestration/v1/stacktemplates/requests_test.go index d31c4ac9..42667c92 100644 --- a/openstack/orchestration/v1/stacktemplates/requests_test.go +++ b/openstack/orchestration/v1/stacktemplates/requests_test.go @@ -16,7 +16,7 @@ func TestGetTemplate(t *testing.T) { th.AssertNoErr(t, err) expected := GetExpected - th.AssertDeepEquals(t, expected, actual) + th.AssertDeepEquals(t, expected, string(actual)) } func TestValidateTemplate(t *testing.T) { @@ -25,29 +25,29 @@ func TestValidateTemplate(t *testing.T) { HandleValidateSuccessfully(t, ValidateOutput) opts := ValidateOpts{ - Template: map[string]interface{}{ - "heat_template_version": "2013-05-23", - "description": "Simple template to test heat commands", - "parameters": map[string]interface{}{ - "flavor": map[string]interface{}{ - "default": "m1.tiny", - "type": "string", - }, - }, - "resources": map[string]interface{}{ - "hello_world": map[string]interface{}{ - "type": "OS::Nova::Server", - "properties": map[string]interface{}{ - "key_name": "heat_key", - "flavor": map[string]interface{}{ - "get_param": "flavor", - }, - "image": "ad091b52-742f-469e-8f3c-fd81cadf0743", - "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n", - }, - }, - }, - }, + Template: `{ + "heat_template_version": "2013-05-23", + "description": "Simple template to test heat commands", + "parameters": { + "flavor": { + "default": "m1.tiny", + "type": "string" + } + }, + "resources": { + "hello_world": { + "type": "OS::Nova::Server", + "properties": { + "key_name": "heat_key", + "flavor": { + "get_param": "flavor" + }, + "image": "ad091b52-742f-469e-8f3c-fd81cadf0743", + "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n" + } + } + } + }`, } actual, err := Validate(fake.ServiceClient(), opts).Extract() th.AssertNoErr(t, err) diff --git a/openstack/orchestration/v1/stacktemplates/results.go b/openstack/orchestration/v1/stacktemplates/results.go index ac2f24b8..4e9ba5a4 100644 --- a/openstack/orchestration/v1/stacktemplates/results.go +++ b/openstack/orchestration/v1/stacktemplates/results.go @@ -1,42 +1,33 @@ package stacktemplates import ( + "encoding/json" "github.com/mitchellh/mapstructure" "github.com/rackspace/gophercloud" ) -// Template represents a stack template. -type Template struct { - Description string `mapstructure:"description"` - HeatTemplateVersion string `mapstructure:"heat_template_version"` - Parameters map[string]interface{} `mapstructure:"parameters"` - Resources map[string]interface{} `mapstructure:"resources"` -} - // GetResult represents the result of a Get operation. type GetResult struct { gophercloud.Result } -// Extract returns a pointer to a Template object and is called after a -// Get operation. -func (r GetResult) Extract() (*Template, error) { +// Extract returns the JSON template and is called after a Get operation. +func (r GetResult) Extract() ([]byte, error) { if r.Err != nil { return nil, r.Err } - - var res Template - if err := mapstructure.Decode(r.Body, &res); err != nil { + template, err := json.MarshalIndent(r.Body, "", " ") + if err != nil { return nil, err } - - return &res, nil + return template, nil } // ValidatedTemplate represents the parsed object returned from a Validate request. type ValidatedTemplate struct { - Description string - Parameters map[string]interface{} + Description string `mapstructure:"Description"` + Parameters map[string]interface{} `mapstructure:"Parameters"` + ParameterGroups map[string]interface{} `mapstructure:"ParameterGroups"` } // ValidateResult represents the result of a Validate operation. diff --git a/rackspace/orchestration/v1/stackresources/delegate_test.go b/rackspace/orchestration/v1/stackresources/delegate_test.go index 18e96141..116e44ce 100644 --- a/rackspace/orchestration/v1/stackresources/delegate_test.go +++ b/rackspace/orchestration/v1/stackresources/delegate_test.go @@ -104,5 +104,5 @@ func TestGetResourceTemplate(t *testing.T) { th.AssertNoErr(t, err) expected := os.GetTemplateExpected - th.AssertDeepEquals(t, expected, actual) + th.AssertDeepEquals(t, expected, string(actual)) } diff --git a/rackspace/orchestration/v1/stacktemplates/delegate_test.go b/rackspace/orchestration/v1/stacktemplates/delegate_test.go index d4006c47..d4d0f8f5 100644 --- a/rackspace/orchestration/v1/stacktemplates/delegate_test.go +++ b/rackspace/orchestration/v1/stacktemplates/delegate_test.go @@ -17,7 +17,7 @@ func TestGetTemplate(t *testing.T) { th.AssertNoErr(t, err) expected := os.GetExpected - th.AssertDeepEquals(t, expected, actual) + th.AssertDeepEquals(t, expected, string(actual)) } func TestValidateTemplate(t *testing.T) { @@ -26,29 +26,18 @@ func TestValidateTemplate(t *testing.T) { os.HandleValidateSuccessfully(t, os.ValidateOutput) opts := os.ValidateOpts{ - Template: map[string]interface{}{ - "heat_template_version": "2013-05-23", - "description": "Simple template to test heat commands", - "parameters": map[string]interface{}{ - "flavor": map[string]interface{}{ - "default": "m1.tiny", - "type": "string", - }, - }, - "resources": map[string]interface{}{ - "hello_world": map[string]interface{}{ - "type": "OS::Nova::Server", - "properties": map[string]interface{}{ - "key_name": "heat_key", - "flavor": map[string]interface{}{ - "get_param": "flavor", - }, - "image": "ad091b52-742f-469e-8f3c-fd81cadf0743", - "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n", - }, - }, - }, - }, + Template: `{ + "Description": "Simple template to test heat commands", + "Parameters": { + "flavor": { + "Default": "m1.tiny", + "Type": "String", + "NoEcho": "false", + "Description": "", + "Label": "flavor" + } + } + }`, } actual, err := Validate(fake.ServiceClient(), opts).Extract() th.AssertNoErr(t, err)