Skip to content

Commit ff553c5

Browse files
committed
NRI: make config reloadable
Signed-off-by: Rob Murray <rob.murray@docker.com>
1 parent 0c01da8 commit ff553c5

File tree

4 files changed

+241
-4
lines changed

4 files changed

+241
-4
lines changed

daemon/internal/nri/nri.go

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,10 @@ const (
4646
)
4747

4848
type NRI struct {
49-
cfg Config
50-
51-
// mu protects nri - read lock for container operations, write lock for sync and shutdown.
49+
// mu protects cfg and adap
50+
// Read lock for container operations, write lock for sync, config update and shutdown.
5251
mu sync.RWMutex
52+
cfg Config
5353
adap *adaptation.Adaptation
5454
}
5555

@@ -103,6 +103,43 @@ func (n *NRI) Shutdown(ctx context.Context) {
103103
n.adap = nil
104104
}
105105

106+
// PrepareReload validates and prepares for a configuration reload. It returns
107+
// a function to perform the actual reload when called.
108+
func (n *NRI) PrepareReload(nriCfg opts.NRIOpts) (func() error, error) {
109+
var newNRI *adaptation.Adaptation
110+
newCfg := n.cfg
111+
newCfg.DaemonConfig = nriCfg
112+
if err := setDefaultPaths(&newCfg.DaemonConfig); err != nil {
113+
return nil, err
114+
}
115+
116+
if nriCfg.Enable {
117+
var err error
118+
newNRI, err = adaptation.New("docker", dockerversion.Version, n.syncFn, n.updateFn, nriOptions(newCfg.DaemonConfig)...)
119+
if err != nil {
120+
return nil, err
121+
}
122+
}
123+
124+
return func() error {
125+
n.mu.Lock()
126+
if n.adap != nil {
127+
log.G(context.TODO()).Info("Shutting down old NRI instance")
128+
n.adap.Stop()
129+
}
130+
n.cfg = newCfg
131+
n.adap = newNRI
132+
// Release the lock before starting newNRI, because it'll call back to syncFn
133+
// which will acquire the lock.
134+
n.mu.Unlock()
135+
136+
if newNRI == nil {
137+
return nil
138+
}
139+
return newNRI.Start()
140+
}, nil
141+
}
142+
106143
// CreateContainer notifies plugins of a "creation" NRI-lifecycle event for a container,
107144
// allowing the plugin to adjust settings before the container is created.
108145
//

daemon/reload.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import (
1010
"github.com/containerd/log"
1111
"github.com/mitchellh/copystructure"
1212
"github.com/moby/moby/api/types/events"
13-
1413
"github.com/moby/moby/v2/daemon/config"
14+
"github.com/moby/moby/v2/daemon/pkg/opts"
1515
)
1616

1717
// reloadTxn is used to defer side effects of a config reload.
@@ -71,6 +71,7 @@ func (tx *reloadTxn) Rollback() error {
7171
// - Insecure registries
7272
// - Registry mirrors
7373
// - Daemon live restore
74+
// - NRI enable and filesystem locations
7475
func (daemon *Daemon) Reload(conf *config.Config) error {
7576
daemon.configReload.Lock()
7677
defer daemon.configReload.Unlock()
@@ -107,6 +108,7 @@ func (daemon *Daemon) Reload(conf *config.Config) error {
107108
daemon.reloadRegistryConfig,
108109
daemon.reloadLiveRestore,
109110
daemon.reloadNetworkDiagnosticPort,
111+
daemon.reloadNRI,
110112
} {
111113
if err := reload(&txn, newCfg, conf, attributes); err != nil {
112114
return errors.Join(err, txn.Rollback())
@@ -276,3 +278,27 @@ func (daemon *Daemon) reloadFeatures(txn *reloadTxn, newCfg *configStore, conf *
276278
attributes["features"] = fmt.Sprintf("%v", newCfg.Features)
277279
return nil
278280
}
281+
282+
// reloadNRI updates NRI configuration
283+
func (daemon *Daemon) reloadNRI(txn *reloadTxn, newCfg *configStore, conf *config.Config, attributes map[string]string) error {
284+
if daemon.nri == nil {
285+
// Daemon not initialised.
286+
return nil
287+
}
288+
if conf.IsValueSet("nri-opts") {
289+
newCfg.Config.NRIOpts = conf.NRIOpts
290+
} else {
291+
newCfg.Config.NRIOpts = opts.NRIOpts{}
292+
}
293+
294+
commit, err := daemon.nri.PrepareReload(newCfg.NRIOpts)
295+
if err != nil {
296+
return err
297+
}
298+
if commit != nil {
299+
txn.OnCommit(commit)
300+
}
301+
302+
attributes["nri-opts"] = fmt.Sprintf("%v", newCfg.NRIOpts)
303+
return nil
304+
}

integration/daemon/nri/nri_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package nri
33
import (
44
"os"
55
"path/filepath"
6+
"strings"
67
"testing"
78

89
"github.com/containerd/nri/pkg/api"
@@ -13,6 +14,7 @@ import (
1314
"github.com/moby/moby/v2/internal/testutil/daemon"
1415
"gotest.tools/v3/assert"
1516
is "gotest.tools/v3/assert/cmp"
17+
"gotest.tools/v3/icmd"
1618
"gotest.tools/v3/skip"
1719
)
1820

@@ -190,3 +192,80 @@ func TestNRIContainerCreateAddMount(t *testing.T) {
190192
})
191193
}
192194
}
195+
196+
func TestNRIReload(t *testing.T) {
197+
skip.If(t, testEnv.IsRemoteDaemon, "cannot run daemon when remote daemon")
198+
skip.If(t, testEnv.DaemonInfo.OSType == "windows", "cannot start a separate daemon with NRI enabled on Windows")
199+
skip.If(t, testEnv.IsRootless)
200+
201+
ctx := testutil.StartSpan(baseContext, t)
202+
203+
const pluginName = "00-nri-test-plugin"
204+
const envVar = "NRI_TEST"
205+
206+
// Build and install a plugin.
207+
pluginDir := t.TempDir()
208+
res := icmd.RunCommand("go", "build", "-o", filepath.Join(pluginDir, pluginName), "./testdata/test_plugin.go")
209+
res.Assert(t, icmd.Success)
210+
211+
// Location and update function for the plugin config file.
212+
pluginConfigDir := t.TempDir()
213+
configurePlugin := func(envVal string) {
214+
t.Helper()
215+
err := os.WriteFile(filepath.Join(pluginConfigDir, pluginName+".conf"),
216+
[]byte(`{"env-var": "`+envVar+`", "env-val": "`+envVal+`"}`), 0o644)
217+
assert.NilError(t, err)
218+
}
219+
220+
// Location and update function for the daemon config file, with empty initial config.
221+
daemonConfigDir := t.TempDir()
222+
daemonConfigFile := filepath.Join(daemonConfigDir, "daemon.json")
223+
configureDaemon := func(json string) {
224+
t.Helper()
225+
err := os.WriteFile(daemonConfigFile, []byte(json), 0o644)
226+
assert.NilError(t, err)
227+
}
228+
configureDaemon("{}")
229+
230+
d := daemon.New(t)
231+
d.StartWithBusybox(ctx, t, "--config-file", daemonConfigFile, "--iptables=false", "--ip6tables=false")
232+
defer d.Stop(t)
233+
c := d.NewClientT(t)
234+
235+
// Function to check envVar in a container has value expEnvVal, or is absent if expEnvVal is "".
236+
checkEnvVar := func(expEnvVal string) {
237+
t.Helper()
238+
res := container.RunAttach(ctx, t, c, container.WithAutoRemove, container.WithCmd("env"))
239+
if expEnvVal == "" {
240+
assert.Check(t, !strings.Contains(res.Stdout.String(), envVar),
241+
"unexpected %q in env:\n%s", envVar, res.Stdout.String())
242+
} else {
243+
assert.Check(t, is.Contains(res.Stdout.String(), envVar+"="+expEnvVal))
244+
}
245+
}
246+
247+
// Without NRI enabled, the env var should not be set.
248+
checkEnvVar("")
249+
250+
// Enable NRI in the daemon config and reload.
251+
configureDaemon(`{"nri-opts": {"enable": true, "plugin-path": "` + pluginDir + `", "plugin-config-path": "` + pluginConfigDir + `"}}`)
252+
configurePlugin("1")
253+
err := d.ReloadConfig()
254+
assert.NilError(t, err)
255+
256+
// Now the env var should be set by the plugin.
257+
checkEnvVar("1")
258+
259+
// Reconfigure the plugin, check the new config takes effect after reload.
260+
configurePlugin("2")
261+
checkEnvVar("1")
262+
err = d.ReloadConfig()
263+
assert.NilError(t, err)
264+
checkEnvVar("2")
265+
266+
// Disable NRI by clearing the config and reloading.
267+
configureDaemon("{}")
268+
err = d.ReloadConfig()
269+
assert.NilError(t, err)
270+
checkEnvVar("")
271+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
Based on https://github.com/containerd/nri/blob/main/plugins/template/ - which is ...
3+
4+
Copyright The containerd Authors.
5+
6+
Licensed under the Apache License, Version 2.0 (the "License");
7+
you may not use this file except in compliance with the License.
8+
You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing, software
13+
distributed under the License is distributed on an "AS IS" BASIS,
14+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
See the License for the specific language governing permissions and
16+
limitations under the License.
17+
*/
18+
19+
package main
20+
21+
import (
22+
"context"
23+
"encoding/json"
24+
"flag"
25+
"fmt"
26+
"os"
27+
28+
"github.com/containerd/nri/pkg/api"
29+
"github.com/containerd/nri/pkg/stub"
30+
)
31+
32+
type config struct {
33+
EnvVar string `json:"env-var"`
34+
EnvVal string `json:"env-val"`
35+
}
36+
37+
type plugin struct {
38+
stub stub.Stub
39+
cfg config
40+
}
41+
42+
func (p *plugin) Configure(_ context.Context, config, runtime, version string) (stub.EventMask, error) {
43+
if config == "" {
44+
return 0, nil
45+
}
46+
47+
err := json.Unmarshal([]byte(config), &p.cfg)
48+
if err != nil {
49+
return 0, fmt.Errorf("failed to parse configuration: %w", err)
50+
}
51+
52+
return api.MustParseEventMask("CreateContainer"), nil
53+
}
54+
55+
func (p *plugin) CreateContainer(_ context.Context, pod *api.PodSandbox, ctr *api.Container) (*api.ContainerAdjustment, []*api.ContainerUpdate, error) {
56+
env := []*api.KeyValue{{Key: "NRI_TEST_PLUGIN", Value: "wozere"}}
57+
if p.cfg.EnvVar != "" {
58+
env = append(env, &api.KeyValue{Key: p.cfg.EnvVar, Value: p.cfg.EnvVal})
59+
}
60+
61+
adjustment := &api.ContainerAdjustment{Env: env}
62+
updates := []*api.ContainerUpdate{}
63+
64+
return adjustment, updates, nil
65+
}
66+
67+
func main() {
68+
var (
69+
pluginName string
70+
pluginIdx string
71+
err error
72+
)
73+
74+
flag.StringVar(&pluginName, "name", "", "plugin name to register to NRI")
75+
flag.StringVar(&pluginIdx, "idx", "", "plugin index to register to NRI")
76+
flag.Parse()
77+
78+
p := &plugin{}
79+
opts := []stub.Option{
80+
stub.WithOnClose(func() { os.Exit(0) }),
81+
}
82+
if pluginName != "" {
83+
opts = append(opts, stub.WithPluginName(pluginName))
84+
}
85+
if pluginIdx != "" {
86+
opts = append(opts, stub.WithPluginIdx(pluginIdx))
87+
}
88+
89+
if p.stub, err = stub.New(p, opts...); err != nil {
90+
os.Exit(1)
91+
}
92+
if err = p.stub.Run(context.Background()); err != nil {
93+
os.Exit(1)
94+
}
95+
}

0 commit comments

Comments
 (0)