diff --git a/cmd/runtimetest/main.go b/cmd/runtimetest/main.go index f1344e916..d26dfbbff 100644 --- a/cmd/runtimetest/main.go +++ b/cmd/runtimetest/main.go @@ -18,9 +18,11 @@ import ( "github.com/hashicorp/go-multierror" "github.com/mndrix/tap-go" rspec "github.com/opencontainers/runtime-spec/specs-go" - "github.com/opencontainers/runtime-tools/cmd/runtimetest/mount" "github.com/syndtr/gocapability/capability" "github.com/urfave/cli" + + "github.com/opencontainers/runtime-tools/cmd/runtimetest/mount" + ociErr "github.com/opencontainers/runtime-tools/validate" ) // PrGetNoNewPrivs isn't exposed in Golang so we define it ourselves copying the value from @@ -30,6 +32,13 @@ const PrGetNoNewPrivs = 39 const specConfig = "config.json" var ( + defaultFS = map[string]string{ + "/proc": "proc", + "/sys": "sysfs", + "/dev/pts": "devpts", + "/dev/shm": "tmpfs", + } + defaultSymlinks = map[string]string{ "/dev/fd": "/proc/self/fd", "/dev/stdin": "/proc/self/fd/0", @@ -308,6 +317,28 @@ func validateRootFS(spec *rspec.Spec) error { return nil } +func validateDefaultFS(spec *rspec.Spec) error { + logrus.Debugf("validating linux default filesystem") + + mountInfos, err := mount.GetMounts() + if err != nil { + return ociErr.NewError(ociErr.DefaultFilesystems, err.Error()) + } + + mountsMap := make(map[string]string) + for _, mountInfo := range mountInfos { + mountsMap[mountInfo.Mountpoint] = mountInfo.Fstype + } + + 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 nil +} + func validateLinuxDevices(spec *rspec.Spec) error { for _, device := range spec.Linux.Devices { fi, err := os.Stat(device.Path) @@ -615,6 +646,10 @@ func validate(context *cli.Context) error { test: validateDefaultSymlinks, description: "default symlinks", }, + { + test: validateDefaultFS, + description: "default file system", + }, { test: validateDefaultDevices, description: "default devices", @@ -660,11 +695,20 @@ func validate(context *cli.Context) error { t := tap.New() t.Header(0) + complianceLevelString := context.String("compliance-level") + complianceLevel, err := ociErr.ParseLevel(complianceLevelString) + if err != nil { + complianceLevel = ociErr.ComplianceMust + 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 { + continue + } validationErrors = multierror.Append(validationErrors, err) } } @@ -674,6 +718,9 @@ 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 { + continue + } validationErrors = multierror.Append(validationErrors, err) } } @@ -700,6 +747,11 @@ func main() { Value: ".", Usage: "Path to the configuration", }, + cli.StringFlag{ + Name: "compliance-level", + Value: "must", + Usage: "Compliance level (may, should or must)", + }, } app.Action = validate diff --git a/completions/bash/oci-runtime-tool b/completions/bash/oci-runtime-tool index c34eb5925..0ffe13632 100644 --- a/completions/bash/oci-runtime-tool +++ b/completions/bash/oci-runtime-tool @@ -119,6 +119,14 @@ __oci-runtime-tool_complete_log_level() { " -- "$cur" ) ) } +__oci-runtime-tool_complete_compliance_level() { + COMPREPLY=( $( compgen -W " + may + should + must + " -- "$cur" ) ) +} + __oci-runtime-tool_complete_propagations() { COMPREPLY=( $( compgen -W " private @@ -218,6 +226,10 @@ _oci-runtime-tool_oci-runtime-tool() { --log-level " + local options_with_args=" + --compliance-level + " + local boolean_options=" --help -h --host-specific @@ -231,6 +243,10 @@ _oci-runtime-tool_oci-runtime-tool() { __oci-runtime-tool_complete_log_level return ;; + --compliance-level) + __oci-runtime-tool_complete_compliance_level + return + ;; esac case "$cur" in diff --git a/man/oci-runtime-tool.1.md b/man/oci-runtime-tool.1.md index 27b2e1708..880b587ff 100644 --- a/man/oci-runtime-tool.1.md +++ b/man/oci-runtime-tool.1.md @@ -32,6 +32,9 @@ oci-runtime-tool is a collection of tools for working with the [OCI runtime spec **--log-level**=LEVEL Log level (panic, fatal, error, warn, info, or debug) (default: "error"). +**--compliance-level**=LEVEL + Compliance level (may, should or must) (default: "must"). + **-v**, **--version** Print version information. diff --git a/validate/error.go b/validate/error.go new file mode 100644 index 000000000..754d032b5 --- /dev/null +++ b/validate/error.go @@ -0,0 +1,110 @@ +package validate + +import ( + "errors" + "fmt" + "strings" + + rspec "github.com/opencontainers/runtime-spec/specs-go" +) + +// ComplianceLevel represents the OCI compliance levels +type ComplianceLevel int + +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 +type ErrorCode int + +const ( + // 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 +} + +const referencePrefix = "https://github.com/opencontainers/runtime-spec/blob" + +var ociErrors = map[ErrorCode]Error{ + DefaultFilesystems: Error{Level: ComplianceShould, Reference: "config-linux.md#default-filesystems"}, +} + +// 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 + } + + 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) +}