Skip to content

Commit 8d515e1

Browse files
committed
specerror: Pull runtime-spec-specific error handling into its own package
With 8f4d367 (error: Pull the RFC 2119 error representation into its own package, 2017-07-28, opencontainers#437), I'd created a package that was completely independent of runtime-spec. As I pointed out in that commit message, this made it possible for image-tools and other projects to reuse the generic RFC 2119 handling (which they care about) without involving the runtime-spec-specific error enumeration and such (which they don't care about). In 2520212 (add stretchr/testify/assert pkgs; use rfc code in bundle validation, 2017-08-29, opencontainers#451), some runtime-spec-specific functionality leaked into the error package. I'd recommended keeping configuration and runtime requirements separate [1], because you're unlikely to be testing both of those at once. But Liang wanted them collected [2,3]. And the NewError and FindError utilities would be the same regardless of target, so that's a good argument for keeping them together. This commit moves the runtime-spec-specific functionality into a new package where both config and runtime validators can share it, but where it won't pollute the generic RFC 2119 error package. I've also changed NewError to take an error argument instead of a string message, because creating an error from a string is easy (e.g. with fmt.Errorf(...)), and using an error allows you to preserve any additional structured information from a system error (e.g. as returned by GetMounts). [1]: opencontainers#451 (comment) [2]: opencontainers#451 (comment) [3]: opencontainers#451 (comment) Signed-off-by: W. Trevor King <[email protected]>
1 parent 12b47b9 commit 8d515e1

6 files changed

Lines changed: 91 additions & 72 deletions

File tree

cmd/runtimetest/main.go

Lines changed: 9 additions & 8 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-
rerr "github.com/opencontainers/runtime-tools/error"
25+
rfc2119 "github.com/opencontainers/runtime-tools/error"
26+
"github.com/opencontainers/runtime-tools/specerror"
2627
)
2728

2829
// PrGetNoNewPrivs isn't exposed in Golang so we define it ourselves copying the value from
@@ -313,7 +314,7 @@ func validateRootFS(spec *rspec.Spec) error {
313314
if spec.Root.Readonly {
314315
err := testWriteAccess("/")
315316
if err == nil {
316-
return rerr.NewError(rerr.ReadonlyFilesystem, "Rootfs should be readonly", rspec.Version)
317+
return specerror.NewError(specerror.ReadonlyFilesystem, fmt.Errorf("rootfs must be readonly"), rspec.Version)
317318
}
318319
}
319320

@@ -325,7 +326,7 @@ func validateDefaultFS(spec *rspec.Spec) error {
325326

326327
mountInfos, err := mount.GetMounts()
327328
if err != nil {
328-
rerr.NewError(rerr.DefaultFilesystems, err.Error(), spec.Version)
329+
specerror.NewError(specerror.DefaultFilesystems, err, spec.Version)
329330
}
330331

331332
mountsMap := make(map[string]string)
@@ -335,7 +336,7 @@ func validateDefaultFS(spec *rspec.Spec) error {
335336

336337
for fs, fstype := range defaultFS {
337338
if !(mountsMap[fs] == fstype) {
338-
return rerr.NewError(rerr.DefaultFilesystems, fmt.Sprintf("%v SHOULD exist and expected type is %v", fs, fstype), rspec.Version)
339+
return specerror.NewError(specerror.DefaultFilesystems, fmt.Errorf("%v SHOULD exist and expected type is %v", fs, fstype), rspec.Version)
339340
}
340341
}
341342

@@ -779,17 +780,17 @@ func run(context *cli.Context) error {
779780
t.Header(0)
780781

781782
complianceLevelString := context.String("compliance-level")
782-
complianceLevel, err := rerr.ParseLevel(complianceLevelString)
783+
complianceLevel, err := rfc2119.ParseLevel(complianceLevelString)
783784
if err != nil {
784-
complianceLevel = rerr.Must
785+
complianceLevel = rfc2119.Must
785786
logrus.Warningf("%s, using 'MUST' by default.", err.Error())
786787
}
787788
var validationErrors error
788789
for _, v := range defaultValidations {
789790
err := v.test(spec)
790791
t.Ok(err == nil, v.description)
791792
if err != nil {
792-
if e, ok := err.(*rerr.Error); ok && e.Level < complianceLevel {
793+
if e, ok := err.(*specerror.Error); ok && e.Err.Level < complianceLevel {
793794
continue
794795
}
795796
validationErrors = multierror.Append(validationErrors, err)
@@ -801,7 +802,7 @@ func run(context *cli.Context) error {
801802
err := v.test(spec)
802803
t.Ok(err == nil, v.description)
803804
if err != nil {
804-
if e, ok := err.(*rerr.Error); ok && e.Level < complianceLevel {
805+
if e, ok := err.(*specerror.Error); ok && e.Err.Level < complianceLevel {
805806
continue
806807
}
807808
validationErrors = multierror.Append(validationErrors, err)

error/rfc2199.go renamed to error/error.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ type Error struct {
4848
Level Level
4949
Reference string
5050
Err error
51-
ErrCode int
5251
}
5352

5453
// ParseLevel takes a string level and returns the OCI compliance level constant.
Lines changed: 50 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
1-
package error
1+
// Package specerror implements runtime-spec-specific tooling for
2+
// tracking RFC 2119 violations.
3+
package specerror
24

35
import (
4-
"errors"
56
"fmt"
67

78
"github.com/hashicorp/go-multierror"
9+
rfc2119 "github.com/opencontainers/runtime-tools/error"
810
)
911

1012
const referenceTemplate = "https://github.com/opencontainers/runtime-spec/blob/v%s/%s"
1113

12-
// SpecErrorCode represents the compliance content.
14+
// SpecErrorCode represents the spec violation, enumerating both
15+
// configuration violations and runtime violations.
1316
type SpecErrorCode int
1417

1518
const (
@@ -53,10 +56,19 @@ const (
5356
)
5457

5558
type errorTemplate struct {
56-
Level Level
59+
Level rfc2119.Level
5760
Reference func(version string) (reference string, err error)
5861
}
5962

63+
// Error represents a runtime-spec violation.
64+
type Error struct {
65+
// Err holds the RFC 2119 violation.
66+
Err rfc2119.Error
67+
68+
// ErrCode is a matchable holds a SpecErrorCode
69+
ErrCode SpecErrorCode
70+
}
71+
6072
var (
6173
containerFormatRef = func(version string) (reference string, err error) {
6274
return fmt.Sprintf(referenceTemplate, version, "bundle.md#container-format"), nil
@@ -78,56 +90,63 @@ var (
7890
var ociErrors = map[SpecErrorCode]errorTemplate{
7991
// Bundle.md
8092
// Container Format
81-
ConfigFileExistence: {Level: Must, Reference: containerFormatRef},
82-
ArtifactsInSingleDir: {Level: Must, Reference: containerFormatRef},
93+
ConfigFileExistence: {Level: rfc2119.Must, Reference: containerFormatRef},
94+
ArtifactsInSingleDir: {Level: rfc2119.Must, Reference: containerFormatRef},
8395

8496
// Config.md
8597
// Specification Version
86-
SpecVersion: {Level: Must, Reference: specVersionRef},
98+
SpecVersion: {Level: rfc2119.Must, Reference: specVersionRef},
8799
// Root
88-
RootOnNonHyperV: {Level: Required, Reference: rootRef},
89-
RootOnHyperV: {Level: Must, Reference: rootRef},
100+
RootOnNonHyperV: {Level: rfc2119.Required, Reference: rootRef},
101+
RootOnHyperV: {Level: rfc2119.Must, Reference: rootRef},
90102
// TODO: add tests for 'PathFormatOnWindows'
91-
PathFormatOnWindows: {Level: Must, Reference: rootRef},
92-
PathName: {Level: Should, Reference: rootRef},
93-
PathExistence: {Level: Must, Reference: rootRef},
94-
ReadonlyFilesystem: {Level: Must, Reference: rootRef},
95-
ReadonlyOnWindows: {Level: Must, Reference: rootRef},
103+
PathFormatOnWindows: {Level: rfc2119.Must, Reference: rootRef},
104+
PathName: {Level: rfc2119.Should, Reference: rootRef},
105+
PathExistence: {Level: rfc2119.Must, Reference: rootRef},
106+
ReadonlyFilesystem: {Level: rfc2119.Must, Reference: rootRef},
107+
ReadonlyOnWindows: {Level: rfc2119.Must, Reference: rootRef},
96108

97109
// Config-Linux.md
98110
// Default Filesystems
99-
DefaultFilesystems: {Level: Should, Reference: defaultFSRef},
111+
DefaultFilesystems: {Level: rfc2119.Should, Reference: defaultFSRef},
100112

101113
// Runtime.md
102114
// Create
103-
CreateWithID: {Level: Must, Reference: runtimeCreateRef},
104-
CreateWithUniqueID: {Level: Must, Reference: runtimeCreateRef},
105-
CreateNewContainer: {Level: Must, Reference: runtimeCreateRef},
115+
CreateWithID: {Level: rfc2119.Must, Reference: runtimeCreateRef},
116+
CreateWithUniqueID: {Level: rfc2119.Must, Reference: runtimeCreateRef},
117+
CreateNewContainer: {Level: rfc2119.Must, Reference: runtimeCreateRef},
118+
}
119+
120+
// Error returns the error message with specification reference.
121+
func (err *Error) Error() string {
122+
return err.Err.Error()
106123
}
107124

108125
// NewError creates an Error referencing a spec violation. The error
109-
// can be cast to a *runtime-tools.error.Error for extracting
110-
// structured information about the level of the violation and a
111-
// reference to the violated spec condition.
126+
// can be cast to an *Error for extracting structured information
127+
// about the level of the violation and a reference to the violated
128+
// spec condition.
112129
//
113130
// A version string (for the version of the spec that was violated)
114131
// must be set to get a working URL.
115-
func NewError(code SpecErrorCode, msg string, version string) (err error) {
132+
func NewError(code SpecErrorCode, err error, version string) error {
116133
template := ociErrors[code]
117-
reference, err := template.Reference(version)
118-
if err != nil {
119-
return err
134+
reference, err2 := template.Reference(version)
135+
if err2 != nil {
136+
return err2
120137
}
121138
return &Error{
122-
Level: template.Level,
123-
Reference: reference,
124-
Err: errors.New(msg),
125-
ErrCode: int(code),
139+
Err: rfc2119.Error{
140+
Level: template.Level,
141+
Reference: reference,
142+
Err: err,
143+
},
144+
ErrCode: code,
126145
}
127146
}
128147

129148
// FindError finds an error from a source error (multiple error) and
130-
// returns the error code if founded.
149+
// returns the error code if found.
131150
// If the source error is nil or empty, return NonError.
132151
// If the source error is not a multiple error, return NonRFCError.
133152
func FindError(err error, code SpecErrorCode) SpecErrorCode {
@@ -141,7 +160,7 @@ func FindError(err error, code SpecErrorCode) SpecErrorCode {
141160
}
142161
for _, e := range merr.Errors {
143162
if rfcErr, ok := e.(*Error); ok {
144-
if rfcErr.ErrCode == int(code) {
163+
if rfcErr.ErrCode == code {
145164
return code
146165
}
147166
}

validate/validate.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import (
2323
"github.com/sirupsen/logrus"
2424
"github.com/syndtr/gocapability/capability"
2525

26-
rerr "github.com/opencontainers/runtime-tools/error"
26+
"github.com/opencontainers/runtime-tools/specerror"
2727
)
2828

2929
const specConfig = "config.json"
@@ -86,7 +86,7 @@ func NewValidatorFromPath(bundlePath string, hostSpecific bool, platform string)
8686
configPath := filepath.Join(bundlePath, specConfig)
8787
content, err := ioutil.ReadFile(configPath)
8888
if err != nil {
89-
return Validator{}, rerr.NewError(rerr.ConfigFileExistence, err.Error(), rspec.Version)
89+
return Validator{}, specerror.NewError(specerror.ConfigFileExistence, err, rspec.Version)
9090
}
9191
if !utf8.Valid(content) {
9292
return Validator{}, fmt.Errorf("%q is not encoded in UTF-8", configPath)
@@ -120,13 +120,13 @@ func (v *Validator) CheckRoot() (errs error) {
120120
if v.platform == "windows" && v.spec.Windows != nil && v.spec.Windows.HyperV != nil {
121121
if v.spec.Root != nil {
122122
errs = multierror.Append(errs,
123-
rerr.NewError(rerr.RootOnHyperV, "for Hyper-V containers, Root must not be set", rspec.Version))
123+
specerror.NewError(specerror.RootOnHyperV, fmt.Errorf("for Hyper-V containers, Root must not be set"), rspec.Version))
124124
return
125125
}
126126
return
127127
} else if v.spec.Root == nil {
128128
errs = multierror.Append(errs,
129-
rerr.NewError(rerr.RootOnNonHyperV, "for non-Hyper-V containers, Root must be set", rspec.Version))
129+
specerror.NewError(specerror.RootOnNonHyperV, fmt.Errorf("for non-Hyper-V containers, Root must be set"), rspec.Version))
130130
return
131131
}
132132

@@ -138,7 +138,7 @@ func (v *Validator) CheckRoot() (errs error) {
138138

139139
if filepath.Base(v.spec.Root.Path) != "rootfs" {
140140
errs = multierror.Append(errs,
141-
rerr.NewError(rerr.PathName, "Path name should be the conventional 'rootfs'", rspec.Version))
141+
specerror.NewError(specerror.PathName, fmt.Errorf("path name should be the conventional 'rootfs'"), rspec.Version))
142142
}
143143

144144
var rootfsPath string
@@ -158,22 +158,22 @@ func (v *Validator) CheckRoot() (errs error) {
158158

159159
if fi, err := os.Stat(rootfsPath); err != nil {
160160
errs = multierror.Append(errs,
161-
rerr.NewError(rerr.PathExistence, fmt.Sprintf("Cannot find the root path %q", rootfsPath), rspec.Version))
161+
specerror.NewError(specerror.PathExistence, fmt.Errorf("cannot find the root path %q", rootfsPath), rspec.Version))
162162
} else if !fi.IsDir() {
163163
errs = multierror.Append(errs,
164-
rerr.NewError(rerr.PathExistence, fmt.Sprintf("The root path %q is not a directory", rootfsPath), rspec.Version))
164+
specerror.NewError(specerror.PathExistence, fmt.Errorf("root.path %q is not a directory", rootfsPath), rspec.Version))
165165
}
166166

167167
rootParent := filepath.Dir(absRootPath)
168168
if absRootPath == string(filepath.Separator) || rootParent != absBundlePath {
169169
errs = multierror.Append(errs,
170-
rerr.NewError(rerr.ArtifactsInSingleDir, fmt.Sprintf("root.path is %q, but it MUST be a child of %q", v.spec.Root.Path, absBundlePath), rspec.Version))
170+
specerror.NewError(specerror.ArtifactsInSingleDir, fmt.Errorf("root.path is %q, but it MUST be a child of %q", v.spec.Root.Path, absBundlePath), rspec.Version))
171171
}
172172

173173
if v.platform == "windows" {
174174
if v.spec.Root.Readonly {
175175
errs = multierror.Append(errs,
176-
rerr.NewError(rerr.ReadonlyOnWindows, "root.readonly field MUST be omitted or false when target platform is windows", rspec.Version))
176+
specerror.NewError(specerror.ReadonlyOnWindows, fmt.Errorf("root.readonly field MUST be omitted or false when target platform is windows"), rspec.Version))
177177
}
178178
}
179179

@@ -188,7 +188,7 @@ func (v *Validator) CheckSemVer() (errs error) {
188188
_, err := semver.Parse(version)
189189
if err != nil {
190190
errs = multierror.Append(errs,
191-
rerr.NewError(rerr.SpecVersion, fmt.Sprintf("%q is not valid SemVer: %s", version, err.Error()), rspec.Version))
191+
specerror.NewError(specerror.SpecVersion, fmt.Errorf("%q is not valid SemVer: %s", version, err.Error()), rspec.Version))
192192
}
193193
if version != rspec.Version {
194194
errs = multierror.Append(errs, fmt.Errorf("validate currently only handles version %s, but the supplied configuration targets %s", rspec.Version, version))

validate/validate_test.go

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
rspec "github.com/opencontainers/runtime-spec/specs-go"
1212
"github.com/stretchr/testify/assert"
1313

14-
rerr "github.com/opencontainers/runtime-tools/error"
14+
"github.com/opencontainers/runtime-tools/specerror"
1515
)
1616

1717
func TestNewValidator(t *testing.T) {
@@ -53,40 +53,40 @@ func TestCheckRoot(t *testing.T) {
5353
cases := []struct {
5454
val rspec.Spec
5555
platform string
56-
expected rerr.SpecErrorCode
56+
expected specerror.SpecErrorCode
5757
}{
58-
{rspec.Spec{Windows: &rspec.Windows{HyperV: &rspec.WindowsHyperV{}}, Root: &rspec.Root{}}, "windows", rerr.RootOnHyperV},
59-
{rspec.Spec{Windows: &rspec.Windows{HyperV: &rspec.WindowsHyperV{}}, Root: nil}, "windows", rerr.NonError},
60-
{rspec.Spec{Root: nil}, "linux", rerr.RootOnNonHyperV},
61-
{rspec.Spec{Root: &rspec.Root{Path: "maverick-rootfs"}}, "linux", rerr.PathName},
62-
{rspec.Spec{Root: &rspec.Root{Path: "rootfs"}}, "linux", rerr.NonError},
63-
{rspec.Spec{Root: &rspec.Root{Path: filepath.Join(tmpBundle, rootfsNonExists)}}, "linux", rerr.PathExistence},
64-
{rspec.Spec{Root: &rspec.Root{Path: filepath.Join(tmpBundle, rootfsNonDir)}}, "linux", rerr.PathExistence},
65-
{rspec.Spec{Root: &rspec.Root{Path: filepath.Join(tmpBundle, "rootfs")}}, "linux", rerr.NonError},
66-
{rspec.Spec{Root: &rspec.Root{Path: "rootfs/rootfs"}}, "linux", rerr.ArtifactsInSingleDir},
67-
{rspec.Spec{Root: &rspec.Root{Readonly: true}}, "windows", rerr.ReadonlyOnWindows},
58+
{rspec.Spec{Windows: &rspec.Windows{HyperV: &rspec.WindowsHyperV{}}, Root: &rspec.Root{}}, "windows", specerror.RootOnHyperV},
59+
{rspec.Spec{Windows: &rspec.Windows{HyperV: &rspec.WindowsHyperV{}}, Root: nil}, "windows", specerror.NonError},
60+
{rspec.Spec{Root: nil}, "linux", specerror.RootOnNonHyperV},
61+
{rspec.Spec{Root: &rspec.Root{Path: "maverick-rootfs"}}, "linux", specerror.PathName},
62+
{rspec.Spec{Root: &rspec.Root{Path: "rootfs"}}, "linux", specerror.NonError},
63+
{rspec.Spec{Root: &rspec.Root{Path: filepath.Join(tmpBundle, rootfsNonExists)}}, "linux", specerror.PathExistence},
64+
{rspec.Spec{Root: &rspec.Root{Path: filepath.Join(tmpBundle, rootfsNonDir)}}, "linux", specerror.PathExistence},
65+
{rspec.Spec{Root: &rspec.Root{Path: filepath.Join(tmpBundle, "rootfs")}}, "linux", specerror.NonError},
66+
{rspec.Spec{Root: &rspec.Root{Path: "rootfs/rootfs"}}, "linux", specerror.ArtifactsInSingleDir},
67+
{rspec.Spec{Root: &rspec.Root{Readonly: true}}, "windows", specerror.ReadonlyOnWindows},
6868
}
6969
for _, c := range cases {
7070
v := NewValidator(&c.val, tmpBundle, false, c.platform)
7171
err := v.CheckRoot()
72-
assert.Equal(t, c.expected, rerr.FindError(err, c.expected), fmt.Sprintf("Fail to check Root: %v %d", err, c.expected))
72+
assert.Equal(t, c.expected, specerror.FindError(err, c.expected), fmt.Sprintf("Fail to check Root: %v %d", err, c.expected))
7373
}
7474
}
7575

7676
func TestCheckSemVer(t *testing.T) {
7777
cases := []struct {
7878
val string
79-
expected rerr.SpecErrorCode
79+
expected specerror.SpecErrorCode
8080
}{
81-
{rspec.Version, rerr.NonError},
81+
{rspec.Version, specerror.NonError},
8282
//FIXME: validate currently only handles rpsec.Version
83-
{"0.0.1", rerr.NonRFCError},
84-
{"invalid", rerr.SpecVersion},
83+
{"0.0.1", specerror.NonRFCError},
84+
{"invalid", specerror.SpecVersion},
8585
}
8686

8787
for _, c := range cases {
8888
v := NewValidator(&rspec.Spec{Version: c.val}, "", false, "linux")
8989
err := v.CheckSemVer()
90-
assert.Equal(t, c.expected, rerr.FindError(err, c.expected), "Fail to check SemVer "+c.val)
90+
assert.Equal(t, c.expected, specerror.FindError(err, c.expected), "Fail to check SemVer "+c.val)
9191
}
9292
}

validation/validation_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import (
1212
"github.com/satori/go.uuid"
1313
"github.com/stretchr/testify/assert"
1414

15-
rerr "github.com/opencontainers/runtime-tools/error"
1615
"github.com/opencontainers/runtime-tools/generate"
16+
"github.com/opencontainers/runtime-tools/specerror"
1717
)
1818

1919
var (
@@ -114,9 +114,9 @@ func TestValidateCreate(t *testing.T) {
114114
errExpected bool
115115
err error
116116
}{
117-
{"", false, rerr.NewError(rerr.CreateWithID, "'Create' MUST generate an error if the ID is not provided", rspecs.Version)},
118-
{containerID, true, rerr.NewError(rerr.CreateNewContainer, "'Create' MUST create a new container", rspecs.Version)},
119-
{containerID, false, rerr.NewError(rerr.CreateWithUniqueID, "'Create' MUST generate an error if the ID provided is not unique", rspecs.Version)},
117+
{"", false, specerror.NewError(specerror.CreateWithID, "'Create' MUST generate an error if the ID is not provided", rspecs.Version)},
118+
{containerID, true, specerror.NewError(specerror.CreateNewContainer, "'Create' MUST create a new container", rspecs.Version)},
119+
{containerID, false, specerror.NewError(specerror.CreateWithUniqueID, "'Create' MUST generate an error if the ID provided is not unique", rspecs.Version)},
120120
}
121121

122122
for _, c := range cases {

0 commit comments

Comments
 (0)