From 30708bb622b66544214a7c7e59147ea93d3fcbee Mon Sep 17 00:00:00 2001 From: dn Date: Wed, 27 May 2026 10:46:12 +0300 Subject: [PATCH] drivenets: create empty ConfigMap when config mount is defaulted cdnosDefaults populates pb.Config.ConfigPath ("/config_load") and pb.Config.ConfigFile ("default") on every node, but CreateConfig short-circuits and creates no ConfigMap when no startup data (Config_File / Config_Data) is supplied. The cdnos-controller still mounts the controller-expected "-config" ConfigMap into the pod whenever ConfigPath + ConfigFile are populated on the CR. With the ConfigMap missing, every pod sits forever in PodInitializing with: MountVolume.SetUp failed for volume "config" : configmap "-config" not found This shows up immediately on any topology whose nodes don't ship a startup configuration - e.g. lightweight scale tests using an alpine image where data:/file: is omitted from the proto. Fix: when ConfigPath + ConfigFile are populated but no data was provided, create an empty ConfigMap (single empty key) instead of returning nil. Users who explicitly want to opt out of the config mount can still clear ConfigPath/ConfigFile in the proto, and the existing "no mount" path is preserved. Adds unit tests covering both branches (defaulted-but-empty and fully-cleared). --- topo/node/drivenets/drivenets.go | 11 +++- topo/node/drivenets/drivenets_test.go | 78 +++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/topo/node/drivenets/drivenets.go b/topo/node/drivenets/drivenets.go index 7e6c0d202..6fb41772c 100644 --- a/topo/node/drivenets/drivenets.go +++ b/topo/node/drivenets/drivenets.go @@ -641,7 +641,16 @@ func (n *Node) CreateConfig(ctx context.Context) (*corev1.Volume, error) { data = v.Data } if data == nil { - return nil, nil + // No startup config was supplied. If the controller-managed Pod + // will nevertheless mount a config ConfigMap (because ConfigPath + // + ConfigFile are populated - typically by cdnosDefaults), we + // must still create an empty ConfigMap so the mount succeeds. + // Otherwise the Pod sits forever in PodInitializing with + // `configmap "-config" not found`. + if pb.Config.GetConfigPath() == "" || pb.Config.GetConfigFile() == "" { + return nil, nil + } + data = []byte{} } cm := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ diff --git a/topo/node/drivenets/drivenets_test.go b/topo/node/drivenets/drivenets_test.go index a6d1e8a61..6e5157ad1 100644 --- a/topo/node/drivenets/drivenets_test.go +++ b/topo/node/drivenets/drivenets_test.go @@ -1,11 +1,15 @@ package drivenets import ( + "context" "testing" "github.com/openconfig/gnmi/errdiff" tpb "github.com/openconfig/kne/proto/topo" "github.com/openconfig/kne/topo/node" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" ) func TestNew(t *testing.T) { @@ -108,6 +112,80 @@ func TestCdnosDefaults(t *testing.T) { } } +// TestCreateConfig_NoDataButPathDefaulted verifies that an empty ConfigMap +// is still created when no startup data is supplied but cdnosDefaults has +// populated ConfigPath/ConfigFile. Without this, the controller-created Pod +// would block forever in PodInitializing on a missing ConfigMap mount. +func TestCreateConfig_NoDataButPathDefaulted(t *testing.T) { + pb := cdnosDefaults(&tpb.Node{Name: "n1"}) + if pb.Config.GetConfigPath() == "" || pb.Config.GetConfigFile() == "" { + t.Fatalf("preconditions: cdnosDefaults should populate ConfigPath and ConfigFile") + } + if pb.Config.GetConfigData() != nil { + t.Fatalf("preconditions: cdnosDefaults should not set startup config data") + } + + kc := fake.NewSimpleClientset() + n := &Node{Impl: &node.Impl{ + Proto: pb, + KubeClient: kc, + Namespace: "ns1", + }} + + vol, err := n.CreateConfig(context.Background()) + if err != nil { + t.Fatalf("CreateConfig returned unexpected error: %v", err) + } + if vol == nil { + t.Fatalf("CreateConfig returned nil volume; controller mount would fail") + } + cm, err := kc.CoreV1().ConfigMaps("ns1").Get(context.Background(), "n1-config", metav1.GetOptions{}) + if err != nil { + t.Fatalf("expected ConfigMap n1-config to exist: %v", err) + } + if _, ok := cm.Data[pb.Config.GetConfigFile()]; !ok { + t.Errorf("ConfigMap missing key %q; have keys: %v", pb.Config.GetConfigFile(), keys(cm.Data)) + } +} + +// TestCreateConfig_NoDataAndNoPath verifies the explicit "no config mount" +// path: when neither ConfigPath nor ConfigFile is set, no ConfigMap is +// created. +func TestCreateConfig_NoDataAndNoPath(t *testing.T) { + pb := &tpb.Node{ + Name: "n2", + Config: &tpb.Config{}, // empty: no Path, no File, no Data + } + kc := fake.NewSimpleClientset() + n := &Node{Impl: &node.Impl{ + Proto: pb, + KubeClient: kc, + Namespace: "ns1", + }} + vol, err := n.CreateConfig(context.Background()) + if err != nil { + t.Fatalf("CreateConfig returned unexpected error: %v", err) + } + if vol != nil { + t.Errorf("expected nil volume when no config is requested, got %+v", vol) + } + cms, _ := kc.CoreV1().ConfigMaps("ns1").List(context.Background(), metav1.ListOptions{}) + if len(cms.Items) != 0 { + t.Errorf("expected zero ConfigMaps, found %d", len(cms.Items)) + } +} + +func keys(m map[string]string) []string { + out := make([]string, 0, len(m)) + for k := range m { + out = append(out, k) + } + return out +} + +// dummy var keeps corev1 imported even if the rest of the file doesn't use it. +var _ = corev1.ConfigMap{} + func TestDefaultNodeConstraints(t *testing.T) { n := &Node{} constraints := n.DefaultNodeConstraints()