Skip to content

Commit 8f4d367

Browse files
committed
error: Pull the RFC 2119 error representation into its own package
As discussed in [1]. This makes it easier for other projects (e.g. image-tools) to use the same tooling if they want. Some components of the old validate/error.go were runtime-spec-specific (e.g. the reference template and ociErrors), so they've stayed in the validate package. I've also expanded NewError to take an explicit version (as requested in [2]). That allows us to link to the proper spec even if we're capable of validating several spec versions (e.g. 1.0 and 1.1 configurations or runtimes). [1]: #354 (comment) [2]: #354 (comment) Signed-off-by: W. Trevor King <[email protected]>
1 parent 8178fda commit 8f4d367

File tree

3 files changed

+128
-98
lines changed

3 files changed

+128
-98
lines changed

cmd/runtimetest/main.go

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ import (
2222
"github.com/urfave/cli"
2323

2424
"github.com/opencontainers/runtime-tools/cmd/runtimetest/mount"
25-
ociErr "github.com/opencontainers/runtime-tools/validate"
25+
rfc2119 "github.com/opencontainers/runtime-tools/error"
26+
"github.com/opencontainers/runtime-tools/validate"
2627
)
2728

2829
// PrGetNoNewPrivs isn't exposed in Golang so we define it ourselves copying the value from
@@ -322,7 +323,7 @@ func validateDefaultFS(spec *rspec.Spec) error {
322323

323324
mountInfos, err := mount.GetMounts()
324325
if err != nil {
325-
return ociErr.NewError(ociErr.DefaultFilesystems, err.Error())
326+
validate.NewError(validate.DefaultFilesystems, err.Error(), spec.Version)
326327
}
327328

328329
mountsMap := make(map[string]string)
@@ -332,7 +333,7 @@ func validateDefaultFS(spec *rspec.Spec) error {
332333

333334
for fs, fstype := range defaultFS {
334335
if !(mountsMap[fs] == fstype) {
335-
return ociErr.NewError(ociErr.DefaultFilesystems, fmt.Sprintf("%v SHOULD exist and expected type is %v", fs, fstype))
336+
return validate.NewError(validate.DefaultFilesystems, fmt.Sprintf("%v SHOULD exist and expected type is %v", fs, fstype), spec.Version)
336337
}
337338
}
338339

@@ -611,7 +612,7 @@ func validateMountsExist(spec *rspec.Spec) error {
611612
return nil
612613
}
613614

614-
func validate(context *cli.Context) error {
615+
func run(context *cli.Context) error {
615616
logLevelString := context.String("log-level")
616617
logLevel, err := logrus.ParseLevel(logLevelString)
617618
if err != nil {
@@ -701,17 +702,17 @@ func validate(context *cli.Context) error {
701702
t.Header(0)
702703

703704
complianceLevelString := context.String("compliance-level")
704-
complianceLevel, err := ociErr.ParseLevel(complianceLevelString)
705+
complianceLevel, err := rfc2119.ParseLevel(complianceLevelString)
705706
if err != nil {
706-
complianceLevel = ociErr.ComplianceMust
707+
complianceLevel = rfc2119.Must
707708
logrus.Warningf("%s, using 'MUST' by default.", err.Error())
708709
}
709710
var validationErrors error
710711
for _, v := range defaultValidations {
711712
err := v.test(spec)
712713
t.Ok(err == nil, v.description)
713714
if err != nil {
714-
if e, ok := err.(*ociErr.Error); ok && e.Level < complianceLevel {
715+
if e, ok := err.(*rfc2119.Error); ok && e.Level < complianceLevel {
715716
continue
716717
}
717718
validationErrors = multierror.Append(validationErrors, err)
@@ -723,7 +724,7 @@ func validate(context *cli.Context) error {
723724
err := v.test(spec)
724725
t.Ok(err == nil, v.description)
725726
if err != nil {
726-
if e, ok := err.(*ociErr.Error); ok && e.Level < complianceLevel {
727+
if e, ok := err.(*rfc2119.Error); ok && e.Level < complianceLevel {
727728
continue
728729
}
729730
validationErrors = multierror.Append(validationErrors, err)
@@ -759,7 +760,7 @@ func main() {
759760
},
760761
}
761762

762-
app.Action = validate
763+
app.Action = run
763764
if err := app.Run(os.Args); err != nil {
764765
logrus.Fatal(err)
765766
}

error/error.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Package error implements generic tooling for tracking RFC 2119
2+
// violations and linking back to the appropriate specification section.
3+
package error
4+
5+
import (
6+
"fmt"
7+
"strings"
8+
)
9+
10+
// Level represents the OCI compliance levels
11+
type Level int
12+
13+
const (
14+
// MAY-level
15+
16+
// May represents 'MAY' in RFC 2119.
17+
May Level = iota
18+
// Optional represents 'OPTIONAL' in RFC 2119.
19+
Optional
20+
21+
// SHOULD-level
22+
23+
// Should represents 'SHOULD' in RFC 2119.
24+
Should
25+
// ShouldNot represents 'SHOULD NOT' in RFC 2119.
26+
ShouldNot
27+
// Recommended represents 'RECOMMENDED' in RFC 2119.
28+
Recommended
29+
// NotRecommended represents 'NOT RECOMMENDED' in RFC 2119.
30+
NotRecommended
31+
32+
// MUST-level
33+
34+
// Must represents 'MUST' in RFC 2119
35+
Must
36+
// MustNot represents 'MUST NOT' in RFC 2119.
37+
MustNot
38+
// Shall represents 'SHALL' in RFC 2119.
39+
Shall
40+
// ShallNot represents 'SHALL NOT' in RFC 2119.
41+
ShallNot
42+
// Required represents 'REQUIRED' in RFC 2119.
43+
Required
44+
)
45+
46+
// Error represents an error with compliance level and OCI reference.
47+
type Error struct {
48+
Level Level
49+
Reference string
50+
Err error
51+
}
52+
53+
// ParseLevel takes a string level and returns the OCI compliance level constant.
54+
func ParseLevel(level string) (Level, error) {
55+
switch strings.ToUpper(level) {
56+
case "MAY":
57+
fallthrough
58+
case "OPTIONAL":
59+
return May, nil
60+
case "SHOULD":
61+
fallthrough
62+
case "SHOULDNOT":
63+
fallthrough
64+
case "RECOMMENDED":
65+
fallthrough
66+
case "NOTRECOMMENDED":
67+
return Should, nil
68+
case "MUST":
69+
fallthrough
70+
case "MUSTNOT":
71+
fallthrough
72+
case "SHALL":
73+
fallthrough
74+
case "SHALLNOT":
75+
fallthrough
76+
case "REQUIRED":
77+
return Must, nil
78+
}
79+
80+
var l Level
81+
return l, fmt.Errorf("%q is not a valid compliance level", level)
82+
}
83+
84+
// Error returns the error message with OCI reference
85+
func (err *Error) Error() string {
86+
return fmt.Sprintf("%s\nRefer to: %s", err.Err.Error(), err.Reference)
87+
}

validate/error.go

Lines changed: 31 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -3,108 +3,50 @@ package validate
33
import (
44
"errors"
55
"fmt"
6-
"strings"
76

8-
rspec "github.com/opencontainers/runtime-spec/specs-go"
7+
rfc2119 "github.com/opencontainers/runtime-tools/error"
98
)
109

11-
// ComplianceLevel represents the OCI compliance levels
12-
type ComplianceLevel int
10+
const referenceTemplate = "https://github.com/opencontainers/runtime-spec/blob/v%s/%s"
1311

14-
const (
15-
// MAY-level
16-
17-
// ComplianceMay represents 'MAY' in RFC2119
18-
ComplianceMay ComplianceLevel = iota
19-
// ComplianceOptional represents 'OPTIONAL' in RFC2119
20-
ComplianceOptional
21-
22-
// SHOULD-level
23-
24-
// ComplianceShould represents 'SHOULD' in RFC2119
25-
ComplianceShould
26-
// ComplianceShouldNot represents 'SHOULD NOT' in RFC2119
27-
ComplianceShouldNot
28-
// ComplianceRecommended represents 'RECOMMENDED' in RFC2119
29-
ComplianceRecommended
30-
// ComplianceNotRecommended represents 'NOT RECOMMENDED' in RFC2119
31-
ComplianceNotRecommended
32-
33-
// MUST-level
34-
35-
// ComplianceMust represents 'MUST' in RFC2119
36-
ComplianceMust
37-
// ComplianceMustNot represents 'MUST NOT' in RFC2119
38-
ComplianceMustNot
39-
// ComplianceShall represents 'SHALL' in RFC2119
40-
ComplianceShall
41-
// ComplianceShallNot represents 'SHALL NOT' in RFC2119
42-
ComplianceShallNot
43-
// ComplianceRequired represents 'REQUIRED' in RFC2119
44-
ComplianceRequired
45-
)
46-
47-
// ErrorCode represents the compliance content
12+
// ErrorCode represents the compliance content.
4813
type ErrorCode int
4914

5015
const (
51-
// DefaultFilesystems represents the error code of default filesystems test
16+
// DefaultFilesystems represents the error code of default filesystems test.
5217
DefaultFilesystems ErrorCode = iota
5318
)
5419

55-
// Error represents an error with compliance level and OCI reference
56-
type Error struct {
57-
Level ComplianceLevel
58-
Reference string
59-
Err error
20+
type errorTemplate struct {
21+
Level rfc2119.Level
22+
Reference func(version string) (reference string, err error)
6023
}
6124

62-
const referencePrefix = "https://github.com/opencontainers/runtime-spec/blob"
63-
64-
var ociErrors = map[ErrorCode]Error{
65-
DefaultFilesystems: Error{Level: ComplianceShould, Reference: "config-linux.md#default-filesystems"},
25+
var ociErrors = map[ErrorCode]errorTemplate{
26+
DefaultFilesystems: errorTemplate{
27+
Level: rfc2119.Should,
28+
Reference: func(version string) (reference string, err error) {
29+
return fmt.Sprintf(referenceTemplate, version, "config-linux.md#default-filesystems"), nil
30+
},
31+
},
6632
}
6733

68-
// ParseLevel takes a string level and returns the OCI compliance level constant
69-
func ParseLevel(level string) (ComplianceLevel, error) {
70-
switch strings.ToUpper(level) {
71-
case "MAY":
72-
fallthrough
73-
case "OPTIONAL":
74-
return ComplianceMay, nil
75-
case "SHOULD":
76-
fallthrough
77-
case "SHOULDNOT":
78-
fallthrough
79-
case "RECOMMENDED":
80-
fallthrough
81-
case "NOTRECOMMENDED":
82-
return ComplianceShould, nil
83-
case "MUST":
84-
fallthrough
85-
case "MUSTNOT":
86-
fallthrough
87-
case "SHALL":
88-
fallthrough
89-
case "SHALLNOT":
90-
fallthrough
91-
case "REQUIRED":
92-
return ComplianceMust, nil
34+
// NewError creates an Error referencing a spec violation. The error
35+
// can be cast to a *runtime-tools.error.Error for extracting
36+
// structured information about the level of the violation and a
37+
// reference to the violated spec condition.
38+
//
39+
// A version string (for the version of the spec that was violated)
40+
// must be set to get a working URL.
41+
func NewError(code ErrorCode, msg string, version string) (err error) {
42+
template := ociErrors[code]
43+
reference, err := template.Reference(version)
44+
if err != nil {
45+
return err
46+
}
47+
return &rfc2119.Error{
48+
Level: template.Level,
49+
Reference: reference,
50+
Err: errors.New(msg),
9351
}
94-
95-
var l ComplianceLevel
96-
return l, fmt.Errorf("%q is not a valid compliance level", level)
97-
}
98-
99-
// NewError creates an Error by ErrorCode and message
100-
func NewError(code ErrorCode, msg string) error {
101-
err := ociErrors[code]
102-
err.Err = errors.New(msg)
103-
104-
return &err
105-
}
106-
107-
// Error returns the error message with OCI reference
108-
func (oci *Error) Error() string {
109-
return fmt.Sprintf("%s\nRefer to: %s/v%s/%s", oci.Err.Error(), referencePrefix, rspec.Version, oci.Reference)
11052
}

0 commit comments

Comments
 (0)