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()