Merge lp:~wallyworld/juju-core/constraints-validation-merge into lp:~go-bot/juju-core/trunk

Proposed by Ian Booth
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
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.

https://codereview.appspot.com/87970043/

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.

https://codereview.appspot.com/87970043/

To post a comment you must log in.
Revision history for this message
Ian Booth (wallyworld) wrote :

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/~wallyworld/juju-core/constraints-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):
   A [revision details]
   M constraints/constraints.go
   M constraints/constraints_test.go
   A constraints/export_test.go
   A constraints/validation.go
   A constraints/validation_test.go
   M state/addmachine.go
   M state/prechecker_test.go
   M state/service.go

Revision history for this message
Ian Booth (wallyworld) wrote :
Revision history for this message
Ian Booth (wallyworld) wrote :
Revision history for this message
Ian Booth (wallyworld) wrote :
Revision history for this message
William Reade (fwereade) wrote :

LGTM apart from the overly-broad interface.

https://codereview.appspot.com/87970043/diff/60001/constraints/validation.go
File constraints/validation.go (right):

https://codereview.appspot.com/87970043/diff/60001/constraints/validation.go#newcode17
constraints/validation.go:17: type Validator interface {
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://codereview.appspot.com/87970043/diff/60001/constraints/validation.go#newcode25
constraints/validation.go:25: RegisterConflicts(reds, blues []string)
I really appreciate blank lines before comments.

https://codereview.appspot.com/87970043/diff/60001/constraints/validation.go#newcode137
constraints/validation.go:137: return v.checkUnsupported(cons)
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)?

https://codereview.appspot.com/87970043/

Revision history for this message
Ian Booth (wallyworld) wrote :

Please take a look.

https://codereview.appspot.com/87970043/diff/60001/constraints/validation.go
File constraints/validation.go (right):

https://codereview.appspot.com/87970043/diff/60001/constraints/validation.go#newcode17
constraints/validation.go:17: type Validator interface {
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://codereview.appspot.com/87970043/diff/60001/constraints/validation.go#newcode25
constraints/validation.go:25: RegisterConflicts(reds, blues []string)
On 2014/04/18 07:48:20, fwereade wrote:
> I really appreciate blank lines before comments.

Done.

https://codereview.appspot.com/87970043/diff/60001/constraints/validation.go#newcode137
constraints/validation.go:137: return v.checkUnsupported(cons)
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.Validate(....)

https://codereview.appspot.com/87970043/

Revision history for this message
William Reade (fwereade) wrote :

Still LGTM, with a vague feeling that an explicit UnsupportedConstraints
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.

https://codereview.appspot.com/87970043/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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

Subscribers

People subscribed via source and target branches

to status/vote changes: