Skip to content

Commit

Permalink
error: Pull the RFC 2119 error representation into its own package
Browse files Browse the repository at this point in the history
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]: opencontainers#354 (comment)
[2]: opencontainers#354 (comment)

Signed-off-by: W. Trevor King <wking@tremily.us>
  • Loading branch information
wking committed Jul 28, 2017
1 parent 8178fda commit 8f4d367
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 98 deletions.
19 changes: 10 additions & 9 deletions cmd/runtimetest/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -701,17 +702,17 @@ 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
for _, v := range defaultValidations {
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)
Expand All @@ -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)
Expand Down Expand Up @@ -759,7 +760,7 @@ func main() {
},
}

app.Action = validate
app.Action = run
if err := app.Run(os.Args); err != nil {
logrus.Fatal(err)
}
Expand Down
87 changes: 87 additions & 0 deletions error/error.go
Original file line number Diff line number Diff line change
@@ -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)
}
120 changes: 31 additions & 89 deletions validate/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

0 comments on commit 8f4d367

Please sign in to comment.