From 0021ab1c65f82cb0872f9921d77a93fba4e5c873 Mon Sep 17 00:00:00 2001 From: savasw Date: Tue, 19 Jun 2018 15:12:10 +0530 Subject: [PATCH 1/9] add rts v1 stack interfaces --- openstack/rts/v1/stacks/doc.go | 138 ++++++++ openstack/rts/v1/stacks/environment.go | 134 ++++++++ openstack/rts/v1/stacks/environment_test.go | 192 +++++++++++ openstack/rts/v1/stacks/errors.go | 33 ++ openstack/rts/v1/stacks/fixtures.go | 199 ++++++++++++ openstack/rts/v1/stacks/requests.go | 302 ++++++++++++++++++ openstack/rts/v1/stacks/results.go | 302 ++++++++++++++++++ openstack/rts/v1/stacks/template.go | 141 ++++++++ openstack/rts/v1/stacks/template_test.go | 148 +++++++++ openstack/rts/v1/stacks/testing/fixtures.go | 245 ++++++++++++++ .../rts/v1/stacks/testing/requests_test.go | 94 ++++++ openstack/rts/v1/stacks/urls.go | 23 ++ openstack/rts/v1/stacks/utils.go | 160 ++++++++++ openstack/rts/v1/stacks/utils_test.go | 94 ++++++ 14 files changed, 2205 insertions(+) create mode 100644 openstack/rts/v1/stacks/doc.go create mode 100644 openstack/rts/v1/stacks/environment.go create mode 100644 openstack/rts/v1/stacks/environment_test.go create mode 100644 openstack/rts/v1/stacks/errors.go create mode 100644 openstack/rts/v1/stacks/fixtures.go create mode 100644 openstack/rts/v1/stacks/requests.go create mode 100644 openstack/rts/v1/stacks/results.go create mode 100644 openstack/rts/v1/stacks/template.go create mode 100644 openstack/rts/v1/stacks/template_test.go create mode 100644 openstack/rts/v1/stacks/testing/fixtures.go create mode 100644 openstack/rts/v1/stacks/testing/requests_test.go create mode 100644 openstack/rts/v1/stacks/urls.go create mode 100644 openstack/rts/v1/stacks/utils.go create mode 100644 openstack/rts/v1/stacks/utils_test.go diff --git a/openstack/rts/v1/stacks/doc.go b/openstack/rts/v1/stacks/doc.go new file mode 100644 index 000000000..0e18b3770 --- /dev/null +++ b/openstack/rts/v1/stacks/doc.go @@ -0,0 +1,138 @@ +// Package stacks provides operation for working with Heat stacks. A stack is a +// group of resources (servers, load balancers, databases, and so forth) +// combined to fulfill a useful purpose. Based on a template, Heat orchestration +// engine creates an instantiated set of resources (a stack) to run the +// application framework or component specified (in the template). A stack is a +// running instance of a template. The result of creating a stack is a deployment +// of the application framework or component. + +/* +Package resources enables management and retrieval of +RTS service. + +Example to List Stacks + +lis:=stacks.ListOpts{SortDir:stacks.SortAsc,SortKey:stacks.SortStatus} + getstack,err:=stacks.List(client,lis).AllPages() + stacks,err:=stacks.ExtractStacks(getstack) + fmt.Println(stacks) + +Example to Create a Stacks +template := new(stacks.Template) + template.Bin = []byte(` + { + "heat_template_version": "2013-05-23", + "description": "Simple template to deploy", + "parameters": { + "image_id": { + "type": "string", + "description": "Image to be used for compute instance", + "label": "Image ID", + "default": "ea67839e-fd7a-4b99-9f81-13c4c8dc317c" + }, + "net_id": { + "type": "string", + "description": "The network to be used", + "label": "Network UUID", + "default": "7eb54ab6-5cdb-446a-abbe-0dda1885c76e" + }, + "instance_type": { + "type": "string", + "description": "Type of instance (flavor) to be used", + "label": "Instance Type", + "default": "s1.medium" + } + }, + "resources": { + "my_instance": { + "type": "OS::Nova::Server", + "properties": { + "image": { + "get_param": "image_id" + }, + "flavor": { + "get_param": "instance_type" + }, + "networks": [ + { + "network": { + "get_param": "net_id" + } + } + ] + } + } + } + }`) + + fmt.Println(template) + stack:=stacks.CreateOpts{Name:"terraform-providerr_disssssss",Timeout: 60, + TemplateOpts: template, } + outstack,err:=stacks.Create(client,stack).Extract() + if err != nil { + panic(err) + } + +Example to Update a Stacks + +template := new(stacks.Template) + template.Bin = []byte(` + { + "heat_template_version": "2013-05-23", + "description": "Simple template disha", + "parameters": { + "image_id": { + "type": "string", + "description": "Image to be used for compute instance", + "label": "Image ID", + "default": "ea67839e-fd7a-4b99-9f81-13c4c8dc317c" + }, + "net_id": { + "type": "string", + "description": "The network to be used", + "label": "Network UUID", + "default": "7eb54ab6-5cdb-446a-abbe-0dda1885c76e" + }, + "instance_type": { + "type": "string", + "description": "Type of instance (flavor) to be used", + "label": "Instance Type", + "default": "s1.medium" + } + }, + "resources": { + "my_instance": { + "type": "OS::Nova::Server", + "properties": { + "image": { + "get_param": "image_id" + }, + "flavor": { + "get_param": "instance_type" + }, + "networks": [ + { + "network": { + "get_param": "net_id" + } + } + ] + } + } + } + }`) + myopt:=stacks.UpdateOpts{TemplateOpts:template} + errup:=stacks.Update(client,"terraform-providerr_stack_bigdata","b631d6ce-9010-4c34-b994-cd5a10b13c86",myopt).ExtractErr() + if err != nil { + panic(err) + } + +Example to Delete a Stacks + + err:=stacks.Delete(client,"terraform-providerr_stack_bigdata","b631d6ce-9010-4c34-b994-cd5a10b13c86") + + if err != nil { + panic(err) + } +*/ +package stacks diff --git a/openstack/rts/v1/stacks/environment.go b/openstack/rts/v1/stacks/environment.go new file mode 100644 index 000000000..86989186f --- /dev/null +++ b/openstack/rts/v1/stacks/environment.go @@ -0,0 +1,134 @@ +package stacks + +import "strings" + +// Environment is a structure that represents stack environments +type Environment struct { + TE +} + +// EnvironmentSections is a map containing allowed sections in a stack environment file +var EnvironmentSections = map[string]bool{ + "parameters": true, + "parameter_defaults": true, + "resource_registry": true, +} + +// Validate validates the contents of the Environment +func (e *Environment) Validate() error { + if e.Parsed == nil { + if err := e.Parse(); err != nil { + return err + } + } + for key := range e.Parsed { + if _, ok := EnvironmentSections[key]; !ok { + return ErrInvalidEnvironment{Section: key} + } + } + return nil +} + +// Parse environment file to resolve the URL's of the resources. This is done by +// reading from the `Resource Registry` section, which is why the function is +// named GetRRFileContents. +func (e *Environment) getRRFileContents(ignoreIf igFunc) error { + // initialize environment if empty + if e.Files == nil { + e.Files = make(map[string]string) + } + if e.fileMaps == nil { + e.fileMaps = make(map[string]string) + } + + // get the resource registry + rr := e.Parsed["resource_registry"] + + // search the resource registry for URLs + switch rr.(type) { + // process further only if the resource registry is a map + case map[string]interface{}, map[interface{}]interface{}: + rrMap, err := toStringKeys(rr) + if err != nil { + return err + } + // the resource registry might contain a base URL for the resource. If + // such a field is present, use it. Otherwise, use the default base URL. + var baseURL string + if val, ok := rrMap["base_url"]; ok { + baseURL = val.(string) + } else { + baseURL = e.baseURL + } + + // The contents of the resource may be located in a remote file, which + // will be a template. Instantiate a temporary template to manage the + // contents. + tempTemplate := new(Template) + tempTemplate.baseURL = baseURL + tempTemplate.client = e.client + + // Fetch the contents of remote resource URL's + if err = tempTemplate.getFileContents(rr, ignoreIf, false); err != nil { + return err + } + // check the `resources` section (if it exists) for more URL's. Note that + // the previous call to GetFileContents was (deliberately) not recursive + // as we want more control over where to look for URL's + if val, ok := rrMap["resources"]; ok { + switch val.(type) { + // process further only if the contents are a map + case map[string]interface{}, map[interface{}]interface{}: + resourcesMap, err := toStringKeys(val) + if err != nil { + return err + } + for _, v := range resourcesMap { + switch v.(type) { + case map[string]interface{}, map[interface{}]interface{}: + resourceMap, err := toStringKeys(v) + if err != nil { + return err + } + var resourceBaseURL string + // if base_url for the resource type is defined, use it + if val, ok := resourceMap["base_url"]; ok { + resourceBaseURL = val.(string) + } else { + resourceBaseURL = baseURL + } + tempTemplate.baseURL = resourceBaseURL + if err := tempTemplate.getFileContents(v, ignoreIf, false); err != nil { + return err + } + } + } + } + } + // if the resource registry contained any URL's, store them. This can + // then be passed as parameter to api calls to Heat api. + e.Files = tempTemplate.Files + return nil + default: + return nil + } +} + +// function to choose keys whose values are other environment files +func ignoreIfEnvironment(key string, value interface{}) bool { + // base_url and hooks refer to components which cannot have urls + if key == "base_url" || key == "hooks" { + return true + } + // if value is not string, it cannot be a URL + valueString, ok := value.(string) + if !ok { + return true + } + // if value contains `::`, it must be a reference to another resource type + // e.g. OS::Nova::Server : Rackspace::Cloud::Server + if strings.Contains(valueString, "::") { + return true + } + return false +} diff --git a/openstack/rts/v1/stacks/environment_test.go b/openstack/rts/v1/stacks/environment_test.go new file mode 100644 index 000000000..18083a518 --- /dev/null +++ b/openstack/rts/v1/stacks/environment_test.go @@ -0,0 +1,192 @@ +package stacks + +import ( + "fmt" + "net/http" + "net/url" + "strings" + "testing" + + th "github.com/huaweicloud/golangsdk/testhelper" +) + +func TestEnvironmentValidation(t *testing.T) { + + environmentJSON := new(Environment) + environmentJSON.Bin = []byte(ValidJSONEnvironment) + err := environmentJSON.Validate() + th.AssertNoErr(t, err) + + environmentYAML := new(Environment) + environmentYAML.Bin = []byte(ValidYAMLEnvironment) + err = environmentYAML.Validate() + th.AssertNoErr(t, err) + + environmentInvalid := new(Environment) + environmentInvalid.Bin = []byte(InvalidEnvironment) + if err = environmentInvalid.Validate(); err == nil { + t.Error("environment validation did not catch invalid environment") + } +} + +func TestEnvironmentParsing(t *testing.T) { + environmentJSON := new(Environment) + environmentJSON.Bin = []byte(ValidJSONEnvironment) + err := environmentJSON.Parse() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, ValidJSONEnvironmentParsed, environmentJSON.Parsed) + + environmentYAML := new(Environment) + environmentYAML.Bin = []byte(ValidJSONEnvironment) + err = environmentYAML.Parse() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, ValidJSONEnvironmentParsed, environmentYAML.Parsed) + + environmentInvalid := new(Environment) + environmentInvalid.Bin = []byte("Keep Austin Weird") + err = environmentInvalid.Parse() + if err == nil { + t.Error("environment parsing did not catch invalid environment") + } +} + +func TestIgnoreIfEnvironment(t *testing.T) { + var keyValueTests = []struct { + key string + value interface{} + out bool + }{ + {"base_url", "afksdf", true}, + {"not_type", "hooks", false}, + {"get_file", "::", true}, + {"hooks", "dfsdfsd", true}, + {"type", "sdfubsduf.yaml", false}, + {"type", "sdfsdufs.environment", false}, + {"type", "sdfsdf.file", false}, + {"type", map[string]string{"key": "value"}, true}, + } + var result bool + for _, kv := range keyValueTests { + result = ignoreIfEnvironment(kv.key, kv.value) + if result != kv.out { + t.Errorf("key: %v, value: %v expected: %v, actual: %v", kv.key, kv.value, kv.out, result) + } + } +} + +func TestGetRRFileContents(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + environmentContent := ` +heat_template_version: 2013-05-23 + +description: + Heat WordPress template to support F18, using only Heat OpenStack-native + resource types, and without the requirement for heat-cfntools in the image. + WordPress is web software you can use to create a beautiful website or blog. + This template installs a single-instance WordPress deployment using a local + MySQL database to store the data. + +parameters: + + key_name: + type: string + description : Name of a KeyPair to enable SSH access to the instance + +resources: + wordpress_instance: + type: OS::Nova::Server + properties: + image: { get_param: image_id } + flavor: { get_param: instance_type } + key_name: { get_param: key_name }` + + dbContent := ` +heat_template_version: 2014-10-16 + +description: + Test template for Trove resource capabilities + +parameters: + db_pass: + type: string + hidden: true + description: Database access password + default: secrete + +resources: + +service_db: + type: OS::Trove::Instance + properties: + name: trove_test_db + datastore_type: mariadb + flavor: 1GB Instance + size: 10 + databases: + - name: test_data + users: + - name: kitchen_sink + password: { get_param: db_pass } + databases: [ test_data ]` + baseurl, err := getBasePath() + th.AssertNoErr(t, err) + + fakeEnvURL := strings.Join([]string{baseurl, "my_env.yaml"}, "/") + urlparsed, err := url.Parse(fakeEnvURL) + th.AssertNoErr(t, err) + // handler for my_env.yaml + th.Mux.HandleFunc(urlparsed.Path, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, environmentContent) + }) + + fakeDBURL := strings.Join([]string{baseurl, "my_db.yaml"}, "/") + urlparsed, err = url.Parse(fakeDBURL) + th.AssertNoErr(t, err) + + // handler for my_db.yaml + th.Mux.HandleFunc(urlparsed.Path, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, dbContent) + }) + + client := fakeClient{BaseClient: getHTTPClient()} + env := new(Environment) + env.Bin = []byte(`{"resource_registry": {"My::WP::Server": "my_env.yaml", "resources": {"my_db_server": {"OS::DBInstance": "my_db.yaml"}}}}`) + env.client = client + + err = env.Parse() + th.AssertNoErr(t, err) + err = env.getRRFileContents(ignoreIfEnvironment) + th.AssertNoErr(t, err) + expectedEnvFilesContent := "\nheat_template_version: 2013-05-23\n\ndescription:\n Heat WordPress template to support F18, using only Heat OpenStack-native\n resource types, and without the requirement for heat-cfntools in the image.\n WordPress is web software you can use to create a beautiful website or blog.\n This template installs a single-instance WordPress deployment using a local\n MySQL database to store the data.\n\nparameters:\n\n key_name:\n type: string\n description : Name of a KeyPair to enable SSH access to the instance\n\nresources:\n wordpress_instance:\n type: OS::Nova::Server\n properties:\n image: { get_param: image_id }\n flavor: { get_param: instance_type }\n key_name: { get_param: key_name }" + expectedDBFilesContent := "\nheat_template_version: 2014-10-16\n\ndescription:\n Test template for Trove resource capabilities\n\nparameters:\n db_pass:\n type: string\n hidden: true\n description: Database access password\n default: secrete\n\nresources:\n\nservice_db:\n type: OS::Trove::Instance\n properties:\n name: trove_test_db\n datastore_type: mariadb\n flavor: 1GB Instance\n size: 10\n databases:\n - name: test_data\n users:\n - name: kitchen_sink\n password: { get_param: db_pass }\n databases: [ test_data ]" + + th.AssertEquals(t, expectedEnvFilesContent, env.Files[fakeEnvURL]) + th.AssertEquals(t, expectedDBFilesContent, env.Files[fakeDBURL]) + + // Update env's fileMaps to replace relative filenames by absolute URLs. + env.fileMaps = map[string]string{ + "my_env.yaml": fakeEnvURL, + "my_db.yaml": fakeDBURL, + } + env.fixFileRefs() + + expectedParsed := map[string]interface{}{ + "resource_registry": map[string]interface{}{ + "My::WP::Server": fakeEnvURL, + "resources": map[string]interface{}{ + "my_db_server": map[string]interface{}{ + "OS::DBInstance": fakeDBURL, + }, + }, + }, + } + env.Parse() + th.AssertDeepEquals(t, expectedParsed, env.Parsed) +} diff --git a/openstack/rts/v1/stacks/errors.go b/openstack/rts/v1/stacks/errors.go new file mode 100644 index 000000000..20cf158e0 --- /dev/null +++ b/openstack/rts/v1/stacks/errors.go @@ -0,0 +1,33 @@ +package stacks + +import ( + "fmt" + + "github.com/huaweicloud/golangsdk" +) + +type ErrInvalidEnvironment struct { + golangsdk.BaseError + Section string +} + +func (e ErrInvalidEnvironment) Error() string { + return fmt.Sprintf("Environment has wrong section: %s", e.Section) +} + +type ErrInvalidDataFormat struct { + golangsdk.BaseError +} + +func (e ErrInvalidDataFormat) Error() string { + return fmt.Sprintf("Data in neither json nor yaml format.") +} + +type ErrInvalidTemplateFormatVersion struct { + golangsdk.BaseError + Version string +} + +func (e ErrInvalidTemplateFormatVersion) Error() string { + return fmt.Sprintf("Template format version not found.") +} diff --git a/openstack/rts/v1/stacks/fixtures.go b/openstack/rts/v1/stacks/fixtures.go new file mode 100644 index 000000000..58987d4bf --- /dev/null +++ b/openstack/rts/v1/stacks/fixtures.go @@ -0,0 +1,199 @@ +package stacks + +// ValidJSONTemplate is a valid OpenStack Heat template in JSON format +const ValidJSONTemplate = ` +{ + "heat_template_version": "2014-10-16", + "parameters": { + "flavor": { + "default": "debian2G", + "description": "Flavor for the server to be created", + "hidden": true, + "type": "string" + } + }, + "resources": { + "test_server": { + "properties": { + "flavor": "2 GB General Purpose v1", + "image": "Debian 7 (Wheezy) (PVHVM)", + "name": "test-server" + }, + "type": "OS::Nova::Server" + } + } +} +` + +// ValidYAMLTemplate is a valid OpenStack Heat template in YAML format +const ValidYAMLTemplate = ` +heat_template_version: 2014-10-16 +parameters: + flavor: + type: string + description: Flavor for the server to be created + default: debian2G + hidden: true +resources: + test_server: + type: "OS::Nova::Server" + properties: + name: test-server + flavor: 2 GB General Purpose v1 + image: Debian 7 (Wheezy) (PVHVM) +` + +// InvalidTemplateNoVersion is an invalid template as it has no `version` section +const InvalidTemplateNoVersion = ` +parameters: + flavor: + type: string + description: Flavor for the server to be created + default: debian2G + hidden: true +resources: + test_server: + type: "OS::Nova::Server" + properties: + name: test-server + flavor: 2 GB General Purpose v1 + image: Debian 7 (Wheezy) (PVHVM) +` + +// ValidJSONEnvironment is a valid environment for a stack in JSON format +const ValidJSONEnvironment = ` +{ + "parameters": { + "user_key": "userkey" + }, + "resource_registry": { + "My::WP::Server": "file:///home/shardy/git/heat-templates/hot/F18/WordPress_Native.yaml", + "OS::Quantum*": "OS::Neutron*", + "AWS::CloudWatch::Alarm": "file:///etc/heat/templates/AWS_CloudWatch_Alarm.yaml", + "OS::Metering::Alarm": "OS::Ceilometer::Alarm", + "AWS::RDS::DBInstance": "file:///etc/heat/templates/AWS_RDS_DBInstance.yaml", + "resources": { + "my_db_server": { + "OS::DBInstance": "file:///home/mine/all_my_cool_templates/db.yaml" + }, + "my_server": { + "OS::DBInstance": "file:///home/mine/all_my_cool_templates/db.yaml", + "hooks": "pre-create" + }, + "nested_stack": { + "nested_resource": { + "hooks": "pre-update" + }, + "another_resource": { + "hooks": [ + "pre-create", + "pre-update" + ] + } + } + } + } +} +` + +// ValidYAMLEnvironment is a valid environment for a stack in YAML format +const ValidYAMLEnvironment = ` +parameters: + user_key: userkey +resource_registry: + My::WP::Server: file:///home/shardy/git/heat-templates/hot/F18/WordPress_Native.yaml + # allow older templates with Quantum in them. + "OS::Quantum*": "OS::Neutron*" + # Choose your implementation of AWS::CloudWatch::Alarm + "AWS::CloudWatch::Alarm": "file:///etc/heat/templates/AWS_CloudWatch_Alarm.yaml" + #"AWS::CloudWatch::Alarm": "OS::Heat::CWLiteAlarm" + "OS::Metering::Alarm": "OS::Ceilometer::Alarm" + "AWS::RDS::DBInstance": "file:///etc/heat/templates/AWS_RDS_DBInstance.yaml" + resources: + my_db_server: + "OS::DBInstance": file:///home/mine/all_my_cool_templates/db.yaml + my_server: + "OS::DBInstance": file:///home/mine/all_my_cool_templates/db.yaml + hooks: pre-create + nested_stack: + nested_resource: + hooks: pre-update + another_resource: + hooks: [pre-create, pre-update] +` + +// InvalidEnvironment is an invalid environment as it has an extra section called `resources` +const InvalidEnvironment = ` +parameters: + flavor: + type: string + description: Flavor for the server to be created + default: debian2G + hidden: true +resources: + test_server: + type: "OS::Nova::Server" + properties: + name: test-server + flavor: 2 GB General Purpose v1 + image: Debian 7 (Wheezy) (PVHVM) +parameter_defaults: + KeyName: heat_key +` + +// ValidJSONEnvironmentParsed is the expected parsed version of ValidJSONEnvironment +var ValidJSONEnvironmentParsed = map[string]interface{}{ + "parameters": map[string]interface{}{ + "user_key": "userkey", + }, + "resource_registry": map[string]interface{}{ + "My::WP::Server": "file:///home/shardy/git/heat-templates/hot/F18/WordPress_Native.yaml", + "OS::Quantum*": "OS::Neutron*", + "AWS::CloudWatch::Alarm": "file:///etc/heat/templates/AWS_CloudWatch_Alarm.yaml", + "OS::Metering::Alarm": "OS::Ceilometer::Alarm", + "AWS::RDS::DBInstance": "file:///etc/heat/templates/AWS_RDS_DBInstance.yaml", + "resources": map[string]interface{}{ + "my_db_server": map[string]interface{}{ + "OS::DBInstance": "file:///home/mine/all_my_cool_templates/db.yaml", + }, + "my_server": map[string]interface{}{ + "OS::DBInstance": "file:///home/mine/all_my_cool_templates/db.yaml", + "hooks": "pre-create", + }, + "nested_stack": map[string]interface{}{ + "nested_resource": map[string]interface{}{ + "hooks": "pre-update", + }, + "another_resource": map[string]interface{}{ + "hooks": []interface{}{ + "pre-create", + "pre-update", + }, + }, + }, + }, + }, +} + +// ValidJSONTemplateParsed is the expected parsed version of ValidJSONTemplate +var ValidJSONTemplateParsed = map[string]interface{}{ + "heat_template_version": "2014-10-16", + "parameters": map[string]interface{}{ + "flavor": map[string]interface{}{ + "default": "debian2G", + "description": "Flavor for the server to be created", + "hidden": true, + "type": "string", + }, + }, + "resources": map[string]interface{}{ + "test_server": map[string]interface{}{ + "properties": map[string]interface{}{ + "flavor": "2 GB General Purpose v1", + "image": "Debian 7 (Wheezy) (PVHVM)", + "name": "test-server", + }, + "type": "OS::Nova::Server", + }, + }, +} diff --git a/openstack/rts/v1/stacks/requests.go b/openstack/rts/v1/stacks/requests.go new file mode 100644 index 000000000..a398ce251 --- /dev/null +++ b/openstack/rts/v1/stacks/requests.go @@ -0,0 +1,302 @@ +package stacks + +import ( + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/pagination" + "reflect" + "strings" +) + +// CreateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Create operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type CreateOptsBuilder interface { + ToStackCreateMap() (map[string]interface{}, error) +} + +// CreateOpts is the common options struct used in this package's Create +// operation. +type CreateOpts struct { + // The name of the stack. It must start with an alphabetic character. + Name string `json:"stack_name" required:"true"` + // A structure that contains either the template file or url. Call the + // associated methods to extract the information relevant to send in a create request. + TemplateOpts *Template `json:"-" required:"true"` + // Enables or disables deletion of all stack resources when a stack + // creation fails. Default is true, meaning all resources are not deleted when + // stack creation fails. + DisableRollback *bool `json:"disable_rollback,omitempty"` + // A structure that contains details for the environment of the stack. + EnvironmentOpts *Environment `json:"-"` + // User-defined parameters to pass to the template. + Parameters map[string]string `json:"parameters,omitempty"` + // The timeout for stack creation in minutes. + Timeout int `json:"timeout_mins,omitempty"` + // A list of tags to assosciate with the Stack + Tags []string `json:"-"` +} + +// ToStackCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToStackCreateMap() (map[string]interface{}, error) { + b, err := golangsdk.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + if err := opts.TemplateOpts.Parse(); err != nil { + return nil, err + } + + if err := opts.TemplateOpts.getFileContents(opts.TemplateOpts.Parsed, ignoreIfTemplate, true); err != nil { + return nil, err + } + opts.TemplateOpts.fixFileRefs() + b["template"] = string(opts.TemplateOpts.Bin) + + files := make(map[string]string) + for k, v := range opts.TemplateOpts.Files { + files[k] = v + } + + if opts.EnvironmentOpts != nil { + if err := opts.EnvironmentOpts.Parse(); err != nil { + return nil, err + } + if err := opts.EnvironmentOpts.getRRFileContents(ignoreIfEnvironment); err != nil { + return nil, err + } + opts.EnvironmentOpts.fixFileRefs() + for k, v := range opts.EnvironmentOpts.Files { + files[k] = v + } + b["environment"] = string(opts.EnvironmentOpts.Bin) + } + + if len(files) > 0 { + b["files"] = files + } + + if opts.Tags != nil { + b["tags"] = strings.Join(opts.Tags, ",") + } + + return b, nil +} + +// Create accepts a CreateOpts struct and creates a new stack using the values +// provided. +func Create(c *golangsdk.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToStackCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(createURL(c), b, &r.Body, nil) + return +} + +// SortDir is a type for specifying in which direction to sort a list of stacks. +type SortDir string + +// SortKey is a type for specifying by which key to sort a list of stacks. +type SortKey string + +var ( + // SortAsc is used to sort a list of stacks in ascending order. + SortAsc SortDir = "asc" + // SortDesc is used to sort a list of stacks in descending order. + SortDesc SortDir = "desc" + // SortName is used to sort a list of stacks by name. + SortName SortKey = "name" + // SortStatus is used to sort a list of stacks by status. + SortStatus SortKey = "status" + // SortCreatedAt is used to sort a list of stacks by date created. + SortCreatedAt SortKey = "created_at" + // SortUpdatedAt is used to sort a list of stacks by date updated. + SortUpdatedAt SortKey = "updated_at" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToStackListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the rts attributes you want to see returned. SortKey allows you to sort +// by a particular network attribute. SortDir sets the direction, and is either +// `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + ID string `q:"id"` + Status string `q:"status"` + Name string `q:"name"` + Marker string `q:"marker"` + Limit int `q:"limit"` + SortKey SortKey `q:"sort_keys"` + SortDir SortDir `q:"sort_dir"` +} + +// ToStackListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToStackListQuery() (string, error) { + q, err := golangsdk.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +func List(c *golangsdk.ServiceClient, opts ListOpts) ([]ListedStack, error) { + u := listURL(c) + pages, err := pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page { + return StackPage{pagination.LinkedPageBase{PageResult: r}} + }).AllPages() + + allStacks, err := ExtractStacks(pages) + if err != nil { + return nil, err + } + + return FilterStacks(allStacks, opts) +} + +func FilterStacks(stacks []ListedStack, opts ListOpts) ([]ListedStack, error) { + + var refinedStacks []ListedStack + var matched bool + m := map[string]interface{}{} + + if opts.ID != "" { + m["ID"] = opts.ID + } + if opts.Name != "" { + m["Name"] = opts.Name + } + if opts.Status != "" { + m["Status"] = opts.Status + } + + if len(m) > 0 && len(stacks) > 0 { + for _, stack := range stacks { + matched = true + + for key, value := range m { + if sVal := getStructField(&stack, key); !(sVal == value) { + matched = false + } + } + + if matched { + refinedStacks = append(refinedStacks, stack) + } + } + + } else { + refinedStacks = stacks + } + + return refinedStacks, nil +} + +func getStructField(v *ListedStack, field string) string { + r := reflect.ValueOf(v) + f := reflect.Indirect(r).FieldByName(field) + return string(f.String()) +} + +func Get(c *golangsdk.ServiceClient, stackName string) (r GetResult) { + _, r.Err = c.Get(getURL(c, stackName), &r.Body, nil) + return +} + +// UpdateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the Update operation in this package. +type UpdateOptsBuilder interface { + ToStackUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts contains the common options struct used in this package's Update +// operation. +type UpdateOpts struct { + // A structure that contains either the template file or url. Call the + // associated methods to extract the information relevant to send in a create request. + TemplateOpts *Template `json:"-" required:"true"` + // A structure that contains details for the environment of the stack. + EnvironmentOpts *Environment `json:"-"` + // User-defined parameters to pass to the template. + Parameters map[string]string `json:"parameters,omitempty"` + // The timeout for stack creation in minutes. + Timeout int `json:"timeout_mins,omitempty"` + // Enables or disables deletion of all stack resources when a stack + // creation fails. Default is true, meaning all resources are not deleted when + // stack creation fails. + DisableRollback *bool `json:"disable_rollback,omitempty"` + // A list of tags to assosciate with the Stack + Tags []string `json:"-"` +} + +// ToStackUpdateMap casts a CreateOpts struct to a map. +func (opts UpdateOpts) ToStackUpdateMap() (map[string]interface{}, error) { + b, err := golangsdk.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + if err := opts.TemplateOpts.Parse(); err != nil { + return nil, err + } + + if err := opts.TemplateOpts.getFileContents(opts.TemplateOpts.Parsed, ignoreIfTemplate, true); err != nil { + return nil, err + } + opts.TemplateOpts.fixFileRefs() + b["template"] = string(opts.TemplateOpts.Bin) + + files := make(map[string]string) + for k, v := range opts.TemplateOpts.Files { + files[k] = v + } + + if opts.EnvironmentOpts != nil { + if err := opts.EnvironmentOpts.Parse(); err != nil { + return nil, err + } + if err := opts.EnvironmentOpts.getRRFileContents(ignoreIfEnvironment); err != nil { + return nil, err + } + opts.EnvironmentOpts.fixFileRefs() + for k, v := range opts.EnvironmentOpts.Files { + files[k] = v + } + b["environment"] = string(opts.EnvironmentOpts.Bin) + } + + if len(files) > 0 { + b["files"] = files + } + + if opts.Tags != nil { + b["tags"] = strings.Join(opts.Tags, ",") + } + + return b, nil +} + +// Update accepts an UpdateOpts struct and updates an existing stack using the values +// provided. +func Update(c *golangsdk.ServiceClient, stackName, stackID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToStackUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(updateURL(c, stackName, stackID), b, nil, nil) + return +} + +// Delete deletes a stack based on the stack name and stack ID. +func Delete(c *golangsdk.ServiceClient, stackName, stackID string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, stackName, stackID), nil) + return +} diff --git a/openstack/rts/v1/stacks/results.go b/openstack/rts/v1/stacks/results.go new file mode 100644 index 000000000..6891daff2 --- /dev/null +++ b/openstack/rts/v1/stacks/results.go @@ -0,0 +1,302 @@ +package stacks + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/pagination" + "io" + "reflect" + "strings" + "time" +) + +// CreatedStack represents the object extracted from a Create operation. +type CreatedStack struct { + ID string `json:"id"` + Links []golangsdk.Link `json:"links"` +} + +// CreateResult represents the result of a Create operation. +type CreateResult struct { + golangsdk.Result +} + +// Extract returns a pointer to a CreatedStack object and is called after a +// Create operation. +func (r CreateResult) Extract() (*CreatedStack, error) { + var s struct { + CreatedStack *CreatedStack `json:"stack"` + } + err := r.ExtractInto(&s) + return s.CreatedStack, err +} + +// StackPage is a pagination.Pager that is returned from a call to the List function. +type StackPage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if a ListResult contains no Stacks. +func (r StackPage) IsEmpty() (bool, error) { + stacks, err := ExtractStacks(r) + return len(stacks) == 0, err +} + +// ListedStack represents an element in the slice extracted from a List operation. +type ListedStack struct { + CreationTime time.Time `json:"-"` + Description string `json:"description"` + ID string `json:"id"` + Links []golangsdk.Link `json:"links"` + Name string `json:"stack_name"` + Status string `json:"stack_status"` + StatusReason string `json:"stack_status_reason"` + UpdatedTime time.Time `json:"-"` +} + +func (r *ListedStack) UnmarshalJSON(b []byte) error { + type tmp ListedStack + var s struct { + tmp + CreationTime golangsdk.JSONRFC3339NoZ `json:"creation_time"` + UpdatedTime golangsdk.JSONRFC3339NoZ `json:"updated_time"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = ListedStack(s.tmp) + + r.CreationTime = time.Time(s.CreationTime) + r.UpdatedTime = time.Time(s.UpdatedTime) + + return nil +} + +// ExtractStacks extracts and returns a slice of ListedStack. It is used while iterating +// over a stacks.List call. +func ExtractStacks(r pagination.Page) ([]ListedStack, error) { + var s struct { + ListedStacks []ListedStack `json:"stacks"` + } + err := (r.(StackPage)).ExtractInto(&s) + return s.ListedStacks, err +} + +// RetrievedStack represents the object extracted from a Get operation. +// RetrievedStack represents the object extracted from a Get operation. +type RetrievedStack struct { + Capabilities []interface{} `json:"capabilities"` + CreationTime time.Time `json:"-"` + Description string `json:"description"` + DisableRollback bool `json:"disable_rollback"` + ID string `json:"id"` + TenantId string `json:"tenant_id"` + Links []golangsdk.Link `json:"links"` + NotificationTopics []interface{} `json:"notification_topics"` + Outputs []*Output `json:"outputs"` + Parameters map[string]string `json:"parameters"` + Name string `json:"stack_name"` + Status string `json:"stack_status"` + StatusReason string `json:"stack_status_reason"` + Tags []string `json:"tags"` + TemplateDescription string `json:"template_description"` + Timeout int `json:"timeout_mins"` + UpdatedTime time.Time `json:"-"` +} + +// The Output data type. +type Output struct { + _ struct{} `type:"structure"` + + // User defined description associated with the output. + Description string `json:"description"` + + // The name of the export associated with the output. + //ExportName *string `json:"name"` + + // The key associated with the output. + OutputKey *string `json:"output_key"` + + // The value associated with the output. + OutputValue *string `json:"output_value"` +} + +// String returns the string representation +func (s Output) String() string { + return Prettify(s) +} + +// GoString returns the string representation +func (s Output) GoString() string { + return s.String() +} + +// SetDescription sets the Description field's value. +func (s *Output) SetDescription(v string) *Output { + s.Description = v + return s +} + +// SetOutputKey sets the OutputKey field's value. +func (s *Output) SetOutputKey(v string) *Output { + s.OutputKey = &v + return s +} + +// SetOutputValue sets the OutputValue field's value. +func (s *Output) SetOutputValue(v string) *Output { + s.OutputValue = &v + return s +} + +// RetrievedStack represents the object extracted from a Get operation. +func (r *RetrievedStack) UnmarshalJSON(b []byte) error { + type tmp RetrievedStack + var s struct { + tmp + CreationTime golangsdk.JSONRFC3339NoZ `json:"creation_time"` + UpdatedTime golangsdk.JSONRFC3339NoZ `json:"updated_time"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = RetrievedStack(s.tmp) + + r.CreationTime = time.Time(s.CreationTime) + r.UpdatedTime = time.Time(s.UpdatedTime) + + return nil +} + +// GetResult represents the result of a Get operation. +type GetResult struct { + golangsdk.Result +} + +// Extract returns a pointer to a CreatedStack object and is called after a +// Create operation. +func (r GetResult) Extract() (*RetrievedStack, error) { + var s struct { + Stack *RetrievedStack `json:"stack"` + } + err := r.ExtractInto(&s) + return s.Stack, err +} + +// UpdateResult represents the result of a Update operation. +type UpdateResult struct { + golangsdk.ErrResult +} + +// DeleteResult represents the result of a Delete operation. +type DeleteResult struct { + golangsdk.ErrResult +} + +// Prettify returns the string representation of a value. +func Prettify(i interface{}) string { + var buf bytes.Buffer + prettify(reflect.ValueOf(i), 0, &buf) + return buf.String() +} + +// prettify will recursively walk value v to build a textual +// representation of the value. +func prettify(v reflect.Value, indent int, buf *bytes.Buffer) { + for v.Kind() == reflect.Ptr { + v = v.Elem() + } + + switch v.Kind() { + case reflect.Struct: + strtype := v.Type().String() + if strtype == "time.Time" { + fmt.Fprintf(buf, "%s", v.Interface()) + break + } else if strings.HasPrefix(strtype, "io.") { + buf.WriteString("") + break + } + + buf.WriteString("{\n") + + names := []string{} + for i := 0; i < v.Type().NumField(); i++ { + name := v.Type().Field(i).Name + f := v.Field(i) + if name[0:1] == strings.ToLower(name[0:1]) { + continue // ignore unexported fields + } + if (f.Kind() == reflect.Ptr || f.Kind() == reflect.Slice || f.Kind() == reflect.Map) && f.IsNil() { + continue // ignore unset fields + } + names = append(names, name) + } + + for i, n := range names { + val := v.FieldByName(n) + buf.WriteString(strings.Repeat(" ", indent+2)) + buf.WriteString(n + ": ") + prettify(val, indent+2, buf) + + if i < len(names)-1 { + buf.WriteString(",\n") + } + } + + buf.WriteString("\n" + strings.Repeat(" ", indent) + "}") + case reflect.Slice: + strtype := v.Type().String() + if strtype == "[]uint8" { + fmt.Fprintf(buf, " len %d", v.Len()) + break + } + + nl, id, id2 := "", "", "" + if v.Len() > 3 { + nl, id, id2 = "\n", strings.Repeat(" ", indent), strings.Repeat(" ", indent+2) + } + buf.WriteString("[" + nl) + for i := 0; i < v.Len(); i++ { + buf.WriteString(id2) + prettify(v.Index(i), indent+2, buf) + + if i < v.Len()-1 { + buf.WriteString("," + nl) + } + } + + buf.WriteString(nl + id + "]") + case reflect.Map: + buf.WriteString("{\n") + + for i, k := range v.MapKeys() { + buf.WriteString(strings.Repeat(" ", indent+2)) + buf.WriteString(k.String() + ": ") + prettify(v.MapIndex(k), indent+2, buf) + + if i < v.Len()-1 { + buf.WriteString(",\n") + } + } + + buf.WriteString("\n" + strings.Repeat(" ", indent) + "}") + default: + if !v.IsValid() { + fmt.Fprint(buf, "") + return + } + format := "%v" + switch v.Interface().(type) { + case string: + format = "%q" + case io.ReadSeeker, io.Reader: + format = "buffer(%p)" + } + fmt.Fprintf(buf, format, v.Interface()) + } +} diff --git a/openstack/rts/v1/stacks/template.go b/openstack/rts/v1/stacks/template.go new file mode 100644 index 000000000..3431b7a0b --- /dev/null +++ b/openstack/rts/v1/stacks/template.go @@ -0,0 +1,141 @@ +package stacks + +import ( + "fmt" + "reflect" + "strings" + + "github.com/huaweicloud/golangsdk" +) + +// Template is a structure that represents OpenStack Heat templates +type Template struct { + TE +} + +// TemplateFormatVersions is a map containing allowed variations of the template format version +// Note that this contains the permitted variations of the _keys_ not the values. +var TemplateFormatVersions = map[string]bool{ + "HeatTemplateFormatVersion": true, + "heat_template_version": true, + "AWSTemplateFormatVersion": true, +} + +// Validate validates the contents of the Template +func (t *Template) Validate() error { + if t.Parsed == nil { + if err := t.Parse(); err != nil { + return err + } + } + var invalid string + for key := range t.Parsed { + if _, ok := TemplateFormatVersions[key]; ok { + return nil + } + invalid = key + } + return ErrInvalidTemplateFormatVersion{Version: invalid} +} + +// GetFileContents recursively parses a template to search for urls. These urls +// are assumed to point to other templates (known in OpenStack Heat as child +// templates). The contents of these urls are fetched and stored in the `Files` +// parameter of the template structure. This is the only way that a user can +// use child templates that are located in their filesystem; urls located on the +// web (e.g. on github or swift) can be fetched directly by Heat engine. +func (t *Template) getFileContents(te interface{}, ignoreIf igFunc, recurse bool) error { + // initialize template if empty + if t.Files == nil { + t.Files = make(map[string]string) + } + if t.fileMaps == nil { + t.fileMaps = make(map[string]string) + } + switch te.(type) { + // if te is a map + case map[string]interface{}, map[interface{}]interface{}: + teMap, err := toStringKeys(te) + if err != nil { + return err + } + for k, v := range teMap { + value, ok := v.(string) + if !ok { + // if the value is not a string, recursively parse that value + if err := t.getFileContents(v, ignoreIf, recurse); err != nil { + return err + } + } else if !ignoreIf(k, value) { + // at this point, the k, v pair has a reference to an external template. + // The assumption of heatclient is that value v is a reference + // to a file in the users environment + + // create a new child template + childTemplate := new(Template) + + // initialize child template + + // get the base location of the child template + baseURL, err := golangsdk.NormalizePathURL(t.baseURL, value) + if err != nil { + return err + } + childTemplate.baseURL = baseURL + childTemplate.client = t.client + + // fetch the contents of the child template + if err := childTemplate.Parse(); err != nil { + return err + } + + // process child template recursively if required. This is + // required if the child template itself contains references to + // other templates + if recurse { + if err := childTemplate.getFileContents(childTemplate.Parsed, ignoreIf, recurse); err != nil { + return err + } + } + // update parent template with current child templates' content. + // At this point, the child template has been parsed recursively. + t.fileMaps[value] = childTemplate.URL + t.Files[childTemplate.URL] = string(childTemplate.Bin) + + } + } + return nil + // if te is a slice, call the function on each element of the slice. + case []interface{}: + teSlice := te.([]interface{}) + for i := range teSlice { + if err := t.getFileContents(teSlice[i], ignoreIf, recurse); err != nil { + return err + } + } + // if te is anything else, return + case string, bool, float64, nil, int: + return nil + default: + return golangsdk.ErrUnexpectedType{Actual: fmt.Sprintf("%v", reflect.TypeOf(te))} + } + return nil +} + +// function to choose keys whose values are other template files +func ignoreIfTemplate(key string, value interface{}) bool { + // key must be either `get_file` or `type` for value to be a URL + if key != "get_file" && key != "type" { + return true + } + // value must be a string + valueString, ok := value.(string) + if !ok { + return true + } + // `.template` and `.yaml` are allowed suffixes for template URLs when referred to by `type` + if key == "type" && !(strings.HasSuffix(valueString, ".template") || strings.HasSuffix(valueString, ".yaml")) { + return true + } + return false +} diff --git a/openstack/rts/v1/stacks/template_test.go b/openstack/rts/v1/stacks/template_test.go new file mode 100644 index 000000000..758e3226a --- /dev/null +++ b/openstack/rts/v1/stacks/template_test.go @@ -0,0 +1,148 @@ +package stacks + +import ( + "fmt" + "net/http" + "net/url" + "strings" + "testing" + + th "github.com/huaweicloud/golangsdk/testhelper" +) + +func TestTemplateValidation(t *testing.T) { + templateJSON := new(Template) + templateJSON.Bin = []byte(ValidJSONTemplate) + err := templateJSON.Validate() + th.AssertNoErr(t, err) + + templateYAML := new(Template) + templateYAML.Bin = []byte(ValidYAMLTemplate) + err = templateYAML.Validate() + th.AssertNoErr(t, err) + + templateInvalid := new(Template) + templateInvalid.Bin = []byte(InvalidTemplateNoVersion) + if err = templateInvalid.Validate(); err == nil { + t.Error("Template validation did not catch invalid template") + } +} + +func TestTemplateParsing(t *testing.T) { + templateJSON := new(Template) + templateJSON.Bin = []byte(ValidJSONTemplate) + err := templateJSON.Parse() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, ValidJSONTemplateParsed, templateJSON.Parsed) + + templateYAML := new(Template) + templateYAML.Bin = []byte(ValidJSONTemplate) + err = templateYAML.Parse() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, ValidJSONTemplateParsed, templateYAML.Parsed) + + templateInvalid := new(Template) + templateInvalid.Bin = []byte("Keep Austin Weird") + err = templateInvalid.Parse() + if err == nil { + t.Error("Template parsing did not catch invalid template") + } +} + +func TestIgnoreIfTemplate(t *testing.T) { + var keyValueTests = []struct { + key string + value interface{} + out bool + }{ + {"not_get_file", "afksdf", true}, + {"not_type", "sdfd", true}, + {"get_file", "shdfuisd", false}, + {"type", "dfsdfsd", true}, + {"type", "sdfubsduf.yaml", false}, + {"type", "sdfsdufs.template", false}, + {"type", "sdfsdf.file", true}, + {"type", map[string]string{"key": "value"}, true}, + } + var result bool + for _, kv := range keyValueTests { + result = ignoreIfTemplate(kv.key, kv.value) + if result != kv.out { + t.Errorf("key: %v, value: %v expected: %v, actual: %v", kv.key, kv.value, result, kv.out) + } + } +} + +func TestGetFileContents(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + baseurl, err := getBasePath() + th.AssertNoErr(t, err) + fakeURL := strings.Join([]string{baseurl, "my_nova.yaml"}, "/") + urlparsed, err := url.Parse(fakeURL) + th.AssertNoErr(t, err) + myNovaContent := `heat_template_version: 2014-10-16 +parameters: + flavor: + type: string + description: Flavor for the server to be created + default: 4353 + hidden: true +resources: + test_server: + type: "OS::Nova::Server" + properties: + name: test-server + flavor: 2 GB General Purpose v1 + image: Debian 7 (Wheezy) (PVHVM) + networks: + - {uuid: 11111111-1111-1111-1111-111111111111}` + th.Mux.HandleFunc(urlparsed.Path, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, myNovaContent) + }) + + client := fakeClient{BaseClient: getHTTPClient()} + te := new(Template) + te.Bin = []byte(`heat_template_version: 2015-04-30 +resources: + my_server: + type: my_nova.yaml`) + te.client = client + + err = te.Parse() + th.AssertNoErr(t, err) + err = te.getFileContents(te.Parsed, ignoreIfTemplate, true) + th.AssertNoErr(t, err) + expectedFiles := map[string]string{ + "my_nova.yaml": `heat_template_version: 2014-10-16 +parameters: + flavor: + type: string + description: Flavor for the server to be created + default: 4353 + hidden: true +resources: + test_server: + type: "OS::Nova::Server" + properties: + name: test-server + flavor: 2 GB General Purpose v1 + image: Debian 7 (Wheezy) (PVHVM) + networks: + - {uuid: 11111111-1111-1111-1111-111111111111}`} + th.AssertEquals(t, expectedFiles["my_nova.yaml"], te.Files[fakeURL]) + te.fixFileRefs() + expectedParsed := map[string]interface{}{ + "heat_template_version": "2015-04-30", + "resources": map[string]interface{}{ + "my_server": map[string]interface{}{ + "type": fakeURL, + }, + }, + } + te.Parse() + th.AssertDeepEquals(t, expectedParsed, te.Parsed) +} diff --git a/openstack/rts/v1/stacks/testing/fixtures.go b/openstack/rts/v1/stacks/testing/fixtures.go new file mode 100644 index 000000000..b762134dd --- /dev/null +++ b/openstack/rts/v1/stacks/testing/fixtures.go @@ -0,0 +1,245 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/openstack/rts/v1/stacks" + th "github.com/huaweicloud/golangsdk/testhelper" + fake "github.com/huaweicloud/golangsdk/testhelper/client" +) + +// CreateExpected represents the expected object from a Create request. +var CreateExpected = &stacks.CreatedStack{ + ID: "16ef0584-4458-41eb-87c8-0dc8d5f66c87", + Links: []golangsdk.Link{ + { + Href: "http://168.28.170.117:8004/v1/98606384f58drad0bhdb7d02779549ac/stacks/stackcreated/16ef0584-4458-41eb-87c8-0dc8d5f66c87", + Rel: "self", + }, + }, +} + +// CreateOutput represents the response body from a Create request. +const CreateOutput = ` +{ + "stack": { + "id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87", + "links": [ + { + "href": "http://168.28.170.117:8004/v1/98606384f58drad0bhdb7d02779549ac/stacks/stackcreated/16ef0584-4458-41eb-87c8-0dc8d5f66c87", + "rel": "self" + } + ] + } +}` + +// HandleCreateSuccessfully creates an HTTP handler at `/stacks` on the test handler mux +// that responds with a `Create` response. +func HandleCreateSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/stacks", 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, "Accept", "application/json") + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, output) + }) +} + +// ListExpected represents the expected object from a List request. +var ListExpected = []stacks.ListedStack{ + { + Description: "Simple template to test heat commands", + Links: []golangsdk.Link{ + { + Href: "http://166.76.160.117:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87", + Rel: "self", + }, + }, + StatusReason: "Stack CREATE completed successfully", + Name: "postman_stack", + CreationTime: time.Date(2015, 2, 3, 20, 7, 39, 0, time.UTC), + Status: "CREATE_COMPLETE", + ID: "16ef0584-4458-41eb-87c8-0dc8d5f66c87", + }, + { + Description: "Simple template to test heat commands", + Links: []golangsdk.Link{ + { + Href: "http://166.76.160.117:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/golangsdk-test-stack-2/db6977b2-27aa-4775-9ae7-6213212d4ada", + Rel: "self", + }, + }, + StatusReason: "Stack successfully updated", + Name: "golangsdk-test-stack-2", + CreationTime: time.Date(2014, 12, 11, 17, 39, 16, 0, time.UTC), + UpdatedTime: time.Date(2014, 12, 11, 17, 40, 37, 0, time.UTC), + Status: "UPDATE_COMPLETE", + ID: "db6977b2-27aa-4775-9ae7-6213212d4ada", + }, +} + +// FullListOutput represents the response body from a List request without a marker. +const FullListOutput = ` +{ + "stacks": [ + { + "description": "Simple template to test heat commands", + "links": [ + { + "href": "http://166.76.160.117:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87", + "rel": "self" + } + ], + "stack_status_reason": "Stack CREATE completed successfully", + "stack_name": "postman_stack", + "creation_time": "2015-02-03T20:07:39", + "updated_time": null, + "stack_status": "CREATE_COMPLETE", + "id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87", + "tags": ["rackspace", "atx"] + }, + { + "description": "Simple template to test heat commands", + "links": [ + { + "href": "http://166.76.160.117:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/golangsdk-test-stack-2/db6977b2-27aa-4775-9ae7-6213212d4ada", + "rel": "self" + } + ], + "stack_status_reason": "Stack successfully updated", + "stack_name": "golangsdk-test-stack-2", + "creation_time": "2014-12-11T17:39:16", + "updated_time": "2014-12-11T17:40:37", + "stack_status": "UPDATE_COMPLETE", + "id": "db6977b2-27aa-4775-9ae7-6213212d4ada", + "tags": ["sfo", "satx"] + } + ] +} +` + +// HandleListSuccessfully creates an HTTP handler at `/stacks` on the test handler mux +// that responds with a `List` response. +func HandleListSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/stacks", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, output) + case "db6977b2-27aa-4775-9ae7-6213212d4ada": + fmt.Fprintf(w, `[]`) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) +} + +// GetExpected represents the expected object from a Get request. +var GetExpected = &stacks.RetrievedStack{ + DisableRollback: true, + Description: "Simple template to test heat commands", + Parameters: map[string]string{ + "flavor": "m1.tiny", + "OS::stack_name": "postman_stack", + "OS::stack_id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87", + }, + StatusReason: "Stack CREATE completed successfully", + Name: "postman_stack", + Outputs: []*stacks.Output{}, + CreationTime: time.Date(2015, 2, 3, 20, 7, 39, 0, time.UTC), + Links: []golangsdk.Link{ + { + Href: "http://166.76.160.117:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87", + Rel: "self", + }, + }, + Capabilities: []interface{}{}, + NotificationTopics: []interface{}{}, + 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. +const GetOutput = ` +{ + "stack": { + "disable_rollback": true, + "description": "Simple template to test heat commands", + "parameters": { + "flavor": "m1.tiny", + "OS::stack_name": "postman_stack", + "OS::stack_id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87" + }, + "stack_status_reason": "Stack CREATE completed successfully", + "stack_name": "postman_stack", + "outputs": [], + "creation_time": "2015-02-03T20:07:39", + "links": [ + { + "href": "http://166.76.160.117:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87", + "rel": "self" + } + ], + "capabilities": [], + "notification_topics": [], + "timeout_mins": null, + "stack_status": "CREATE_COMPLETE", + "updated_time": null, + "id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87", + "template_description": "Simple template to test heat commands", + "tags": ["rackspace", "atx"] + } +} +` + +// HandleGetSuccessfully creates an HTTP handler at `/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87` +// on the test handler mux that responds with a `Get` response. +func HandleGetSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/stacks/postman_stack", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + 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, output) + }) +} + +// HandleUpdateSuccessfully creates an HTTP handler at `/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87` +// on the test handler mux that responds with an `Update` response. +func HandleUpdateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/stacks/golangsdk-test-stack-2/db6977b2-27aa-4775-9ae7-6213212d4ada", 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, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + }) +} + +// HandleDeleteSuccessfully creates an HTTP handler at `/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87` +// on the test handler mux that responds with a `Delete` response. +func HandleDeleteSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/stacks/golangsdk-test-stack-2/db6977b2-27aa-4775-9ae7-6213212d4ada", 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.StatusNoContent) + }) +} diff --git a/openstack/rts/v1/stacks/testing/requests_test.go b/openstack/rts/v1/stacks/testing/requests_test.go new file mode 100644 index 000000000..79765415c --- /dev/null +++ b/openstack/rts/v1/stacks/testing/requests_test.go @@ -0,0 +1,94 @@ +package testing + +import ( + "testing" + + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/openstack/rts/v1/stacks" + th "github.com/huaweicloud/golangsdk/testhelper" + fake "github.com/huaweicloud/golangsdk/testhelper/client" +) + +func TestCreateStack(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateSuccessfully(t, CreateOutput) + template := new(stacks.Template) + template.Bin = []byte(` + { + "heat_template_version": "2013-05-23", + "description": "Simple template to test heat commands", + "parameters": { + "flavor": { + "default": "m1.tiny", + "type": "string" + } + } + }`) + createOpts := stacks.CreateOpts{ + Name: "stackcreated", + Timeout: 60, + TemplateOpts: template, + DisableRollback: golangsdk.Disabled, + } + actual, err := stacks.Create(fake.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + + expected := CreateExpected + th.AssertDeepEquals(t, expected, actual) +} + +func TestListStack(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListSuccessfully(t, FullListOutput) + + actual, err := stacks.List(fake.ServiceClient(), stacks.ListOpts{}) + th.AssertDeepEquals(t, ListExpected, actual) + th.AssertNoErr(t, err) +} + +func TestGetStack(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetSuccessfully(t, GetOutput) + + actual, err := stacks.Get(fake.ServiceClient(), "postman_stack").Extract() + th.AssertNoErr(t, err) + + expected := GetExpected + th.AssertDeepEquals(t, expected, actual) +} + +func TestUpdateStack(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleUpdateSuccessfully(t) + + template := new(stacks.Template) + template.Bin = []byte(` + { + "heat_template_version": "2013-05-23", + "description": "Simple template to test heat commands", + "parameters": { + "flavor": { + "default": "m1.tiny", + "type": "string" + } + } + }`) + updateOpts := stacks.UpdateOpts{ + TemplateOpts: template, + } + err := stacks.Update(fake.ServiceClient(), "golangsdk-test-stack-2", "db6977b2-27aa-4775-9ae7-6213212d4ada", updateOpts).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestDeleteStack(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteSuccessfully(t) + + err := stacks.Delete(fake.ServiceClient(), "golangsdk-test-stack-2", "db6977b2-27aa-4775-9ae7-6213212d4ada").ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/openstack/rts/v1/stacks/urls.go b/openstack/rts/v1/stacks/urls.go new file mode 100644 index 000000000..c59eacc9b --- /dev/null +++ b/openstack/rts/v1/stacks/urls.go @@ -0,0 +1,23 @@ +package stacks + +import ( + "github.com/huaweicloud/golangsdk" +) + +func createURL(c *golangsdk.ServiceClient) string { + return c.ServiceURL("stacks") +} +func listURL(c *golangsdk.ServiceClient) string { + return createURL(c) +} + +func getURL(c *golangsdk.ServiceClient, name string) string { + return c.ServiceURL("stacks", name) +} +func updateURL(c *golangsdk.ServiceClient, name, id string) string { + return c.ServiceURL("stacks", name, id) +} + +func deleteURL(c *golangsdk.ServiceClient, name, id string) string { + return updateURL(c, name, id) +} diff --git a/openstack/rts/v1/stacks/utils.go b/openstack/rts/v1/stacks/utils.go new file mode 100644 index 000000000..53a258492 --- /dev/null +++ b/openstack/rts/v1/stacks/utils.go @@ -0,0 +1,160 @@ +package stacks + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "path/filepath" + "reflect" + "strings" + + "github.com/huaweicloud/golangsdk" + "gopkg.in/yaml.v2" +) + +// Client is an interface that expects a Get method similar to http.Get. This +// is needed for unit testing, since we can mock an http client. Thus, the +// client will usually be an http.Client EXCEPT in unit tests. +type Client interface { + Get(string) (*http.Response, error) +} + +// TE is a base structure for both Template and Environment +type TE struct { + // Bin stores the contents of the template or environment. + Bin []byte + // URL stores the URL of the template. This is allowed to be a 'file://' + // for local files. + URL string + // Parsed contains a parsed version of Bin. Since there are 2 different + // fields referring to the same value, you must be careful when accessing + // this filed. + Parsed map[string]interface{} + // Files contains a mapping between the urls in templates to their contents. + Files map[string]string + // fileMaps is a map used internally when determining Files. + fileMaps map[string]string + // baseURL represents the location of the template or environment file. + baseURL string + // client is an interface which allows TE to fetch contents from URLS + client Client +} + +// Fetch fetches the contents of a TE from its URL. Once a TE structure has a +// URL, call the fetch method to fetch the contents. +func (t *TE) Fetch() error { + // if the baseURL is not provided, use the current directors as the base URL + if t.baseURL == "" { + u, err := getBasePath() + if err != nil { + return err + } + t.baseURL = u + } + + // if the contents are already present, do nothing. + if t.Bin != nil { + return nil + } + + // get a fqdn from the URL using the baseURL of the TE. For local files, + // the URL's will have the `file` scheme. + u, err := golangsdk.NormalizePathURL(t.baseURL, t.URL) + if err != nil { + return err + } + t.URL = u + + // get an HTTP client if none present + if t.client == nil { + t.client = getHTTPClient() + } + + // use the client to fetch the contents of the TE + resp, err := t.client.Get(t.URL) + if err != nil { + return err + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + t.Bin = body + return nil +} + +// get the basepath of the TE +func getBasePath() (string, error) { + basePath, err := filepath.Abs(".") + if err != nil { + return "", err + } + u, err := golangsdk.NormalizePathURL("", basePath) + if err != nil { + return "", err + } + return u, nil +} + +// get a an HTTP client to retrieve URL's. This client allows the use of `file` +// scheme since we may need to fetch files from users filesystem +func getHTTPClient() Client { + transport := &http.Transport{} + transport.RegisterProtocol("file", http.NewFileTransport(http.Dir("/"))) + return &http.Client{Transport: transport} +} + +// Parse will parse the contents and then validate. The contents MUST be either JSON or YAML. +func (t *TE) Parse() error { + if err := t.Fetch(); err != nil { + return err + } + if jerr := json.Unmarshal(t.Bin, &t.Parsed); jerr != nil { + if yerr := yaml.Unmarshal(t.Bin, &t.Parsed); yerr != nil { + return ErrInvalidDataFormat{} + } + } + return t.Validate() +} + +// Validate validates the contents of TE +func (t *TE) Validate() error { + return nil +} + +// igfunc is a parameter used by GetFileContents and GetRRFileContents to check +// for valid URL's. +type igFunc func(string, interface{}) bool + +// convert map[interface{}]interface{} to map[string]interface{} +func toStringKeys(m interface{}) (map[string]interface{}, error) { + switch m.(type) { + case map[string]interface{}, map[interface{}]interface{}: + typedMap := make(map[string]interface{}) + if _, ok := m.(map[interface{}]interface{}); ok { + for k, v := range m.(map[interface{}]interface{}) { + typedMap[k.(string)] = v + } + } else { + typedMap = m.(map[string]interface{}) + } + return typedMap, nil + default: + return nil, golangsdk.ErrUnexpectedType{Expected: "map[string]interface{}/map[interface{}]interface{}", Actual: fmt.Sprintf("%v", reflect.TypeOf(m))} + } +} + +// fix the reference to files by replacing relative URL's by absolute +// URL's +func (t *TE) fixFileRefs() { + tStr := string(t.Bin) + if t.fileMaps == nil { + return + } + for k, v := range t.fileMaps { + tStr = strings.Replace(tStr, k, v, -1) + } + t.Bin = []byte(tStr) +} diff --git a/openstack/rts/v1/stacks/utils_test.go b/openstack/rts/v1/stacks/utils_test.go new file mode 100644 index 000000000..ff450d56a --- /dev/null +++ b/openstack/rts/v1/stacks/utils_test.go @@ -0,0 +1,94 @@ +package stacks + +import ( + "fmt" + "net/http" + "net/url" + "strings" + "testing" + + th "github.com/huaweicloud/golangsdk/testhelper" +) + +func TestTEFixFileRefs(t *testing.T) { + te := TE{ + Bin: []byte(`string_to_replace: my fair lady`), + fileMaps: map[string]string{ + "string_to_replace": "london bridge is falling down", + }, + } + te.fixFileRefs() + th.AssertEquals(t, string(te.Bin), `london bridge is falling down: my fair lady`) +} + +func TestToStringKeys(t *testing.T) { + var test1 interface{} = map[interface{}]interface{}{ + "Adam": "Smith", + "Isaac": "Newton", + } + result1, err := toStringKeys(test1) + th.AssertNoErr(t, err) + + expected := map[string]interface{}{ + "Adam": "Smith", + "Isaac": "Newton", + } + th.AssertDeepEquals(t, result1, expected) +} + +func TestGetBasePath(t *testing.T) { + _, err := getBasePath() + th.AssertNoErr(t, err) +} + +// test if HTTP client can read file type URLS. Read the URL of this file +// because if this test is running, it means this file _must_ exist +func TestGetHTTPClient(t *testing.T) { + client := getHTTPClient() + baseurl, err := getBasePath() + th.AssertNoErr(t, err) + resp, err := client.Get(baseurl) + th.AssertNoErr(t, err) + th.AssertEquals(t, resp.StatusCode, 200) +} + +// Implement a fakeclient that can be used to mock out HTTP requests +type fakeClient struct { + BaseClient Client +} + +// this client's Get method first changes the URL given to point to +// testhelper's (th) endpoints. This is done because the http Mux does not seem +// to work for fqdns with the `file` scheme +func (c fakeClient) Get(url string) (*http.Response, error) { + newurl := strings.Replace(url, "file://", th.Endpoint(), 1) + return c.BaseClient.Get(newurl) +} + +// test the fetch function +func TestFetch(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + baseurl, err := getBasePath() + th.AssertNoErr(t, err) + fakeURL := strings.Join([]string{baseurl, "file.yaml"}, "/") + urlparsed, err := url.Parse(fakeURL) + th.AssertNoErr(t, err) + + th.Mux.HandleFunc(urlparsed.Path, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + w.Header().Set("Content-Type", "application/jason") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, "Fee-fi-fo-fum") + }) + + client := fakeClient{BaseClient: getHTTPClient()} + te := TE{ + URL: "file.yaml", + client: client, + } + err = te.Fetch() + th.AssertNoErr(t, err) + th.AssertEquals(t, fakeURL, te.URL) + th.AssertEquals(t, "Fee-fi-fo-fum", string(te.Bin)) +} From da7fd2089619a3c35da0465aa402e5835ee96f8f Mon Sep 17 00:00:00 2001 From: savasw Date: Tue, 19 Jun 2018 15:12:40 +0530 Subject: [PATCH 2/9] add rts v1 stack events get interfaces --- openstack/rts/v1/stackevents/doc.go | 4 + openstack/rts/v1/stackevents/requests.go | 113 ++++++++++++++ openstack/rts/v1/stackevents/results.go | 98 +++++++++++++ openstack/rts/v1/stackevents/testing/doc.go | 2 + .../rts/v1/stackevents/testing/fixtures.go | 138 ++++++++++++++++++ .../v1/stackevents/testing/requests_test.go | 29 ++++ openstack/rts/v1/stackevents/urls.go | 7 + 7 files changed, 391 insertions(+) create mode 100644 openstack/rts/v1/stackevents/doc.go create mode 100644 openstack/rts/v1/stackevents/requests.go create mode 100644 openstack/rts/v1/stackevents/results.go create mode 100644 openstack/rts/v1/stackevents/testing/doc.go create mode 100644 openstack/rts/v1/stackevents/testing/fixtures.go create mode 100644 openstack/rts/v1/stackevents/testing/requests_test.go create mode 100644 openstack/rts/v1/stackevents/urls.go diff --git a/openstack/rts/v1/stackevents/doc.go b/openstack/rts/v1/stackevents/doc.go new file mode 100644 index 000000000..51cdd9747 --- /dev/null +++ b/openstack/rts/v1/stackevents/doc.go @@ -0,0 +1,4 @@ +// Package stackevents provides operations for finding, listing, and retrieving +// stack events. Stack events are events that take place on stacks such as +// updating and abandoning. +package stackevents diff --git a/openstack/rts/v1/stackevents/requests.go b/openstack/rts/v1/stackevents/requests.go new file mode 100644 index 000000000..3b1e89476 --- /dev/null +++ b/openstack/rts/v1/stackevents/requests.go @@ -0,0 +1,113 @@ +package stackevents + +import ( + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/pagination" +) + +// SortDir is a type for specifying in which direction to sort a list of events. +type SortDir string + +// SortKey is a type for specifying by which key to sort a list of events. +type SortKey string + +// ResourceStatus is a type for specifying by which resource status to filter a +// list of events. +type ResourceStatus string + +// ResourceAction is a type for specifying by which resource action to filter a +// list of events. +type ResourceAction string + +var ( + // ResourceStatusInProgress is used to filter a List request by the 'IN_PROGRESS' status. + ResourceStatusInProgress ResourceStatus = "IN_PROGRESS" + // ResourceStatusComplete is used to filter a List request by the 'COMPLETE' status. + ResourceStatusComplete ResourceStatus = "COMPLETE" + // ResourceStatusFailed is used to filter a List request by the 'FAILED' status. + ResourceStatusFailed ResourceStatus = "FAILED" + + // ResourceActionCreate is used to filter a List request by the 'CREATE' action. + ResourceActionCreate ResourceAction = "CREATE" + // ResourceActionDelete is used to filter a List request by the 'DELETE' action. + ResourceActionDelete ResourceAction = "DELETE" + // ResourceActionUpdate is used to filter a List request by the 'UPDATE' action. + ResourceActionUpdate ResourceAction = "UPDATE" + // ResourceActionRollback is used to filter a List request by the 'ROLLBACK' action. + ResourceActionRollback ResourceAction = "ROLLBACK" + // ResourceActionSuspend is used to filter a List request by the 'SUSPEND' action. + ResourceActionSuspend ResourceAction = "SUSPEND" + // ResourceActionResume is used to filter a List request by the 'RESUME' action. + ResourceActionResume ResourceAction = "RESUME" + // ResourceActionAbandon is used to filter a List request by the 'ABANDON' action. + ResourceActionAbandon ResourceAction = "ABANDON" + + // SortAsc is used to sort a list of stacks in ascending order. + SortAsc SortDir = "asc" + // SortDesc is used to sort a list of stacks in descending order. + SortDesc SortDir = "desc" + + // SortName is used to sort a list of stacks by name. + SortName SortKey = "name" + // SortResourceType is used to sort a list of stacks by resource type. + SortResourceType SortKey = "resource_type" + // SortCreatedAt is used to sort a list of stacks by date created. + SortCreatedAt SortKey = "created_at" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToStackEventListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Marker and Limit are used for pagination. +type ListOpts struct { + // The stack resource ID with which to start the listing. + Marker string `q:"marker"` + // Integer value for the limit of values to return. + Limit int `q:"limit"` + // Filters the event list by the specified ResourceAction. You can use this + // filter multiple times to filter by multiple resource actions: CREATE, DELETE, + // UPDATE, ROLLBACK, SUSPEND, RESUME or ADOPT. + ResourceActions []ResourceAction `q:"resource_action"` + // Filters the event list by the specified resource_status. You can use this + // filter multiple times to filter by multiple resource statuses: IN_PROGRESS, + // COMPLETE or FAILED. + ResourceStatuses []ResourceStatus `q:"resource_status"` + // Filters the event list by the specified resource_name. You can use this + // filter multiple times to filter by multiple resource names. + ResourceNames []string `q:"resource_name"` + // Filters the event list by the specified resource_type. You can use this + // filter multiple times to filter by multiple resource types: OS::Nova::Server, + // OS::Cinder::Volume, and so on. + ResourceTypes []string `q:"resource_type"` + // Sorts the event list by: resource_type or created_at. + SortKey SortKey `q:"sort_keys"` + // The sort direction of the event list. Which is asc (ascending) or desc (descending). + SortDir SortDir `q:"sort_dir"` +} + +// ToStackEventListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToStackEventListQuery() (string, error) { + q, err := golangsdk.BuildQueryString(opts) + return q.String(), err +} + +// List makes a request against the API to list resources for the given stack. +func List(client *golangsdk.ServiceClient, stackName, stackID string, opts ListOptsBuilder) pagination.Pager { + url := listURL(client, stackName, stackID) + if opts != nil { + query, err := opts.ToStackEventListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + p := EventPage{pagination.MarkerPageBase{PageResult: r}} + p.MarkerPageBase.Owner = p + return p + }) +} diff --git a/openstack/rts/v1/stackevents/results.go b/openstack/rts/v1/stackevents/results.go new file mode 100644 index 000000000..4487d7ab0 --- /dev/null +++ b/openstack/rts/v1/stackevents/results.go @@ -0,0 +1,98 @@ +package stackevents + +import ( + "encoding/json" + "time" + + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/pagination" +) + +// Event represents a stack event. +type Event struct { + // The name of the resource for which the event occurred. + ResourceName string `json:"resource_name"` + // The time the event occurred. + Time time.Time `json:"-"` + // The URLs to the event. + Links []golangsdk.Link `json:"links"` + // The logical ID of the stack resource. + LogicalResourceID string `json:"logical_resource_id"` + // The reason of the status of the event. + ResourceStatusReason string `json:"resource_status_reason"` + // The status of the event. + ResourceStatus string `json:"resource_status"` + // The physical ID of the stack resource. + PhysicalResourceID string `json:"physical_resource_id"` + // The event ID. + ID string `json:"id"` + // Properties of the stack resource. + ResourceProperties map[string]interface{} `json:"resource_properties"` +} + +func (r *Event) UnmarshalJSON(b []byte) error { + type tmp Event + var s struct { + tmp + Time golangsdk.JSONRFC3339NoZ `json:"event_time"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = Event(s.tmp) + + r.Time = time.Time(s.Time) + + return nil +} + +// FindResult represents the result of a Find operation. +type FindResult struct { + golangsdk.Result +} + +// Extract returns a slice of Event objects and is called after a +// Find operation. +func (r FindResult) Extract() ([]Event, error) { + var s struct { + Events []Event `json:"events"` + } + err := r.ExtractInto(&s) + return s.Events, err +} + +// EventPage abstracts the raw results of making a List() request against the API. +// As OpenStack extensions may freely alter the response bodies of structures returned to the client, you may only safely access the +// data provided through the ExtractResources call. +type EventPage struct { + pagination.MarkerPageBase +} + +// IsEmpty returns true if a page contains no Server results. +func (r EventPage) IsEmpty() (bool, error) { + events, err := ExtractEvents(r) + return len(events) == 0, err +} + +// LastMarker returns the last stack ID in a ListResult. +func (r EventPage) LastMarker() (string, error) { + events, err := ExtractEvents(r) + if err != nil { + return "", err + } + if len(events) == 0 { + return "", nil + } + return events[len(events)-1].ID, nil +} + +// ExtractEvents interprets the results of a single page from a List() call, producing a slice of Event entities. +func ExtractEvents(r pagination.Page) ([]Event, error) { + var s struct { + Events []Event `json:"events"` + } + err := (r.(EventPage)).ExtractInto(&s) + return s.Events, err +} diff --git a/openstack/rts/v1/stackevents/testing/doc.go b/openstack/rts/v1/stackevents/testing/doc.go new file mode 100644 index 000000000..2e22a6c16 --- /dev/null +++ b/openstack/rts/v1/stackevents/testing/doc.go @@ -0,0 +1,2 @@ +// orchestration_stackevents_v1 +package testing diff --git a/openstack/rts/v1/stackevents/testing/fixtures.go b/openstack/rts/v1/stackevents/testing/fixtures.go new file mode 100644 index 000000000..9e4d1daa8 --- /dev/null +++ b/openstack/rts/v1/stackevents/testing/fixtures.go @@ -0,0 +1,138 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/openstack/rts/v1/stackevents" + th "github.com/huaweicloud/golangsdk/testhelper" + fake "github.com/huaweicloud/golangsdk/testhelper/client" +) + +// ListExpected represents the expected object from a List request. +var ListExpected = []stackevents.Event{ + { + ResourceName: "hello_world", + Time: time.Date(2015, 2, 5, 21, 33, 11, 0, time.UTC), + Links: []golangsdk.Link{ + { + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/06feb26f-9298-4a9b-8749-9d770e5d577a", + Rel: "self", + }, + { + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + Rel: "resource", + }, + { + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + Rel: "stack", + }, + }, + LogicalResourceID: "hello_world", + ResourceStatusReason: "state changed", + ResourceStatus: "CREATE_IN_PROGRESS", + PhysicalResourceID: "", + ID: "06feb26f-9298-4a9b-8749-9d770e5d577a", + }, + { + ResourceName: "hello_world", + Time: time.Date(2015, 2, 5, 21, 33, 27, 0, time.UTC), + Links: []golangsdk.Link{ + { + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18", + Rel: "self", + }, + { + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + Rel: "resource", + }, + { + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + Rel: "stack", + }, + }, + LogicalResourceID: "hello_world", + ResourceStatusReason: "state changed", + ResourceStatus: "CREATE_COMPLETE", + PhysicalResourceID: "49181cd6-169a-4130-9455-31185bbfc5bf", + ID: "93940999-7d40-44ae-8de4-19624e7b8d18", + }, +} + +// ListOutput represents the response body from a List request. +const ListOutput = ` +{ + "events": [ + { + "resource_name": "hello_world", + "event_time": "2015-02-05T21:33:11", + "links": [ + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/06feb26f-9298-4a9b-8749-9d770e5d577a", + "rel": "self" + }, + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + "rel": "resource" + }, + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + "rel": "stack" + } + ], + "logical_resource_id": "hello_world", + "resource_status_reason": "state changed", + "resource_status": "CREATE_IN_PROGRESS", + "physical_resource_id": null, + "id": "06feb26f-9298-4a9b-8749-9d770e5d577a" + }, + { + "resource_name": "hello_world", + "event_time": "2015-02-05T21:33:27", + "links": [ + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18", + "rel": "self" + }, + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + "rel": "resource" + }, + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + "rel": "stack" + } + ], + "logical_resource_id": "hello_world", + "resource_status_reason": "state changed", + "resource_status": "CREATE_COMPLETE", + "physical_resource_id": "49181cd6-169a-4130-9455-31185bbfc5bf", + "id": "93940999-7d40-44ae-8de4-19624e7b8d18" + } + ] +}` + +// HandleListSuccessfully creates an HTTP handler at `/stacks/hello_world/49181cd6-169a-4130-9455-31185bbfc5bf/events` +// on the test handler mux that responds with a `List` response. +func HandleListSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/stacks/hello_world/49181cd6-169a-4130-9455-31185bbfc5bf/events", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, output) + case "93940999-7d40-44ae-8de4-19624e7b8d18": + fmt.Fprintf(w, `{"events":[]}`) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) +} diff --git a/openstack/rts/v1/stackevents/testing/requests_test.go b/openstack/rts/v1/stackevents/testing/requests_test.go new file mode 100644 index 000000000..f45ddc36d --- /dev/null +++ b/openstack/rts/v1/stackevents/testing/requests_test.go @@ -0,0 +1,29 @@ +package testing + +import ( + "testing" + + "github.com/huaweicloud/golangsdk/openstack/rts/v1/stackevents" + "github.com/huaweicloud/golangsdk/pagination" + th "github.com/huaweicloud/golangsdk/testhelper" + fake "github.com/huaweicloud/golangsdk/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListSuccessfully(t, ListOutput) + + count := 0 + err := stackevents.List(fake.ServiceClient(), "hello_world", "49181cd6-169a-4130-9455-31185bbfc5bf", nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := stackevents.ExtractEvents(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ListExpected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} diff --git a/openstack/rts/v1/stackevents/urls.go b/openstack/rts/v1/stackevents/urls.go new file mode 100644 index 000000000..b35ea6166 --- /dev/null +++ b/openstack/rts/v1/stackevents/urls.go @@ -0,0 +1,7 @@ +package stackevents + +import "github.com/huaweicloud/golangsdk" + +func listURL(c *golangsdk.ServiceClient, stackName, stackID string) string { + return c.ServiceURL("stacks", stackName, stackID, "events") +} From f8125675034e839e814315c430d2138e45aa0fd5 Mon Sep 17 00:00:00 2001 From: savasw Date: Tue, 19 Jun 2018 15:13:06 +0530 Subject: [PATCH 3/9] add rts v1 stack resource list interface --- openstack/rts/v1/stackresources/doc.go | 18 +++ openstack/rts/v1/stackresources/requests.go | 103 ++++++++++++++++++ openstack/rts/v1/stackresources/results.go | 64 +++++++++++ .../rts/v1/stackresources/testing/doc.go | 2 + .../rts/v1/stackresources/testing/fixtures.go | 88 +++++++++++++++ .../stackresources/testing/requests_test.go | 21 ++++ openstack/rts/v1/stackresources/urls.go | 7 ++ 7 files changed, 303 insertions(+) create mode 100644 openstack/rts/v1/stackresources/doc.go create mode 100644 openstack/rts/v1/stackresources/requests.go create mode 100644 openstack/rts/v1/stackresources/results.go create mode 100644 openstack/rts/v1/stackresources/testing/doc.go create mode 100644 openstack/rts/v1/stackresources/testing/fixtures.go create mode 100644 openstack/rts/v1/stackresources/testing/requests_test.go create mode 100644 openstack/rts/v1/stackresources/urls.go diff --git a/openstack/rts/v1/stackresources/doc.go b/openstack/rts/v1/stackresources/doc.go new file mode 100644 index 000000000..959b0f02d --- /dev/null +++ b/openstack/rts/v1/stackresources/doc.go @@ -0,0 +1,18 @@ +/* +Package resources enables management and retrieval of Resources +RTS service. + +Example to List Resources + +listOpts := stackresources.ListOpts{} +allResources, err := stackresources.List(orchestrationClient, listOpts) +if err != nil { +panic(err) +} + +for _, resource := range allResources { +fmt.Printf("%+v\n", resource) +} +*/ + +package stackresources diff --git a/openstack/rts/v1/stackresources/requests.go b/openstack/rts/v1/stackresources/requests.go new file mode 100644 index 000000000..9330467e5 --- /dev/null +++ b/openstack/rts/v1/stackresources/requests.go @@ -0,0 +1,103 @@ +package stackresources + +import ( + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/pagination" + "reflect" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToStackResourceListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the rts attributes you want to see returned. +type ListOpts struct { + //Specifies the logical resource ID of the resource. + LogicalID string `q:"logical_resource_id"` + + //Name is the human readable name for the Resource. + Name string `q:"resource_name"` + + //Specifies the Physical resource ID of the resource. + PhysicalID string `q:"physical_resource_id"` + + //Status indicates whether or not a subnet is currently operational. + Status string `q:"resource_status"` + + //Specifies the resource type that are defined in the template. + Type string `q:"resource_type"` +} + +// List returns collection of +// resources. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those resources that are owned by the +// tenant who submits the request, unless an admin user submits the request. +func List(client *golangsdk.ServiceClient, stackName string, opts ListOpts) ([]Resource, error) { + u := listURL(client, stackName) + pages, err := pagination.NewPager(client, u, func(r pagination.PageResult) pagination.Page { + return ResourcePage{pagination.LinkedPageBase{PageResult: r}} + }).AllPages() + + allResources, err := ExtractResources(pages) + if err != nil { + return nil, err + } + + return FilterResources(allResources, opts) +} + +func FilterResources(resources []Resource, opts ListOpts) ([]Resource, error) { + + var refinedResources []Resource + var matched bool + m := map[string]interface{}{} + + if opts.LogicalID != "" { + m["LogicalID"] = opts.LogicalID + } + if opts.Name != "" { + m["Name"] = opts.Name + } + if opts.PhysicalID != "" { + m["PhysicalID"] = opts.PhysicalID + } + if opts.Status != "" { + m["Status"] = opts.Status + } + if opts.Type != "" { + m["Type"] = opts.Type + } + + if len(m) > 0 && len(resources) > 0 { + for _, resource := range resources { + matched = true + + for key, value := range m { + if sVal := getStructField(&resource, key); !(sVal == value) { + matched = false + } + } + + if matched { + refinedResources = append(refinedResources, resource) + } + } + + } else { + refinedResources = resources + } + + return refinedResources, nil +} + +func getStructField(v *Resource, field string) string { + r := reflect.ValueOf(v) + f := reflect.Indirect(r).FieldByName(field) + return string(f.String()) +} diff --git a/openstack/rts/v1/stackresources/results.go b/openstack/rts/v1/stackresources/results.go new file mode 100644 index 000000000..1045fdc8c --- /dev/null +++ b/openstack/rts/v1/stackresources/results.go @@ -0,0 +1,64 @@ +package stackresources + +import ( + "encoding/json" + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/pagination" + "time" +) + +// Resource represents a stack resource. +type Resource struct { + CreationTime time.Time `json:"-"` + Links []golangsdk.Link `json:"links"` + LogicalID string `json:"logical_resource_id"` + Name string `json:"resource_name"` + PhysicalID string `json:"physical_resource_id"` + RequiredBy []string `json:"required_by"` + Status string `json:"resource_status"` + StatusReason string `json:"resource_status_reason"` + Type string `json:"resource_type"` + UpdatedTime time.Time `json:"-"` +} + +// ResourcePage is the page returned by a pager when traversing over a +// collection of resources. +type ResourcePage struct { + pagination.LinkedPageBase +} + +func (r *Resource) UnmarshalJSON(b []byte) error { + type tmp Resource + var s struct { + tmp + CreationTime golangsdk.JSONRFC3339NoZ `json:"creation_time"` + UpdatedTime golangsdk.JSONRFC3339NoZ `json:"updated_time"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Resource(s.tmp) + + r.CreationTime = time.Time(s.CreationTime) + r.UpdatedTime = time.Time(s.UpdatedTime) + + return nil +} + +// IsEmpty returns true if a page contains no Server results. +func (r ResourcePage) IsEmpty() (bool, error) { + resources, err := ExtractResources(r) + return len(resources) == 0, err +} + +// ExtractResources accepts a Page struct, specifically a ResourcePage struct, +// and extracts the elements into a slice of Resource structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractResources(r pagination.Page) ([]Resource, error) { + var s struct { + Resources []Resource `json:"resources"` + } + err := (r.(ResourcePage)).ExtractInto(&s) + return s.Resources, err +} diff --git a/openstack/rts/v1/stackresources/testing/doc.go b/openstack/rts/v1/stackresources/testing/doc.go new file mode 100644 index 000000000..16e1dae29 --- /dev/null +++ b/openstack/rts/v1/stackresources/testing/doc.go @@ -0,0 +1,2 @@ +// orchestration_stackresources_v1 +package testing diff --git a/openstack/rts/v1/stackresources/testing/fixtures.go b/openstack/rts/v1/stackresources/testing/fixtures.go new file mode 100644 index 000000000..c411bc402 --- /dev/null +++ b/openstack/rts/v1/stackresources/testing/fixtures.go @@ -0,0 +1,88 @@ +package testing + +import ( + "fmt" + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/openstack/rts/v1/stackresources" + th "github.com/huaweicloud/golangsdk/testhelper" + fake "github.com/huaweicloud/golangsdk/testhelper/client" + "net/http" + "testing" + "time" +) + +// ListExpected represents the expected object from a List request. +var ListExpected = []stackresources.Resource{ + { + Name: "hello_world", + Links: []golangsdk.Link{ + { + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + Rel: "self", + }, + { + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + Rel: "stack", + }, + }, + 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: []string{}, + Status: "CREATE_IN_PROGRESS", + PhysicalID: "49181cd6-169a-4130-9455-31185bbfc5bf", + Type: "OS::Nova::Server", + }, +} + +// ListOutput represents the response body from a List request. +const ListOutput = `{ + "resources": [ + { + "resource_name": "hello_world", + "links": [ + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + "rel": "self" + }, + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + "rel": "stack" + } + ], + "logical_resource_id": "hello_world", + "resource_status_reason": "state changed", + "updated_time": "2015-02-05T21:33:11", + "required_by": [], + "resource_status": "CREATE_IN_PROGRESS", + "physical_resource_id": "49181cd6-169a-4130-9455-31185bbfc5bf", + "creation_time": "2015-02-05T21:33:10", + "resource_type": "OS::Nova::Server", + "attributes": {"SXSW": "atx"}, + "description": "Some resource" + } +] +}` + +// HandleListSuccessfully creates an HTTP handler at `/stacks/hello_world/49181cd6-169a-4130-9455-31185bbfc5bf/resources` +// on the test handler mux that responds with a `List` response. +func HandleListSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/stacks/hello_world/resources", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, output) + case "49181cd6-169a-4130-9455-31185bbfc5bf": + fmt.Fprintf(w, `{"resources":[]}`) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) +} diff --git a/openstack/rts/v1/stackresources/testing/requests_test.go b/openstack/rts/v1/stackresources/testing/requests_test.go new file mode 100644 index 000000000..d3661e821 --- /dev/null +++ b/openstack/rts/v1/stackresources/testing/requests_test.go @@ -0,0 +1,21 @@ +package testing + +import ( + "github.com/huaweicloud/golangsdk/openstack/rts/v1/stackresources" + th "github.com/huaweicloud/golangsdk/testhelper" + fake "github.com/huaweicloud/golangsdk/testhelper/client" + "testing" +) + +func TestListResources(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListSuccessfully(t, ListOutput) + + //count := 0 + actual, err := stackresources.List(fake.ServiceClient(), "hello_world", stackresources.ListOpts{}) + if err != nil { + t.Errorf("Failed to extract resources: %v", err) + } + th.AssertDeepEquals(t, ListExpected, actual) +} diff --git a/openstack/rts/v1/stackresources/urls.go b/openstack/rts/v1/stackresources/urls.go new file mode 100644 index 000000000..8c893fb53 --- /dev/null +++ b/openstack/rts/v1/stackresources/urls.go @@ -0,0 +1,7 @@ +package stackresources + +import "github.com/huaweicloud/golangsdk" + +func listURL(c *golangsdk.ServiceClient, stackName string) string { + return c.ServiceURL("stacks", stackName, "resources") +} From ebdc87ac4e0d106a5bc7b4516ae9be5c6982a20f Mon Sep 17 00:00:00 2001 From: savasw Date: Tue, 19 Jun 2018 15:13:38 +0530 Subject: [PATCH 4/9] add rts v1 stack template get interface --- openstack/rts/v1/stacktemplates/doc.go | 12 +++++ openstack/rts/v1/stacktemplates/requests.go | 9 ++++ openstack/rts/v1/stacktemplates/results.go | 24 +++++++++ .../rts/v1/stacktemplates/testing/fixtures.go | 51 +++++++++++++++++++ .../stacktemplates/testing/requests_test.go | 21 ++++++++ openstack/rts/v1/stacktemplates/urls.go | 7 +++ 6 files changed, 124 insertions(+) create mode 100644 openstack/rts/v1/stacktemplates/doc.go create mode 100644 openstack/rts/v1/stacktemplates/requests.go create mode 100644 openstack/rts/v1/stacktemplates/results.go create mode 100644 openstack/rts/v1/stacktemplates/testing/fixtures.go create mode 100644 openstack/rts/v1/stacktemplates/testing/requests_test.go create mode 100644 openstack/rts/v1/stacktemplates/urls.go diff --git a/openstack/rts/v1/stacktemplates/doc.go b/openstack/rts/v1/stacktemplates/doc.go new file mode 100644 index 000000000..1e625148a --- /dev/null +++ b/openstack/rts/v1/stacktemplates/doc.go @@ -0,0 +1,12 @@ +/* +Package resources enables management and retrieval of +RTS service. + +Example to List Resources + +result := stacktemplates.Get(client, "stackTrace", "e56cac00-463a-4e27-be14-abf414fc9816") + out, err := result.Extract() + fmt.Println(out) + +*/ +package stacktemplates diff --git a/openstack/rts/v1/stacktemplates/requests.go b/openstack/rts/v1/stacktemplates/requests.go new file mode 100644 index 000000000..9e3036b51 --- /dev/null +++ b/openstack/rts/v1/stacktemplates/requests.go @@ -0,0 +1,9 @@ +package stacktemplates + +import "github.com/huaweicloud/golangsdk" + +// Get retreives data for the given stack template. +func Get(c *golangsdk.ServiceClient, stackName, stackID string) (r GetResult) { + _, r.Err = c.Get(getURL(c, stackName, stackID), &r.Body, nil) + return +} diff --git a/openstack/rts/v1/stacktemplates/results.go b/openstack/rts/v1/stacktemplates/results.go new file mode 100644 index 000000000..6184bd7f7 --- /dev/null +++ b/openstack/rts/v1/stacktemplates/results.go @@ -0,0 +1,24 @@ +package stacktemplates + +import ( + "encoding/json" + + "github.com/huaweicloud/golangsdk" +) + +// GetResult represents the result of a Get operation. +type GetResult struct { + golangsdk.Result +} + +// 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 + } + template, err := json.MarshalIndent(r.Body, "", " ") + if err != nil { + return nil, err + } + return template, nil +} diff --git a/openstack/rts/v1/stacktemplates/testing/fixtures.go b/openstack/rts/v1/stacktemplates/testing/fixtures.go new file mode 100644 index 000000000..ab43fa74a --- /dev/null +++ b/openstack/rts/v1/stacktemplates/testing/fixtures.go @@ -0,0 +1,51 @@ +package testing + +import ( + "fmt" + th "github.com/huaweicloud/golangsdk/testhelper" + fake "github.com/huaweicloud/golangsdk/testhelper/client" + "net/http" + "testing" +) + +// GetExpected represents the expected object from a Get request. +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 = ` +{ + "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" + } + } + } +}` + +// HandleGetSuccessfully creates an HTTP handler at `/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87/template` +// on the test handler mux that responds with a `Get` response. +func HandleGetSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87/template", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + 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, output) + }) +} diff --git a/openstack/rts/v1/stacktemplates/testing/requests_test.go b/openstack/rts/v1/stacktemplates/testing/requests_test.go new file mode 100644 index 000000000..300a94670 --- /dev/null +++ b/openstack/rts/v1/stacktemplates/testing/requests_test.go @@ -0,0 +1,21 @@ +package testing + +import ( + "testing" + + "github.com/huaweicloud/golangsdk/openstack/rts/v1/stacktemplates" + th "github.com/huaweicloud/golangsdk/testhelper" + fake "github.com/huaweicloud/golangsdk/testhelper/client" +) + +func TestGetTemplate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetSuccessfully(t, GetOutput) + + actual, err := stacktemplates.Get(fake.ServiceClient(), "postman_stack", "16ef0584-4458-41eb-87c8-0dc8d5f66c87").Extract() + th.AssertNoErr(t, err) + + expected := GetExpected + th.AssertDeepEquals(t, expected, string(actual)) +} diff --git a/openstack/rts/v1/stacktemplates/urls.go b/openstack/rts/v1/stacktemplates/urls.go new file mode 100644 index 000000000..27db437e0 --- /dev/null +++ b/openstack/rts/v1/stacktemplates/urls.go @@ -0,0 +1,7 @@ +package stacktemplates + +import "github.com/huaweicloud/golangsdk" + +func getURL(c *golangsdk.ServiceClient, stackName, stackID string) string { + return c.ServiceURL("stacks", stackName, stackID, "template") +} From a38482bf7cad0682692fffe4619d267d0e6244f5 Mon Sep 17 00:00:00 2001 From: disha-wani Date: Fri, 29 Jun 2018 16:20:24 +0530 Subject: [PATCH 5/9] changes for format --- openstack/rts/v1/stackresources/requests.go | 2 +- openstack/rts/v1/stackresources/results.go | 2 +- openstack/rts/v1/stackresources/testing/fixtures.go | 7 ++++--- openstack/rts/v1/stackresources/testing/requests_test.go | 3 ++- openstack/rts/v1/stacks/requests.go | 5 +++-- openstack/rts/v1/stacks/results.go | 4 ++-- openstack/rts/v1/stacktemplates/testing/fixtures.go | 5 +++-- 7 files changed, 16 insertions(+), 12 deletions(-) diff --git a/openstack/rts/v1/stackresources/requests.go b/openstack/rts/v1/stackresources/requests.go index 9330467e5..23be847c7 100644 --- a/openstack/rts/v1/stackresources/requests.go +++ b/openstack/rts/v1/stackresources/requests.go @@ -1,9 +1,9 @@ package stackresources import ( + "reflect" "github.com/huaweicloud/golangsdk" "github.com/huaweicloud/golangsdk/pagination" - "reflect" ) // ListOptsBuilder allows extensions to add additional parameters to the diff --git a/openstack/rts/v1/stackresources/results.go b/openstack/rts/v1/stackresources/results.go index 1045fdc8c..4133758c0 100644 --- a/openstack/rts/v1/stackresources/results.go +++ b/openstack/rts/v1/stackresources/results.go @@ -1,10 +1,10 @@ package stackresources import ( + "time" "encoding/json" "github.com/huaweicloud/golangsdk" "github.com/huaweicloud/golangsdk/pagination" - "time" ) // Resource represents a stack resource. diff --git a/openstack/rts/v1/stackresources/testing/fixtures.go b/openstack/rts/v1/stackresources/testing/fixtures.go index c411bc402..620c95331 100644 --- a/openstack/rts/v1/stackresources/testing/fixtures.go +++ b/openstack/rts/v1/stackresources/testing/fixtures.go @@ -2,13 +2,14 @@ package testing import ( "fmt" + "net/http" + "testing" + "time" "github.com/huaweicloud/golangsdk" "github.com/huaweicloud/golangsdk/openstack/rts/v1/stackresources" th "github.com/huaweicloud/golangsdk/testhelper" fake "github.com/huaweicloud/golangsdk/testhelper/client" - "net/http" - "testing" - "time" + ) // ListExpected represents the expected object from a List request. diff --git a/openstack/rts/v1/stackresources/testing/requests_test.go b/openstack/rts/v1/stackresources/testing/requests_test.go index d3661e821..afbc8f6d7 100644 --- a/openstack/rts/v1/stackresources/testing/requests_test.go +++ b/openstack/rts/v1/stackresources/testing/requests_test.go @@ -1,10 +1,11 @@ package testing import ( + "testing" "github.com/huaweicloud/golangsdk/openstack/rts/v1/stackresources" th "github.com/huaweicloud/golangsdk/testhelper" fake "github.com/huaweicloud/golangsdk/testhelper/client" - "testing" + ) func TestListResources(t *testing.T) { diff --git a/openstack/rts/v1/stacks/requests.go b/openstack/rts/v1/stacks/requests.go index a398ce251..e68bdef3d 100644 --- a/openstack/rts/v1/stacks/requests.go +++ b/openstack/rts/v1/stacks/requests.go @@ -1,10 +1,11 @@ package stacks import ( - "github.com/huaweicloud/golangsdk" - "github.com/huaweicloud/golangsdk/pagination" "reflect" "strings" + + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/pagination" ) // CreateOptsBuilder is the interface options structs have to satisfy in order diff --git a/openstack/rts/v1/stacks/results.go b/openstack/rts/v1/stacks/results.go index 6891daff2..39fe09e7d 100644 --- a/openstack/rts/v1/stacks/results.go +++ b/openstack/rts/v1/stacks/results.go @@ -4,12 +4,12 @@ import ( "bytes" "encoding/json" "fmt" - "github.com/huaweicloud/golangsdk" - "github.com/huaweicloud/golangsdk/pagination" "io" "reflect" "strings" "time" + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/pagination" ) // CreatedStack represents the object extracted from a Create operation. diff --git a/openstack/rts/v1/stacktemplates/testing/fixtures.go b/openstack/rts/v1/stacktemplates/testing/fixtures.go index ab43fa74a..1f2b8de23 100644 --- a/openstack/rts/v1/stacktemplates/testing/fixtures.go +++ b/openstack/rts/v1/stacktemplates/testing/fixtures.go @@ -2,10 +2,11 @@ package testing import ( "fmt" - th "github.com/huaweicloud/golangsdk/testhelper" - fake "github.com/huaweicloud/golangsdk/testhelper/client" "net/http" "testing" + + th "github.com/huaweicloud/golangsdk/testhelper" + fake "github.com/huaweicloud/golangsdk/testhelper/client" ) // GetExpected represents the expected object from a Get request. From c1723b075b5f82fd0b9e2f96eeed79a9fd34bbba Mon Sep 17 00:00:00 2001 From: disha-wani Date: Fri, 29 Jun 2018 16:28:45 +0530 Subject: [PATCH 6/9] changes for relates import format --- openstack/rts/v1/stackresources/requests.go | 1 + openstack/rts/v1/stackresources/results.go | 1 + openstack/rts/v1/stackresources/testing/fixtures.go | 2 +- openstack/rts/v1/stackresources/testing/requests_test.go | 2 +- openstack/rts/v1/stacks/results.go | 1 + 5 files changed, 5 insertions(+), 2 deletions(-) diff --git a/openstack/rts/v1/stackresources/requests.go b/openstack/rts/v1/stackresources/requests.go index 23be847c7..1bd95461e 100644 --- a/openstack/rts/v1/stackresources/requests.go +++ b/openstack/rts/v1/stackresources/requests.go @@ -2,6 +2,7 @@ package stackresources import ( "reflect" + "github.com/huaweicloud/golangsdk" "github.com/huaweicloud/golangsdk/pagination" ) diff --git a/openstack/rts/v1/stackresources/results.go b/openstack/rts/v1/stackresources/results.go index 4133758c0..e67b1f390 100644 --- a/openstack/rts/v1/stackresources/results.go +++ b/openstack/rts/v1/stackresources/results.go @@ -2,6 +2,7 @@ package stackresources import ( "time" + "encoding/json" "github.com/huaweicloud/golangsdk" "github.com/huaweicloud/golangsdk/pagination" diff --git a/openstack/rts/v1/stackresources/testing/fixtures.go b/openstack/rts/v1/stackresources/testing/fixtures.go index 620c95331..c47e0ce13 100644 --- a/openstack/rts/v1/stackresources/testing/fixtures.go +++ b/openstack/rts/v1/stackresources/testing/fixtures.go @@ -5,11 +5,11 @@ import ( "net/http" "testing" "time" + "github.com/huaweicloud/golangsdk" "github.com/huaweicloud/golangsdk/openstack/rts/v1/stackresources" th "github.com/huaweicloud/golangsdk/testhelper" fake "github.com/huaweicloud/golangsdk/testhelper/client" - ) // ListExpected represents the expected object from a List request. diff --git a/openstack/rts/v1/stackresources/testing/requests_test.go b/openstack/rts/v1/stackresources/testing/requests_test.go index afbc8f6d7..e2fcffafa 100644 --- a/openstack/rts/v1/stackresources/testing/requests_test.go +++ b/openstack/rts/v1/stackresources/testing/requests_test.go @@ -2,10 +2,10 @@ package testing import ( "testing" + "github.com/huaweicloud/golangsdk/openstack/rts/v1/stackresources" th "github.com/huaweicloud/golangsdk/testhelper" fake "github.com/huaweicloud/golangsdk/testhelper/client" - ) func TestListResources(t *testing.T) { diff --git a/openstack/rts/v1/stacks/results.go b/openstack/rts/v1/stacks/results.go index 39fe09e7d..d3bc77331 100644 --- a/openstack/rts/v1/stacks/results.go +++ b/openstack/rts/v1/stacks/results.go @@ -8,6 +8,7 @@ import ( "reflect" "strings" "time" + "github.com/huaweicloud/golangsdk" "github.com/huaweicloud/golangsdk/pagination" ) From 8e7cc42da5aa066b31bb611c07c3158abaf760f1 Mon Sep 17 00:00:00 2001 From: disha-wani Date: Fri, 29 Jun 2018 16:32:58 +0530 Subject: [PATCH 7/9] changes for related to import format --- openstack/rts/v1/stackresources/results.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/rts/v1/stackresources/results.go b/openstack/rts/v1/stackresources/results.go index e67b1f390..74f0498aa 100644 --- a/openstack/rts/v1/stackresources/results.go +++ b/openstack/rts/v1/stackresources/results.go @@ -2,8 +2,8 @@ package stackresources import ( "time" - "encoding/json" + "github.com/huaweicloud/golangsdk" "github.com/huaweicloud/golangsdk/pagination" ) From 8c09f545a73c99f0a8fb01a4a36cd2c35206098f Mon Sep 17 00:00:00 2001 From: disha-wani Date: Fri, 29 Jun 2018 16:35:44 +0530 Subject: [PATCH 8/9] changes for related to import --- openstack/rts/v1/stackresources/results.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/rts/v1/stackresources/results.go b/openstack/rts/v1/stackresources/results.go index 74f0498aa..a7dc82e16 100644 --- a/openstack/rts/v1/stackresources/results.go +++ b/openstack/rts/v1/stackresources/results.go @@ -1,8 +1,8 @@ package stackresources import ( - "time" "encoding/json" + "time" "github.com/huaweicloud/golangsdk" "github.com/huaweicloud/golangsdk/pagination" From a52c316be3cb8454cf6b8a4b25128718449036b7 Mon Sep 17 00:00:00 2001 From: disha-wani Date: Fri, 29 Jun 2018 17:14:34 +0530 Subject: [PATCH 9/9] removed comments --- openstack/rts/v1/stackresources/doc.go | 18 +--- openstack/rts/v1/stacks/doc.go | 129 ------------------------- openstack/rts/v1/stacktemplates/doc.go | 12 +-- 3 files changed, 2 insertions(+), 157 deletions(-) diff --git a/openstack/rts/v1/stackresources/doc.go b/openstack/rts/v1/stackresources/doc.go index 959b0f02d..1deebd08d 100644 --- a/openstack/rts/v1/stackresources/doc.go +++ b/openstack/rts/v1/stackresources/doc.go @@ -1,18 +1,2 @@ -/* -Package resources enables management and retrieval of Resources -RTS service. - -Example to List Resources - -listOpts := stackresources.ListOpts{} -allResources, err := stackresources.List(orchestrationClient, listOpts) -if err != nil { -panic(err) -} - -for _, resource := range allResources { -fmt.Printf("%+v\n", resource) -} -*/ - +// Package stackresources provides operations for working with stack resources. package stackresources diff --git a/openstack/rts/v1/stacks/doc.go b/openstack/rts/v1/stacks/doc.go index 0e18b3770..e4cbdabab 100644 --- a/openstack/rts/v1/stacks/doc.go +++ b/openstack/rts/v1/stacks/doc.go @@ -6,133 +6,4 @@ // running instance of a template. The result of creating a stack is a deployment // of the application framework or component. -/* -Package resources enables management and retrieval of -RTS service. - -Example to List Stacks - -lis:=stacks.ListOpts{SortDir:stacks.SortAsc,SortKey:stacks.SortStatus} - getstack,err:=stacks.List(client,lis).AllPages() - stacks,err:=stacks.ExtractStacks(getstack) - fmt.Println(stacks) - -Example to Create a Stacks -template := new(stacks.Template) - template.Bin = []byte(` - { - "heat_template_version": "2013-05-23", - "description": "Simple template to deploy", - "parameters": { - "image_id": { - "type": "string", - "description": "Image to be used for compute instance", - "label": "Image ID", - "default": "ea67839e-fd7a-4b99-9f81-13c4c8dc317c" - }, - "net_id": { - "type": "string", - "description": "The network to be used", - "label": "Network UUID", - "default": "7eb54ab6-5cdb-446a-abbe-0dda1885c76e" - }, - "instance_type": { - "type": "string", - "description": "Type of instance (flavor) to be used", - "label": "Instance Type", - "default": "s1.medium" - } - }, - "resources": { - "my_instance": { - "type": "OS::Nova::Server", - "properties": { - "image": { - "get_param": "image_id" - }, - "flavor": { - "get_param": "instance_type" - }, - "networks": [ - { - "network": { - "get_param": "net_id" - } - } - ] - } - } - } - }`) - - fmt.Println(template) - stack:=stacks.CreateOpts{Name:"terraform-providerr_disssssss",Timeout: 60, - TemplateOpts: template, } - outstack,err:=stacks.Create(client,stack).Extract() - if err != nil { - panic(err) - } - -Example to Update a Stacks - -template := new(stacks.Template) - template.Bin = []byte(` - { - "heat_template_version": "2013-05-23", - "description": "Simple template disha", - "parameters": { - "image_id": { - "type": "string", - "description": "Image to be used for compute instance", - "label": "Image ID", - "default": "ea67839e-fd7a-4b99-9f81-13c4c8dc317c" - }, - "net_id": { - "type": "string", - "description": "The network to be used", - "label": "Network UUID", - "default": "7eb54ab6-5cdb-446a-abbe-0dda1885c76e" - }, - "instance_type": { - "type": "string", - "description": "Type of instance (flavor) to be used", - "label": "Instance Type", - "default": "s1.medium" - } - }, - "resources": { - "my_instance": { - "type": "OS::Nova::Server", - "properties": { - "image": { - "get_param": "image_id" - }, - "flavor": { - "get_param": "instance_type" - }, - "networks": [ - { - "network": { - "get_param": "net_id" - } - } - ] - } - } - } - }`) - myopt:=stacks.UpdateOpts{TemplateOpts:template} - errup:=stacks.Update(client,"terraform-providerr_stack_bigdata","b631d6ce-9010-4c34-b994-cd5a10b13c86",myopt).ExtractErr() - if err != nil { - panic(err) - } - -Example to Delete a Stacks - - err:=stacks.Delete(client,"terraform-providerr_stack_bigdata","b631d6ce-9010-4c34-b994-cd5a10b13c86") - - if err != nil { - panic(err) - } -*/ package stacks diff --git a/openstack/rts/v1/stacktemplates/doc.go b/openstack/rts/v1/stacktemplates/doc.go index 1e625148a..40e3d7347 100644 --- a/openstack/rts/v1/stacktemplates/doc.go +++ b/openstack/rts/v1/stacktemplates/doc.go @@ -1,12 +1,2 @@ -/* -Package resources enables management and retrieval of -RTS service. - -Example to List Resources - -result := stacktemplates.Get(client, "stackTrace", "e56cac00-463a-4e27-be14-abf414fc9816") - out, err := result.Extract() - fmt.Println(out) - -*/ +// Package stacktemplates provides operations for working with Heat templates. package stacktemplates