Skip to content

Commit d268fb2

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, #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, #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]: #451 (comment) [2]: #451 (comment) [3]: #451 (comment) Signed-off-by: W. Trevor King <[email protected]>
1 parent 12b47b9 commit d268fb2

File tree

6 files changed

+96
-76
lines changed

6 files changed

+96
-76
lines changed

cmd/runtimetest/main.go

+9-8
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

-1
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.

error/runtime_spec.go renamed to specerror/error.go

+54-35
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
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.
13-
type SpecErrorCode int
14+
// Code represents the spec violation, enumerating both
15+
// configuration violations and runtime violations.
16+
type Code int
1417

1518
const (
1619
// NonError represents that an input is not an error
17-
NonError SpecErrorCode = iota
20+
NonError Code = iota
1821
// NonRFCError represents that an error is not a rfc2119 error
1922
NonRFCError
2023

@@ -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+
// Code is a matchable holds a Code
69+
Code Code
70+
}
71+
6072
var (
6173
containerFormatRef = func(version string) (reference string, err error) {
6274
return fmt.Sprintf(referenceTemplate, version, "bundle.md#container-format"), nil
@@ -75,62 +87,69 @@ var (
7587
}
7688
)
7789

78-
var ociErrors = map[SpecErrorCode]errorTemplate{
90+
var ociErrors = map[Code]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 Code, 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+
Code: 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.
133-
func FindError(err error, code SpecErrorCode) SpecErrorCode {
152+
func FindError(err error, code Code) Code {
134153
if err == nil {
135154
return NonError
136155
}
@@ -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.Code == code {
145164
return code
146165
}
147166
}

validate/validate.go

+10-10
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

+18-18
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.Code
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.Code
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
}

0 commit comments

Comments
 (0)