Skip to content
This repository was archived by the owner on Aug 1, 2023. It is now read-only.

Commit 5fddb2a

Browse files
committed
Add template and environment parsing to gophercloud
Openstack Heat expects the client to do some parsing client side, specifically for nested templates and environments which refer to local files. This patch adds a recursive parser for both the template and environment files to gophercloud. The interfaces are also changed to make use of the new parsing functionality.
1 parent 827c03e commit 5fddb2a

File tree

12 files changed

+1861
-41
lines changed

12 files changed

+1861
-41
lines changed
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package stacks
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"strings"
7+
)
8+
9+
// an interface to represent stack environments
10+
type Environment struct {
11+
TE
12+
}
13+
14+
// allowed sections in a stack environment file
15+
var EnvironmentSections = map[string]bool{
16+
"parameters": true,
17+
"parameter_defaults": true,
18+
"resource_registry": true,
19+
}
20+
21+
func (e *Environment) Validate() error {
22+
if e.Parsed == nil {
23+
if err := e.Parse(); err != nil {
24+
return err
25+
}
26+
}
27+
for key, _ := range e.Parsed {
28+
if _, ok := EnvironmentSections[key]; !ok {
29+
return errors.New(fmt.Sprintf("Environment has wrong section: %s", key))
30+
}
31+
}
32+
return nil
33+
}
34+
35+
// Parse environment file to resolve the urls of the resources
36+
func GetRRFileContents(e *Environment, ignoreIf igFunc) error {
37+
if e.Files == nil {
38+
e.Files = make(map[string]string)
39+
}
40+
if e.fileMaps == nil {
41+
e.fileMaps = make(map[string]string)
42+
}
43+
rr := e.Parsed["resource_registry"]
44+
// search the resource registry for URLs
45+
switch rr.(type) {
46+
case map[string]interface{}, map[interface{}]interface{}:
47+
rr_map, err := toStringKeys(rr)
48+
if err != nil {
49+
return err
50+
}
51+
var baseURL string
52+
if val, ok := rr_map["base_url"]; ok {
53+
baseURL = val.(string)
54+
} else {
55+
baseURL = e.baseURL
56+
}
57+
// use a fake template to fetch contents from URLs
58+
tempTemplate := new(Template)
59+
tempTemplate.baseURL = baseURL
60+
tempTemplate.client = e.client
61+
62+
if err = GetFileContents(tempTemplate, rr, ignoreIf, false); err != nil {
63+
return err
64+
}
65+
// check the `resources` section (if it exists) for more URLs
66+
if val, ok := rr_map["resources"]; ok {
67+
switch val.(type) {
68+
case map[string]interface{}, map[interface{}]interface{}:
69+
resources_map, err := toStringKeys(val)
70+
if err != nil {
71+
return err
72+
}
73+
for _, v := range resources_map {
74+
switch v.(type) {
75+
case map[string]interface{}, map[interface{}]interface{}:
76+
resource_map, err := toStringKeys(v)
77+
if err != nil {
78+
return err
79+
}
80+
var resourceBaseURL string
81+
// if base_url for the resource type is defined, use it
82+
if val, ok := resource_map["base_url"]; ok {
83+
resourceBaseURL = val.(string)
84+
} else {
85+
resourceBaseURL = baseURL
86+
}
87+
tempTemplate.baseURL = resourceBaseURL
88+
if err := GetFileContents(tempTemplate, v, ignoreIf, false); err != nil {
89+
return err
90+
}
91+
}
92+
93+
}
94+
95+
}
96+
}
97+
e.Files = tempTemplate.Files
98+
return nil
99+
default:
100+
return nil
101+
}
102+
}
103+
104+
// function to choose keys whose values are other environment files
105+
func ignoreIfEnvironment(key string, value interface{}) bool {
106+
// base_url and hooks refer to components which cannot have urls
107+
if key == "base_url" || key == "hooks" {
108+
return true
109+
}
110+
// if value is not string, it cannot be a URL
111+
valueString, ok := value.(string)
112+
if !ok {
113+
return true
114+
}
115+
// if value contains `::`, it must be a reference to another resource type
116+
// e.g. OS::Nova::Server : Rackspace::Cloud::Server
117+
if strings.Contains(valueString, "::") {
118+
return true
119+
}
120+
return false
121+
}
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
package stacks
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"net/url"
7+
"strings"
8+
"testing"
9+
10+
th "github.com/rackspace/gophercloud/testhelper"
11+
)
12+
13+
func TestEnvironmentValidation(t *testing.T) {
14+
environmentJSON := new(Environment)
15+
environmentJSON.Bin = []byte(ValidJSONEnvironment)
16+
err := environmentJSON.Validate()
17+
th.AssertNoErr(t, err)
18+
19+
environmentYAML := new(Environment)
20+
environmentYAML.Bin = []byte(ValidYAMLEnvironment)
21+
err = environmentYAML.Validate()
22+
th.AssertNoErr(t, err)
23+
24+
environmentInvalid := new(Environment)
25+
environmentInvalid.Bin = []byte(InvalidEnvironment)
26+
if err = environmentInvalid.Validate(); err == nil {
27+
t.Error("environment validation did not catch invalid environment")
28+
}
29+
}
30+
31+
func TestEnvironmentParsing(t *testing.T) {
32+
environmentJSON := new(Environment)
33+
environmentJSON.Bin = []byte(ValidJSONEnvironment)
34+
err := environmentJSON.Parse()
35+
th.AssertNoErr(t, err)
36+
th.AssertDeepEquals(t, ValidJSONEnvironmentParsed, environmentJSON.Parsed)
37+
38+
environmentYAML := new(Environment)
39+
environmentYAML.Bin = []byte(ValidJSONEnvironment)
40+
err = environmentYAML.Parse()
41+
th.AssertNoErr(t, err)
42+
th.AssertDeepEquals(t, ValidJSONEnvironmentParsed, environmentYAML.Parsed)
43+
44+
environmentInvalid := new(Environment)
45+
environmentInvalid.Bin = []byte("Keep Austin Weird")
46+
err = environmentInvalid.Parse()
47+
if err == nil {
48+
t.Error("environment parsing did not catch invalid environment")
49+
}
50+
}
51+
52+
func TestIgnoreIfEnvironment(t *testing.T) {
53+
var keyValueTests = []struct {
54+
key string
55+
value interface{}
56+
out bool
57+
}{
58+
{"base_url", "afksdf", true},
59+
{"not_type", "hooks", false},
60+
{"get_file", "::", true},
61+
{"hooks", "dfsdfsd", true},
62+
{"type", "sdfubsduf.yaml", false},
63+
{"type", "sdfsdufs.environment", false},
64+
{"type", "sdfsdf.file", false},
65+
{"type", map[string]string{"key": "value"}, true},
66+
}
67+
var result bool
68+
for _, kv := range keyValueTests {
69+
result = ignoreIfEnvironment(kv.key, kv.value)
70+
if result != kv.out {
71+
t.Errorf("key: %v, value: %v expected: %v, actual: %v", kv.key, kv.value, kv.out, result)
72+
}
73+
}
74+
}
75+
76+
func TestGetRRFileContents(t *testing.T) {
77+
th.SetupHTTP()
78+
defer th.TeardownHTTP()
79+
environment_content := `
80+
heat_template_version: 2013-05-23
81+
82+
description:
83+
Heat WordPress template to support F18, using only Heat OpenStack-native
84+
resource types, and without the requirement for heat-cfntools in the image.
85+
WordPress is web software you can use to create a beautiful website or blog.
86+
This template installs a single-instance WordPress deployment using a local
87+
MySQL database to store the data.
88+
89+
parameters:
90+
91+
key_name:
92+
type: string
93+
description : Name of a KeyPair to enable SSH access to the instance
94+
95+
resources:
96+
wordpress_instance:
97+
type: OS::Nova::Server
98+
properties:
99+
image: { get_param: image_id }
100+
flavor: { get_param: instance_type }
101+
key_name: { get_param: key_name }`
102+
103+
db_content := `
104+
heat_template_version: 2014-10-16
105+
106+
description:
107+
Test template for Trove resource capabilities
108+
109+
parameters:
110+
db_pass:
111+
type: string
112+
hidden: true
113+
description: Database access password
114+
default: secrete
115+
116+
resources:
117+
118+
service_db:
119+
type: OS::Trove::Instance
120+
properties:
121+
name: trove_test_db
122+
datastore_type: mariadb
123+
flavor: 1GB Instance
124+
size: 10
125+
databases:
126+
- name: test_data
127+
users:
128+
- name: kitchen_sink
129+
password: { get_param: db_pass }
130+
databases: [ test_data ]`
131+
baseurl, err := getBasePath()
132+
th.AssertNoErr(t, err)
133+
134+
fakeEnvURL := strings.Join([]string{baseurl, "my_env.yaml"}, "/")
135+
urlparsed, err := url.Parse(fakeEnvURL)
136+
th.AssertNoErr(t, err)
137+
// handler for my_env.yaml
138+
th.Mux.HandleFunc(urlparsed.Path, func(w http.ResponseWriter, r *http.Request) {
139+
th.TestMethod(t, r, "GET")
140+
w.Header().Set("Content-Type", "application/jason")
141+
w.WriteHeader(http.StatusOK)
142+
fmt.Fprintf(w, environment_content)
143+
})
144+
145+
fakeDBURL := strings.Join([]string{baseurl, "my_db.yaml"}, "/")
146+
urlparsed, err = url.Parse(fakeDBURL)
147+
th.AssertNoErr(t, err)
148+
149+
// handler for my_db.yaml
150+
th.Mux.HandleFunc(urlparsed.Path, func(w http.ResponseWriter, r *http.Request) {
151+
th.TestMethod(t, r, "GET")
152+
w.Header().Set("Content-Type", "application/jason")
153+
w.WriteHeader(http.StatusOK)
154+
fmt.Fprintf(w, db_content)
155+
})
156+
157+
client := fakeClient{BaseClient: getHTTPClient()}
158+
env := new(Environment)
159+
env.Bin = []byte(`{"resource_registry": {"My::WP::Server": "my_env.yaml", "resources": {"my_db_server": {"OS::DBInstance": "my_db.yaml"}}}}`)
160+
env.client = client
161+
162+
err = env.Parse()
163+
th.AssertNoErr(t, err)
164+
err = GetRRFileContents(env, ignoreIfEnvironment)
165+
th.AssertNoErr(t, err)
166+
expected_env_files_content := "\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 }"
167+
expected_db_files_content := "\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 ]"
168+
169+
th.AssertEquals(t, expected_env_files_content, env.Files[fakeEnvURL])
170+
th.AssertEquals(t, expected_db_files_content, env.Files[fakeDBURL])
171+
172+
env.FixFileRefs()
173+
expected_parsed := map[string]interface{}{
174+
"resource_registry": "2015-04-30",
175+
"My::WP::Server": fakeEnvURL,
176+
"resources": map[string]interface{}{
177+
"my_db_server": map[string]interface{}{
178+
"OS::DBInstance": fakeDBURL,
179+
},
180+
},
181+
}
182+
env.Parse()
183+
th.AssertDeepEquals(t, expected_parsed, env.Parsed)
184+
}

0 commit comments

Comments
 (0)