Skip to content

Commit 9e5bd5a

Browse files
committed
namespaces, identifiers: split validation
After review, there are cases where having common requirements for namespaces and identifiers creates contention between applications. One example is that it is nice to have namespaces comply with domain name requirement, but that does not allow underscores, which are required for certain identifiers. The namespaces validation has been reverted to be in line with RFC 1035. Existing identifiers has been modified to allow simply alpha-numeric identifiers, while limiting adjacent separators. We may follow up tweaks for the identifier charset but this split should remove the hard decisions. Signed-off-by: Stephen J Day <[email protected]>
1 parent c63b84d commit 9e5bd5a

File tree

10 files changed

+162
-32
lines changed

10 files changed

+162
-32
lines changed

Diff for: identifiers/validate.go

+16-21
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
1-
// Package identifiers provides common validation for identifiers, keys and ids
1+
// Package identifiers provides common validation for identifiers and keys
22
// across containerd.
33
//
4-
// To allow such identifiers to be used across various contexts safely, the character
5-
// set has been restricted to that defined for domains in RFC 1035, section
6-
// 2.3.1. This will make identifiers safe for use across networks, filesystems
7-
// and other media.
4+
// Identifiers in containerd must be a alphanumeric, allowing limited
5+
// underscores, dashes and dots.
86
//
9-
// The identifier specification departs from RFC 1035 in that it allows
10-
// "labels" to start with number and only enforces a total length restriction
11-
// of 76 characters.
12-
//
13-
// While the character set may be expanded in the future, identifiers are
14-
// guaranteed to be safely used as filesystem path components.
7+
// While the character set may be expanded in the future, identifiers
8+
// are guaranteed to be safely used as filesystem path components.
159
package identifiers
1610

1711
import (
@@ -22,27 +16,28 @@ import (
2216
)
2317

2418
const (
25-
maxLength = 76
26-
charclass = `[A-Za-z0-9]+`
27-
label = charclass + `(:?[-]+` + charclass + `)*`
19+
maxLength = 76
20+
alphanum = `[A-Za-z0-9]+`
21+
separators = `[._-]`
2822
)
2923

3024
var (
31-
// identifierRe validates that a identifier matches valid identifiers.
32-
//
33-
// Rules for domains, defined in RFC 1035, section 2.3.1, are used for
34-
// identifiers.
35-
identifierRe = regexp.MustCompile(reAnchor(label + reGroup("[.]"+reGroup(label)) + "*"))
25+
// identifierRe defines the pattern for valid identifiers.
26+
identifierRe = regexp.MustCompile(reAnchor(alphanum + reGroup(separators+reGroup(alphanum)) + "*"))
3627
)
3728

3829
// Validate return nil if the string s is a valid identifier.
3930
//
40-
// identifiers must be valid domain identifiers according to RFC 1035, section 2.3.1. To
31+
// identifiers must be valid domain names according to RFC 1035, section 2.3.1. To
4132
// enforce case insensitvity, all characters must be lower case.
4233
//
4334
// In general, identifiers that pass this validation, should be safe for use as
44-
// a domain identifier or filesystem path component.
35+
// a domain names or filesystem path component.
4536
func Validate(s string) error {
37+
if len(s) == 0 {
38+
return errors.Wrapf(errdefs.ErrInvalidArgument, "identifier must not be empty")
39+
}
40+
4641
if len(s) > maxLength {
4742
return errors.Wrapf(errdefs.ErrInvalidArgument, "identifier %q greater than maximum length (%d characters)", s, maxLength)
4843
}

Diff for: identifiers/validate_test.go

+5-3
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,12 @@ func TestValidIdentifiers(t *testing.T) {
1313
"Default",
1414
t.Name(),
1515
"default-default",
16-
"default--default",
1716
"containerd.io",
1817
"foo.boo",
1918
"swarmkit.docker.io",
20-
"zn--e9.org", // or something like it!
2119
"0912341234",
2220
"task.0.0123456789",
21+
"underscores_are_allowed",
2322
strings.Repeat("a", maxLength),
2423
} {
2524
t.Run(input, func(t *testing.T) {
@@ -32,14 +31,17 @@ func TestValidIdentifiers(t *testing.T) {
3231

3332
func TestInvalidIdentifiers(t *testing.T) {
3433
for _, input := range []string{
34+
"",
3535
".foo..foo",
3636
"foo/foo",
3737
"foo/..",
3838
"foo..foo",
3939
"foo.-boo",
4040
"-foo.boo",
4141
"foo.boo-",
42-
"foo_foo.boo_underscores", // boo-urns?
42+
"but__only_tasteful_underscores",
43+
"zn--e9.org", // or something like it!
44+
"default--default",
4345
strings.Repeat("a", maxLength+1),
4446
} {
4547

Diff for: linux/runtime.go

+6
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313

1414
"github.com/boltdb/bolt"
1515
"github.com/containerd/containerd/api/types"
16+
"github.com/containerd/containerd/identifiers"
1617
client "github.com/containerd/containerd/linux/shim"
1718
shim "github.com/containerd/containerd/linux/shim/v1"
1819
"github.com/containerd/containerd/log"
@@ -127,6 +128,11 @@ func (r *Runtime) Create(ctx context.Context, id string, opts runtime.CreateOpts
127128
if err != nil {
128129
return nil, err
129130
}
131+
132+
if err := identifiers.Validate(id); err != nil {
133+
return nil, errors.Wrapf(err, "invalid task id")
134+
}
135+
130136
bundle, err := newBundle(filepath.Join(r.root, namespace), namespace, id, opts.Spec.Value)
131137
if err != nil {
132138
return nil, err

Diff for: linux/shim/exec.go

+5
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"golang.org/x/sys/unix"
1717

1818
"github.com/containerd/console"
19+
"github.com/containerd/containerd/identifiers"
1920
shimapi "github.com/containerd/containerd/linux/shim/v1"
2021
"github.com/containerd/fifo"
2122
runc "github.com/containerd/go-runc"
@@ -40,6 +41,10 @@ type execProcess struct {
4041
}
4142

4243
func newExecProcess(context context.Context, path string, r *shimapi.ExecProcessRequest, parent *initProcess, id string) (process, error) {
44+
if err := identifiers.Validate(id); err != nil {
45+
return nil, errors.Wrapf(err, "invalid exec id")
46+
}
47+
4348
e := &execProcess{
4449
id: id,
4550
parent: parent,

Diff for: linux/shim/init.go

+4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"golang.org/x/sys/unix"
1818

1919
"github.com/containerd/console"
20+
"github.com/containerd/containerd/identifiers"
2021
"github.com/containerd/containerd/linux/runcopts"
2122
shimapi "github.com/containerd/containerd/linux/shim/v1"
2223
"github.com/containerd/containerd/log"
@@ -52,6 +53,9 @@ type initProcess struct {
5253
}
5354

5455
func newInitProcess(context context.Context, path, namespace string, r *shimapi.CreateTaskRequest) (*initProcess, error) {
56+
if err := identifiers.Validate(r.ID); err != nil {
57+
return nil, errors.Wrapf(err, "invalid task id")
58+
}
5559
var options runcopts.CreateOptions
5660
if r.Options != nil {
5761
v, err := typeurl.UnmarshalAny(r.Options)

Diff for: linux/shim/service.go

+2-4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/containerd/console"
1515
events "github.com/containerd/containerd/api/services/events/v1"
1616
"github.com/containerd/containerd/api/types/task"
17+
"github.com/containerd/containerd/errdefs"
1718
evt "github.com/containerd/containerd/events"
1819
shimapi "github.com/containerd/containerd/linux/shim/v1"
1920
"github.com/containerd/containerd/log"
@@ -77,12 +78,9 @@ type Service struct {
7778
}
7879

7980
func (s *Service) Create(ctx context.Context, r *shimapi.CreateTaskRequest) (*shimapi.CreateTaskResponse, error) {
80-
if r.ID == "" {
81-
return nil, grpc.Errorf(codes.InvalidArgument, "task id cannot be empty")
82-
}
8381
process, err := newInitProcess(ctx, s.path, s.namespace, r)
8482
if err != nil {
85-
return nil, err
83+
return nil, errdefs.ToGRPC(err)
8684
}
8785
s.mu.Lock()
8886
// save the main task id and bundle to the shim for additional requests

Diff for: metadata/namespaces.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55

66
"github.com/boltdb/bolt"
77
"github.com/containerd/containerd/errdefs"
8-
"github.com/containerd/containerd/identifiers"
98
"github.com/containerd/containerd/namespaces"
109
"github.com/pkg/errors"
1110
)
@@ -24,7 +23,7 @@ func (s *namespaceStore) Create(ctx context.Context, namespace string, labels ma
2423
return err
2524
}
2625

27-
if err := identifiers.Validate(namespace); err != nil {
26+
if err := namespaces.Validate(namespace); err != nil {
2827
return err
2928
}
3029

Diff for: namespaces/context.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"os"
55

66
"github.com/containerd/containerd/errdefs"
7-
"github.com/containerd/containerd/identifiers"
87
"github.com/pkg/errors"
98
"golang.org/x/net/context"
109
)
@@ -54,7 +53,7 @@ func NamespaceRequired(ctx context.Context) (string, error) {
5453
return "", errors.Wrapf(errdefs.ErrFailedPrecondition, "namespace is required")
5554
}
5655

57-
if err := identifiers.Validate(namespace); err != nil {
56+
if err := Validate(namespace); err != nil {
5857
return "", errors.Wrap(err, "namespace validation")
5958
}
6059

Diff for: namespaces/validate.go

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Package namespaces provides tools for working with namespaces across
2+
// containerd.
3+
//
4+
// Namespaces collect resources such as containers and images, into a unique
5+
// identifier space. This means that two applications can use the same
6+
// identifiers and not conflict while using containerd.
7+
//
8+
// This package can be used to ensure that client and server functions
9+
// correctly store the namespace on the context.
10+
package namespaces
11+
12+
import (
13+
"regexp"
14+
15+
"github.com/containerd/containerd/errdefs"
16+
"github.com/pkg/errors"
17+
)
18+
19+
const (
20+
maxLength = 76
21+
alpha = `[A-Za-z]`
22+
alphanum = `[A-Za-z0-9]+`
23+
label = alpha + alphanum + `(:?[-]+` + alpha + alphanum + `)*`
24+
)
25+
26+
var (
27+
// namespaceRe validates that a namespace matches valid identifiers.
28+
//
29+
// Rules for domains, defined in RFC 1035, section 2.3.1, are used for
30+
// namespaces.
31+
namespaceRe = regexp.MustCompile(reAnchor(label + reGroup("[.]"+reGroup(label)) + "*"))
32+
)
33+
34+
// Validate returns nil if the string s is a valid namespace.
35+
//
36+
// To allow such namespace identifiers to be used across various contexts
37+
// safely, the character set has been restricted to that defined for domains in
38+
// RFC 1035, section 2.3.1. This will make namespace identifiers safe for use
39+
// across networks, filesystems and other media.
40+
//
41+
// The identifier specification departs from RFC 1035 in that it allows
42+
// "labels" to start with number and only enforces a total length restriction
43+
// of 76 characters.
44+
//
45+
// While the character set may be expanded in the future, namespace identifiers
46+
// are guaranteed to be safely used as filesystem path components.
47+
//
48+
// For the most part, this doesn't need to be called directly when using the
49+
// context-oriented functions.
50+
func Validate(s string) error {
51+
if len(s) > maxLength {
52+
return errors.Wrapf(errdefs.ErrInvalidArgument, "namespace %q greater than maximum length (%d characters)", s, maxLength)
53+
}
54+
55+
if !namespaceRe.MatchString(s) {
56+
return errors.Wrapf(errdefs.ErrInvalidArgument, "namespace %q must match %v", s, namespaceRe)
57+
}
58+
return nil
59+
}
60+
61+
func reGroup(s string) string {
62+
return `(?:` + s + `)`
63+
}
64+
65+
func reAnchor(s string) string {
66+
return `^` + s + `$`
67+
}

Diff for: namespaces/validate_test.go

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package namespaces
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/containerd/containerd/errdefs"
8+
)
9+
10+
func TestValidNamespaces(t *testing.T) {
11+
for _, input := range []string{
12+
"default",
13+
"Default",
14+
t.Name(),
15+
"default-default",
16+
"default--default",
17+
"containerd.io",
18+
"foo.boo",
19+
"swarmkit.docker.io",
20+
"zn--e9.org", // or something like it!
21+
strings.Repeat("a", maxLength),
22+
} {
23+
t.Run(input, func(t *testing.T) {
24+
if err := Validate(input); err != nil {
25+
t.Fatalf("unexpected error: %v != nil", err)
26+
}
27+
})
28+
}
29+
}
30+
31+
func TestInvalidNamespaces(t *testing.T) {
32+
for _, input := range []string{
33+
".foo..foo",
34+
"foo/foo",
35+
"foo/..",
36+
"foo..foo",
37+
"foo.-boo",
38+
"foo.-boo.bar",
39+
"-foo.boo",
40+
"foo.boo-",
41+
"foo_foo.boo_underscores", // boo-urns?
42+
"0912341234",
43+
"task.0.0123456789",
44+
strings.Repeat("a", maxLength+1),
45+
} {
46+
47+
t.Run(input, func(t *testing.T) {
48+
if err := Validate(input); err == nil {
49+
t.Fatal("expected invalid error")
50+
} else if !errdefs.IsInvalidArgument(err) {
51+
t.Fatal("error should be an invalid identifier error")
52+
}
53+
})
54+
}
55+
}

0 commit comments

Comments
 (0)