diff --git a/assets/components/openshift-dns/dns/configmap.yaml b/assets/components/openshift-dns/dns/configmap.yaml index 2b4194e991..8e7cdc3ae0 100644 --- a/assets/components/openshift-dns/dns/configmap.yaml +++ b/assets/components/openshift-dns/dns/configmap.yaml @@ -1,6 +1,7 @@ apiVersion: v1 data: Corefile: | + {{- .C2CCDNSBlocks }} .:5353 { bufsize 1232 errors diff --git a/pkg/components/controllers.go b/pkg/components/controllers.go index ded367dfca..8e14684c1e 100644 --- a/pkg/components/controllers.go +++ b/pkg/components/controllers.go @@ -304,8 +304,12 @@ func startDNSController(ctx context.Context, cfg *config.Config, kubeconfigPath hostsEnabled := cfg.DNS.Hosts.Status == config.HostsStatusEnabled extraParams := assets.RenderParams{ - "ClusterIP": cfg.Network.DNS, - "HostsEnabled": hostsEnabled, + "ClusterIP": cfg.Network.DNS, + "HostsEnabled": hostsEnabled, + "C2CCDNSBlocks": "", + } + if cfg.C2CC.IsEnabled() { + extraParams["C2CCDNSBlocks"] = config.RenderC2CCDNSBlocks(cfg.C2CC.Resolved) } if err := assets.ApplyServices(ctx, svc, renderTemplate, renderParamsFromConfig(cfg, extraParams), kubeconfigPath); err != nil { diff --git a/pkg/config/c2cc.go b/pkg/config/c2cc.go index ff77458eb0..6ee400215f 100644 --- a/pkg/config/c2cc.go +++ b/pkg/config/c2cc.go @@ -39,6 +39,7 @@ type ResolvedRemoteCluster struct { ClusterNetwork []*net.IPNet ServiceNetwork []*net.IPNet Domain string + DNSIP string // 10th IP of ServiceNetwork[0], computed during validation when Domain is set } func (rc *ResolvedRemoteCluster) AllCIDRs() []*net.IPNet { @@ -266,6 +267,15 @@ func validateRemoteCluster( } } + if rc.Domain != "" && len(rc.ServiceNetwork) > 0 { + dnsIP, err := getClusterDNS(rc.ServiceNetwork[0]) + if err != nil { + errs = append(errs, fmt.Errorf("%s: failed to compute DNS IP from serviceNetwork[0] %q: %w", label, rc.ServiceNetwork[0], err)) + } else { + res.DNSIP = dnsIP + } + } + errs = append(errs, validateIPFamilyConsistencyNets(res.ClusterNetwork, label+".clusterNetwork")...) errs = append(errs, validateIPFamilyConsistencyNets(res.ServiceNetwork, label+".serviceNetwork")...) errs = append(errs, validateNetworkShapeNets(res.ClusterNetwork, res.ServiceNetwork, label)...) @@ -345,3 +355,33 @@ func checkCIDRConflicts(cidr *net.IPNet, cidrStr, label string, seenCIDRs []labe func cidrsOverlap(a, b *net.IPNet) bool { return a.Contains(b.IP) || b.Contains(a.IP) } + +// RenderC2CCDNSBlocks generates CoreDNS server blocks for cross-cluster DNS. +func RenderC2CCDNSBlocks(resolved []ResolvedRemoteCluster) string { + var blocks []string + for _, rc := range resolved { + if rc.Domain == "" { + continue + } + blocks = append(blocks, formatDNSBlock(rc.Domain, rc.DNSIP)) + } + if len(blocks) == 0 { + return "" + } + return "\n" + strings.Join(blocks, "\n") +} + +func formatDNSBlock(domain, dnsIP string) string { + return fmt.Sprintf(` %s:5353 { + bufsize 1232 + errors + log . { + class error + } + rewrite stop name suffix .%s .cluster.local answer auto + forward . %s + cache 10 { + denial 9984 10 + } + }`, domain, domain, dnsIP) +} diff --git a/pkg/config/c2cc_test.go b/pkg/config/c2cc_test.go index b215027e96..1423a9fd09 100644 --- a/pkg/config/c2cc_test.go +++ b/pkg/config/c2cc_test.go @@ -2,9 +2,11 @@ package config import ( "net" + "strings" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestC2CC_IsEnabled(t *testing.T) { @@ -541,3 +543,113 @@ func TestC2CC_ValidateDualStack(t *testing.T) { assert.NoError(t, cfg.C2CC.validate(cfg)) }) } + +func TestC2CC_DNSIP(t *testing.T) { + stubHostIPs(t, nil) + + t.Run("DNSIP populated when domain is set", func(t *testing.T) { + cfg := mkC2CCConfig(C2CC{ + RemoteClusters: []RemoteCluster{{ + NextHop: "10.100.0.2", + ClusterNetwork: []string{"10.45.0.0/16"}, + ServiceNetwork: []string{"10.46.0.0/16"}, + Domain: "cluster-b.remote", + }}, + }) + require.NoError(t, cfg.C2CC.validate(cfg)) + assert.Equal(t, "10.46.0.10", cfg.C2CC.Resolved[0].DNSIP) + }) + + t.Run("DNSIP empty when domain is not set", func(t *testing.T) { + cfg := mkC2CCConfig(C2CC{ + RemoteClusters: []RemoteCluster{{ + NextHop: "10.100.0.2", + ClusterNetwork: []string{"10.45.0.0/16"}, + ServiceNetwork: []string{"10.46.0.0/16"}, + }}, + }) + require.NoError(t, cfg.C2CC.validate(cfg)) + assert.Empty(t, cfg.C2CC.Resolved[0].DNSIP) + }) + + t.Run("DNSIP for IPv6 service network", func(t *testing.T) { + cfg := mkIPv6OnlyC2CCConfig(C2CC{ + RemoteClusters: []RemoteCluster{{ + NextHop: "fd00::2", + ClusterNetwork: []string{"fd03::/48"}, + ServiceNetwork: []string{"fd04::/112"}, + Domain: "cluster-b.remote", + }}, + }) + require.NoError(t, cfg.C2CC.validate(cfg)) + assert.Equal(t, "fd04::a", cfg.C2CC.Resolved[0].DNSIP) + }) +} + +func parseCIDR(t *testing.T, s string) *net.IPNet { + t.Helper() + _, ipNet, err := net.ParseCIDR(s) + require.NoError(t, err) + return ipNet +} + +func TestRenderC2CCDNSBlocks(t *testing.T) { + t.Run("no domains configured", func(t *testing.T) { + resolved := []ResolvedRemoteCluster{{ + NextHop: net.ParseIP("10.100.0.2"), + ClusterNetwork: []*net.IPNet{parseCIDR(t, "10.45.0.0/16")}, + ServiceNetwork: []*net.IPNet{parseCIDR(t, "10.46.0.0/16")}, + }} + result := RenderC2CCDNSBlocks(resolved) + assert.Empty(t, result) + }) + + t.Run("single domain", func(t *testing.T) { + resolved := []ResolvedRemoteCluster{{ + NextHop: net.ParseIP("10.100.0.2"), + ClusterNetwork: []*net.IPNet{parseCIDR(t, "10.45.0.0/16")}, + ServiceNetwork: []*net.IPNet{parseCIDR(t, "10.46.0.0/16")}, + Domain: "cluster-b.remote", + DNSIP: "10.46.0.10", + }} + result := RenderC2CCDNSBlocks(resolved) + assert.True(t, strings.HasPrefix(result, "\n"), "result should start with newline for YAML block scalar") + assert.Contains(t, result, "cluster-b.remote:5353") + assert.Contains(t, result, "rewrite stop name suffix .cluster-b.remote .cluster.local answer auto") + assert.Contains(t, result, "forward . 10.46.0.10") + assert.Contains(t, result, "denial 9984 10") + }) + + t.Run("multiple domains", func(t *testing.T) { + resolved := []ResolvedRemoteCluster{ + { + Domain: "cluster-b.remote", + DNSIP: "10.46.0.10", + }, + { + Domain: "cluster-c.remote", + DNSIP: "10.56.0.10", + }, + } + result := RenderC2CCDNSBlocks(resolved) + assert.Contains(t, result, "cluster-b.remote:5353") + assert.Contains(t, result, "forward . 10.46.0.10") + assert.Contains(t, result, "cluster-c.remote:5353") + assert.Contains(t, result, "forward . 10.56.0.10") + }) + + t.Run("mixed domain and no-domain", func(t *testing.T) { + resolved := []ResolvedRemoteCluster{ + { + Domain: "cluster-b.remote", + DNSIP: "10.46.0.10", + }, + { + Domain: "", + }, + } + result := RenderC2CCDNSBlocks(resolved) + assert.Contains(t, result, "cluster-b.remote:5353") + assert.NotContains(t, result, "cluster-c") + }) +} diff --git a/pkg/controllers/c2cc/helpers_test.go b/pkg/controllers/c2cc/helpers_test.go index 605c6251e0..474840488c 100644 --- a/pkg/controllers/c2cc/helpers_test.go +++ b/pkg/controllers/c2cc/helpers_test.go @@ -12,6 +12,7 @@ type testRemoteConfig struct { nextHop string clusterNetwork []string serviceNetwork []string + domain string } func testRemote(nextHop string, clusterNetwork, serviceNetwork []string) testRemoteConfig { @@ -22,6 +23,15 @@ func testRemote(nextHop string, clusterNetwork, serviceNetwork []string) testRem } } +func testRemoteWithDomain(nextHop string, clusterNetwork, serviceNetwork []string, domain string) testRemoteConfig { + return testRemoteConfig{ + nextHop: nextHop, + clusterNetwork: clusterNetwork, + serviceNetwork: serviceNetwork, + domain: domain, + } +} + func testConfigWithRemotes(t *testing.T, remotes ...testRemoteConfig) *config.Config { t.Helper() @@ -32,6 +42,7 @@ func testConfigWithRemotes(t *testing.T, remotes ...testRemoteConfig) *config.Co for _, r := range remotes { resolved := config.ResolvedRemoteCluster{ NextHop: net.ParseIP(r.nextHop), + Domain: r.domain, } require.NotNil(t, resolved.NextHop, "invalid nextHop: %s", r.nextHop) diff --git a/test/assets/c2cc/hello-microshift.yaml b/test/assets/c2cc/hello-microshift.yaml index 6169449b78..34ffcb717f 100644 --- a/test/assets/c2cc/hello-microshift.yaml +++ b/test/assets/c2cc/hello-microshift.yaml @@ -10,10 +10,27 @@ spec: - name: hello-microshift image: quay.io/microshift/busybox:1.36 command: ["/bin/sh"] - args: ["-c", "while true; do echo -ne \"HTTP/1.0 200 OK\r\nContent-Length: 16\r\n\r\nHello MicroShift\" | nc -l -p 8080 ; done"] + args: + - -c + - | + mkdir -p /tmp/www/cgi-bin + cat > /tmp/www/cgi-bin/hello <