Skip to content

Commit 239a49d

Browse files
authored
Merge pull request moby#51711 from robmry/nri-config-reload
NRI: config reload
2 parents 5b60725 + ff553c5 commit 239a49d

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
@@ -47,10 +47,10 @@ const (
4747
)
4848

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

@@ -123,6 +123,43 @@ func (n *NRI) Shutdown(ctx context.Context) {
123123
n.adap = nil
124124
}
125125

126+
// PrepareReload validates and prepares for a configuration reload. It returns
127+
// a function to perform the actual reload when called.
128+
func (n *NRI) PrepareReload(nriCfg opts.NRIOpts) (func() error, error) {
129+
var newNRI *adaptation.Adaptation
130+
newCfg := n.cfg
131+
newCfg.DaemonConfig = nriCfg
132+
if err := setDefaultPaths(&newCfg.DaemonConfig); err != nil {
133+
return nil, err
134+
}
135+
136+
if nriCfg.Enable {
137+
var err error
138+
newNRI, err = adaptation.New("docker", dockerversion.Version, n.syncFn, n.updateFn, nriOptions(newCfg.DaemonConfig)...)
139+
if err != nil {
140+
return nil, err
141+
}
142+
}
143+
144+
return func() error {
145+
n.mu.Lock()
146+
if n.adap != nil {
147+
log.G(context.TODO()).Info("Shutting down old NRI instance")
148+
n.adap.Stop()
149+
}
150+
n.cfg = newCfg
151+
n.adap = newNRI
152+
// Release the lock before starting newNRI, because it'll call back to syncFn
153+
// which will acquire the lock.
154+
n.mu.Unlock()
155+
156+
if newNRI == nil {
157+
return nil
158+
}
159+
return newNRI.Start()
160+
}, nil
161+
}
162+
126163
// CreateContainer notifies plugins of a "creation" NRI-lifecycle event for a container,
127164
// allowing the plugin to adjust settings before the container is created.
128165
//

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

@@ -250,3 +252,80 @@ func TestNRIContainerCreateAddMount(t *testing.T) {
250252
})
251253
}
252254
}
255+
256+
func TestNRIReload(t *testing.T) {
257+
skip.If(t, testEnv.IsRemoteDaemon, "cannot run daemon when remote daemon")
258+
skip.If(t, testEnv.DaemonInfo.OSType == "windows", "cannot start a separate daemon with NRI enabled on Windows")
259+
skip.If(t, testEnv.IsRootless)
260+
261+
ctx := testutil.StartSpan(baseContext, t)
262+
263+
const pluginName = "00-nri-test-plugin"
264+
const envVar = "NRI_TEST"
265+
266+
// Build and install a plugin.
267+
pluginDir := t.TempDir()
268+
res := icmd.RunCommand("go", "build", "-o", filepath.Join(pluginDir, pluginName), "./testdata/test_plugin.go")
269+
res.Assert(t, icmd.Success)
270+
271+
// Location and update function for the plugin config file.
272+
pluginConfigDir := t.TempDir()
273+
configurePlugin := func(envVal string) {
274+
t.Helper()
275+
err := os.WriteFile(filepath.Join(pluginConfigDir, pluginName+".conf"),
276+
[]byte(`{"env-var": "`+envVar+`", "env-val": "`+envVal+`"}`), 0o644)
277+
assert.NilError(t, err)
278+
}
279+
280+
// Location and update function for the daemon config file, with empty initial config.
281+
daemonConfigDir := t.TempDir()
282+
daemonConfigFile := filepath.Join(daemonConfigDir, "daemon.json")
283+
configureDaemon := func(json string) {
284+
t.Helper()
285+
err := os.WriteFile(daemonConfigFile, []byte(json), 0o644)
286+
assert.NilError(t, err)
287+
}
288+
configureDaemon("{}")
289+
290+
d := daemon.New(t)
291+
d.StartWithBusybox(ctx, t, "--config-file", daemonConfigFile, "--iptables=false", "--ip6tables=false")
292+
defer d.Stop(t)
293+
c := d.NewClientT(t)
294+
295+
// Function to check envVar in a container has value expEnvVal, or is absent if expEnvVal is "".
296+
checkEnvVar := func(expEnvVal string) {
297+
t.Helper()
298+
res := container.RunAttach(ctx, t, c, container.WithAutoRemove, container.WithCmd("env"))
299+
if expEnvVal == "" {
300+
assert.Check(t, !strings.Contains(res.Stdout.String(), envVar),
301+
"unexpected %q in env:\n%s", envVar, res.Stdout.String())
302+
} else {
303+
assert.Check(t, is.Contains(res.Stdout.String(), envVar+"="+expEnvVal))
304+
}
305+
}
306+
307+
// Without NRI enabled, the env var should not be set.
308+
checkEnvVar("")
309+
310+
// Enable NRI in the daemon config and reload.
311+
configureDaemon(`{"nri-opts": {"enable": true, "plugin-path": "` + pluginDir + `", "plugin-config-path": "` + pluginConfigDir + `"}}`)
312+
configurePlugin("1")
313+
err := d.ReloadConfig()
314+
assert.NilError(t, err)
315+
316+
// Now the env var should be set by the plugin.
317+
checkEnvVar("1")
318+
319+
// Reconfigure the plugin, check the new config takes effect after reload.
320+
configurePlugin("2")
321+
checkEnvVar("1")
322+
err = d.ReloadConfig()
323+
assert.NilError(t, err)
324+
checkEnvVar("2")
325+
326+
// Disable NRI by clearing the config and reloading.
327+
configureDaemon("{}")
328+
err = d.ReloadConfig()
329+
assert.NilError(t, err)
330+
checkEnvVar("")
331+
}
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)