Skip to content

Commit f0deb59

Browse files
committed
ulimit-adjuster: new sample plugin
Signed-off-by: Samuel Karp <[email protected]>
1 parent d2dd708 commit f0deb59

5 files changed

Lines changed: 384 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,7 @@ The following sample plugins exist for NRI:
309309
- [differ](plugins/differ)
310310
- [device injector](plugins/device-injector)
311311
- [OCI hook injector](plugins/hook-injector)
312+
- [ulimit adjuster](plugins/ulimit-adjuster)
312313
- [NRI v0.1.0 plugin adapter](plugins/v010-adapter)
313314

314315
Please see the documentation of these plugins for further details

plugins/ulimit-adjuster/README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
## ulimit Adjuster Plugin
2+
3+
This sample plugin can adjust ulimits for containers using pod annotations.
4+
5+
### Annotations
6+
7+
ulimits are annotated using the key
8+
`ulimits.nri.containerd.io/container.$CONTAINER_NAME`, which adjusts ulimits
9+
for `$CONTAINER_NAME`. The ulimit names are the valid names of Linux resource
10+
limits, which can be seen on the
11+
[`setrlimit(2)` manual page](https://linux.die.net/man/2/setrlimit).
12+
13+
The annotation syntax for ulimit adjustment is
14+
15+
```
16+
- type: RLIMIT_NOFILE
17+
soft: 1024
18+
hard: 4096
19+
- path: RLIMIT_MEMLOCK
20+
soft: 1073741824
21+
hard: 1073741824
22+
...
23+
```
24+
25+
All fields are mandatory (`soft` and `hard` will be interpreted as 0 if
26+
missing). The `type` field accepts names in uppercase letters
27+
("RLIMIT_NOFILE"), lowercase letters ("rlimit_memlock"), and omitting the
28+
"RLIMIT_" prefix ("nproc").
29+
30+
## Testing
31+
32+
You can test this plugin using a kubernetes cluster/node with a container
33+
runtime that has NRI support enabled. Start the plugin on the target node
34+
(`ulimit-adjuster -idx 10`), create a pod with some annotated ulimits, then
35+
verify that those get adjusted in the container. See the
36+
[sample pod spec](sample-ulimit-adjust.yaml) for an example.
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package main
18+
19+
import (
20+
"context"
21+
"flag"
22+
"fmt"
23+
"os"
24+
"strings"
25+
26+
"github.com/containerd/containerd/log"
27+
"github.com/sirupsen/logrus"
28+
"sigs.k8s.io/yaml"
29+
30+
"github.com/containerd/nri/pkg/api"
31+
"github.com/containerd/nri/pkg/stub"
32+
)
33+
34+
const (
35+
ulimitKey = "ulimits.nri.containerd.io"
36+
rlimitPrefix = "RLIMIT_"
37+
)
38+
39+
var (
40+
valid = map[string]struct{}{
41+
"AS": {},
42+
"CORE": {},
43+
"CPU": {},
44+
"DATA": {},
45+
"FSIZE": {},
46+
"LOCKS": {},
47+
"MEMLOCK": {},
48+
"MSGQUEUE": {},
49+
"NICE": {},
50+
"NOFILE": {},
51+
"NPROC": {},
52+
"RSS": {},
53+
"RTPRIO": {},
54+
"RTTIME": {},
55+
"SIGPENDING": {},
56+
"STACK": {},
57+
}
58+
)
59+
60+
func main() {
61+
var (
62+
pluginName string
63+
pluginIdx string
64+
verbose bool
65+
opts []stub.Option
66+
)
67+
68+
l := logrus.StandardLogger()
69+
l.SetFormatter(&logrus.TextFormatter{
70+
PadLevelText: true,
71+
})
72+
73+
flag.StringVar(&pluginName, "name", "", "plugin name to register to NRI")
74+
flag.StringVar(&pluginIdx, "idx", "", "plugin index to register to NRI")
75+
flag.BoolVar(&verbose, "verbose", false, "enable (more) verbose logging")
76+
flag.Parse()
77+
ctx := log.WithLogger(context.Background(), l.WithField("name", pluginName).WithField("idx", pluginIdx))
78+
log.G(ctx).WithField("verbose", verbose).Info("starting plugin")
79+
80+
if verbose {
81+
l.SetLevel(logrus.DebugLevel)
82+
}
83+
84+
if pluginName != "" {
85+
opts = append(opts, stub.WithPluginName(pluginName))
86+
}
87+
if pluginIdx != "" {
88+
opts = append(opts, stub.WithPluginIdx(pluginIdx))
89+
}
90+
91+
p := &plugin{l: log.G(ctx)}
92+
var err error
93+
if p.stub, err = stub.New(p, opts...); err != nil {
94+
log.G(ctx).Fatalf("failed to create plugin stub: %v", err)
95+
}
96+
97+
if err := p.stub.Run(context.Background()); err != nil {
98+
log.G(ctx).Errorf("plugin exited with error %v", err)
99+
os.Exit(1)
100+
}
101+
}
102+
103+
type plugin struct {
104+
stub stub.Stub
105+
l *logrus.Entry
106+
}
107+
108+
func (p *plugin) CreateContainer(
109+
ctx context.Context,
110+
pod *api.PodSandbox,
111+
container *api.Container) (*api.ContainerAdjustment, []*api.ContainerUpdate, error) {
112+
if pod != nil {
113+
p.l = p.l.WithField("pod", pod.Name)
114+
}
115+
if container != nil {
116+
p.l = p.l.WithField("container", container.Name)
117+
}
118+
ctx = log.WithLogger(ctx, p.l)
119+
log.G(ctx).Debug("create container")
120+
121+
ulimits, err := parseUlimits(ctx, container.Name, pod.Annotations)
122+
if err != nil {
123+
log.G(ctx).WithError(err).Debug("failed to parse annotations")
124+
return nil, nil, err
125+
}
126+
adjust := &api.ContainerAdjustment{}
127+
for _, u := range ulimits {
128+
log.G(ctx).WithField("type", u.Type).WithField("hard", u.Hard).WithField("soft", u.Soft).Debug("adjust rlimit")
129+
adjust.AddRlimit(u.Type, u.Hard, u.Soft)
130+
}
131+
return adjust, nil, nil
132+
}
133+
134+
type ulimit struct {
135+
Type string `json:"type"`
136+
Hard uint64 `json:"hard"`
137+
Soft uint64 `json:"soft"`
138+
}
139+
140+
func parseUlimits(ctx context.Context, container string, annotations map[string]string) ([]ulimit, error) {
141+
key := ulimitKey + "/container." + container
142+
val, ok := annotations[key]
143+
if !ok {
144+
log.G(ctx).Debugf("no annotations found with key %q", key)
145+
return nil, nil
146+
}
147+
ulimits := make([]ulimit, 0)
148+
if err := yaml.Unmarshal([]byte(val), &ulimits); err != nil {
149+
return nil, err
150+
}
151+
for i := range ulimits {
152+
u := ulimits[i]
153+
typ := strings.TrimPrefix(strings.ToUpper(u.Type), rlimitPrefix)
154+
if _, ok := valid[typ]; !ok {
155+
log.G(ctx).WithField("raw", u.Type).WithField("trimmed", typ).Debug("failed to parse type")
156+
return nil, fmt.Errorf("failed to parse type: %q", u.Type)
157+
}
158+
ulimits[i].Type = rlimitPrefix + typ
159+
}
160+
return ulimits, nil
161+
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package main
18+
19+
import (
20+
"context"
21+
"testing"
22+
23+
"github.com/stretchr/testify/assert"
24+
)
25+
26+
func TestParseAnnotations(t *testing.T) {
27+
tests := map[string]struct {
28+
container string
29+
annotations map[string]string
30+
expected []ulimit
31+
errStr string
32+
}{
33+
"no-annotations": {
34+
container: "foo",
35+
},
36+
"unrelated-annotation": {
37+
container: "foo",
38+
annotations: map[string]string{"bar": "baz"},
39+
},
40+
"one-valid": {
41+
container: "foo",
42+
annotations: map[string]string{
43+
"ulimits.nri.containerd.io/container.foo": `
44+
- type: RLIMIT_NOFILE
45+
soft: 123
46+
hard: 456
47+
`},
48+
expected: []ulimit{{
49+
Type: "RLIMIT_NOFILE",
50+
Hard: 456,
51+
Soft: 123,
52+
}},
53+
},
54+
"multiple-valid": {
55+
container: "foo",
56+
annotations: map[string]string{
57+
"ulimits.nri.containerd.io/container.foo": `
58+
- type: RLIMIT_NOFILE
59+
soft: 123
60+
hard: 456
61+
- type: RLIMIT_NPROC
62+
soft: 456
63+
hard: 789
64+
`},
65+
expected: []ulimit{{
66+
Type: "RLIMIT_NOFILE",
67+
Hard: 456,
68+
Soft: 123,
69+
}, {
70+
Type: "RLIMIT_NPROC",
71+
Hard: 789,
72+
Soft: 456,
73+
}},
74+
},
75+
"missing-prefix": {
76+
container: "foo",
77+
annotations: map[string]string{
78+
"ulimits.nri.containerd.io/container.foo": `
79+
- type: AS
80+
soft: 123
81+
hard: 456
82+
`},
83+
expected: []ulimit{{
84+
Type: "RLIMIT_AS",
85+
Hard: 456,
86+
Soft: 123,
87+
}},
88+
},
89+
"lower-case": {
90+
container: "foo",
91+
annotations: map[string]string{
92+
"ulimits.nri.containerd.io/container.foo": `
93+
- type: rlimit_core
94+
soft: 123
95+
hard: 456
96+
`},
97+
expected: []ulimit{{
98+
Type: "RLIMIT_CORE",
99+
Hard: 456,
100+
Soft: 123,
101+
}},
102+
},
103+
"lower-case-missing-prefix": {
104+
container: "foo",
105+
annotations: map[string]string{
106+
"ulimits.nri.containerd.io/container.foo": `
107+
- type: cpu
108+
soft: 123
109+
hard: 456
110+
`},
111+
expected: []ulimit{{
112+
Type: "RLIMIT_CPU",
113+
Hard: 456,
114+
Soft: 123,
115+
}},
116+
},
117+
"invalid-prefix": {
118+
container: "foo",
119+
annotations: map[string]string{
120+
"ulimits.nri.containerd.io/container.foo": `
121+
- type: ULIMIT_NOFILE
122+
soft: 123
123+
hard: 456
124+
`},
125+
errStr: `failed to parse type: "ULIMIT_NOFILE"`,
126+
},
127+
"invalid-rlimit": {
128+
container: "foo",
129+
annotations: map[string]string{
130+
"ulimits.nri.containerd.io/container.foo": `
131+
- type: RLIMIT_FOO
132+
soft: 123
133+
hard: 456
134+
`},
135+
errStr: `failed to parse type: "RLIMIT_FOO"`,
136+
},
137+
"one-invalid": {
138+
container: "foo",
139+
annotations: map[string]string{
140+
"ulimits.nri.containerd.io/container.foo": `
141+
- type: RLIMIT_NICE
142+
soft: 456
143+
hard: 789
144+
- type: RLIMIT_BAR
145+
soft: 123
146+
hard: 456
147+
`},
148+
errStr: `failed to parse type: "RLIMIT_BAR"`,
149+
},
150+
}
151+
for name, tc := range tests {
152+
t.Run(name, func(t *testing.T) {
153+
ulimits, err := parseUlimits(context.Background(), tc.container, tc.annotations)
154+
if tc.errStr != "" {
155+
assert.EqualError(t, err, tc.errStr)
156+
assert.Nil(t, ulimits)
157+
} else {
158+
assert.NoError(t, err)
159+
assert.EqualValues(t, tc.expected, ulimits)
160+
}
161+
})
162+
}
163+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
apiVersion: v1
2+
kind: Pod
3+
metadata:
4+
name: sleep
5+
annotations:
6+
ulimits.nri.containerd.io/container.sleep: |
7+
- type: memlock
8+
hard: 987654
9+
soft: 645321
10+
- type: RLIMIT_NOFILE
11+
hard: 4096
12+
soft: 1024
13+
- type: nproc
14+
hard: 9000
15+
spec:
16+
containers:
17+
- name: sleep
18+
image: ubuntu:latest
19+
command:
20+
- /bin/bash
21+
- -c
22+
- "ulimit -a; ulimit -Ha; sleep inf"
23+
terminationGracePeriodSeconds: 3

0 commit comments

Comments
 (0)