From 583793b7ae57a342f0e1f435a88e5fc5b63e755b Mon Sep 17 00:00:00 2001 From: Evan Lezar Date: Thu, 21 Apr 2022 17:25:43 +0200 Subject: [PATCH] Add processing for requirements and constraints This change adds a Requirements abstraction that can be used to check an images' NVIDIA_REQUIRE_* envvars against the host properties such as CUDA version or architecture. Signed-off-by: Evan Lezar --- internal/requirements/constants.go | 25 ++ internal/requirements/constraints/binary.go | 76 ++++++ .../requirements/constraints/constants.go | 51 ++++ .../constraints/constraint_mock.go | 108 ++++++++ .../requirements/constraints/constraints.go | 24 ++ .../constraints/constraints_test.go | 17 ++ internal/requirements/constraints/factory.go | 143 +++++++++++ .../requirements/constraints/factory_test.go | 187 ++++++++++++++ internal/requirements/constraints/logical.go | 91 +++++++ .../requirements/constraints/logical_test.go | 152 +++++++++++ internal/requirements/constraints/property.go | 129 ++++++++++ .../requirements/constraints/property_mock.go | 241 ++++++++++++++++++ internal/requirements/requirements.go | 69 +++++ 13 files changed, 1313 insertions(+) create mode 100644 internal/requirements/constants.go create mode 100644 internal/requirements/constraints/binary.go create mode 100644 internal/requirements/constraints/constants.go create mode 100644 internal/requirements/constraints/constraint_mock.go create mode 100644 internal/requirements/constraints/constraints.go create mode 100644 internal/requirements/constraints/constraints_test.go create mode 100644 internal/requirements/constraints/factory.go create mode 100644 internal/requirements/constraints/factory_test.go create mode 100644 internal/requirements/constraints/logical.go create mode 100644 internal/requirements/constraints/logical_test.go create mode 100644 internal/requirements/constraints/property.go create mode 100644 internal/requirements/constraints/property_mock.go create mode 100644 internal/requirements/requirements.go diff --git a/internal/requirements/constants.go b/internal/requirements/constants.go new file mode 100644 index 00000000..e9ea7077 --- /dev/null +++ b/internal/requirements/constants.go @@ -0,0 +1,25 @@ +/** +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +**/ + +package requirements + +// A list of supported requirements / properties +const ( + ARCH = "arch" + BRAND = "brand" + CUDA = "cuda" + DRIVER = "driver" +) diff --git a/internal/requirements/constraints/binary.go b/internal/requirements/constraints/binary.go new file mode 100644 index 00000000..5ca37408 --- /dev/null +++ b/internal/requirements/constraints/binary.go @@ -0,0 +1,76 @@ +/** +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +**/ + +package constraints + +import ( + "fmt" +) + +// binary represents a binary operation. This can be used to compare a specified +// property to a value +type binary struct { + left Property + operator string + right string +} + +// String returns the string representation of the binary comparator +func (c binary) String() string { + return fmt.Sprintf("%v%v%v", c.left.Name(), c.operator, c.right) +} + +// Assert compares the property to the required value using the supplied comparator +func (c binary) Assert() error { + satisfied, err := c.eval() + if err != nil { + return err + } + if satisfied { + return nil + } + + // error_setx(err, "unsatisfied condition: %s, please update your driver to a newer version, or use an earlier cuda container", predicate_format); + return fmt.Errorf("unsatisfied condition: %v (%v)", c.String(), c.left.String()) +} + +func (c binary) eval() (bool, error) { + if c.left == nil { + return true, nil + } + + compare, err := c.left.CompareTo(c.right) + if err != nil { + return false, err + } + + switch string(c.operator) { + case equal: + return compare == 0, nil + case notEqual: + return compare != 0, nil + case less: + return compare < 0, nil + case lessEqual: + return compare <= 0, nil + case greater: + return compare > 0, nil + case greaterEqual: + return compare >= 0, nil + } + + return false, fmt.Errorf("invalid operator %v", c.operator) +} diff --git a/internal/requirements/constraints/constants.go b/internal/requirements/constraints/constants.go new file mode 100644 index 00000000..9e515214 --- /dev/null +++ b/internal/requirements/constraints/constants.go @@ -0,0 +1,51 @@ +/** +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +**/ + +package constraints + +import "fmt" + +const ( + equal = "=" + notEqual = "!=" + less = "<" + lessEqual = "<=" + greater = ">" + greaterEqual = ">=" +) + +// always is a constraint that is always met +type always struct{} + +func (c always) Assert() error { + return nil +} + +func (c always) String() string { + return "true" +} + +// invalid is an invalid constraint and can never be met +type invalid string + +func (c invalid) Assert() error { + return fmt.Errorf("invalid constraint: %v", c.String()) +} + +// String returns the string representation of the contraint +func (c invalid) String() string { + return string(c) +} diff --git a/internal/requirements/constraints/constraint_mock.go b/internal/requirements/constraints/constraint_mock.go new file mode 100644 index 00000000..08cd46a1 --- /dev/null +++ b/internal/requirements/constraints/constraint_mock.go @@ -0,0 +1,108 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package constraints + +import ( + "sync" +) + +// Ensure, that ConstraintMock does implement Constraint. +// If this is not the case, regenerate this file with moq. +var _ Constraint = &ConstraintMock{} + +// ConstraintMock is a mock implementation of Constraint. +// +// func TestSomethingThatUsesConstraint(t *testing.T) { +// +// // make and configure a mocked Constraint +// mockedConstraint := &ConstraintMock{ +// AssertFunc: func() error { +// panic("mock out the Assert method") +// }, +// StringFunc: func() string { +// panic("mock out the String method") +// }, +// } +// +// // use mockedConstraint in code that requires Constraint +// // and then make assertions. +// +// } +type ConstraintMock struct { + // AssertFunc mocks the Assert method. + AssertFunc func() error + + // StringFunc mocks the String method. + StringFunc func() string + + // calls tracks calls to the methods. + calls struct { + // Assert holds details about calls to the Assert method. + Assert []struct { + } + // String holds details about calls to the String method. + String []struct { + } + } + lockAssert sync.RWMutex + lockString sync.RWMutex +} + +// Assert calls AssertFunc. +func (mock *ConstraintMock) Assert() error { + callInfo := struct { + }{} + mock.lockAssert.Lock() + mock.calls.Assert = append(mock.calls.Assert, callInfo) + mock.lockAssert.Unlock() + if mock.AssertFunc == nil { + var ( + errOut error + ) + return errOut + } + return mock.AssertFunc() +} + +// AssertCalls gets all the calls that were made to Assert. +// Check the length with: +// len(mockedConstraint.AssertCalls()) +func (mock *ConstraintMock) AssertCalls() []struct { +} { + var calls []struct { + } + mock.lockAssert.RLock() + calls = mock.calls.Assert + mock.lockAssert.RUnlock() + return calls +} + +// String calls StringFunc. +func (mock *ConstraintMock) String() string { + callInfo := struct { + }{} + mock.lockString.Lock() + mock.calls.String = append(mock.calls.String, callInfo) + mock.lockString.Unlock() + if mock.StringFunc == nil { + var ( + sOut string + ) + return sOut + } + return mock.StringFunc() +} + +// StringCalls gets all the calls that were made to String. +// Check the length with: +// len(mockedConstraint.StringCalls()) +func (mock *ConstraintMock) StringCalls() []struct { +} { + var calls []struct { + } + mock.lockString.RLock() + calls = mock.calls.String + mock.lockString.RUnlock() + return calls +} diff --git a/internal/requirements/constraints/constraints.go b/internal/requirements/constraints/constraints.go new file mode 100644 index 00000000..48e5310a --- /dev/null +++ b/internal/requirements/constraints/constraints.go @@ -0,0 +1,24 @@ +/** +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +**/ + +package constraints + +//go:generate moq -stub -out constraint_mock.go . Constraint +// Constraint represents a constraint that is to be evaluated +type Constraint interface { + String() string + Assert() error +} diff --git a/internal/requirements/constraints/constraints_test.go b/internal/requirements/constraints/constraints_test.go new file mode 100644 index 00000000..ad6a172a --- /dev/null +++ b/internal/requirements/constraints/constraints_test.go @@ -0,0 +1,17 @@ +/** +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +**/ + +package constraints diff --git a/internal/requirements/constraints/factory.go b/internal/requirements/constraints/factory.go new file mode 100644 index 00000000..29227c66 --- /dev/null +++ b/internal/requirements/constraints/factory.go @@ -0,0 +1,143 @@ +/** +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +**/ + +package constraints + +import ( + "fmt" + "strings" + + "github.com/sirupsen/logrus" +) + +type factory struct { + logger *logrus.Logger + properties map[string]Property +} + +// New creates a new constraint for the supplied requirements and properties +func New(logger *logrus.Logger, requirements []string, properties map[string]Property) (Constraint, error) { + if len(requirements) == 0 { + return &always{}, nil + } + + f := factory{ + logger: logger, + properties: properties, + } + + var constraints []Constraint + for _, r := range requirements { + c, err := f.newConstraintFromRequirement(r) + if err != nil { + return nil, err + } + if c == nil { + continue + } + constraints = append(constraints, c) + } + + return AND(constraints), nil +} + +// newConstraintFromRequirement takes a requirement string and generates +// the associated constraint(s). Invalid constraints are ignored. +// Each requirement can consist of multiple constraints, with space-separated constraints being ORed +// together and comma-separated constraints being ANDed together. +func (r factory) newConstraintFromRequirement(requirement string) (Constraint, error) { + const ( + orSeparator = " " + andSeparator = "," + ) + if strings.TrimSpace(requirement) == "" { + return nil, nil + } + + var terms []Constraint + for _, term := range strings.Split(requirement, orSeparator) { + var factors []Constraint + for _, factor := range strings.Split(term, andSeparator) { + f, err := r.parse(factor) + if err != nil { + return nil, err + } + if f == nil { + r.logger.Debugf("Skipping unsupported constraint: %v", factor) + continue + } + factors = append(factors, f) + } + + if len(factors) == 0 { + continue + } + + if len(factors) == 1 { + terms = append(terms, factors[0]) + } else { + terms = append(terms, and(factors)) + } + } + + return OR(terms), nil +} + +// parse constructs a constraint from the specified string. +// The string is expected to be of the form [PROPERTY][OPERATOR][VALUE] +func (r factory) parse(condition string) (Constraint, error) { + if strings.TrimSpace(condition) == "" { + return nil, nil + } + + operators := []string{ + notEqual, + lessEqual, + greaterEqual, + equal, + less, + greater, + } + + propertyEnd := strings.IndexAny(condition, "<>=!") + if propertyEnd == -1 { + return nil, fmt.Errorf("invalid constraint: %v", condition) + } + + property := condition[:propertyEnd] + condition = strings.TrimPrefix(condition, property) + + p, ok := r.properties[property] + if !ok || p == nil { + return nil, nil + } + + var op string + for _, o := range operators { + if strings.HasPrefix(condition, o) { + op = o + break + } + } + value := strings.TrimPrefix(condition, op) + + c := binary{ + left: p, + right: value, + operator: op, + } + return c, p.Validate(value) +} diff --git a/internal/requirements/constraints/factory_test.go b/internal/requirements/constraints/factory_test.go new file mode 100644 index 00000000..019209f0 --- /dev/null +++ b/internal/requirements/constraints/factory_test.go @@ -0,0 +1,187 @@ +/** +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +**/ + +package constraints + +import ( + "testing" + + testlog "github.com/sirupsen/logrus/hooks/test" + "github.com/stretchr/testify/require" +) + +func TestParse(t *testing.T) { + logger, _ := testlog.NewNullLogger() + + cuda := NewVersionProperty("cuda", "") + + f := factory{ + logger: logger, + properties: map[string]Property{ + "cuda": cuda, + }, + } + + testCases := []struct { + description string + condition string + expectedError bool + expected Constraint + }{ + { + description: "empty is nil", + condition: "", + expected: nil, + }, + { + description: "missing operator is invalid", + condition: "notvalid", + expectedError: true, + }, + { + description: "invalid property is invalid", + condition: "foo=45", + expected: nil, + }, + { + description: "cuda must be semver", + condition: "cuda=foo", + expectedError: true, + expected: binary{cuda, equal, "foo"}, + }, + { + description: "cuda greater than equal", + condition: "cuda>=11.6", + expected: binary{cuda, greaterEqual, "11.6"}, + }, + { + description: "cuda greater than", + condition: "cuda>11.6", + expected: binary{cuda, greater, "11.6"}, + }, + { + description: "cuda less than equal", + condition: "cuda<=11.6", + expected: binary{cuda, lessEqual, "11.6"}, + }, + { + description: "cuda less than", + condition: "cuda<11.6", + expected: binary{cuda, less, "11.6"}, + }, + { + description: "cuda equal", + condition: "cuda=11.6", + expected: binary{cuda, equal, "11.6"}, + }, + { + description: "cuda not equal", + condition: "cuda!=11.6", + expected: binary{cuda, notEqual, "11.6"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + c, err := f.parse(tc.condition) + if tc.expectedError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + require.EqualValues(t, tc.expected, c) + }) + } +} + +func TestNewConstraintFromRequirement(t *testing.T) { + logger, _ := testlog.NewNullLogger() + + cuda := &PropertyMock{} + arch := &PropertyMock{} + + f := factory{ + logger: logger, + properties: map[string]Property{ + "cuda": cuda, + "arch": arch, + }, + } + + testCases := []struct { + description string + requirement string + expectedError bool + expected Constraint + }{ + { + description: "empty is nil", + requirement: "", + expected: nil, + }, + { + description: "malformed constraint is invalid", + requirement: "notvalid", + expectedError: true, + expected: nil, + }, + { + description: "unsupported property is ignored", + requirement: "cuda>=11.6 foo=bar", + expected: binary{cuda, greaterEqual, "11.6"}, + }, + { + description: "space-separated is and", + requirement: "cuda>=11.6 arch=5.3", + expected: and([]Constraint{ + binary{cuda, greaterEqual, "11.6"}, + binary{arch, equal, "5.3"}, + }), + }, + { + description: "comma-separated is or", + requirement: "cuda>=11.6,arch=5.3", + expected: or([]Constraint{ + binary{cuda, greaterEqual, "11.6"}, + binary{arch, equal, "5.3"}, + }), + }, + { + description: "and takes precedence", + requirement: "cuda<13.6 cuda>=11.6,arch=5.3", + expected: or([]Constraint{ + binary{cuda, less, "13.6"}, + and([]Constraint{ + binary{cuda, greaterEqual, "11.6"}, + binary{arch, equal, "5.3"}, + }), + }), + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + c, err := f.newConstraintFromRequirement(tc.requirement) + if tc.expectedError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + require.EqualValues(t, tc.expected, c) + }) + } + +} diff --git a/internal/requirements/constraints/logical.go b/internal/requirements/constraints/logical.go new file mode 100644 index 00000000..c1dda521 --- /dev/null +++ b/internal/requirements/constraints/logical.go @@ -0,0 +1,91 @@ +/** +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +**/ + +package constraints + +import ( + "fmt" + "strings" +) + +// or represents an OR operation on a set of constraints +type or []Constraint + +// and represents an AND (ALL) operation on a set of contraints +type and []Constraint + +// AND constructs a new constraint that is the logical AND of the supplied constraints +func AND(constraints []Constraint) Constraint { + if len(constraints) == 0 { + return &always{} + } + if len(constraints) == 1 { + return constraints[0] + } + return and(constraints) +} + +// OR constructs a new constrant that is the logical OR of the supplied constraints +func OR(constraints []Constraint) Constraint { + if len(constraints) == 0 { + return nil + } + if len(constraints) == 1 { + return constraints[0] + } + + return or(constraints) +} + +func (operands or) Assert() error { + for _, o := range operands { + // We stop on the first nil + if err := o.Assert(); err == nil { + return nil + } + } + return fmt.Errorf("%v not met", operands) +} + +func (operands or) String() string { + var terms []string + + for _, o := range operands { + terms = append(terms, o.String()) + } + + return strings.Join(terms, "||") +} + +func (operands and) Assert() error { + for _, o := range operands { + // We stop on the first Assert + if err := o.Assert(); err != nil { + return err + } + } + return nil +} + +func (operands and) String() string { + var terms []string + + for _, o := range operands { + terms = append(terms, o.String()) + } + + return strings.Join(terms, "&&") +} diff --git a/internal/requirements/constraints/logical_test.go b/internal/requirements/constraints/logical_test.go new file mode 100644 index 00000000..657cf6d8 --- /dev/null +++ b/internal/requirements/constraints/logical_test.go @@ -0,0 +1,152 @@ +/** +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +**/ + +package constraints + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestANDConstraint(t *testing.T) { + + never := ConstraintMock{AssertFunc: func() error { return fmt.Errorf("false") }} + + testCases := []struct { + description string + constraints []Constraint + expected bool + }{ + { + description: "empty is always true", + constraints: []Constraint{}, + expected: true, + }, + { + description: "single true constraint is true", + constraints: []Constraint{ + &always{}, + }, + expected: true, + }, + { + description: "single false constraint is false", + constraints: []Constraint{ + &never, + }, + expected: false, + }, + { + description: "multiple true constraints are true", + constraints: []Constraint{ + &always{}, &always{}, + }, + expected: true, + }, + { + description: "mixed constraints are false (first is true)", + constraints: []Constraint{ + &always{}, &never, + }, + expected: false, + }, + { + description: "mixed constraints are false (last is true)", + constraints: []Constraint{ + &never, &always{}, + }, + expected: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + err := and(tc.constraints).Assert() + if tc.expected { + require.NoError(t, err) + } else { + require.Error(t, err) + } + }) + } + +} + +func TestORConstraint(t *testing.T) { + + never := ConstraintMock{AssertFunc: func() error { return fmt.Errorf("false") }} + + testCases := []struct { + description string + constraints []Constraint + expected bool + }{ + { + description: "empty is always false", + constraints: []Constraint{}, + expected: false, + }, + { + description: "single true constraint is true", + constraints: []Constraint{ + &always{}, + }, + expected: true, + }, + { + description: "single false constraint is false", + constraints: []Constraint{ + &never, + }, + expected: false, + }, + { + description: "multiple true constraints are true", + constraints: []Constraint{ + &always{}, &always{}, + }, + expected: true, + }, + { + description: "mixed constraints are true (first is true)", + constraints: []Constraint{ + &always{}, &never, + }, + expected: true, + }, + { + description: "mixed constraints are true (last is true)", + constraints: []Constraint{ + &never, &always{}, + }, + expected: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + err := or(tc.constraints).Assert() + if tc.expected { + require.NoError(t, err) + } else { + require.Error(t, err) + } + }) + } + +} diff --git a/internal/requirements/constraints/property.go b/internal/requirements/constraints/property.go new file mode 100644 index 00000000..852b2b40 --- /dev/null +++ b/internal/requirements/constraints/property.go @@ -0,0 +1,129 @@ +/** +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +**/ + +package constraints + +import ( + "fmt" + "strings" + + "golang.org/x/mod/semver" +) + +//go:generate moq -stub -out property_mock.go . Property +// Property represents a property that is used to check requirements +type Property interface { + Name() string + Value() (string, error) + String() string + CompareTo(string) (int, error) + Validate(string) error +} + +// NewStringProperty creates a string property based on the name-value pair +func NewStringProperty(name string, value string) Property { + p := stringProperty{ + name: name, + value: value, + } + + return p +} + +// NewVersionProperty creates a property representing a semantic version based on the name-value pair +func NewVersionProperty(name string, value string) Property { + p := versionProperty{ + stringProperty: stringProperty{ + name: name, + value: value, + }, + } + + return p +} + +// stringProperty represents a property that is used to check requirements +type stringProperty struct { + name string + value string +} + +type versionProperty struct { + stringProperty +} + +// Name returns a stringProperty's name +func (p stringProperty) Name() string { + return p.name +} + +// Value returns a stringProperty's value or an error if this cannot be determined +func (p stringProperty) Value() (string, error) { + return p.value, nil +} + +// CompareTo compares two strings to each other +func (p stringProperty) CompareTo(other string) (int, error) { + value := p.value + + if value < other { + return -1, nil + } + + if value > other { + return 1, nil + } + + return 0, nil +} + +// Validate returns nil for all input strings +func (p stringProperty) Validate(string) error { + return nil +} + +// String returns the string representation of the name value combination +func (p stringProperty) String() string { + v, err := p.Value() + if err != nil { + return fmt.Sprintf("invalid %v: %v", p.name, err) + } + return fmt.Sprintf("%v=%v", p.name, v) +} + +// CompareTo compares two versions to each other as semantic versions +func (p versionProperty) CompareTo(other string) (int, error) { + if err := p.Validate(other); err != nil { + return 0, fmt.Errorf("invailid value for %v: %v", p.name, err) + } + + vValue := ensurePrefix(p.value, "v") + vOther := ensurePrefix(other, "v") + return semver.Compare(vValue, vOther), nil +} + +// Validate checks whether the supplied value is a valid semantic version +func (p versionProperty) Validate(value string) error { + if !semver.IsValid(ensurePrefix(value, "v")) { + return fmt.Errorf("invailid value %v; expected a valid version string", value) + } + + return nil +} + +func ensurePrefix(s string, prefix string) string { + return prefix + strings.TrimPrefix(s, prefix) +} diff --git a/internal/requirements/constraints/property_mock.go b/internal/requirements/constraints/property_mock.go new file mode 100644 index 00000000..656f074a --- /dev/null +++ b/internal/requirements/constraints/property_mock.go @@ -0,0 +1,241 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package constraints + +import ( + "sync" +) + +// Ensure, that PropertyMock does implement Property. +// If this is not the case, regenerate this file with moq. +var _ Property = &PropertyMock{} + +// PropertyMock is a mock implementation of Property. +// +// func TestSomethingThatUsesProperty(t *testing.T) { +// +// // make and configure a mocked Property +// mockedProperty := &PropertyMock{ +// CompareToFunc: func(s string) (int, error) { +// panic("mock out the CompareTo method") +// }, +// NameFunc: func() string { +// panic("mock out the Name method") +// }, +// StringFunc: func() string { +// panic("mock out the String method") +// }, +// ValidateFunc: func(s string) error { +// panic("mock out the Validate method") +// }, +// ValueFunc: func() (string, error) { +// panic("mock out the Value method") +// }, +// } +// +// // use mockedProperty in code that requires Property +// // and then make assertions. +// +// } +type PropertyMock struct { + // CompareToFunc mocks the CompareTo method. + CompareToFunc func(s string) (int, error) + + // NameFunc mocks the Name method. + NameFunc func() string + + // StringFunc mocks the String method. + StringFunc func() string + + // ValidateFunc mocks the Validate method. + ValidateFunc func(s string) error + + // ValueFunc mocks the Value method. + ValueFunc func() (string, error) + + // calls tracks calls to the methods. + calls struct { + // CompareTo holds details about calls to the CompareTo method. + CompareTo []struct { + // S is the s argument value. + S string + } + // Name holds details about calls to the Name method. + Name []struct { + } + // String holds details about calls to the String method. + String []struct { + } + // Validate holds details about calls to the Validate method. + Validate []struct { + // S is the s argument value. + S string + } + // Value holds details about calls to the Value method. + Value []struct { + } + } + lockCompareTo sync.RWMutex + lockName sync.RWMutex + lockString sync.RWMutex + lockValidate sync.RWMutex + lockValue sync.RWMutex +} + +// CompareTo calls CompareToFunc. +func (mock *PropertyMock) CompareTo(s string) (int, error) { + callInfo := struct { + S string + }{ + S: s, + } + mock.lockCompareTo.Lock() + mock.calls.CompareTo = append(mock.calls.CompareTo, callInfo) + mock.lockCompareTo.Unlock() + if mock.CompareToFunc == nil { + var ( + nOut int + errOut error + ) + return nOut, errOut + } + return mock.CompareToFunc(s) +} + +// CompareToCalls gets all the calls that were made to CompareTo. +// Check the length with: +// len(mockedProperty.CompareToCalls()) +func (mock *PropertyMock) CompareToCalls() []struct { + S string +} { + var calls []struct { + S string + } + mock.lockCompareTo.RLock() + calls = mock.calls.CompareTo + mock.lockCompareTo.RUnlock() + return calls +} + +// Name calls NameFunc. +func (mock *PropertyMock) Name() string { + callInfo := struct { + }{} + mock.lockName.Lock() + mock.calls.Name = append(mock.calls.Name, callInfo) + mock.lockName.Unlock() + if mock.NameFunc == nil { + var ( + sOut string + ) + return sOut + } + return mock.NameFunc() +} + +// NameCalls gets all the calls that were made to Name. +// Check the length with: +// len(mockedProperty.NameCalls()) +func (mock *PropertyMock) NameCalls() []struct { +} { + var calls []struct { + } + mock.lockName.RLock() + calls = mock.calls.Name + mock.lockName.RUnlock() + return calls +} + +// String calls StringFunc. +func (mock *PropertyMock) String() string { + callInfo := struct { + }{} + mock.lockString.Lock() + mock.calls.String = append(mock.calls.String, callInfo) + mock.lockString.Unlock() + if mock.StringFunc == nil { + var ( + sOut string + ) + return sOut + } + return mock.StringFunc() +} + +// StringCalls gets all the calls that were made to String. +// Check the length with: +// len(mockedProperty.StringCalls()) +func (mock *PropertyMock) StringCalls() []struct { +} { + var calls []struct { + } + mock.lockString.RLock() + calls = mock.calls.String + mock.lockString.RUnlock() + return calls +} + +// Validate calls ValidateFunc. +func (mock *PropertyMock) Validate(s string) error { + callInfo := struct { + S string + }{ + S: s, + } + mock.lockValidate.Lock() + mock.calls.Validate = append(mock.calls.Validate, callInfo) + mock.lockValidate.Unlock() + if mock.ValidateFunc == nil { + var ( + errOut error + ) + return errOut + } + return mock.ValidateFunc(s) +} + +// ValidateCalls gets all the calls that were made to Validate. +// Check the length with: +// len(mockedProperty.ValidateCalls()) +func (mock *PropertyMock) ValidateCalls() []struct { + S string +} { + var calls []struct { + S string + } + mock.lockValidate.RLock() + calls = mock.calls.Validate + mock.lockValidate.RUnlock() + return calls +} + +// Value calls ValueFunc. +func (mock *PropertyMock) Value() (string, error) { + callInfo := struct { + }{} + mock.lockValue.Lock() + mock.calls.Value = append(mock.calls.Value, callInfo) + mock.lockValue.Unlock() + if mock.ValueFunc == nil { + var ( + sOut string + errOut error + ) + return sOut, errOut + } + return mock.ValueFunc() +} + +// ValueCalls gets all the calls that were made to Value. +// Check the length with: +// len(mockedProperty.ValueCalls()) +func (mock *PropertyMock) ValueCalls() []struct { +} { + var calls []struct { + } + mock.lockValue.RLock() + calls = mock.calls.Value + mock.lockValue.RUnlock() + return calls +} diff --git a/internal/requirements/requirements.go b/internal/requirements/requirements.go new file mode 100644 index 00000000..bf51cc96 --- /dev/null +++ b/internal/requirements/requirements.go @@ -0,0 +1,69 @@ +/** +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +**/ + +package requirements + +import ( + "github.com/NVIDIA/nvidia-container-toolkit/internal/requirements/constraints" + "github.com/sirupsen/logrus" +) + +// Requirements represents a collection of requirements that can be compared to properties +type Requirements struct { + logger *logrus.Logger + requirements []string + properties map[string]constraints.Property +} + +// New creates a new set of requirements +func New(logger *logrus.Logger, requirements []string) *Requirements { + r := Requirements{ + logger: logger, + requirements: requirements, + properties: map[string]constraints.Property{ + // Set up the supported properties. These are overridden with actual values. + CUDA: constraints.NewVersionProperty(CUDA, ""), + ARCH: constraints.NewVersionProperty(ARCH, ""), + DRIVER: constraints.NewVersionProperty(DRIVER, ""), + BRAND: constraints.NewStringProperty(BRAND, ""), + }, + } + + return &r +} + +// AddVersionProperty adds the specified property (name, value pair) to the requirements +func (r *Requirements) AddVersionProperty(name string, value string) { + r.properties[name] = constraints.NewVersionProperty(name, value) +} + +// AddStringProperty adds the specified property (name, value pair) to the requirements +func (r *Requirements) AddStringProperty(name string, value string) { + r.properties[name] = constraints.NewStringProperty(name, value) +} + +// Assert checks the specified requirements +func (r Requirements) Assert() error { + if len(r.requirements) == 0 { + return nil + } + + c, err := constraints.New(r.logger, r.requirements, r.properties) + if err != nil { + return err + } + return c.Assert() +}