Merge lp:~wallyworld/juju-core/constraints-validation-merge into lp:~go-bot/juju-core/trunk
- constraints-validation-merge
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Ian Booth | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 2658 | ||||
Proposed branch: | lp:~wallyworld/juju-core/constraints-validation-merge | ||||
Merge into: | lp:~go-bot/juju-core/trunk | ||||
Diff against target: |
1106 lines (+744/-184) 8 files modified
constraints/constraints.go (+131/-50) constraints/constraints_test.go (+152/-131) constraints/export_test.go (+18/-0) constraints/validation.go (+134/-0) constraints/validation_test.go (+296/-0) state/addmachine.go (+5/-1) state/prechecker_test.go (+3/-1) state/service.go (+5/-1) |
||||
To merge this branch: | bzr merge lp:~wallyworld/juju-core/constraints-validation-merge | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju Engineering | Pending | ||
Review via email: mp+215807@code.launchpad.net |
Commit message
New constraints validation and merge
A new constraints Validator implementation
is introduced, which provides validation
and merging support for constraints. It
allows for conflicting attributes and
unsupported attributes to be registered,
and does the right thing with these.
To illustrate the point, a new constraints
attribute instance-type is also added, which
will be used in a followup branch.
Description of the change
New constraints validation and merge
A new constraints Validator implementation
is introduced, which provides validation
and merging support for constraints. It
allows for conflicting attributes and
unsupported attributes to be registered,
and does the right thing with these.
To illustrate the point, a new constraints
attribute instance-type is also added, which
will be used in a followup branch.
Ian Booth (wallyworld) wrote : | # |
Ian Booth (wallyworld) wrote : | # |
Please take a look.
Ian Booth (wallyworld) wrote : | # |
Please take a look.
Ian Booth (wallyworld) wrote : | # |
Please take a look.
William Reade (fwereade) wrote : | # |
LGTM apart from the overly-broad interface.
https:/
File constraints/
https:/
constraints/
I'm not sure why this is an interface when it's impossible for other
people to implement it (unexported methods...). In general, I'm all for
interfaces at judiciously chosen boundaries, but I'm not 100% sure this
is one. Something like:
type Validator interface {
Validate(cons Value) error
Merge(consA, consB Value) (Value, error)
}
...would feel neater to me. Although "consA, consB" is suboptimal...
"fallbacks, primary" doesn't work either, but I'd really like something
about the names to indicate which is overriding which.
https:/
constraints/
I really appreciate blank lines before comments.
https:/
constraints/
Callers will indeed probably want to ignore them. I wonder how we can
cleanly feed this stuff back to CLI users (or, well, any users)?
Ian Booth (wallyworld) wrote : | # |
Please take a look.
https:/
File constraints/
https:/
constraints/
On 2014/04/18 07:48:20, fwereade wrote:
> I'm not sure why this is an interface when it's impossible for other
people to
> implement it (unexported methods...). In general, I'm all for
interfaces at
> judiciously chosen boundaries, but I'm not 100% sure this is one.
Something
> like:
> type Validator interface {
> Validate(cons Value) error
> Merge(consA, consB Value) (Value, error)
> }
> ...would feel neater to me. Although "consA, consB" is suboptimal...
"fallbacks,
> primary" doesn't work either, but I'd really like something about the
names to
> indicate which is overriding which.
I renamed consA to consFallbacks, and consB to cons
I kept only the exported methods on the interface. This includes both
the Register methods and Validate and Merge.
https:/
constraints/
On 2014/04/18 07:48:20, fwereade wrote:
> I really appreciate blank lines before comments.
Done.
https:/
constraints/
On 2014/04/18 07:48:20, fwereade wrote:
> Callers will indeed probably want to ignore them. I wonder how we can
cleanly
> feed this stuff back to CLI users (or, well, any users)?
I reworked the Validate interface - it now returns ([]string, error).
[]string is the unsupported attributes, error is the true validation
error. The NotSUpportedError is gone. So if error is nil, the caller can
then see if the unsupported attrs is populated and do as it wishes, or
just call _, err := validator.
William Reade (fwereade) wrote : | # |
Still LGTM, with a vague feeling that an explicit UnsupportedCons
error, with a method that itself returns the []string, would be even
nicer -- and then it's a similar story, you try to validate and can
either ignore or handle the specific error as you please. I'll leave it
to your judgment, though.
Preview Diff
1 | === modified file 'constraints/constraints.go' |
2 | --- constraints/constraints.go 2014-03-17 05:04:51 +0000 |
3 | +++ constraints/constraints.go 2014-04-18 13:10:25 +0000 |
4 | @@ -1,4 +1,4 @@ |
5 | -// Copyright 2013 Canonical Ltd. |
6 | +// Copyright 2013, 2014 Canonical Ltd. |
7 | // Licensed under the AGPLv3, see LICENCE file for details. |
8 | |
9 | package constraints |
10 | @@ -6,6 +6,7 @@ |
11 | import ( |
12 | "fmt" |
13 | "math" |
14 | + "reflect" |
15 | "strconv" |
16 | "strings" |
17 | |
18 | @@ -15,6 +16,19 @@ |
19 | "launchpad.net/juju-core/juju/arch" |
20 | ) |
21 | |
22 | +// The following constants list the supported constraint attribute names, as defined |
23 | +// by the fields in the Value struct. |
24 | +const ( |
25 | + Arch = "arch" |
26 | + Container = "container" |
27 | + CpuCores = "cpu-cores" |
28 | + CpuPower = "cpu-power" |
29 | + Mem = "mem" |
30 | + RootDisk = "root-disk" |
31 | + Tags = "tags" |
32 | + InstanceType = "instance-type" |
33 | +) |
34 | + |
35 | // Value describes a user's requirements of the hardware on which units |
36 | // of a service will run. Constraints are used to choose an existing machine |
37 | // onto which a unit will be deployed, or to provision a new machine if no |
38 | @@ -52,18 +66,45 @@ |
39 | // An empty list is treated the same as a nil (unspecified) list, except an |
40 | // empty list will override any default tags, where a nil list will not. |
41 | Tags *[]string `json:"tags,omitempty" yaml:"tags,omitempty"` |
42 | + |
43 | + // InstanceType, if not nil, indicates that the specified cloud instance type |
44 | + // be used. Only valid for clouds which support instance types. |
45 | + InstanceType *string `json:"instance-type,omitempty" yaml:"instance-type,omitempty"` |
46 | +} |
47 | + |
48 | +// fieldNames records a mapping from the constraint tag to struct field name. |
49 | +// eg "root-disk" maps to RootDisk. |
50 | +var fieldNames map[string]string |
51 | + |
52 | +func init() { |
53 | + // Create the fieldNames map by inspecting the json tags for each of |
54 | + // the Value struct fields. |
55 | + fieldNames = make(map[string]string) |
56 | + typ := reflect.TypeOf(Value{}) |
57 | + for i := 0; i < typ.NumField(); i++ { |
58 | + field := typ.Field(i) |
59 | + if tag := field.Tag.Get("json"); tag != "" { |
60 | + if i := strings.Index(tag, ","); i >= 0 { |
61 | + tag = tag[0:i] |
62 | + } |
63 | + if tag == "-" { |
64 | + continue |
65 | + } |
66 | + if tag != "" { |
67 | + fieldNames[tag] = field.Name |
68 | + } |
69 | + } |
70 | + } |
71 | } |
72 | |
73 | // IsEmpty returns if the given constraints value has no constraints set |
74 | func IsEmpty(v *Value) bool { |
75 | - return v == nil || |
76 | - v.Arch == nil && |
77 | - v.Container == nil && |
78 | - v.CpuCores == nil && |
79 | - v.CpuPower == nil && |
80 | - v.Mem == nil && |
81 | - v.RootDisk == nil && |
82 | - v.Tags == nil |
83 | + return v.String() == "" |
84 | +} |
85 | + |
86 | +// HasInstanceType returns true if the constraints.Value specifies an instance type. |
87 | +func (v *Value) HasInstanceType() bool { |
88 | + return v.InstanceType != nil && *v.InstanceType != "" |
89 | } |
90 | |
91 | // String expresses a constraints.Value in the language in which it was specified. |
92 | @@ -81,6 +122,9 @@ |
93 | if v.CpuPower != nil { |
94 | strs = append(strs, "cpu-power="+uintStr(*v.CpuPower)) |
95 | } |
96 | + if v.InstanceType != nil { |
97 | + strs = append(strs, "instance-type="+string(*v.InstanceType)) |
98 | + } |
99 | if v.Mem != nil { |
100 | s := uintStr(*v.Mem) |
101 | if s != "" { |
102 | @@ -102,33 +146,6 @@ |
103 | return strings.Join(strs, " ") |
104 | } |
105 | |
106 | -// WithFallbacks returns a copy of v with nil values taken from v0. |
107 | -func (v Value) WithFallbacks(v0 Value) Value { |
108 | - v1 := v0 |
109 | - if v.Arch != nil { |
110 | - v1.Arch = v.Arch |
111 | - } |
112 | - if v.Container != nil { |
113 | - v1.Container = v.Container |
114 | - } |
115 | - if v.CpuCores != nil { |
116 | - v1.CpuCores = v.CpuCores |
117 | - } |
118 | - if v.CpuPower != nil { |
119 | - v1.CpuPower = v.CpuPower |
120 | - } |
121 | - if v.Mem != nil { |
122 | - v1.Mem = v.Mem |
123 | - } |
124 | - if v.RootDisk != nil { |
125 | - v1.RootDisk = v.RootDisk |
126 | - } |
127 | - if v.Tags != nil { |
128 | - v1.Tags = v.Tags |
129 | - } |
130 | - return v1 |
131 | -} |
132 | - |
133 | func uintStr(i uint64) string { |
134 | if i == 0 { |
135 | return "" |
136 | @@ -183,6 +200,58 @@ |
137 | return v.Target.String() |
138 | } |
139 | |
140 | +func (v *Value) fieldFromTag(tagName string) (reflect.Value, bool) { |
141 | + fieldName := fieldNames[tagName] |
142 | + val := reflect.ValueOf(v).Elem().FieldByName(fieldName) |
143 | + return val, val.IsValid() |
144 | +} |
145 | + |
146 | +// attributesWithValues returns the non-zero attribute tags from the constraint. |
147 | +func (v *Value) attributesWithValues() []string { |
148 | + var result []string = []string{} |
149 | + for fieldTag, fieldName := range fieldNames { |
150 | + val := reflect.ValueOf(v).Elem().FieldByName(fieldName) |
151 | + if !val.IsNil() { |
152 | + result = append(result, fieldTag) |
153 | + } |
154 | + } |
155 | + return result |
156 | +} |
157 | + |
158 | +// hasAny returns any attrTags for which the constraint has a non-nil value. |
159 | +func (v *Value) hasAny(attrTags ...string) []string { |
160 | + withValues := v.attributesWithValues() |
161 | + containsFunc := func(values []string, value string) bool { |
162 | + for _, v := range values { |
163 | + if v == value { |
164 | + return true |
165 | + } |
166 | + } |
167 | + return false |
168 | + } |
169 | + var result []string = []string{} |
170 | + for _, tag := range attrTags { |
171 | + if containsFunc(withValues, tag) { |
172 | + result = append(result, tag) |
173 | + } |
174 | + } |
175 | + return result |
176 | +} |
177 | + |
178 | +// without returns a copy of the constraint without values for |
179 | +// the specified attributes. |
180 | +func (v *Value) without(attrTags ...string) (Value, error) { |
181 | + result := *v |
182 | + for _, tag := range attrTags { |
183 | + val, ok := result.fieldFromTag(tag) |
184 | + if !ok { |
185 | + return Value{}, fmt.Errorf("unknown constraint %q", tag) |
186 | + } |
187 | + val.Set(reflect.Zero(val.Type())) |
188 | + } |
189 | + return result, nil |
190 | +} |
191 | + |
192 | // setRaw interprets a name=value string and sets the supplied value. |
193 | func (v *Value) setRaw(raw string) error { |
194 | eq := strings.Index(raw, "=") |
195 | @@ -192,20 +261,22 @@ |
196 | name, str := raw[:eq], raw[eq+1:] |
197 | var err error |
198 | switch name { |
199 | - case "arch": |
200 | + case Arch: |
201 | err = v.setArch(str) |
202 | - case "container": |
203 | + case Container: |
204 | err = v.setContainer(str) |
205 | - case "cpu-cores": |
206 | + case CpuCores: |
207 | err = v.setCpuCores(str) |
208 | - case "cpu-power": |
209 | + case CpuPower: |
210 | err = v.setCpuPower(str) |
211 | - case "mem": |
212 | + case Mem: |
213 | err = v.setMem(str) |
214 | - case "root-disk": |
215 | + case RootDisk: |
216 | err = v.setRootDisk(str) |
217 | - case "tags": |
218 | + case Tags: |
219 | err = v.setTags(str) |
220 | + case InstanceType: |
221 | + err = v.setInstanceType(str) |
222 | default: |
223 | return fmt.Errorf("unknown constraint %q", name) |
224 | } |
225 | @@ -229,20 +300,22 @@ |
226 | vstr := fmt.Sprintf("%v", val) |
227 | var err error |
228 | switch k { |
229 | - case "arch": |
230 | + case Arch: |
231 | v.Arch = &vstr |
232 | - case "container": |
233 | + case Container: |
234 | ctype := instance.ContainerType(vstr) |
235 | v.Container = &ctype |
236 | - case "cpu-cores": |
237 | + case InstanceType: |
238 | + v.InstanceType = &vstr |
239 | + case CpuCores: |
240 | v.CpuCores, err = parseUint64(vstr) |
241 | - case "cpu-power": |
242 | + case CpuPower: |
243 | v.CpuPower, err = parseUint64(vstr) |
244 | - case "mem": |
245 | + case Mem: |
246 | v.Mem, err = parseUint64(vstr) |
247 | - case "root-disk": |
248 | + case RootDisk: |
249 | v.RootDisk, err = parseUint64(vstr) |
250 | - case "tags": |
251 | + case Tags: |
252 | v.Tags, err = parseYamlTags(val) |
253 | default: |
254 | return false |
255 | @@ -303,6 +376,14 @@ |
256 | return |
257 | } |
258 | |
259 | +func (v *Value) setInstanceType(str string) error { |
260 | + if v.InstanceType != nil { |
261 | + return fmt.Errorf("already set") |
262 | + } |
263 | + v.InstanceType = &str |
264 | + return nil |
265 | +} |
266 | + |
267 | func (v *Value) setMem(str string) (err error) { |
268 | if v.Mem != nil { |
269 | return fmt.Errorf("already set") |
270 | |
271 | === modified file 'constraints/constraints_test.go' |
272 | --- constraints/constraints_test.go 2014-04-09 16:36:12 +0000 |
273 | +++ constraints/constraints_test.go 2014-04-18 13:10:25 +0000 |
274 | @@ -263,13 +263,26 @@ |
275 | args: []string{"tags="}, |
276 | }, |
277 | |
278 | + // instance type |
279 | + { |
280 | + summary: "set instance type", |
281 | + args: []string{"instance-type=foo"}, |
282 | + }, { |
283 | + summary: "instance type empty", |
284 | + args: []string{"instance-type="}, |
285 | + }, |
286 | + |
287 | // Everything at once. |
288 | { |
289 | summary: "kitchen sink together", |
290 | - args: []string{" root-disk=8G mem=2T arch=i386 cpu-cores=4096 cpu-power=9001 container=lxc tags=foo,bar"}, |
291 | + args: []string{ |
292 | + "root-disk=8G mem=2T arch=i386 cpu-cores=4096 cpu-power=9001 container=lxc " + |
293 | + "tags=foo,bar instance-type=foo"}, |
294 | }, { |
295 | summary: "kitchen sink separately", |
296 | - args: []string{"root-disk=8G", "mem=2T", "cpu-cores=4096", "cpu-power=9001", "arch=armhf", "container=lxc", "tags=foo,bar"}, |
297 | + args: []string{ |
298 | + "root-disk=8G", "mem=2T", "cpu-cores=4096", "cpu-power=9001", "arch=armhf", |
299 | + "container=lxc", "tags=foo,bar", "instance-type=foo"}, |
300 | }, |
301 | } |
302 | |
303 | @@ -321,6 +334,8 @@ |
304 | c.Check(&con, gc.Not(jc.Satisfies), constraints.IsEmpty) |
305 | con = constraints.MustParse("container=") |
306 | c.Check(&con, gc.Not(jc.Satisfies), constraints.IsEmpty) |
307 | + con = constraints.MustParse("instance-type=") |
308 | + c.Check(&con, gc.Not(jc.Satisfies), constraints.IsEmpty) |
309 | } |
310 | |
311 | func uint64p(i uint64) *uint64 { |
312 | @@ -363,14 +378,17 @@ |
313 | {"Tags1", constraints.Value{Tags: nil}}, |
314 | {"Tags2", constraints.Value{Tags: &[]string{}}}, |
315 | {"Tags3", constraints.Value{Tags: &[]string{"foo", "bar"}}}, |
316 | + {"InstanceType1", constraints.Value{InstanceType: strp("")}}, |
317 | + {"InstanceType2", constraints.Value{InstanceType: strp("foo")}}, |
318 | {"All", constraints.Value{ |
319 | - Arch: strp("i386"), |
320 | - Container: ctypep("lxc"), |
321 | - CpuCores: uint64p(4096), |
322 | - CpuPower: uint64p(9001), |
323 | - Mem: uint64p(18000000000), |
324 | - RootDisk: uint64p(24000000000), |
325 | - Tags: &[]string{"foo", "bar"}, |
326 | + Arch: strp("i386"), |
327 | + Container: ctypep("lxc"), |
328 | + CpuCores: uint64p(4096), |
329 | + CpuPower: uint64p(9001), |
330 | + Mem: uint64p(18000000000), |
331 | + RootDisk: uint64p(24000000000), |
332 | + Tags: &[]string{"foo", "bar"}, |
333 | + InstanceType: strp("foo"), |
334 | }}, |
335 | } |
336 | |
337 | @@ -418,128 +436,6 @@ |
338 | } |
339 | } |
340 | |
341 | -var withFallbacksTests = []struct { |
342 | - desc string |
343 | - initial string |
344 | - fallbacks string |
345 | - final string |
346 | -}{ |
347 | - { |
348 | - desc: "empty all round", |
349 | - }, { |
350 | - desc: "container with empty fallback", |
351 | - initial: "container=lxc", |
352 | - final: "container=lxc", |
353 | - }, { |
354 | - desc: "container from fallback", |
355 | - fallbacks: "container=lxc", |
356 | - final: "container=lxc", |
357 | - }, { |
358 | - desc: "arch with empty fallback", |
359 | - initial: "arch=amd64", |
360 | - final: "arch=amd64", |
361 | - }, { |
362 | - desc: "arch with ignored fallback", |
363 | - initial: "arch=amd64", |
364 | - fallbacks: "arch=i386", |
365 | - final: "arch=amd64", |
366 | - }, { |
367 | - desc: "arch from fallback", |
368 | - fallbacks: "arch=i386", |
369 | - final: "arch=i386", |
370 | - }, { |
371 | - desc: "cpu-cores with empty fallback", |
372 | - initial: "cpu-cores=2", |
373 | - final: "cpu-cores=2", |
374 | - }, { |
375 | - desc: "cpu-cores with ignored fallback", |
376 | - initial: "cpu-cores=4", |
377 | - fallbacks: "cpu-cores=8", |
378 | - final: "cpu-cores=4", |
379 | - }, { |
380 | - desc: "cpu-cores from fallback", |
381 | - fallbacks: "cpu-cores=8", |
382 | - final: "cpu-cores=8", |
383 | - }, { |
384 | - desc: "cpu-power with empty fallback", |
385 | - initial: "cpu-power=100", |
386 | - final: "cpu-power=100", |
387 | - }, { |
388 | - desc: "cpu-power with ignored fallback", |
389 | - initial: "cpu-power=100", |
390 | - fallbacks: "cpu-power=200", |
391 | - final: "cpu-power=100", |
392 | - }, { |
393 | - desc: "cpu-power from fallback", |
394 | - fallbacks: "cpu-power=200", |
395 | - final: "cpu-power=200", |
396 | - }, { |
397 | - desc: "tags with empty fallback", |
398 | - initial: "tags=foo,bar", |
399 | - final: "tags=foo,bar", |
400 | - }, { |
401 | - desc: "tags with ignored fallback", |
402 | - initial: "tags=foo,bar", |
403 | - fallbacks: "tags=baz", |
404 | - final: "tags=foo,bar", |
405 | - }, { |
406 | - desc: "tags from fallback", |
407 | - fallbacks: "tags=foo,bar", |
408 | - final: "tags=foo,bar", |
409 | - }, { |
410 | - desc: "tags inital empty", |
411 | - initial: "tags=", |
412 | - fallbacks: "tags=foo,bar", |
413 | - final: "tags=", |
414 | - }, { |
415 | - desc: "mem with empty fallback", |
416 | - initial: "mem=4G", |
417 | - final: "mem=4G", |
418 | - }, { |
419 | - desc: "mem with ignored fallback", |
420 | - initial: "mem=4G", |
421 | - fallbacks: "mem=8G", |
422 | - final: "mem=4G", |
423 | - }, { |
424 | - desc: "mem from fallback", |
425 | - fallbacks: "mem=8G", |
426 | - final: "mem=8G", |
427 | - }, { |
428 | - desc: "root-disk with empty fallback", |
429 | - initial: "root-disk=4G", |
430 | - final: "root-disk=4G", |
431 | - }, { |
432 | - desc: "root-disk with ignored fallback", |
433 | - initial: "root-disk=4G", |
434 | - fallbacks: "root-disk=8G", |
435 | - final: "root-disk=4G", |
436 | - }, { |
437 | - desc: "root-disk from fallback", |
438 | - fallbacks: "root-disk=8G", |
439 | - final: "root-disk=8G", |
440 | - }, { |
441 | - desc: "non-overlapping mix", |
442 | - initial: "root-disk=8G mem=4G arch=amd64", |
443 | - fallbacks: "cpu-power=1000 cpu-cores=4", |
444 | - final: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cpu-cores=4", |
445 | - }, { |
446 | - desc: "overlapping mix", |
447 | - initial: "root-disk=8G mem=4G arch=amd64", |
448 | - fallbacks: "cpu-power=1000 cpu-cores=4 mem=8G", |
449 | - final: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cpu-cores=4", |
450 | - }, |
451 | -} |
452 | - |
453 | -func (s *ConstraintsSuite) TestWithFallbacks(c *gc.C) { |
454 | - for i, t := range withFallbacksTests { |
455 | - c.Logf("test %d: %s", i, t.desc) |
456 | - initial := constraints.MustParse(t.initial) |
457 | - fallbacks := constraints.MustParse(t.fallbacks) |
458 | - final := constraints.MustParse(t.final) |
459 | - c.Check(initial.WithFallbacks(fallbacks), gc.DeepEquals, final) |
460 | - } |
461 | -} |
462 | - |
463 | var hasContainerTests = []struct { |
464 | constraints string |
465 | hasContainer bool |
466 | @@ -562,3 +458,128 @@ |
467 | c.Check(cons.HasContainer(), gc.Equals, t.hasContainer) |
468 | } |
469 | } |
470 | + |
471 | +func (s *ConstraintsSuite) TestHasInstanceType(c *gc.C) { |
472 | + cons := constraints.MustParse("arch=amd64") |
473 | + c.Check(cons.HasInstanceType(), jc.IsFalse) |
474 | + cons = constraints.MustParse("arch=amd64 instance-type=foo") |
475 | + c.Check(cons.HasInstanceType(), jc.IsTrue) |
476 | +} |
477 | + |
478 | +var withoutTests = []struct { |
479 | + initial string |
480 | + without []string |
481 | + final string |
482 | +}{ |
483 | + { |
484 | + initial: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cpu-cores=4 tags=foo container=lxc instance-type=bar", |
485 | + without: []string{"root-disk"}, |
486 | + final: "mem=4G arch=amd64 cpu-power=1000 cpu-cores=4 tags=foo container=lxc instance-type=bar", |
487 | + }, |
488 | + { |
489 | + initial: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cpu-cores=4 tags=foo container=lxc instance-type=bar", |
490 | + without: []string{"mem"}, |
491 | + final: "root-disk=8G arch=amd64 cpu-power=1000 cpu-cores=4 tags=foo container=lxc instance-type=bar", |
492 | + }, |
493 | + { |
494 | + initial: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cpu-cores=4 tags=foo container=lxc instance-type=bar", |
495 | + without: []string{"arch"}, |
496 | + final: "root-disk=8G mem=4G cpu-power=1000 cpu-cores=4 tags=foo container=lxc instance-type=bar", |
497 | + }, |
498 | + { |
499 | + initial: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cpu-cores=4 tags=foo container=lxc instance-type=bar", |
500 | + without: []string{"cpu-power"}, |
501 | + final: "root-disk=8G mem=4G arch=amd64 cpu-cores=4 tags=foo container=lxc instance-type=bar", |
502 | + }, |
503 | + { |
504 | + initial: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cpu-cores=4 tags=foo container=lxc instance-type=bar", |
505 | + without: []string{"cpu-cores"}, |
506 | + final: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 tags=foo container=lxc instance-type=bar", |
507 | + }, |
508 | + { |
509 | + initial: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cpu-cores=4 tags=foo container=lxc instance-type=bar", |
510 | + without: []string{"tags"}, |
511 | + final: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cpu-cores=4 container=lxc instance-type=bar", |
512 | + }, |
513 | + { |
514 | + initial: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cpu-cores=4 tags=foo container=lxc instance-type=bar", |
515 | + without: []string{"container"}, |
516 | + final: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cpu-cores=4 tags=foo instance-type=bar", |
517 | + }, |
518 | + { |
519 | + initial: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cpu-cores=4 tags=foo container=lxc instance-type=bar", |
520 | + without: []string{"instance-type"}, |
521 | + final: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cpu-cores=4 tags=foo container=lxc", |
522 | + }, |
523 | + { |
524 | + initial: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cpu-cores=4 tags=foo container=lxc instance-type=bar", |
525 | + without: []string{"root-disk", "mem", "arch"}, |
526 | + final: "cpu-power=1000 cpu-cores=4 tags=foo container=lxc instance-type=bar", |
527 | + }, |
528 | +} |
529 | + |
530 | +func (s *ConstraintsSuite) TestWithout(c *gc.C) { |
531 | + for i, t := range withoutTests { |
532 | + c.Logf("test %d", i) |
533 | + initial := constraints.MustParse(t.initial) |
534 | + final, err := constraints.Without(initial, t.without...) |
535 | + c.Assert(err, gc.IsNil) |
536 | + c.Check(final, gc.DeepEquals, constraints.MustParse(t.final)) |
537 | + } |
538 | +} |
539 | + |
540 | +func (s *ConstraintsSuite) TestWithoutError(c *gc.C) { |
541 | + cons := constraints.MustParse("mem=4G") |
542 | + _, err := constraints.Without(cons, "foo") |
543 | + c.Assert(err, gc.ErrorMatches, `unknown constraint "foo"`) |
544 | +} |
545 | + |
546 | +var attributesWithValuesTests = []struct { |
547 | + cons string |
548 | + attrs []string |
549 | + expected []string |
550 | +}{ |
551 | + { |
552 | + cons: "", |
553 | + expected: []string{}, |
554 | + }, |
555 | + { |
556 | + cons: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cpu-cores=4", |
557 | + expected: []string{"arch", "cpu-cores", "cpu-power", "mem", "root-disk"}, |
558 | + }, |
559 | +} |
560 | + |
561 | +func (s *ConstraintsSuite) TestAttributesWithValues(c *gc.C) { |
562 | + for i, t := range attributesWithValuesTests { |
563 | + c.Logf("test %d", i) |
564 | + cons := constraints.MustParse(t.cons) |
565 | + obtained := constraints.AttributesWithValues(cons) |
566 | + c.Check(obtained, gc.DeepEquals, t.expected) |
567 | + } |
568 | +} |
569 | + |
570 | +var hasAnyTests = []struct { |
571 | + cons string |
572 | + attrs []string |
573 | + expected []string |
574 | +}{ |
575 | + { |
576 | + cons: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cpu-cores=4", |
577 | + attrs: []string{"root-disk", "tags", "mem"}, |
578 | + expected: []string{"root-disk", "mem"}, |
579 | + }, |
580 | + { |
581 | + cons: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cpu-cores=4", |
582 | + attrs: []string{"tags"}, |
583 | + expected: []string{}, |
584 | + }, |
585 | +} |
586 | + |
587 | +func (s *ConstraintsSuite) TestHasAny(c *gc.C) { |
588 | + for i, t := range hasAnyTests { |
589 | + c.Logf("test %d", i) |
590 | + cons := constraints.MustParse(t.cons) |
591 | + obtained := constraints.HasAny(cons, t.attrs...) |
592 | + c.Check(obtained, gc.DeepEquals, t.expected) |
593 | + } |
594 | +} |
595 | |
596 | === added file 'constraints/export_test.go' |
597 | --- constraints/export_test.go 1970-01-01 00:00:00 +0000 |
598 | +++ constraints/export_test.go 2014-04-18 13:10:25 +0000 |
599 | @@ -0,0 +1,18 @@ |
600 | +// Copyright 2014 Canonical Ltd. |
601 | +// Licensed under the AGPLv3, see LICENCE file for details. |
602 | + |
603 | +package constraints |
604 | + |
605 | +var WithFallbacks = withFallbacks |
606 | + |
607 | +func Without(cons Value, attrTags ...string) (Value, error) { |
608 | + return cons.without(attrTags...) |
609 | +} |
610 | + |
611 | +func HasAny(cons Value, attrTags ...string) []string { |
612 | + return cons.hasAny(attrTags...) |
613 | +} |
614 | + |
615 | +func AttributesWithValues(cons Value) []string { |
616 | + return cons.attributesWithValues() |
617 | +} |
618 | |
619 | === added file 'constraints/validation.go' |
620 | --- constraints/validation.go 1970-01-01 00:00:00 +0000 |
621 | +++ constraints/validation.go 2014-04-18 13:10:25 +0000 |
622 | @@ -0,0 +1,134 @@ |
623 | +// Copyright 2014 Canonical Ltd. |
624 | +// Licensed under the AGPLv3, see LICENCE file for details. |
625 | + |
626 | +package constraints |
627 | + |
628 | +import ( |
629 | + "fmt" |
630 | + "reflect" |
631 | + |
632 | + "launchpad.net/juju-core/utils/set" |
633 | +) |
634 | + |
635 | +// Validator defines operations on constraints attributes which are |
636 | +// used to ensure a constraints value is valid, as well as being able |
637 | +// to handle overridden attributes. |
638 | +type Validator interface { |
639 | + |
640 | + // RegisterConflicts is used to define cross-constraint override behaviour. |
641 | + // The red and blue attribute lists contain attribute names which conflict |
642 | + // with those in the other list. |
643 | + // When two constraints conflict: |
644 | + // it is an error to set both constraints in the same constraints Value. |
645 | + // when a constraints Value overrides another which specifies a conflicting |
646 | + // attribute, the attribute in the overridden Value is cleared. |
647 | + RegisterConflicts(reds, blues []string) |
648 | + |
649 | + // RegisterUnsupported records attributes which are not supported by a constraints Value. |
650 | + RegisterUnsupported(unsupported []string) |
651 | + |
652 | + // Validate returns an error if the given constraints are not valid, and also |
653 | + // any unsupported attributes. |
654 | + Validate(cons Value) ([]string, error) |
655 | + |
656 | + // Merge merges cons into consFallback, with any conflicting attributes from cons |
657 | + // overriding those from consFallback. |
658 | + Merge(consFallback, cons Value) (Value, error) |
659 | +} |
660 | + |
661 | +// NewValidator returns a new constraints Validator instance. |
662 | +func NewValidator() Validator { |
663 | + c := validator{} |
664 | + c.conflicts = make(map[string]set.Strings) |
665 | + return &c |
666 | +} |
667 | + |
668 | +type validator struct { |
669 | + unsupported set.Strings |
670 | + conflicts map[string]set.Strings |
671 | +} |
672 | + |
673 | +// RegisterConflicts is defined on Validator. |
674 | +func (v *validator) RegisterConflicts(reds, blues []string) { |
675 | + for _, red := range reds { |
676 | + v.conflicts[red] = set.NewStrings(blues...) |
677 | + } |
678 | + for _, blue := range blues { |
679 | + v.conflicts[blue] = set.NewStrings(reds...) |
680 | + } |
681 | +} |
682 | + |
683 | +// RegisterUnsupported is defined on Validator. |
684 | +func (v *validator) RegisterUnsupported(unsupported []string) { |
685 | + v.unsupported = set.NewStrings(unsupported...) |
686 | +} |
687 | + |
688 | +// checkConflicts returns an error if the constraints Value contains conflicting attributes. |
689 | +func (v *validator) checkConflicts(cons Value) error { |
690 | + attrNames := cons.attributesWithValues() |
691 | + attrSet := set.NewStrings(attrNames...) |
692 | + for _, attr := range attrNames { |
693 | + conflicts, ok := v.conflicts[attr] |
694 | + if !ok { |
695 | + continue |
696 | + } |
697 | + for _, conflict := range conflicts.Values() { |
698 | + if attrSet.Contains(conflict) { |
699 | + return fmt.Errorf("ambiguous constraints: %q overlaps with %q", attr, conflict) |
700 | + } |
701 | + } |
702 | + } |
703 | + return nil |
704 | +} |
705 | + |
706 | +// checkUnsupported returns any unsupported attributes. |
707 | +func (v *validator) checkUnsupported(cons Value) []string { |
708 | + return cons.hasAny(v.unsupported.Values()...) |
709 | +} |
710 | + |
711 | +// withFallbacks returns a copy of v with nil values taken from vFallback. |
712 | +func withFallbacks(v Value, vFallback Value) Value { |
713 | + result := vFallback |
714 | + for _, fieldName := range fieldNames { |
715 | + resultVal := reflect.ValueOf(&result).Elem().FieldByName(fieldName) |
716 | + val := reflect.ValueOf(&v).Elem().FieldByName(fieldName) |
717 | + if !val.IsNil() { |
718 | + resultVal.Set(val) |
719 | + } |
720 | + } |
721 | + return result |
722 | +} |
723 | + |
724 | +// Validate is defined on Validator. |
725 | +func (v *validator) Validate(cons Value) ([]string, error) { |
726 | + unsupported := v.checkUnsupported(cons) |
727 | + if err := v.checkConflicts(cons); err != nil { |
728 | + return unsupported, err |
729 | + } |
730 | + return unsupported, nil |
731 | +} |
732 | + |
733 | +// Merge is defined on Validator. |
734 | +func (v *validator) Merge(consFallback, cons Value) (Value, error) { |
735 | + // First ensure both constraints are valid. We don't care if there |
736 | + // are constraint attributes that are unsupported. |
737 | + if _, err := v.Validate(consFallback); err != nil { |
738 | + return Value{}, err |
739 | + } |
740 | + if _, err := v.Validate(cons); err != nil { |
741 | + return Value{}, err |
742 | + } |
743 | + // Gather any attributes from consFallback which conflict with those on cons. |
744 | + attrs := cons.attributesWithValues() |
745 | + var fallbackConflicts []string |
746 | + for _, attr := range attrs { |
747 | + fallbackConflicts = append(fallbackConflicts, v.conflicts[attr].Values()...) |
748 | + } |
749 | + // Null out the conflicting consFallback attribute values because |
750 | + // cons takes priority. We can't error here because we |
751 | + // know that aConflicts contains valid attr names. |
752 | + consFallbackMinusConflicts, _ := consFallback.without(fallbackConflicts...) |
753 | + // The result is cons with fallbacks coming from any |
754 | + // non conflicting consFallback attributes. |
755 | + return withFallbacks(cons, consFallbackMinusConflicts), nil |
756 | +} |
757 | |
758 | === added file 'constraints/validation_test.go' |
759 | --- constraints/validation_test.go 1970-01-01 00:00:00 +0000 |
760 | +++ constraints/validation_test.go 2014-04-18 13:10:25 +0000 |
761 | @@ -0,0 +1,296 @@ |
762 | +// Copyright 2013 Canonical Ltd. |
763 | +// Licensed under the AGPLv3, see LICENCE file for details. |
764 | + |
765 | +package constraints_test |
766 | + |
767 | +import ( |
768 | + jc "github.com/juju/testing/checkers" |
769 | + gc "launchpad.net/gocheck" |
770 | + |
771 | + "launchpad.net/juju-core/constraints" |
772 | +) |
773 | + |
774 | +type validationSuite struct{} |
775 | + |
776 | +var _ = gc.Suite(&validationSuite{}) |
777 | + |
778 | +var validationTests = []struct { |
779 | + cons string |
780 | + unsupported []string |
781 | + reds []string |
782 | + blues []string |
783 | + err string |
784 | +}{ |
785 | + { |
786 | + cons: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cpu-cores=4", |
787 | + }, |
788 | + { |
789 | + cons: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cpu-cores=4 tags=foo", |
790 | + unsupported: []string{"tags"}, |
791 | + }, |
792 | + { |
793 | + cons: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cpu-cores=4 instance-type=foo", |
794 | + unsupported: []string{"cpu-power", "instance-type"}, |
795 | + }, |
796 | + { |
797 | + // Ambiguous constraint errors take precedence over unsupported errors. |
798 | + cons: "root-disk=8G mem=4G cpu-cores=4 instance-type=foo", |
799 | + reds: []string{"mem", "arch"}, |
800 | + blues: []string{"instance-type"}, |
801 | + unsupported: []string{"cpu-cores"}, |
802 | + err: `ambiguous constraints: "mem" overlaps with "instance-type"`, |
803 | + }, |
804 | + { |
805 | + cons: "root-disk=8G mem=4G arch=amd64 cpu-cores=4 instance-type=foo", |
806 | + reds: []string{"mem", "arch"}, |
807 | + err: "", |
808 | + }, |
809 | + { |
810 | + cons: "root-disk=8G mem=4G arch=amd64 cpu-cores=4 instance-type=foo", |
811 | + blues: []string{"mem", "arch"}, |
812 | + err: "", |
813 | + }, |
814 | + { |
815 | + cons: "root-disk=8G mem=4G arch=amd64 cpu-cores=4 instance-type=foo", |
816 | + reds: []string{"mem", "arch"}, |
817 | + blues: []string{"instance-type"}, |
818 | + err: `ambiguous constraints: "arch" overlaps with "instance-type"`, |
819 | + }, |
820 | + { |
821 | + cons: "root-disk=8G mem=4G arch=amd64 cpu-cores=4 instance-type=foo", |
822 | + reds: []string{"instance-type"}, |
823 | + blues: []string{"mem", "arch"}, |
824 | + err: `ambiguous constraints: "arch" overlaps with "instance-type"`, |
825 | + }, |
826 | + { |
827 | + cons: "root-disk=8G mem=4G cpu-cores=4 instance-type=foo", |
828 | + reds: []string{"mem", "arch"}, |
829 | + blues: []string{"instance-type"}, |
830 | + err: `ambiguous constraints: "mem" overlaps with "instance-type"`, |
831 | + }, |
832 | +} |
833 | + |
834 | +func (s *validationSuite) TestValidation(c *gc.C) { |
835 | + for i, t := range validationTests { |
836 | + c.Logf("test %d", i) |
837 | + validator := constraints.NewValidator() |
838 | + validator.RegisterUnsupported(t.unsupported) |
839 | + validator.RegisterConflicts(t.reds, t.blues) |
840 | + cons := constraints.MustParse(t.cons) |
841 | + unsupported, err := validator.Validate(cons) |
842 | + if t.err == "" { |
843 | + c.Assert(err, gc.IsNil) |
844 | + c.Assert(unsupported, jc.DeepEquals, t.unsupported) |
845 | + } else { |
846 | + c.Assert(err, gc.ErrorMatches, t.err) |
847 | + } |
848 | + } |
849 | +} |
850 | + |
851 | +var mergeTests = []struct { |
852 | + desc string |
853 | + consFallback string |
854 | + cons string |
855 | + unsupported []string |
856 | + reds []string |
857 | + blues []string |
858 | + expected string |
859 | +}{ |
860 | + { |
861 | + desc: "empty all round", |
862 | + }, { |
863 | + desc: "container with empty fallback", |
864 | + cons: "container=lxc", |
865 | + expected: "container=lxc", |
866 | + }, { |
867 | + desc: "container from fallback", |
868 | + consFallback: "container=lxc", |
869 | + expected: "container=lxc", |
870 | + }, { |
871 | + desc: "arch with empty fallback", |
872 | + cons: "arch=amd64", |
873 | + expected: "arch=amd64", |
874 | + }, { |
875 | + desc: "arch with ignored fallback", |
876 | + cons: "arch=amd64", |
877 | + consFallback: "arch=i386", |
878 | + expected: "arch=amd64", |
879 | + }, { |
880 | + desc: "arch from fallback", |
881 | + consFallback: "arch=i386", |
882 | + expected: "arch=i386", |
883 | + }, { |
884 | + desc: "instance type with empty fallback", |
885 | + cons: "instance-type=foo", |
886 | + expected: "instance-type=foo", |
887 | + }, { |
888 | + desc: "instance type with ignored fallback", |
889 | + cons: "instance-type=foo", |
890 | + consFallback: "instance-type=bar", |
891 | + expected: "instance-type=foo", |
892 | + }, { |
893 | + desc: "instance type from fallback", |
894 | + consFallback: "instance-type=foo", |
895 | + expected: "instance-type=foo", |
896 | + }, { |
897 | + desc: "cpu-cores with empty fallback", |
898 | + cons: "cpu-cores=2", |
899 | + expected: "cpu-cores=2", |
900 | + }, { |
901 | + desc: "cpu-cores with ignored fallback", |
902 | + cons: "cpu-cores=4", |
903 | + consFallback: "cpu-cores=8", |
904 | + expected: "cpu-cores=4", |
905 | + }, { |
906 | + desc: "cpu-cores from fallback", |
907 | + consFallback: "cpu-cores=8", |
908 | + expected: "cpu-cores=8", |
909 | + }, { |
910 | + desc: "cpu-power with empty fallback", |
911 | + cons: "cpu-power=100", |
912 | + expected: "cpu-power=100", |
913 | + }, { |
914 | + desc: "cpu-power with ignored fallback", |
915 | + cons: "cpu-power=100", |
916 | + consFallback: "cpu-power=200", |
917 | + expected: "cpu-power=100", |
918 | + }, { |
919 | + desc: "cpu-power from fallback", |
920 | + consFallback: "cpu-power=200", |
921 | + expected: "cpu-power=200", |
922 | + }, { |
923 | + desc: "tags with empty fallback", |
924 | + cons: "tags=foo,bar", |
925 | + expected: "tags=foo,bar", |
926 | + }, { |
927 | + desc: "tags with ignored fallback", |
928 | + cons: "tags=foo,bar", |
929 | + consFallback: "tags=baz", |
930 | + expected: "tags=foo,bar", |
931 | + }, { |
932 | + desc: "tags from fallback", |
933 | + consFallback: "tags=foo,bar", |
934 | + expected: "tags=foo,bar", |
935 | + }, { |
936 | + desc: "tags inital empty", |
937 | + cons: "tags=", |
938 | + consFallback: "tags=foo,bar", |
939 | + expected: "tags=", |
940 | + }, { |
941 | + desc: "mem with empty fallback", |
942 | + cons: "mem=4G", |
943 | + expected: "mem=4G", |
944 | + }, { |
945 | + desc: "mem with ignored fallback", |
946 | + cons: "mem=4G", |
947 | + consFallback: "mem=8G", |
948 | + expected: "mem=4G", |
949 | + }, { |
950 | + desc: "mem from fallback", |
951 | + consFallback: "mem=8G", |
952 | + expected: "mem=8G", |
953 | + }, { |
954 | + desc: "root-disk with empty fallback", |
955 | + cons: "root-disk=4G", |
956 | + expected: "root-disk=4G", |
957 | + }, { |
958 | + desc: "root-disk with ignored fallback", |
959 | + cons: "root-disk=4G", |
960 | + consFallback: "root-disk=8G", |
961 | + expected: "root-disk=4G", |
962 | + }, { |
963 | + desc: "root-disk from fallback", |
964 | + consFallback: "root-disk=8G", |
965 | + expected: "root-disk=8G", |
966 | + }, { |
967 | + desc: "non-overlapping mix", |
968 | + cons: "root-disk=8G mem=4G arch=amd64", |
969 | + consFallback: "cpu-power=1000 cpu-cores=4", |
970 | + expected: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cpu-cores=4", |
971 | + }, { |
972 | + desc: "overlapping mix", |
973 | + cons: "root-disk=8G mem=4G arch=amd64", |
974 | + consFallback: "cpu-power=1000 cpu-cores=4 mem=8G", |
975 | + expected: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cpu-cores=4", |
976 | + }, { |
977 | + desc: "fallback only, no conflicts", |
978 | + consFallback: "root-disk=8G cpu-cores=4 instance-type=foo", |
979 | + reds: []string{"mem", "arch"}, |
980 | + blues: []string{"instance-type"}, |
981 | + expected: "root-disk=8G cpu-cores=4 instance-type=foo", |
982 | + }, { |
983 | + desc: "no fallback, no conflicts", |
984 | + cons: "root-disk=8G cpu-cores=4 instance-type=foo", |
985 | + reds: []string{"mem", "arch"}, |
986 | + blues: []string{"instance-type"}, |
987 | + expected: "root-disk=8G cpu-cores=4 instance-type=foo", |
988 | + }, { |
989 | + desc: "conflict value from override", |
990 | + consFallback: "root-disk=8G instance-type=foo", |
991 | + cons: "root-disk=8G cpu-cores=4 instance-type=bar", |
992 | + reds: []string{"mem", "arch"}, |
993 | + blues: []string{"instance-type"}, |
994 | + expected: "root-disk=8G cpu-cores=4 instance-type=bar", |
995 | + }, { |
996 | + desc: "unsupported attributes ignored", |
997 | + consFallback: "root-disk=8G instance-type=foo", |
998 | + cons: "root-disk=8G cpu-cores=4 instance-type=bar", |
999 | + reds: []string{"mem", "arch"}, |
1000 | + blues: []string{"instance-type"}, |
1001 | + unsupported: []string{"instance-type"}, |
1002 | + expected: "root-disk=8G cpu-cores=4 instance-type=bar", |
1003 | + }, { |
1004 | + desc: "red conflict masked from fallback", |
1005 | + consFallback: "root-disk=8G mem=4G", |
1006 | + cons: "root-disk=8G cpu-cores=4 instance-type=bar", |
1007 | + reds: []string{"mem", "arch"}, |
1008 | + blues: []string{"instance-type"}, |
1009 | + expected: "root-disk=8G cpu-cores=4 instance-type=bar", |
1010 | + }, { |
1011 | + desc: "second red conflict masked from fallback", |
1012 | + consFallback: "root-disk=8G arch=amd64", |
1013 | + cons: "root-disk=8G cpu-cores=4 instance-type=bar", |
1014 | + reds: []string{"mem", "arch"}, |
1015 | + blues: []string{"instance-type"}, |
1016 | + expected: "root-disk=8G cpu-cores=4 instance-type=bar", |
1017 | + }, { |
1018 | + desc: "blue conflict masked from fallback", |
1019 | + consFallback: "root-disk=8G cpu-cores=4 instance-type=bar", |
1020 | + cons: "root-disk=8G mem=4G", |
1021 | + reds: []string{"mem", "arch"}, |
1022 | + blues: []string{"instance-type"}, |
1023 | + expected: "root-disk=8G cpu-cores=4 mem=4G", |
1024 | + }, { |
1025 | + desc: "both red conflicts used, blue mased from fallback", |
1026 | + consFallback: "root-disk=8G cpu-cores=4 instance-type=bar", |
1027 | + cons: "root-disk=8G arch=amd64 mem=4G", |
1028 | + reds: []string{"mem", "arch"}, |
1029 | + blues: []string{"instance-type"}, |
1030 | + expected: "root-disk=8G cpu-cores=4 arch=amd64 mem=4G", |
1031 | + }, |
1032 | +} |
1033 | + |
1034 | +func (s *validationSuite) TestMerge(c *gc.C) { |
1035 | + for i, t := range mergeTests { |
1036 | + c.Logf("test %d: %s", i, t.desc) |
1037 | + validator := constraints.NewValidator() |
1038 | + validator.RegisterConflicts(t.reds, t.blues) |
1039 | + consFallback := constraints.MustParse(t.consFallback) |
1040 | + cons := constraints.MustParse(t.cons) |
1041 | + merged, err := validator.Merge(consFallback, cons) |
1042 | + c.Assert(err, gc.IsNil) |
1043 | + expected := constraints.MustParse(t.expected) |
1044 | + c.Check(merged, gc.DeepEquals, expected) |
1045 | + } |
1046 | +} |
1047 | + |
1048 | +func (s *validationSuite) TestMergeError(c *gc.C) { |
1049 | + validator := constraints.NewValidator() |
1050 | + validator.RegisterConflicts([]string{"instance-type"}, []string{"mem"}) |
1051 | + consFallback := constraints.MustParse("instance-type=foo mem=4G") |
1052 | + cons := constraints.MustParse("cpu-cores=2") |
1053 | + _, err := validator.Merge(consFallback, cons) |
1054 | + c.Assert(err, gc.ErrorMatches, `ambiguous constraints: "mem" overlaps with "instance-type"`) |
1055 | + _, err = validator.Merge(cons, consFallback) |
1056 | + c.Assert(err, gc.ErrorMatches, `ambiguous constraints: "mem" overlaps with "instance-type"`) |
1057 | +} |
1058 | |
1059 | === modified file 'state/addmachine.go' |
1060 | --- state/addmachine.go 2014-04-17 12:53:23 +0000 |
1061 | +++ state/addmachine.go 2014-04-18 13:10:25 +0000 |
1062 | @@ -191,7 +191,11 @@ |
1063 | if err != nil { |
1064 | return MachineTemplate{}, err |
1065 | } |
1066 | - p.Constraints = p.Constraints.WithFallbacks(cons) |
1067 | + validator := constraints.NewValidator() |
1068 | + p.Constraints, err = validator.Merge(cons, p.Constraints) |
1069 | + if err != nil { |
1070 | + return MachineTemplate{}, err |
1071 | + } |
1072 | // Machine constraints do not use a container constraint value. |
1073 | // Both provisioning and deployment constraints use the same |
1074 | // constraints.Value struct so here we clear the container |
1075 | |
1076 | === modified file 'state/prechecker_test.go' |
1077 | --- state/prechecker_test.go 2014-04-14 12:36:13 +0000 |
1078 | +++ state/prechecker_test.go 2014-04-18 13:10:25 +0000 |
1079 | @@ -51,7 +51,9 @@ |
1080 | template, err := s.addOneMachine(c, envCons) |
1081 | c.Assert(err, gc.IsNil) |
1082 | c.Assert(s.prechecker.precheckInstanceSeries, gc.Equals, template.Series) |
1083 | - cons := template.Constraints.WithFallbacks(envCons) |
1084 | + validator := constraints.NewValidator() |
1085 | + cons, err := validator.Merge(envCons, template.Constraints) |
1086 | + c.Assert(err, gc.IsNil) |
1087 | c.Assert(s.prechecker.precheckInstanceConstraints, gc.DeepEquals, cons) |
1088 | } |
1089 | |
1090 | |
1091 | === modified file 'state/service.go' |
1092 | --- state/service.go 2014-04-17 12:47:50 +0000 |
1093 | +++ state/service.go 2014-04-18 13:10:25 +0000 |
1094 | @@ -598,7 +598,11 @@ |
1095 | if err != nil { |
1096 | return "", nil, err |
1097 | } |
1098 | - cons := scons.WithFallbacks(econs) |
1099 | + validator := constraints.NewValidator() |
1100 | + cons, err := validator.Merge(econs, scons) |
1101 | + if err != nil { |
1102 | + return "", nil, err |
1103 | + } |
1104 | ops = append(ops, createConstraintsOp(s.st, globalKey, cons)) |
1105 | } |
1106 | return name, ops, nil |
Reviewers: mp+215807_ code.launchpad. net,
Message:
Please take a look.
Description:
New constraints validation and merge
A new constraints Validator implementation
is introduced, which provides validation
and merging support for constraints. It
allows for conflicting attributes and
unsupported attributes to be registered,
and does the right thing with these.
To illustrate the point, a new constraints
attribute instance-type is also added, which
will be used in a followup branch.
https:/ /code.launchpad .net/~wallyworl d/juju- core/constraint s-validation- merge/+ merge/215807
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/87970043/
Affected files (+757, -184 lines): constraints. go constraints_ test.go export_ test.go validation. go validation_ test.go r_test. go
A [revision details]
M constraints/
M constraints/
A constraints/
A constraints/
A constraints/
M state/addmachine.go
M state/prechecke
M state/service.go