github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/core/constraints/validation_test.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package constraints_test 5 6 import ( 7 "fmt" 8 "regexp" 9 10 jc "github.com/juju/testing/checkers" 11 gc "gopkg.in/check.v1" 12 13 "github.com/juju/juju/core/constraints" 14 ) 15 16 type validationSuite struct{} 17 18 var _ = gc.Suite(&validationSuite{}) 19 20 var validationTests = []struct { 21 desc string 22 cons string 23 unsupported []string 24 vocab map[string][]interface{} 25 reds []string 26 blues []string 27 err string 28 }{ 29 { 30 desc: "base good", 31 cons: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cores=4", 32 }, 33 { 34 desc: "unsupported", 35 cons: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cores=4 tags=foo", 36 unsupported: []string{"tags"}, 37 }, 38 { 39 desc: "multiple unsupported", 40 cons: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cores=4 instance-type=foo", 41 unsupported: []string{"cpu-power", "instance-type"}, 42 }, 43 { 44 desc: "Ambiguous constraint errors take precedence over unsupported errors.", 45 cons: "root-disk=8G mem=4G cores=4 instance-type=foo", 46 reds: []string{"mem", "arch"}, 47 blues: []string{"instance-type"}, 48 unsupported: []string{"cores"}, 49 err: `ambiguous constraints: "instance-type" overlaps with "mem"`, 50 }, 51 { 52 desc: "red conflicts", 53 cons: "root-disk=8G mem=4G arch=amd64 cores=4 instance-type=foo", 54 reds: []string{"mem", "arch"}, 55 }, 56 { 57 desc: "blue conflicts", 58 cons: "root-disk=8G mem=4G arch=amd64 cores=4 instance-type=foo", 59 blues: []string{"mem", "arch"}, 60 }, 61 { 62 desc: "red and blue conflicts", 63 cons: "root-disk=8G mem=4G arch=amd64 cores=4 instance-type=foo", 64 reds: []string{"mem", "arch"}, 65 blues: []string{"instance-type"}, 66 err: `ambiguous constraints: "arch" overlaps with "instance-type"`, 67 }, 68 { 69 desc: "ambiguous constraints red to blue", 70 cons: "root-disk=8G mem=4G arch=amd64 cores=4 instance-type=foo", 71 reds: []string{"instance-type"}, 72 blues: []string{"mem", "arch"}, 73 err: `ambiguous constraints: "arch" overlaps with "instance-type"`, 74 }, 75 { 76 desc: "ambiguous constraints blue to red", 77 cons: "root-disk=8G mem=4G cores=4 instance-type=foo", 78 reds: []string{"mem", "arch"}, 79 blues: []string{"instance-type"}, 80 err: `ambiguous constraints: "instance-type" overlaps with "mem"`, 81 }, 82 { 83 desc: "arch vocab", 84 cons: "arch=amd64 mem=4G cores=4", 85 vocab: map[string][]interface{}{"arch": {"amd64", "arm64"}}, 86 }, 87 { 88 desc: "cores vocab", 89 cons: "mem=4G cores=4", 90 vocab: map[string][]interface{}{"cores": {2, 4, 8}}, 91 }, 92 { 93 desc: "instance-type vocab", 94 cons: "mem=4G instance-type=foo", 95 vocab: map[string][]interface{}{"instance-type": {"foo", "bar"}}, 96 }, 97 { 98 desc: "tags vocab", 99 cons: "mem=4G tags=foo,bar", 100 vocab: map[string][]interface{}{"tags": {"foo", "bar", "another"}}, 101 }, 102 { 103 desc: "invalid arch vocab", 104 cons: "arch=arm64 mem=4G cores=4", 105 vocab: map[string][]interface{}{"arch": {"amd64"}}, 106 err: "invalid constraint value: arch=arm64\nvalid values are:.*", 107 }, 108 { 109 desc: "invalid cores vocab", 110 cons: "mem=4G cores=5", 111 vocab: map[string][]interface{}{"cores": {2, 4, 8}}, 112 err: "invalid constraint value: cores=5\nvalid values are:.*", 113 }, 114 { 115 desc: "invalid instance-type vocab", 116 cons: "mem=4G instance-type=foo", 117 vocab: map[string][]interface{}{"instance-type": {"bar"}}, 118 err: "invalid constraint value: instance-type=foo\nvalid values are:.*", 119 }, 120 { 121 desc: "invalid tags vocab", 122 cons: "mem=4G tags=foo,other", 123 vocab: map[string][]interface{}{"tags": {"foo", "bar", "another"}}, 124 err: "invalid constraint value: tags=other\nvalid values are:.*", 125 }, 126 { 127 desc: "instance-type and arch", 128 cons: "arch=arm64 mem=4G instance-type=foo", 129 vocab: map[string][]interface{}{ 130 "instance-type": {"foo", "bar"}, 131 "arch": {"amd64", "arm64"}}, 132 }, 133 { 134 desc: "virt-type", 135 cons: "virt-type=bar", 136 vocab: map[string][]interface{}{"virt-type": {"bar"}}, 137 }, 138 } 139 140 func (s *validationSuite) TestValidation(c *gc.C) { 141 for i, t := range validationTests { 142 c.Logf("test %d: %s", i, t.desc) 143 validator := constraints.NewValidator() 144 validator.RegisterUnsupported(t.unsupported) 145 validator.RegisterConflicts(t.reds, t.blues) 146 for a, v := range t.vocab { 147 validator.RegisterVocabulary(a, v) 148 } 149 cons := constraints.MustParse(t.cons) 150 unsupported, err := validator.Validate(cons) 151 if t.err == "" { 152 c.Assert(err, jc.ErrorIsNil) 153 c.Assert(unsupported, jc.SameContents, t.unsupported) 154 } else { 155 c.Assert(err, gc.ErrorMatches, t.err) 156 } 157 } 158 } 159 160 func (s *validationSuite) TestConstraintResolver(c *gc.C) { 161 validator := constraints.NewValidator() 162 validator.RegisterConflicts([]string{"instance-type"}, []string{"arch"}) 163 cons := constraints.MustParse("arch=amd64 instance-type=foo-amd64") 164 _, err := validator.Validate(cons) 165 c.Assert(err, gc.ErrorMatches, `ambiguous constraints: "arch" overlaps with "instance-type"`) 166 validator.RegisterConflictResolver("instance-type", "arch", func(attrValues map[string]interface{}) error { 167 if attrValues["arch"] == "amd64" && attrValues["instance-type"] == "foo-amd64" { 168 return nil 169 } 170 return fmt.Errorf("instance-type=%q and arch=%q are incompatible", attrValues["instance-type"], attrValues["arch"]) 171 }) 172 _, err = validator.Validate(cons) 173 c.Assert(err, jc.ErrorIsNil) 174 175 cons = constraints.MustParse("arch=arm64 instance-type=foo-s390x") 176 _, err = validator.Validate(cons) 177 c.Assert(err, gc.ErrorMatches, `ambiguous constraints: "arch" overlaps with "instance-type": instance-type="foo-s390x" and arch="arm64" are incompatible`) 178 } 179 180 var mergeTests = []struct { 181 desc string 182 consFallback string 183 cons string 184 unsupported []string 185 reds []string 186 blues []string 187 expected string 188 }{ 189 { 190 desc: "empty all round", 191 }, { 192 desc: "container with empty fallback", 193 cons: "container=lxd", 194 expected: "container=lxd", 195 }, { 196 desc: "container from fallback", 197 consFallback: "container=lxd", 198 expected: "container=lxd", 199 }, { 200 desc: "arch with empty fallback", 201 cons: "arch=amd64", 202 expected: "arch=amd64", 203 }, { 204 desc: "arch with ignored fallback", 205 cons: "arch=amd64", 206 consFallback: "arch=arm64", 207 expected: "arch=amd64", 208 }, { 209 desc: "arch from fallback", 210 consFallback: "arch=arm64", 211 expected: "arch=arm64", 212 }, { 213 desc: "instance type with empty fallback", 214 cons: "instance-type=foo", 215 expected: "instance-type=foo", 216 }, { 217 desc: "instance type with ignored fallback", 218 cons: "instance-type=foo", 219 consFallback: "instance-type=bar", 220 expected: "instance-type=foo", 221 }, { 222 desc: "instance type from fallback", 223 consFallback: "instance-type=foo", 224 expected: "instance-type=foo", 225 }, { 226 desc: "cores with empty fallback", 227 cons: "cores=2", 228 expected: "cores=2", 229 }, { 230 desc: "cores with ignored fallback", 231 cons: "cores=4", 232 consFallback: "cores=8", 233 expected: "cores=4", 234 }, { 235 desc: "cores from fallback", 236 consFallback: "cores=8", 237 expected: "cores=8", 238 }, { 239 desc: "cpu-power with empty fallback", 240 cons: "cpu-power=100", 241 expected: "cpu-power=100", 242 }, { 243 desc: "cpu-power with ignored fallback", 244 cons: "cpu-power=100", 245 consFallback: "cpu-power=200", 246 expected: "cpu-power=100", 247 }, { 248 desc: "cpu-power from fallback", 249 consFallback: "cpu-power=200", 250 expected: "cpu-power=200", 251 }, { 252 desc: "tags with empty fallback", 253 cons: "tags=foo,bar", 254 expected: "tags=foo,bar", 255 }, { 256 desc: "tags with ignored fallback", 257 cons: "tags=foo,bar", 258 consFallback: "tags=baz", 259 expected: "tags=foo,bar", 260 }, { 261 desc: "tags from fallback", 262 consFallback: "tags=foo,bar", 263 expected: "tags=foo,bar", 264 }, { 265 desc: "tags initial empty", 266 cons: "tags=", 267 consFallback: "tags=foo,bar", 268 expected: "tags=", 269 }, { 270 desc: "mem with empty fallback", 271 cons: "mem=4G", 272 expected: "mem=4G", 273 }, { 274 desc: "mem with ignored fallback", 275 cons: "mem=4G", 276 consFallback: "mem=8G", 277 expected: "mem=4G", 278 }, { 279 desc: "mem from fallback", 280 consFallback: "mem=8G", 281 expected: "mem=8G", 282 }, { 283 desc: "root-disk with empty fallback", 284 cons: "root-disk=4G", 285 expected: "root-disk=4G", 286 }, { 287 desc: "root-disk with ignored fallback", 288 cons: "root-disk=4G", 289 consFallback: "root-disk=8G", 290 expected: "root-disk=4G", 291 }, { 292 desc: "root-disk from fallback", 293 consFallback: "root-disk=8G", 294 expected: "root-disk=8G", 295 }, { 296 desc: "non-overlapping mix", 297 cons: "root-disk=8G mem=4G arch=amd64", 298 consFallback: "cpu-power=1000 cores=4", 299 expected: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cores=4", 300 }, { 301 desc: "overlapping mix", 302 cons: "root-disk=8G mem=4G arch=amd64", 303 consFallback: "cpu-power=1000 cores=4 mem=8G", 304 expected: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cores=4", 305 }, { 306 desc: "fallback only, no conflicts", 307 consFallback: "root-disk=8G cores=4 instance-type=foo", 308 reds: []string{"mem", "arch"}, 309 blues: []string{"instance-type"}, 310 expected: "root-disk=8G cores=4 instance-type=foo", 311 }, { 312 desc: "no fallback, no conflicts", 313 cons: "root-disk=8G cores=4 instance-type=foo", 314 reds: []string{"mem", "arch"}, 315 blues: []string{"instance-type"}, 316 expected: "root-disk=8G cores=4 instance-type=foo", 317 }, { 318 desc: "conflict value from override", 319 consFallback: "root-disk=8G instance-type=foo", 320 cons: "root-disk=8G cores=4 instance-type=bar", 321 reds: []string{"mem", "arch"}, 322 blues: []string{"instance-type"}, 323 expected: "root-disk=8G cores=4 instance-type=bar", 324 }, { 325 desc: "unsupported attributes ignored", 326 consFallback: "root-disk=8G instance-type=foo", 327 cons: "root-disk=8G cores=4 instance-type=bar", 328 reds: []string{"mem", "arch"}, 329 blues: []string{"instance-type"}, 330 unsupported: []string{"instance-type"}, 331 expected: "root-disk=8G cores=4 instance-type=bar", 332 }, { 333 desc: "red conflict masked from fallback", 334 consFallback: "root-disk=8G mem=4G", 335 cons: "root-disk=8G cores=4 instance-type=bar", 336 reds: []string{"mem", "arch"}, 337 blues: []string{"instance-type"}, 338 expected: "root-disk=8G cores=4 instance-type=bar", 339 }, { 340 desc: "second red conflict masked from fallback", 341 consFallback: "root-disk=8G arch=amd64", 342 cons: "root-disk=8G cores=4 instance-type=bar", 343 reds: []string{"mem", "arch"}, 344 blues: []string{"instance-type"}, 345 expected: "root-disk=8G cores=4 instance-type=bar", 346 }, { 347 desc: "blue conflict masked from fallback", 348 consFallback: "root-disk=8G cores=4 instance-type=bar", 349 cons: "root-disk=8G mem=4G", 350 reds: []string{"mem", "arch"}, 351 blues: []string{"instance-type"}, 352 expected: "root-disk=8G cores=4 mem=4G", 353 }, { 354 desc: "both red conflicts used, blue mased from fallback", 355 consFallback: "root-disk=8G cores=4 instance-type=bar", 356 cons: "root-disk=8G arch=amd64 mem=4G", 357 reds: []string{"mem", "arch"}, 358 blues: []string{"instance-type"}, 359 expected: "root-disk=8G cores=4 arch=amd64 mem=4G", 360 }, 361 } 362 363 func (s *validationSuite) TestMerge(c *gc.C) { 364 for i, t := range mergeTests { 365 c.Logf("test %d: %s", i, t.desc) 366 validator := constraints.NewValidator() 367 validator.RegisterConflicts(t.reds, t.blues) 368 consFallback := constraints.MustParse(t.consFallback) 369 cons := constraints.MustParse(t.cons) 370 merged, err := validator.Merge(consFallback, cons) 371 c.Assert(err, jc.ErrorIsNil) 372 expected := constraints.MustParse(t.expected) 373 c.Check(merged, gc.DeepEquals, expected) 374 } 375 } 376 377 func (s *validationSuite) TestMergeError(c *gc.C) { 378 validator := constraints.NewValidator() 379 validator.RegisterConflicts([]string{"instance-type"}, []string{"mem"}) 380 consFallback := constraints.MustParse("instance-type=foo mem=4G") 381 cons := constraints.MustParse("cores=2") 382 _, err := validator.Merge(consFallback, cons) 383 c.Assert(err, gc.ErrorMatches, `ambiguous constraints: "instance-type" overlaps with "mem"`) 384 _, err = validator.Merge(cons, consFallback) 385 c.Assert(err, gc.ErrorMatches, `ambiguous constraints: "instance-type" overlaps with "mem"`) 386 } 387 388 func (s *validationSuite) TestUpdateVocabulary(c *gc.C) { 389 validator := constraints.NewValidator() 390 attributeName := "arch" 391 originalValues := []string{"amd64"} 392 validator.RegisterVocabulary(attributeName, originalValues) 393 394 cons := constraints.MustParse("arch=amd64") 395 _, err := validator.Validate(cons) 396 c.Assert(err, jc.ErrorIsNil) 397 398 cons2 := constraints.MustParse("arch=ppc64el") 399 _, err = validator.Validate(cons2) 400 c.Assert(err, gc.ErrorMatches, regexp.QuoteMeta(`invalid constraint value: arch=ppc64el 401 valid values are: [amd64]`)) 402 403 additionalValues := []string{"ppc64el"} 404 validator.UpdateVocabulary(attributeName, additionalValues) 405 406 _, err = validator.Validate(cons) 407 c.Assert(err, jc.ErrorIsNil) 408 _, err = validator.Validate(cons2) 409 c.Assert(err, jc.ErrorIsNil) 410 }