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  }