Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions hypershift-operator/controllers/nodepool/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1"
cpomanifests "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/manifests"
kvnetwork "github.com/openshift/hypershift/hypershift-operator/controllers/nodepool/kubevirt"
"github.com/openshift/hypershift/support/api"
"github.com/openshift/hypershift/support/backwardcompat"
"github.com/openshift/hypershift/support/capabilities"
Expand Down Expand Up @@ -204,9 +205,55 @@ func (cg *ConfigGenerator) generateMCORawConfig(ctx context.Context, caps *hyper
configs = append(configs, nodeTuningGeneratedConfigs...)
}

// Generate platform-specific MachineConfigs.
platformConfigs, err := cg.getPlatformConfigs()
if err != nil {
return "", err
}
configs = append(configs, platformConfigs...)

return cg.parse(configs)
}

// getPlatformConfigs returns platform-specific MachineConfig ConfigMaps
// based on the NodePool's platform configuration.
func (cg *ConfigGenerator) getPlatformConfigs() ([]corev1.ConfigMap, error) {
var rawConfig string
var err error

switch cg.nodePool.Spec.Platform.Type {
case hyperv1.KubevirtPlatform:
rawConfig, err = cg.kubevirtPlatformConfig()
}
if err != nil {
return nil, fmt.Errorf("failed to generate platform config: %w", err)
}

if rawConfig == "" {
return nil, nil
}

return []corev1.ConfigMap{
{
Data: map[string]string{
TokenSecretConfigKey: rawConfig,
},
},
}, nil
}

// kubevirtPlatformConfig generates KubeVirt-specific MachineConfig content.
// For NodePools using multus as primary network (AttachDefaultNetwork=false) on
// clusters with IPv6 networking, it generates an override that replaces the
// MCO-rendered nmstate files (which assume the default pod network) with no-op
// content, allowing standard network auto-configuration (SLAAC) to work.
// For every other NodePool nothing is generated, keeping the NodePool config
// hash unchanged so that upgrading the HyperShift operator does not trigger a
// rollout.
func (cg *ConfigGenerator) kubevirtPlatformConfig() (string, error) {
return kvnetwork.GenerateNetworkOverrideMachineConfig(cg.nodePool, cg.hostedCluster.Spec.Networking)
}

// getUserConfigs returns a slice with all the configMaps in nodePool.Spec.Config.
func (cg *ConfigGenerator) getUserConfigs(ctx context.Context) ([]corev1.ConfigMap, error) {
var errors []error
Expand Down
174 changes: 174 additions & 0 deletions hypershift-operator/controllers/nodepool/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/ptr"

crclient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
Expand Down Expand Up @@ -1941,6 +1942,179 @@ spec:
}
}

func TestGetPlatformConfigs(t *testing.T) {
ipv4Networking := hyperv1.ClusterNetworking{
ClusterNetwork: []hyperv1.ClusterNetworkEntry{{CIDR: *ipnet.MustParseCIDR("10.132.0.0/14")}},
ServiceNetwork: []hyperv1.ServiceNetworkEntry{{CIDR: *ipnet.MustParseCIDR("172.31.0.0/16")}},
}
dualStackNetworking := hyperv1.ClusterNetworking{
ClusterNetwork: []hyperv1.ClusterNetworkEntry{
{CIDR: *ipnet.MustParseCIDR("10.132.0.0/14")},
{CIDR: *ipnet.MustParseCIDR("fd01::/48")},
},
ServiceNetwork: []hyperv1.ServiceNetworkEntry{
{CIDR: *ipnet.MustParseCIDR("172.31.0.0/16")},
{CIDR: *ipnet.MustParseCIDR("fd02::/112")},
},
}

testCases := []struct {
name string
nodePool *hyperv1.NodePool
networking hyperv1.ClusterNetworking
expectConfigs bool
}{
{
// No config may ever be generated for default-network NodePools:
// anything emitted here becomes part of the NodePool config hash and
// would trigger a fleet-wide rollout when the HyperShift operator is
// upgraded.
name: "When platform is KubeVirt with default network on a dual-stack cluster it should return no configs to keep the config hash unchanged",
nodePool: &hyperv1.NodePool{
Spec: hyperv1.NodePoolSpec{
Platform: hyperv1.NodePoolPlatform{
Type: hyperv1.KubevirtPlatform,
Kubevirt: &hyperv1.KubevirtNodePoolPlatform{
// AttachDefaultNetwork defaults to true when nil
},
},
},
},
networking: dualStackNetworking,
expectConfigs: false,
},
{
name: "When platform is KubeVirt with multus primary network on an IPv4-only cluster it should return no configs to keep the config hash unchanged",
nodePool: &hyperv1.NodePool{
Spec: hyperv1.NodePoolSpec{
Platform: hyperv1.NodePoolPlatform{
Type: hyperv1.KubevirtPlatform,
Kubevirt: &hyperv1.KubevirtNodePoolPlatform{
AttachDefaultNetwork: ptr.To(false),
AdditionalNetworks: []hyperv1.KubevirtNetwork{
{Name: "ns1/localnet-net"},
},
},
},
},
},
networking: ipv4Networking,
expectConfigs: false,
},
{
name: "When platform is KubeVirt with multus primary network on a dual-stack cluster it should return the override config",
nodePool: &hyperv1.NodePool{
Spec: hyperv1.NodePoolSpec{
Platform: hyperv1.NodePoolPlatform{
Type: hyperv1.KubevirtPlatform,
Kubevirt: &hyperv1.KubevirtNodePoolPlatform{
AttachDefaultNetwork: ptr.To(false),
AdditionalNetworks: []hyperv1.KubevirtNetwork{
{Name: "ns1/localnet-net"},
},
},
},
},
},
networking: dualStackNetworking,
expectConfigs: true,
},
{
name: "When platform is AWS it should return no configs",
nodePool: &hyperv1.NodePool{
Spec: hyperv1.NodePoolSpec{
Platform: hyperv1.NodePoolPlatform{
Type: hyperv1.AWSPlatform,
},
},
},
networking: dualStackNetworking,
expectConfigs: false,
},
{
name: "When platform is KubeVirt with nil Kubevirt spec it should return no configs",
nodePool: &hyperv1.NodePool{
Spec: hyperv1.NodePoolSpec{
Platform: hyperv1.NodePoolPlatform{
Type: hyperv1.KubevirtPlatform,
},
},
},
networking: dualStackNetworking,
expectConfigs: false,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
g := NewWithT(t)

cg := &ConfigGenerator{
nodePool: tc.nodePool,
hostedCluster: &hyperv1.HostedCluster{
Spec: hyperv1.HostedClusterSpec{
Networking: tc.networking,
},
},
}

configs, err := cg.getPlatformConfigs()
g.Expect(err).ToNot(HaveOccurred())

if tc.expectConfigs {
g.Expect(configs).ToNot(BeNil())
g.Expect(configs).To(HaveLen(1))
g.Expect(configs[0].Data).To(HaveKey(TokenSecretConfigKey))
g.Expect(configs[0].Data[TokenSecretConfigKey]).ToNot(BeEmpty())
} else {
g.Expect(configs).To(BeNil())
}
})
}
}

func TestKubevirtPlatformConfig(t *testing.T) {
// The generation matrix (multus/default network, IPv4/dual-stack, non-KubeVirt,
// nil spec) is covered by network_test.go's TestGenerateNetworkOverrideMachineConfig,
// and the ConfigMap wiring is covered by TestGetPlatformConfigs (which calls this
// method). This test only asserts the wiring: the HostedCluster networking is passed
// through, so a multus NodePool on a dual-stack cluster yields the no-op override.
g := NewWithT(t)

cg := &ConfigGenerator{
nodePool: &hyperv1.NodePool{
Spec: hyperv1.NodePoolSpec{
Platform: hyperv1.NodePoolPlatform{
Type: hyperv1.KubevirtPlatform,
Kubevirt: &hyperv1.KubevirtNodePoolPlatform{
AttachDefaultNetwork: ptr.To(false),
AdditionalNetworks: []hyperv1.KubevirtNetwork{
{Name: "ns1/localnet-net"},
},
},
},
},
},
hostedCluster: &hyperv1.HostedCluster{
Spec: hyperv1.HostedClusterSpec{
Networking: hyperv1.ClusterNetworking{
ClusterNetwork: []hyperv1.ClusterNetworkEntry{
{CIDR: *ipnet.MustParseCIDR("10.132.0.0/14")},
{CIDR: *ipnet.MustParseCIDR("fd01::/48")},
},
},
},
},
}

result, err := cg.kubevirtPlatformConfig()
g.Expect(err).ToNot(HaveOccurred())
// The override MachineConfig replacing the MCO-rendered nmstate files is generated.
g.Expect(result).To(ContainSubstring("01-kubevirt-network"))
g.Expect(result).To(ContainSubstring("001-nmstate-disable-ipv6-autoconf"))
g.Expect(result).To(ContainSubstring("002-nmstate-arp-proxy-ipv6-gw"))
}

func TestGlobalConfigString(t *testing.T) {
expectedGlobalConfigStringWhenEmpty := `{"metadata":{"name":"cluster","creationTimestamp":null},"spec":{"trustedCA":{"name":""}},"status":{}}
{"metadata":{"name":"cluster","creationTimestamp":null},"spec":{"additionalTrustedCA":{"name":""},"registrySources":{}},"status":{}}
Expand Down
Loading
Loading