]>
Commit | Line | Data |
---|---|---|
bae9f6d2 JC |
1 | package terraform |
2 | ||
3 | import ( | |
4 | "fmt" | |
5 | "reflect" | |
6 | "regexp" | |
7 | "strconv" | |
8 | "strings" | |
9 | ||
10 | "github.com/hashicorp/terraform/config" | |
11 | ) | |
12 | ||
13 | // ResourceAddress is a way of identifying an individual resource (or, | |
14 | // eventually, a subset of resources) within the state. It is used for Targets. | |
15 | type ResourceAddress struct { | |
16 | // Addresses a resource falling somewhere in the module path | |
17 | // When specified alone, addresses all resources within a module path | |
18 | Path []string | |
19 | ||
20 | // Addresses a specific resource that occurs in a list | |
21 | Index int | |
22 | ||
23 | InstanceType InstanceType | |
24 | InstanceTypeSet bool | |
25 | Name string | |
26 | Type string | |
27 | Mode config.ResourceMode // significant only if InstanceTypeSet | |
28 | } | |
29 | ||
30 | // Copy returns a copy of this ResourceAddress | |
31 | func (r *ResourceAddress) Copy() *ResourceAddress { | |
32 | if r == nil { | |
33 | return nil | |
34 | } | |
35 | ||
36 | n := &ResourceAddress{ | |
37 | Path: make([]string, 0, len(r.Path)), | |
38 | Index: r.Index, | |
39 | InstanceType: r.InstanceType, | |
40 | Name: r.Name, | |
41 | Type: r.Type, | |
42 | Mode: r.Mode, | |
43 | } | |
44 | for _, p := range r.Path { | |
45 | n.Path = append(n.Path, p) | |
46 | } | |
47 | return n | |
48 | } | |
49 | ||
50 | // String outputs the address that parses into this address. | |
51 | func (r *ResourceAddress) String() string { | |
52 | var result []string | |
53 | for _, p := range r.Path { | |
54 | result = append(result, "module", p) | |
55 | } | |
56 | ||
57 | switch r.Mode { | |
58 | case config.ManagedResourceMode: | |
59 | // nothing to do | |
60 | case config.DataResourceMode: | |
61 | result = append(result, "data") | |
62 | default: | |
63 | panic(fmt.Errorf("unsupported resource mode %s", r.Mode)) | |
64 | } | |
65 | ||
66 | if r.Type != "" { | |
67 | result = append(result, r.Type) | |
68 | } | |
69 | ||
70 | if r.Name != "" { | |
71 | name := r.Name | |
72 | if r.InstanceTypeSet { | |
73 | switch r.InstanceType { | |
74 | case TypePrimary: | |
75 | name += ".primary" | |
76 | case TypeDeposed: | |
77 | name += ".deposed" | |
78 | case TypeTainted: | |
79 | name += ".tainted" | |
80 | } | |
81 | } | |
82 | ||
83 | if r.Index >= 0 { | |
84 | name += fmt.Sprintf("[%d]", r.Index) | |
85 | } | |
86 | result = append(result, name) | |
87 | } | |
88 | ||
89 | return strings.Join(result, ".") | |
90 | } | |
91 | ||
92 | // stateId returns the ID that this resource should be entered with | |
93 | // in the state. This is also used for diffs. In the future, we'd like to | |
94 | // move away from this string field so I don't export this. | |
95 | func (r *ResourceAddress) stateId() string { | |
96 | result := fmt.Sprintf("%s.%s", r.Type, r.Name) | |
97 | switch r.Mode { | |
98 | case config.ManagedResourceMode: | |
99 | // Done | |
100 | case config.DataResourceMode: | |
101 | result = fmt.Sprintf("data.%s", result) | |
102 | default: | |
103 | panic(fmt.Errorf("unknown resource mode: %s", r.Mode)) | |
104 | } | |
105 | if r.Index >= 0 { | |
106 | result += fmt.Sprintf(".%d", r.Index) | |
107 | } | |
108 | ||
109 | return result | |
110 | } | |
111 | ||
112 | // parseResourceAddressConfig creates a resource address from a config.Resource | |
113 | func parseResourceAddressConfig(r *config.Resource) (*ResourceAddress, error) { | |
114 | return &ResourceAddress{ | |
115 | Type: r.Type, | |
116 | Name: r.Name, | |
117 | Index: -1, | |
118 | InstanceType: TypePrimary, | |
119 | Mode: r.Mode, | |
120 | }, nil | |
121 | } | |
122 | ||
123 | // parseResourceAddressInternal parses the somewhat bespoke resource | |
124 | // identifier used in states and diffs, such as "instance.name.0". | |
125 | func parseResourceAddressInternal(s string) (*ResourceAddress, error) { | |
126 | // Split based on ".". Every resource address should have at least two | |
127 | // elements (type and name). | |
128 | parts := strings.Split(s, ".") | |
129 | if len(parts) < 2 || len(parts) > 4 { | |
130 | return nil, fmt.Errorf("Invalid internal resource address format: %s", s) | |
131 | } | |
132 | ||
133 | // Data resource if we have at least 3 parts and the first one is data | |
134 | mode := config.ManagedResourceMode | |
135 | if len(parts) > 2 && parts[0] == "data" { | |
136 | mode = config.DataResourceMode | |
137 | parts = parts[1:] | |
138 | } | |
139 | ||
140 | // If we're not a data resource and we have more than 3, then it is an error | |
141 | if len(parts) > 3 && mode != config.DataResourceMode { | |
142 | return nil, fmt.Errorf("Invalid internal resource address format: %s", s) | |
143 | } | |
144 | ||
145 | // Build the parts of the resource address that are guaranteed to exist | |
146 | addr := &ResourceAddress{ | |
147 | Type: parts[0], | |
148 | Name: parts[1], | |
149 | Index: -1, | |
150 | InstanceType: TypePrimary, | |
151 | Mode: mode, | |
152 | } | |
153 | ||
154 | // If we have more parts, then we have an index. Parse that. | |
155 | if len(parts) > 2 { | |
156 | idx, err := strconv.ParseInt(parts[2], 0, 0) | |
157 | if err != nil { | |
158 | return nil, fmt.Errorf("Error parsing resource address %q: %s", s, err) | |
159 | } | |
160 | ||
161 | addr.Index = int(idx) | |
162 | } | |
163 | ||
164 | return addr, nil | |
165 | } | |
166 | ||
167 | func ParseResourceAddress(s string) (*ResourceAddress, error) { | |
168 | matches, err := tokenizeResourceAddress(s) | |
169 | if err != nil { | |
170 | return nil, err | |
171 | } | |
172 | mode := config.ManagedResourceMode | |
173 | if matches["data_prefix"] != "" { | |
174 | mode = config.DataResourceMode | |
175 | } | |
176 | resourceIndex, err := ParseResourceIndex(matches["index"]) | |
177 | if err != nil { | |
178 | return nil, err | |
179 | } | |
180 | instanceType, err := ParseInstanceType(matches["instance_type"]) | |
181 | if err != nil { | |
182 | return nil, err | |
183 | } | |
184 | path := ParseResourcePath(matches["path"]) | |
185 | ||
186 | // not allowed to say "data." without a type following | |
187 | if mode == config.DataResourceMode && matches["type"] == "" { | |
188 | return nil, fmt.Errorf("must target specific data instance") | |
189 | } | |
190 | ||
191 | return &ResourceAddress{ | |
192 | Path: path, | |
193 | Index: resourceIndex, | |
194 | InstanceType: instanceType, | |
195 | InstanceTypeSet: matches["instance_type"] != "", | |
196 | Name: matches["name"], | |
197 | Type: matches["type"], | |
198 | Mode: mode, | |
199 | }, nil | |
200 | } | |
201 | ||
202 | func (addr *ResourceAddress) Equals(raw interface{}) bool { | |
203 | other, ok := raw.(*ResourceAddress) | |
204 | if !ok { | |
205 | return false | |
206 | } | |
207 | ||
208 | pathMatch := len(addr.Path) == 0 && len(other.Path) == 0 || | |
209 | reflect.DeepEqual(addr.Path, other.Path) | |
210 | ||
211 | indexMatch := addr.Index == -1 || | |
212 | other.Index == -1 || | |
213 | addr.Index == other.Index | |
214 | ||
215 | nameMatch := addr.Name == "" || | |
216 | other.Name == "" || | |
217 | addr.Name == other.Name | |
218 | ||
219 | typeMatch := addr.Type == "" || | |
220 | other.Type == "" || | |
221 | addr.Type == other.Type | |
222 | ||
223 | // mode is significant only when type is set | |
224 | modeMatch := addr.Type == "" || | |
225 | other.Type == "" || | |
226 | addr.Mode == other.Mode | |
227 | ||
228 | return pathMatch && | |
229 | indexMatch && | |
230 | addr.InstanceType == other.InstanceType && | |
231 | nameMatch && | |
232 | typeMatch && | |
233 | modeMatch | |
234 | } | |
235 | ||
236 | func ParseResourceIndex(s string) (int, error) { | |
237 | if s == "" { | |
238 | return -1, nil | |
239 | } | |
240 | return strconv.Atoi(s) | |
241 | } | |
242 | ||
243 | func ParseResourcePath(s string) []string { | |
244 | if s == "" { | |
245 | return nil | |
246 | } | |
247 | parts := strings.Split(s, ".") | |
248 | path := make([]string, 0, len(parts)) | |
249 | for _, s := range parts { | |
250 | // Due to the limitations of the regexp match below, the path match has | |
251 | // some noise in it we have to filter out :| | |
252 | if s == "" || s == "module" { | |
253 | continue | |
254 | } | |
255 | path = append(path, s) | |
256 | } | |
257 | return path | |
258 | } | |
259 | ||
260 | func ParseInstanceType(s string) (InstanceType, error) { | |
261 | switch s { | |
262 | case "", "primary": | |
263 | return TypePrimary, nil | |
264 | case "deposed": | |
265 | return TypeDeposed, nil | |
266 | case "tainted": | |
267 | return TypeTainted, nil | |
268 | default: | |
269 | return TypeInvalid, fmt.Errorf("Unexpected value for InstanceType field: %q", s) | |
270 | } | |
271 | } | |
272 | ||
273 | func tokenizeResourceAddress(s string) (map[string]string, error) { | |
274 | // Example of portions of the regexp below using the | |
275 | // string "aws_instance.web.tainted[1]" | |
276 | re := regexp.MustCompile(`\A` + | |
277 | // "module.foo.module.bar" (optional) | |
278 | `(?P<path>(?:module\.[^.]+\.?)*)` + | |
279 | // possibly "data.", if targeting is a data resource | |
280 | `(?P<data_prefix>(?:data\.)?)` + | |
281 | // "aws_instance.web" (optional when module path specified) | |
282 | `(?:(?P<type>[^.]+)\.(?P<name>[^.[]+))?` + | |
283 | // "tainted" (optional, omission implies: "primary") | |
284 | `(?:\.(?P<instance_type>\w+))?` + | |
285 | // "1" (optional, omission implies: "0") | |
286 | `(?:\[(?P<index>\d+)\])?` + | |
287 | `\z`) | |
288 | ||
289 | groupNames := re.SubexpNames() | |
290 | rawMatches := re.FindAllStringSubmatch(s, -1) | |
291 | if len(rawMatches) != 1 { | |
292 | return nil, fmt.Errorf("Problem parsing address: %q", s) | |
293 | } | |
294 | ||
295 | matches := make(map[string]string) | |
296 | for i, m := range rawMatches[0] { | |
297 | matches[groupNames[i]] = m | |
298 | } | |
299 | ||
300 | return matches, nil | |
301 | } |