diff --git a/cmd/runtimetest/main.go b/cmd/runtimetest/main.go index 8a22a74f9..1c874aca1 100644 --- a/cmd/runtimetest/main.go +++ b/cmd/runtimetest/main.go @@ -22,7 +22,8 @@ import ( "github.com/urfave/cli" "github.com/opencontainers/runtime-tools/cmd/runtimetest/mount" - ociErr "github.com/opencontainers/runtime-tools/validate" + rfc2119 "github.com/opencontainers/runtime-tools/error" + "github.com/opencontainers/runtime-tools/validate" ) // 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 { mountInfos, err := mount.GetMounts() if err != nil { - return ociErr.NewError(ociErr.DefaultFilesystems, err.Error()) + validate.NewError(validate.DefaultFilesystems, err.Error(), spec.Version) } mountsMap := make(map[string]string) @@ -332,7 +333,7 @@ func validateDefaultFS(spec *rspec.Spec) error { for fs, fstype := range defaultFS { if !(mountsMap[fs] == fstype) { - return ociErr.NewError(ociErr.DefaultFilesystems, fmt.Sprintf("%v SHOULD exist and expected type is %v", fs, fstype)) + return validate.NewError(validate.DefaultFilesystems, fmt.Sprintf("%v SHOULD exist and expected type is %v", fs, fstype), spec.Version) } } @@ -611,7 +612,7 @@ func validateMountsExist(spec *rspec.Spec) error { return nil } -func validate(context *cli.Context) error { +func run(context *cli.Context) error { logLevelString := context.String("log-level") logLevel, err := logrus.ParseLevel(logLevelString) if err != nil { @@ -701,9 +702,9 @@ func validate(context *cli.Context) error { t.Header(0) complianceLevelString := context.String("compliance-level") - complianceLevel, err := ociErr.ParseLevel(complianceLevelString) + complianceLevel, err := rfc2119.ParseLevel(complianceLevelString) if err != nil { - complianceLevel = ociErr.ComplianceMust + complianceLevel = rfc2119.Must logrus.Warningf("%s, using 'MUST' by default.", err.Error()) } var validationErrors error @@ -711,7 +712,7 @@ func validate(context *cli.Context) error { err := v.test(spec) t.Ok(err == nil, v.description) if err != nil { - if e, ok := err.(*ociErr.Error); ok && e.Level < complianceLevel { + if e, ok := err.(*rfc2119.Error); ok && e.Level < complianceLevel { continue } validationErrors = multierror.Append(validationErrors, err) @@ -723,7 +724,7 @@ func validate(context *cli.Context) error { err := v.test(spec) t.Ok(err == nil, v.description) if err != nil { - if e, ok := err.(*ociErr.Error); ok && e.Level < complianceLevel { + if e, ok := err.(*rfc2119.Error); ok && e.Level < complianceLevel { continue } validationErrors = multierror.Append(validationErrors, err) @@ -759,7 +760,7 @@ func main() { }, } - app.Action = validate + app.Action = run if err := app.Run(os.Args); err != nil { logrus.Fatal(err) } diff --git a/error/error.go b/error/error.go new file mode 100644 index 000000000..c2345c1d8 --- /dev/null +++ b/error/error.go @@ -0,0 +1,87 @@ +// Package error implements generic tooling for tracking RFC 2119 +// violations and linking back to the appropriate specification section. +package error + +import ( + "fmt" + "strings" +) + +// Level represents the OCI compliance levels +type Level int + +const ( + // MAY-level + + // May represents 'MAY' in RFC 2119. + May Level = iota + // Optional represents 'OPTIONAL' in RFC 2119. + Optional + + // SHOULD-level + + // Should represents 'SHOULD' in RFC 2119. + Should + // ShouldNot represents 'SHOULD NOT' in RFC 2119. + ShouldNot + // Recommended represents 'RECOMMENDED' in RFC 2119. + Recommended + // NotRecommended represents 'NOT RECOMMENDED' in RFC 2119. + NotRecommended + + // MUST-level + + // Must represents 'MUST' in RFC 2119 + Must + // MustNot represents 'MUST NOT' in RFC 2119. + MustNot + // Shall represents 'SHALL' in RFC 2119. + Shall + // ShallNot represents 'SHALL NOT' in RFC 2119. + ShallNot + // Required represents 'REQUIRED' in RFC 2119. + Required +) + +// Error represents an error with compliance level and OCI reference. +type Error struct { + Level Level + Reference string + Err error +} + +// ParseLevel takes a string level and returns the OCI compliance level constant. +func ParseLevel(level string) (Level, error) { + switch strings.ToUpper(level) { + case "MAY": + fallthrough + case "OPTIONAL": + return May, nil + case "SHOULD": + fallthrough + case "SHOULDNOT": + fallthrough + case "RECOMMENDED": + fallthrough + case "NOTRECOMMENDED": + return Should, nil + case "MUST": + fallthrough + case "MUSTNOT": + fallthrough + case "SHALL": + fallthrough + case "SHALLNOT": + fallthrough + case "REQUIRED": + return Must, nil + } + + var l Level + return l, fmt.Errorf("%q is not a valid compliance level", level) +} + +// Error returns the error message with OCI reference +func (err *Error) Error() string { + return fmt.Sprintf("%s\nRefer to: %s", err.Err.Error(), err.Reference) +} diff --git a/validate/error.go b/validate/error.go index 754d032b5..0311c2304 100644 --- a/validate/error.go +++ b/validate/error.go @@ -3,108 +3,50 @@ package validate import ( "errors" "fmt" - "strings" - rspec "github.com/opencontainers/runtime-spec/specs-go" + rfc2119 "github.com/opencontainers/runtime-tools/error" ) -// ComplianceLevel represents the OCI compliance levels -type ComplianceLevel int +const referenceTemplate = "https://github.com/opencontainers/runtime-spec/blob/v%s/%s" -const ( - // MAY-level - - // ComplianceMay represents 'MAY' in RFC2119 - ComplianceMay ComplianceLevel = iota - // ComplianceOptional represents 'OPTIONAL' in RFC2119 - ComplianceOptional - - // SHOULD-level - - // ComplianceShould represents 'SHOULD' in RFC2119 - ComplianceShould - // ComplianceShouldNot represents 'SHOULD NOT' in RFC2119 - ComplianceShouldNot - // ComplianceRecommended represents 'RECOMMENDED' in RFC2119 - ComplianceRecommended - // ComplianceNotRecommended represents 'NOT RECOMMENDED' in RFC2119 - ComplianceNotRecommended - - // MUST-level - - // ComplianceMust represents 'MUST' in RFC2119 - ComplianceMust - // ComplianceMustNot represents 'MUST NOT' in RFC2119 - ComplianceMustNot - // ComplianceShall represents 'SHALL' in RFC2119 - ComplianceShall - // ComplianceShallNot represents 'SHALL NOT' in RFC2119 - ComplianceShallNot - // ComplianceRequired represents 'REQUIRED' in RFC2119 - ComplianceRequired -) - -// ErrorCode represents the compliance content +// ErrorCode represents the compliance content. type ErrorCode int const ( - // DefaultFilesystems represents the error code of default filesystems test + // DefaultFilesystems represents the error code of default filesystems test. DefaultFilesystems ErrorCode = iota ) -// Error represents an error with compliance level and OCI reference -type Error struct { - Level ComplianceLevel - Reference string - Err error +type errorTemplate struct { + Level rfc2119.Level + Reference func(version string) (reference string, err error) } -const referencePrefix = "https://github.com/opencontainers/runtime-spec/blob" - -var ociErrors = map[ErrorCode]Error{ - DefaultFilesystems: Error{Level: ComplianceShould, Reference: "config-linux.md#default-filesystems"}, +var ociErrors = map[ErrorCode]errorTemplate{ + DefaultFilesystems: errorTemplate{ + Level: rfc2119.Should, + Reference: func(version string) (reference string, err error) { + return fmt.Sprintf(referenceTemplate, version, "config-linux.md#default-filesystems"), nil + }, + }, } -// ParseLevel takes a string level and returns the OCI compliance level constant -func ParseLevel(level string) (ComplianceLevel, error) { - switch strings.ToUpper(level) { - case "MAY": - fallthrough - case "OPTIONAL": - return ComplianceMay, nil - case "SHOULD": - fallthrough - case "SHOULDNOT": - fallthrough - case "RECOMMENDED": - fallthrough - case "NOTRECOMMENDED": - return ComplianceShould, nil - case "MUST": - fallthrough - case "MUSTNOT": - fallthrough - case "SHALL": - fallthrough - case "SHALLNOT": - fallthrough - case "REQUIRED": - return ComplianceMust, nil +// NewError creates an Error referencing a spec violation. The error +// can be cast to a *runtime-tools.error.Error for extracting +// structured information about the level of the violation and a +// reference to the violated spec condition. +// +// A version string (for the version of the spec that was violated) +// must be set to get a working URL. +func NewError(code ErrorCode, msg string, version string) (err error) { + template := ociErrors[code] + reference, err := template.Reference(version) + if err != nil { + return err + } + return &rfc2119.Error{ + Level: template.Level, + Reference: reference, + Err: errors.New(msg), } - - var l ComplianceLevel - return l, fmt.Errorf("%q is not a valid compliance level", level) -} - -// NewError creates an Error by ErrorCode and message -func NewError(code ErrorCode, msg string) error { - err := ociErrors[code] - err.Err = errors.New(msg) - - return &err -} - -// Error returns the error message with OCI reference -func (oci *Error) Error() string { - return fmt.Sprintf("%s\nRefer to: %s/v%s/%s", oci.Err.Error(), referencePrefix, rspec.Version, oci.Reference) }