From f7da12c8579e9d3e82c29cb8c6eee954d5cffa0e Mon Sep 17 00:00:00 2001 From: Jona Neef Date: Fri, 31 Jul 2026 15:06:24 +0200 Subject: [PATCH 1/5] fix(gcp): give every data center its own DNS records MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Workspaces resolve per data center, and so does the platform: the frontend asks . for the configuration of the data center a workspace lives in. OMS only created cs. and its wildcard, both pointing at the first data center's gateway, so with more than one data center the second one's endpoint resolved to the first's gateway, which has no route for that host — every browser request for it was reset, and since the frontend fetches that config before rendering, the whole UI failed. EnsureDNSRecords now creates, per data center, its workspace hosting names and SSH proxy name pointing at its own public gateway and SSH proxy, plus .cs. and its wildcard pointing at its own platform gateway. The per-data-center platform names are only created when there is more than one data center: a single one is the primary, which cs. already resolves to. The records that were created are recorded in the infra file, so cleanup deletes exactly those. DeleteDNSRecordSets therefore takes the record list instead of a base domain, and cleanup falls back to deriving the names for infra files written before this and for a cleanup driven only by --project-id. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Jona Neef --- cli/cmd/bootstrap_gcp_cleanup_test.go | 33 +++++- internal/bootstrap/datacenter/datacenter.go | 10 ++ internal/bootstrap/gcp/cleanup.go | 21 +++- internal/bootstrap/gcp/datacenter_test.go | 27 +++++ internal/bootstrap/gcp/gcp.go | 118 +++++++++++++------- internal/bootstrap/gcp/gcp_client.go | 8 +- internal/bootstrap/gcp/gcp_test.go | 38 +++++++ internal/bootstrap/gcp/mocks.go | 22 ++-- push-multi-dc-stack.sh | 77 +++++++++++++ 9 files changed, 298 insertions(+), 56 deletions(-) create mode 100755 push-multi-dc-stack.sh diff --git a/cli/cmd/bootstrap_gcp_cleanup_test.go b/cli/cmd/bootstrap_gcp_cleanup_test.go index dc4873ef9..4db21e3da 100644 --- a/cli/cmd/bootstrap_gcp_cleanup_test.go +++ b/cli/cmd/bootstrap_gcp_cleanup_test.go @@ -328,7 +328,38 @@ var _ = Describe("BootstrapGcpCleanupCmd", func() { mockFileIO.EXPECT().Exists("/tmp/test-infra.json").Return(true) mockFileIO.EXPECT().ReadFile("/tmp/test-infra.json").Return(envData, nil) - mockGCPClient.EXPECT().DeleteDNSRecordSets("test-project", "test-zone", "example.com").Return(nil) + // An infra file without a recorded record list predates multi-DC support, so + // cleanup falls back to the single-data-center record names. + mockGCPClient.EXPECT().DeleteDNSRecordSets("test-project", "test-zone", gcp.GetDNSRecordNames("example.com")).Return(nil) + mockGCPClient.EXPECT().DeleteProject("test-project").Return(nil) + mockFileIO.EXPECT().Remove("/tmp/test-infra.json").Return(nil) + + err := cleanupCmd.ExecuteCleanup(deps) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Context("when the infra file recorded the DNS records it created", func() { + It("should delete exactly those records", func() { + cleanupCmd.Opts.ProjectID = "test-project" + cleanupCmd.Opts.Force = true + + recorded := []gcp.DNSRecordName{ + {Name: "cs.example.com.", Rtype: "A"}, + {Name: "2.ws.example.com.", Rtype: "A"}, + } + validEnv := gcp.CodesphereEnvironment{ + ProjectID: "test-project", + BaseDomain: "example.com", + DNSZoneName: "test-zone", + MultiDC: true, + DNSRecords: recorded, + } + envData, _ := json.Marshal(validEnv) + + mockFileIO.EXPECT().Exists("/tmp/test-infra.json").Return(true) + mockFileIO.EXPECT().ReadFile("/tmp/test-infra.json").Return(envData, nil) + mockGCPClient.EXPECT().DeleteDNSRecordSets("test-project", "test-zone", recorded).Return(nil) mockGCPClient.EXPECT().DeleteProject("test-project").Return(nil) mockFileIO.EXPECT().Remove("/tmp/test-infra.json").Return(nil) diff --git a/internal/bootstrap/datacenter/datacenter.go b/internal/bootstrap/datacenter/datacenter.go index 5b9a531ee..1cb152e77 100644 --- a/internal/bootstrap/datacenter/datacenter.go +++ b/internal/bootstrap/datacenter/datacenter.go @@ -79,6 +79,16 @@ func (dc *DataCenter) RemoteAgeKeyPath() string { return filepath.Join(dc.SecretsDir, "age_key.txt") } +// PlatformDomain returns the host this data center serves its own platform services on. The +// platform derives it from the data center ID and codesphere.domain, which OMS sets to +// cs., and the frontend calls it directly — the browser asks +// .cs. for the config of the data center a workspace lives in. So it has to +// resolve to this data center's platform gateway, not to the primary's, which is what +// cs. and its wildcard point at. +func (dc *DataCenter) PlatformDomain(baseDomain string) string { + return fmt.Sprintf("%d.cs.%s", dc.ID, baseDomain) +} + // K0sConfigScriptPath returns the local filename of this data center's k0s configuration script. func (dc *DataCenter) K0sConfigScriptPath() string { return fmt.Sprintf("configure-k0s%s.sh", dc.Suffix) diff --git a/internal/bootstrap/gcp/cleanup.go b/internal/bootstrap/gcp/cleanup.go index c29a464bc..5789954a3 100644 --- a/internal/bootstrap/gcp/cleanup.go +++ b/internal/bootstrap/gcp/cleanup.go @@ -199,7 +199,26 @@ func (e *CleanupExecutor) CleanupDNSRecords() error { return nil } - return e.Deps.GCPClient.DeleteDNSRecordSets(e.DNSProjectID, e.DNSZoneName, e.BaseDomain) + if err := e.Deps.GCPClient.DeleteDNSRecordSets(e.DNSProjectID, e.DNSZoneName, e.dnsRecords()); err != nil { + return fmt.Errorf("failed to delete DNS record sets: %w", err) + } + + return nil +} + +// dnsRecords returns the DNS records to delete. The bootstrap records what it created in the +// infra file, which is authoritative. Older infra files predate that, and a cleanup driven only +// by --project-id has no infra file at all, so both fall back to deriving the names. +func (e *CleanupExecutor) dnsRecords() []DNSRecordName { + if len(e.InfraEnv.DNSRecords) > 0 { + return e.InfraEnv.DNSRecords + } + + if len(e.InfraEnv.DataCenters) > 0 { + return DataCenterDNSRecordNames(e.BaseDomain, e.InfraEnv.DataCenters) + } + + return GetDNSRecordNames(e.BaseDomain) } // RemoveDNSIAMBinding removes the cloud-controller service account's IAM binding diff --git a/internal/bootstrap/gcp/datacenter_test.go b/internal/bootstrap/gcp/datacenter_test.go index fd44e48f9..a2ad873b9 100644 --- a/internal/bootstrap/gcp/datacenter_test.go +++ b/internal/bootstrap/gcp/datacenter_test.go @@ -99,3 +99,30 @@ var _ = Describe("BuildDataCenters", func() { Expect(gcp.BuildDataCenters(env)[0].Name).To(Equal("dev")) }) }) + +var _ = Describe("DataCenterDNSRecordNames", func() { + It("returns the single-DC records for one data center", func() { + dcs := gcp.BuildDataCenters(&gcp.CodesphereEnvironment{BaseDomain: "example.com"}) + + Expect(gcp.DataCenterDNSRecordNames("example.com", dcs)).To(ConsistOf(gcp.GetDNSRecordNames("example.com"))) + }) + + It("shares the platform names and scopes the workspace names per data center", func() { + dcs := gcp.BuildDataCenters(&gcp.CodesphereEnvironment{MultiDC: true, BaseDomain: "example.com"}) + + Expect(gcp.DataCenterDNSRecordNames("example.com", dcs)).To(Equal([]gcp.DNSRecordName{ + {Name: "cs.example.com.", Rtype: "A"}, + {Name: "*.cs.example.com.", Rtype: "A"}, + {Name: "1.ws.example.com.", Rtype: "A"}, + {Name: "*.1.ws.example.com.", Rtype: "A"}, + {Name: "*.1.ssh.cs.example.com.", Rtype: "A"}, + {Name: "1.cs.example.com.", Rtype: "A"}, + {Name: "*.1.cs.example.com.", Rtype: "A"}, + {Name: "2.ws.example.com.", Rtype: "A"}, + {Name: "*.2.ws.example.com.", Rtype: "A"}, + {Name: "*.2.ssh.cs.example.com.", Rtype: "A"}, + {Name: "2.cs.example.com.", Rtype: "A"}, + {Name: "*.2.cs.example.com.", Rtype: "A"}, + })) + }) +}) diff --git a/internal/bootstrap/gcp/gcp.go b/internal/bootstrap/gcp/gcp.go index 16c828823..1b771f905 100644 --- a/internal/bootstrap/gcp/gcp.go +++ b/internal/bootstrap/gcp/gcp.go @@ -60,15 +60,17 @@ func CheckOMSManagedLabel(labels map[string]string) bool { return exists && value == "true" } -// GetDNSRecordNames returns the DNS record names that OMS creates for a given base domain. -func GetDNSRecordNames(baseDomain string) []struct { - Name string - Rtype string -} { - return []struct { - Name string - Rtype string - }{ +// DNSRecordName identifies a DNS record set that OMS manages. +type DNSRecordName struct { + Name string `json:"name"` + Rtype string `json:"rtype"` +} + +// GetDNSRecordNames returns the DNS record names a single-data-center bootstrap creates for a +// given base domain. It is the fallback for infra files written before multi-DC support, which +// do not record the created records. +func GetDNSRecordNames(baseDomain string) []DNSRecordName { + return []DNSRecordName{ {fmt.Sprintf("cs.%s.", baseDomain), "A"}, {fmt.Sprintf("*.cs.%s.", baseDomain), "A"}, {fmt.Sprintf("ws.%s.", baseDomain), "A"}, @@ -77,6 +79,30 @@ func GetDNSRecordNames(baseDomain string) []struct { } } +// DataCenterDNSRecordNames returns every DNS record OMS creates for the given data center +// layout: the shared platform gateway names plus each data center's workspace and SSH names. +func DataCenterDNSRecordNames(baseDomain string, dcs []*datacenter.DataCenter) []DNSRecordName { + records := []DNSRecordName{ + {fmt.Sprintf("cs.%s.", baseDomain), "A"}, + {fmt.Sprintf("*.cs.%s.", baseDomain), "A"}, + } + for _, dc := range dcs { + records = append(records, + DNSRecordName{fmt.Sprintf("%s.", dc.WorkspaceHostingBaseDomain), "A"}, + DNSRecordName{fmt.Sprintf("*.%s.", dc.WorkspaceHostingBaseDomain), "A"}, + DNSRecordName{fmt.Sprintf("*.%s.", dc.SSHBaseDomain), "A"}, + ) + if len(dcs) > 1 { + records = append(records, + DNSRecordName{fmt.Sprintf("%s.", dc.PlatformDomain(baseDomain)), "A"}, + DNSRecordName{fmt.Sprintf("*.%s.", dc.PlatformDomain(baseDomain)), "A"}, + ) + } + } + + return records +} + // This should ALWAYS be empty. Internal flags are for internal feature // development and not intended for customer use. // Atm. it's not empty as the internal flags below are likely preview or @@ -152,6 +178,9 @@ type CodesphereEnvironment struct { MultiDC bool `json:"multi_dc"` // DataCenters holds the per-data-center state. It always has at least one entry. DataCenters []*datacenter.DataCenter `json:"datacenters"` + // DNSRecords records the DNS records the bootstrap created, so cleanup deletes exactly + // those instead of recomputing the list. + DNSRecords []DNSRecordName `json:"dns_records,omitempty"` // ControlPlaneNodes and CephNodes are where the primary data center's nodes lived before // multi-DC support. The steps that have not been migrated to DataCenters yet still use // them, and infra files written by an earlier OMS carry the nodes here. @@ -1026,6 +1055,10 @@ func (b *GCPBootstrapper) EnsureHostsConfigured() error { } func (b *GCPBootstrapper) EnsureDNSRecords() error { + if err := b.ensureDataCenters(); err != nil { + return err + } + gcpProject := b.Env.DNSProjectID if b.Env.DNSProjectID == "" { gcpProject = b.Env.ProjectID @@ -1038,37 +1071,30 @@ func (b *GCPBootstrapper) EnsureDNSRecords() error { return fmt.Errorf("failed to ensure DNS managed zone: %w", err) } + // The platform is served from one domain shared by all data centers, pointing at the + // primary data center's gateway. records := []*dns.ResourceRecordSet{ - { - Name: fmt.Sprintf("cs.%s.", b.Env.BaseDomain), - Type: "A", - Ttl: 300, - Rrdatas: []string{b.Env.GatewayIP}, - }, - { - Name: fmt.Sprintf("*.cs.%s.", b.Env.BaseDomain), - Type: "A", - Ttl: 300, - Rrdatas: []string{b.Env.GatewayIP}, - }, - { - Name: fmt.Sprintf("*.ws.%s.", b.Env.BaseDomain), - Type: "A", - Ttl: 300, - Rrdatas: []string{b.Env.PublicGatewayIP}, - }, - { - Name: fmt.Sprintf("ws.%s.", b.Env.BaseDomain), - Type: "A", - Ttl: 300, - Rrdatas: []string{b.Env.PublicGatewayIP}, - }, - { - Name: fmt.Sprintf("*.ssh.cs.%s.", b.Env.BaseDomain), - Type: "A", - Ttl: 300, - Rrdatas: []string{b.Env.SshProxyIP}, - }, + dnsARecord(fmt.Sprintf("cs.%s.", b.Env.BaseDomain), b.primaryDC().GatewayIP), + dnsARecord(fmt.Sprintf("*.cs.%s.", b.Env.BaseDomain), b.primaryDC().GatewayIP), + } + // Workspaces and their SSH endpoints resolve per data center, so each one gets its own + // names pointing at its own public gateway and SSH proxy. + for _, dc := range b.Env.DataCenters { + records = append(records, + dnsARecord(fmt.Sprintf("%s.", dc.WorkspaceHostingBaseDomain), dc.PublicGatewayIP), + dnsARecord(fmt.Sprintf("*.%s.", dc.WorkspaceHostingBaseDomain), dc.PublicGatewayIP), + dnsARecord(fmt.Sprintf("*.%s.", dc.SSHBaseDomain), dc.SSHProxyIP), + ) + // The platform calls each data center's own services at .cs., which + // the wildcard above would send to the primary data center's gateway. A single data + // center is that primary, so it needs no record of its own. + if len(b.Env.DataCenters) > 1 { + platformDomain := dc.PlatformDomain(b.Env.BaseDomain) + records = append(records, + dnsARecord(fmt.Sprintf("%s.", platformDomain), dc.GatewayIP), + dnsARecord(fmt.Sprintf("*.%s.", platformDomain), dc.GatewayIP), + ) + } } err = b.GCPClient.EnsureDNSRecordSets(gcpProject, zoneName, records) @@ -1076,9 +1102,23 @@ func (b *GCPBootstrapper) EnsureDNSRecords() error { return fmt.Errorf("failed to ensure DNS record sets: %w", err) } + // Record what was created so cleanup deletes exactly these records instead of recomputing + // the list from the base domain. + b.Env.DNSRecords = DataCenterDNSRecordNames(b.Env.BaseDomain, b.Env.DataCenters) + return nil } +// dnsARecord builds a short-TTL A record set, as used during initial setup. +func dnsARecord(name, ip string) *dns.ResourceRecordSet { + return &dns.ResourceRecordSet{ + Name: name, + Type: "A", + Ttl: 300, + Rrdatas: []string{ip}, + } +} + // InstallCodesphere installs Codesphere into every data center from the shared jumpbox, in // ascending data center order. The order matters: the primary data center's install creates the // database, roles and schema that the secondary ones reuse. diff --git a/internal/bootstrap/gcp/gcp_client.go b/internal/bootstrap/gcp/gcp_client.go index 4e41c6ebc..a2ed445bf 100644 --- a/internal/bootstrap/gcp/gcp_client.go +++ b/internal/bootstrap/gcp/gcp_client.go @@ -63,7 +63,7 @@ type GCPClientManager interface { GetAddress(projectID, region, addressName string) (*computepb.Address, error) EnsureDNSManagedZone(projectID, zoneName, dnsName, description string) error EnsureDNSRecordSets(projectID, zoneName string, records []*dns.ResourceRecordSet) error - DeleteDNSRecordSets(projectID, zoneName, baseDomain string) error + DeleteDNSRecordSets(projectID, zoneName string, records []DNSRecordName) error CreatePublicCAExternalAccountKey(projectID string) (keyID, b64MacKey string, err error) EnsureStorageBucket(projectID, bucketName, location string) error CreateHMACKey(projectID, serviceAccountEmail string) (accessID, secret string, err error) @@ -877,8 +877,8 @@ func (c *GCPClient) EnsureDNSRecordSets(projectID, zoneName string, records []*d return nil } -// DeleteDNSRecordSets deletes DNS record sets created by OMS for the given base domain. -func (c *GCPClient) DeleteDNSRecordSets(projectID, zoneName, baseDomain string) error { +// DeleteDNSRecordSets deletes the given DNS record sets, ignoring those that no longer exist. +func (c *GCPClient) DeleteDNSRecordSets(projectID, zoneName string, records []DNSRecordName) error { service, err := dns.NewService(c.ctx) if err != nil { return fmt.Errorf("failed to create DNS service: %w", err) @@ -886,7 +886,7 @@ func (c *GCPClient) DeleteDNSRecordSets(projectID, zoneName, baseDomain string) var deletions []*dns.ResourceRecordSet - for _, record := range GetDNSRecordNames(baseDomain) { + for _, record := range records { existing, err := service.ResourceRecordSets.Get(projectID, zoneName, record.Name, record.Rtype).Context(c.ctx).Do() if IsNotFoundError(err) { continue diff --git a/internal/bootstrap/gcp/gcp_test.go b/internal/bootstrap/gcp/gcp_test.go index e498ad478..b37564107 100644 --- a/internal/bootstrap/gcp/gcp_test.go +++ b/internal/bootstrap/gcp/gcp_test.go @@ -1485,6 +1485,44 @@ var _ = Describe("GCP Bootstrapper", func() { err := bs.EnsureDNSRecords() Expect(err).NotTo(HaveOccurred()) }) + + It("points each data center's platform host at its own gateway", func() { + bs.Env.DataCenters = []*datacenter.DataCenter{ + { + ID: 1, GatewayIP: "1.1.1.1", PublicGatewayIP: "1.1.1.2", SSHProxyIP: "1.1.1.3", + WorkspaceHostingBaseDomain: "1.ws.example.com", SSHBaseDomain: "1.ssh.cs.example.com", + }, + { + ID: 2, Suffix: "-dc2", GatewayIP: "2.2.2.1", PublicGatewayIP: "2.2.2.2", SSHProxyIP: "2.2.2.3", + WorkspaceHostingBaseDomain: "2.ws.example.com", SSHBaseDomain: "2.ssh.cs.example.com", + }, + } + + gc.EXPECT().EnsureDNSManagedZone(csEnv.DNSProjectID, csEnv.DNSZoneName, csEnv.BaseDomain+".", mock.Anything).Return(nil) + + targets := map[string]string{} + + gc.EXPECT().EnsureDNSRecordSets(csEnv.DNSProjectID, csEnv.DNSZoneName, mock.Anything). + RunAndReturn(func(_ string, _ string, records []*dns.ResourceRecordSet) error { + for _, r := range records { + targets[r.Name] = r.Rrdatas[0] + } + + return nil + }) + + Expect(bs.EnsureDNSRecords()).To(Succeed()) + // The shared platform name stays on the primary data center's gateway, but the + // per-data-center platform host the frontend calls resolves to its own gateway. + Expect(targets["cs.example.com."]).To(Equal("1.1.1.1")) + Expect(targets["*.cs.example.com."]).To(Equal("1.1.1.1")) + Expect(targets["1.cs.example.com."]).To(Equal("1.1.1.1")) + Expect(targets["2.cs.example.com."]).To(Equal("2.2.2.1")) + Expect(targets["*.2.cs.example.com."]).To(Equal("2.2.2.1")) + // Workspaces and SSH keep pointing at the public gateway and SSH proxy. + Expect(targets["2.ws.example.com."]).To(Equal("2.2.2.2")) + Expect(targets["*.2.ssh.cs.example.com."]).To(Equal("2.2.2.3")) + }) }) Describe("Invalid cases", func() { diff --git a/internal/bootstrap/gcp/mocks.go b/internal/bootstrap/gcp/mocks.go index 7fff6c97f..8ce8dff21 100644 --- a/internal/bootstrap/gcp/mocks.go +++ b/internal/bootstrap/gcp/mocks.go @@ -868,16 +868,16 @@ func (_c *MockGCPClientManager_CreateVPC_Call) RunAndReturn(run func(projectID s } // DeleteDNSRecordSets provides a mock function for the type MockGCPClientManager -func (_mock *MockGCPClientManager) DeleteDNSRecordSets(projectID string, zoneName string, baseDomain string) error { - ret := _mock.Called(projectID, zoneName, baseDomain) +func (_mock *MockGCPClientManager) DeleteDNSRecordSets(projectID string, zoneName string, records []DNSRecordName) error { + ret := _mock.Called(projectID, zoneName, records) if len(ret) == 0 { panic("no return value specified for DeleteDNSRecordSets") } var r0 error - if returnFunc, ok := ret.Get(0).(func(string, string, string) error); ok { - r0 = returnFunc(projectID, zoneName, baseDomain) + if returnFunc, ok := ret.Get(0).(func(string, string, []DNSRecordName) error); ok { + r0 = returnFunc(projectID, zoneName, records) } else { r0 = ret.Error(0) } @@ -892,12 +892,12 @@ type MockGCPClientManager_DeleteDNSRecordSets_Call struct { // DeleteDNSRecordSets is a helper method to define mock.On call // - projectID string // - zoneName string -// - baseDomain string -func (_e *MockGCPClientManager_Expecter) DeleteDNSRecordSets(projectID any, zoneName any, baseDomain any) *MockGCPClientManager_DeleteDNSRecordSets_Call { - return &MockGCPClientManager_DeleteDNSRecordSets_Call{Call: _e.mock.On("DeleteDNSRecordSets", projectID, zoneName, baseDomain)} +// - records []DNSRecordName +func (_e *MockGCPClientManager_Expecter) DeleteDNSRecordSets(projectID any, zoneName any, records any) *MockGCPClientManager_DeleteDNSRecordSets_Call { + return &MockGCPClientManager_DeleteDNSRecordSets_Call{Call: _e.mock.On("DeleteDNSRecordSets", projectID, zoneName, records)} } -func (_c *MockGCPClientManager_DeleteDNSRecordSets_Call) Run(run func(projectID string, zoneName string, baseDomain string)) *MockGCPClientManager_DeleteDNSRecordSets_Call { +func (_c *MockGCPClientManager_DeleteDNSRecordSets_Call) Run(run func(projectID string, zoneName string, records []DNSRecordName)) *MockGCPClientManager_DeleteDNSRecordSets_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { @@ -907,9 +907,9 @@ func (_c *MockGCPClientManager_DeleteDNSRecordSets_Call) Run(run func(projectID if args[1] != nil { arg1 = args[1].(string) } - var arg2 string + var arg2 []DNSRecordName if args[2] != nil { - arg2 = args[2].(string) + arg2 = args[2].([]DNSRecordName) } run( arg0, @@ -925,7 +925,7 @@ func (_c *MockGCPClientManager_DeleteDNSRecordSets_Call) Return(err error) *Mock return _c } -func (_c *MockGCPClientManager_DeleteDNSRecordSets_Call) RunAndReturn(run func(projectID string, zoneName string, baseDomain string) error) *MockGCPClientManager_DeleteDNSRecordSets_Call { +func (_c *MockGCPClientManager_DeleteDNSRecordSets_Call) RunAndReturn(run func(projectID string, zoneName string, records []DNSRecordName) error) *MockGCPClientManager_DeleteDNSRecordSets_Call { _c.Call.Return(run) return _c } diff --git a/push-multi-dc-stack.sh b/push-multi-dc-stack.sh new file mode 100755 index 000000000..5a7a65f80 --- /dev/null +++ b/push-multi-dc-stack.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# Force-push the rebased multi-dc stack (06..11) and retarget each PR onto its new base. +# The stack was rebased onto main after multi-dc-05 was squash-merged as #627. +# +# Usage: +# ./push-multi-dc-stack.sh --dry-run # verify and print what would happen +# ./push-multi-dc-stack.sh # push and retarget +# +# Written for bash 3.2 (the /bin/bash macOS ships), so no associative arrays. +set -eu + +cd "$(dirname "$0")" + +DRY_RUN=0 +if [ "${1:-}" = "--dry-run" ]; then + DRY_RUN=1 +fi + +run() { + if [ "$DRY_RUN" -eq 1 ]; then + echo " would run: $*" + else + "$@" + fi +} + +# branch:expected-tip:pr-base — in stack order, bottom first. +STACK=" +multi-dc-06-per-datacenter-dns:4923161:main +multi-dc-07-thread-datacenter-config:79b6465:multi-dc-06-per-datacenter-dns +multi-dc-08-shared-registry:d333fe2:multi-dc-07-thread-datacenter-config +multi-dc-09-multi-dc-bootstrap:2e8f44b:multi-dc-08-shared-registry +multi-dc-10-multi-dc-tests-docs:c170bc4:multi-dc-09-multi-dc-bootstrap +multi-dc-11-drop-legacy-env-mirror:0c170b3:multi-dc-10-multi-dc-tests-docs +" + +echo "==> Verifying local state" +prev=main +echo "$STACK" | while IFS=: read -r branch want base; do + [ -n "$branch" ] || continue + + have=$(git rev-parse --short "$branch" 2>/dev/null || echo MISSING) + if [ "$have" != "$want" ]; then + echo "ABORT: $branch is at $have, expected $want" >&2 + exit 1 + fi + + # Each branch must contain the one below it, or the PRs show each other's commits. + if ! git merge-base --is-ancestor "$base" "$branch"; then + echo "ABORT: $branch does not contain $base" >&2 + exit 1 + fi + + printf " %-38s %s on %s\n" "$branch" "$have" "$base" +done + +echo "==> Pushing (force-with-lease), bottom of the stack first" +echo "$STACK" | while IFS=: read -r branch want base; do + [ -n "$branch" ] || continue + echo "--- $branch" + run git push --force-with-lease origin "$branch" +done + +echo "==> Retargeting PR bases" +# multi-dc-05 is merged, so 06 now sits on main; the rest keep their predecessor. +echo "$STACK" | while IFS=: read -r branch want base; do + [ -n "$branch" ] || continue + echo "--- $branch -> base $base" + run gh pr edit "$branch" --base "$base" +done + +echo "==> Done" +if [ "$DRY_RUN" -eq 0 ]; then + gh pr list --state open --search "multi-dc" \ + --json number,headRefName,baseRefName,mergeable \ + --template '{{range .}}{{printf "#%v %s <- %s (%s)\n" .number .baseRefName .headRefName .mergeable}}{{end}}' +fi From 94c6bf9292e702e63a707134b7d04f6c69aced64 Mon Sep 17 00:00:00 2001 From: NJona <25478046+NJona@users.noreply.github.com> Date: Fri, 11 Sep 2026 06:33:46 +0000 Subject: [PATCH 2/5] chore(docs): Auto-update docs and licenses Signed-off-by: NJona <25478046+NJona@users.noreply.github.com> --- push-multi-dc-stack.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/push-multi-dc-stack.sh b/push-multi-dc-stack.sh index 5a7a65f80..cdb767a28 100755 --- a/push-multi-dc-stack.sh +++ b/push-multi-dc-stack.sh @@ -1,4 +1,7 @@ #!/usr/bin/env bash +# Copyright (c) Codesphere Inc. +# SPDX-License-Identifier: Apache-2.0 + # Force-push the rebased multi-dc stack (06..11) and retarget each PR onto its new base. # The stack was rebased onto main after multi-dc-05 was squash-merged as #627. # From 7190264b3ebcc8eaf733487639fd119749296824 Mon Sep 17 00:00:00 2001 From: Jona Neef Date: Wed, 16 Sep 2026 15:35:32 +0200 Subject: [PATCH 3/5] refactor(gcp): move DNS record handling into dns.go and drop stray script Review feedback on #628: the DNS record helpers, EnsureDNSRecords, ensureDnsPermissions and dnsARecord now live in their own file in the gcp package, with the standalone DataCenterDNSRecordNames test next to them. push-multi-dc-stack.sh is a local helper that was committed by accident. Co-Authored-By: Claude Fable 5.1 Signed-off-by: Jona Neef --- internal/bootstrap/gcp/datacenter_test.go | 27 ----- internal/bootstrap/gcp/dns.go | 133 ++++++++++++++++++++++ internal/bootstrap/gcp/dns_test.go | 38 +++++++ internal/bootstrap/gcp/gcp.go | 123 -------------------- push-multi-dc-stack.sh | 80 ------------- 5 files changed, 171 insertions(+), 230 deletions(-) create mode 100644 internal/bootstrap/gcp/dns.go create mode 100644 internal/bootstrap/gcp/dns_test.go delete mode 100755 push-multi-dc-stack.sh diff --git a/internal/bootstrap/gcp/datacenter_test.go b/internal/bootstrap/gcp/datacenter_test.go index a2ad873b9..fd44e48f9 100644 --- a/internal/bootstrap/gcp/datacenter_test.go +++ b/internal/bootstrap/gcp/datacenter_test.go @@ -99,30 +99,3 @@ var _ = Describe("BuildDataCenters", func() { Expect(gcp.BuildDataCenters(env)[0].Name).To(Equal("dev")) }) }) - -var _ = Describe("DataCenterDNSRecordNames", func() { - It("returns the single-DC records for one data center", func() { - dcs := gcp.BuildDataCenters(&gcp.CodesphereEnvironment{BaseDomain: "example.com"}) - - Expect(gcp.DataCenterDNSRecordNames("example.com", dcs)).To(ConsistOf(gcp.GetDNSRecordNames("example.com"))) - }) - - It("shares the platform names and scopes the workspace names per data center", func() { - dcs := gcp.BuildDataCenters(&gcp.CodesphereEnvironment{MultiDC: true, BaseDomain: "example.com"}) - - Expect(gcp.DataCenterDNSRecordNames("example.com", dcs)).To(Equal([]gcp.DNSRecordName{ - {Name: "cs.example.com.", Rtype: "A"}, - {Name: "*.cs.example.com.", Rtype: "A"}, - {Name: "1.ws.example.com.", Rtype: "A"}, - {Name: "*.1.ws.example.com.", Rtype: "A"}, - {Name: "*.1.ssh.cs.example.com.", Rtype: "A"}, - {Name: "1.cs.example.com.", Rtype: "A"}, - {Name: "*.1.cs.example.com.", Rtype: "A"}, - {Name: "2.ws.example.com.", Rtype: "A"}, - {Name: "*.2.ws.example.com.", Rtype: "A"}, - {Name: "*.2.ssh.cs.example.com.", Rtype: "A"}, - {Name: "2.cs.example.com.", Rtype: "A"}, - {Name: "*.2.cs.example.com.", Rtype: "A"}, - })) - }) -}) diff --git a/internal/bootstrap/gcp/dns.go b/internal/bootstrap/gcp/dns.go new file mode 100644 index 000000000..cd82e9141 --- /dev/null +++ b/internal/bootstrap/gcp/dns.go @@ -0,0 +1,133 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package gcp + +import ( + "fmt" + + "github.com/codesphere-cloud/oms/internal/bootstrap/datacenter" + "google.golang.org/api/dns/v1" +) + +// DNSRecordName identifies a DNS record set that OMS manages. +type DNSRecordName struct { + Name string `json:"name"` + Rtype string `json:"rtype"` +} + +// GetDNSRecordNames returns the DNS record names a single-data-center bootstrap creates for a +// given base domain. It is the fallback for infra files written before multi-DC support, which +// do not record the created records. +func GetDNSRecordNames(baseDomain string) []DNSRecordName { + return []DNSRecordName{ + {fmt.Sprintf("cs.%s.", baseDomain), "A"}, + {fmt.Sprintf("*.cs.%s.", baseDomain), "A"}, + {fmt.Sprintf("ws.%s.", baseDomain), "A"}, + {fmt.Sprintf("*.ws.%s.", baseDomain), "A"}, + {fmt.Sprintf("*.ssh.cs.%s.", baseDomain), "A"}, + } +} + +// DataCenterDNSRecordNames returns every DNS record OMS creates for the given data center +// layout: the shared platform gateway names plus each data center's workspace and SSH names. +func DataCenterDNSRecordNames(baseDomain string, dcs []*datacenter.DataCenter) []DNSRecordName { + records := []DNSRecordName{ + {fmt.Sprintf("cs.%s.", baseDomain), "A"}, + {fmt.Sprintf("*.cs.%s.", baseDomain), "A"}, + } + for _, dc := range dcs { + records = append(records, + DNSRecordName{fmt.Sprintf("%s.", dc.WorkspaceHostingBaseDomain), "A"}, + DNSRecordName{fmt.Sprintf("*.%s.", dc.WorkspaceHostingBaseDomain), "A"}, + DNSRecordName{fmt.Sprintf("*.%s.", dc.SSHBaseDomain), "A"}, + ) + if len(dcs) > 1 { + records = append(records, + DNSRecordName{fmt.Sprintf("%s.", dc.PlatformDomain(baseDomain)), "A"}, + DNSRecordName{fmt.Sprintf("*.%s.", dc.PlatformDomain(baseDomain)), "A"}, + ) + } + } + + return records +} + +func (b *GCPBootstrapper) ensureDnsPermissions() error { + dnsProject := b.Env.DNSProjectID + if b.Env.DNSProjectID == "" { + dnsProject = b.Env.ProjectID + } + + err := b.ensureIAMRoleWithRetry(dnsProject, "cloud-controller", b.Env.ProjectID, []string{"roles/dns.admin"}) + if err != nil { + return err + } + + return nil +} + +func (b *GCPBootstrapper) EnsureDNSRecords() error { + if err := b.ensureDataCenters(); err != nil { + return err + } + + gcpProject := b.Env.DNSProjectID + if b.Env.DNSProjectID == "" { + gcpProject = b.Env.ProjectID + } + + zoneName := b.Env.DNSZoneName + + err := b.GCPClient.EnsureDNSManagedZone(gcpProject, zoneName, b.Env.BaseDomain+".", "Codesphere DNS zone") + if err != nil { + return fmt.Errorf("failed to ensure DNS managed zone: %w", err) + } + + // The platform is served from one domain shared by all data centers, pointing at the + // primary data center's gateway. + records := []*dns.ResourceRecordSet{ + dnsARecord(fmt.Sprintf("cs.%s.", b.Env.BaseDomain), b.primaryDC().GatewayIP), + dnsARecord(fmt.Sprintf("*.cs.%s.", b.Env.BaseDomain), b.primaryDC().GatewayIP), + } + // Workspaces and their SSH endpoints resolve per data center, so each one gets its own + // names pointing at its own public gateway and SSH proxy. + for _, dc := range b.Env.DataCenters { + records = append(records, + dnsARecord(fmt.Sprintf("%s.", dc.WorkspaceHostingBaseDomain), dc.PublicGatewayIP), + dnsARecord(fmt.Sprintf("*.%s.", dc.WorkspaceHostingBaseDomain), dc.PublicGatewayIP), + dnsARecord(fmt.Sprintf("*.%s.", dc.SSHBaseDomain), dc.SSHProxyIP), + ) + // The platform calls each data center's own services at .cs., which + // the wildcard above would send to the primary data center's gateway. A single data + // center is that primary, so it needs no record of its own. + if len(b.Env.DataCenters) > 1 { + platformDomain := dc.PlatformDomain(b.Env.BaseDomain) + records = append(records, + dnsARecord(fmt.Sprintf("%s.", platformDomain), dc.GatewayIP), + dnsARecord(fmt.Sprintf("*.%s.", platformDomain), dc.GatewayIP), + ) + } + } + + err = b.GCPClient.EnsureDNSRecordSets(gcpProject, zoneName, records) + if err != nil { + return fmt.Errorf("failed to ensure DNS record sets: %w", err) + } + + // Record what was created so cleanup deletes exactly these records instead of recomputing + // the list from the base domain. + b.Env.DNSRecords = DataCenterDNSRecordNames(b.Env.BaseDomain, b.Env.DataCenters) + + return nil +} + +// dnsARecord builds a short-TTL A record set, as used during initial setup. +func dnsARecord(name, ip string) *dns.ResourceRecordSet { + return &dns.ResourceRecordSet{ + Name: name, + Type: "A", + Ttl: 300, + Rrdatas: []string{ip}, + } +} diff --git a/internal/bootstrap/gcp/dns_test.go b/internal/bootstrap/gcp/dns_test.go new file mode 100644 index 000000000..e5ca43032 --- /dev/null +++ b/internal/bootstrap/gcp/dns_test.go @@ -0,0 +1,38 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package gcp_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/codesphere-cloud/oms/internal/bootstrap/gcp" +) + +var _ = Describe("DataCenterDNSRecordNames", func() { + It("returns the single-DC records for one data center", func() { + dcs := gcp.BuildDataCenters(&gcp.CodesphereEnvironment{BaseDomain: "example.com"}) + + Expect(gcp.DataCenterDNSRecordNames("example.com", dcs)).To(ConsistOf(gcp.GetDNSRecordNames("example.com"))) + }) + + It("shares the platform names and scopes the workspace names per data center", func() { + dcs := gcp.BuildDataCenters(&gcp.CodesphereEnvironment{MultiDC: true, BaseDomain: "example.com"}) + + Expect(gcp.DataCenterDNSRecordNames("example.com", dcs)).To(Equal([]gcp.DNSRecordName{ + {Name: "cs.example.com.", Rtype: "A"}, + {Name: "*.cs.example.com.", Rtype: "A"}, + {Name: "1.ws.example.com.", Rtype: "A"}, + {Name: "*.1.ws.example.com.", Rtype: "A"}, + {Name: "*.1.ssh.cs.example.com.", Rtype: "A"}, + {Name: "1.cs.example.com.", Rtype: "A"}, + {Name: "*.1.cs.example.com.", Rtype: "A"}, + {Name: "2.ws.example.com.", Rtype: "A"}, + {Name: "*.2.ws.example.com.", Rtype: "A"}, + {Name: "*.2.ssh.cs.example.com.", Rtype: "A"}, + {Name: "2.cs.example.com.", Rtype: "A"}, + {Name: "*.2.cs.example.com.", Rtype: "A"}, + })) + }) +}) diff --git a/internal/bootstrap/gcp/gcp.go b/internal/bootstrap/gcp/gcp.go index 1b771f905..5e8b12e40 100644 --- a/internal/bootstrap/gcp/gcp.go +++ b/internal/bootstrap/gcp/gcp.go @@ -24,7 +24,6 @@ import ( "github.com/codesphere-cloud/oms/internal/portal" "github.com/codesphere-cloud/oms/internal/testuser" "github.com/codesphere-cloud/oms/internal/util" - "google.golang.org/api/dns/v1" ) // InstallerArchiveName is the package artifact bootstrapping downloads and installs. @@ -60,49 +59,6 @@ func CheckOMSManagedLabel(labels map[string]string) bool { return exists && value == "true" } -// DNSRecordName identifies a DNS record set that OMS manages. -type DNSRecordName struct { - Name string `json:"name"` - Rtype string `json:"rtype"` -} - -// GetDNSRecordNames returns the DNS record names a single-data-center bootstrap creates for a -// given base domain. It is the fallback for infra files written before multi-DC support, which -// do not record the created records. -func GetDNSRecordNames(baseDomain string) []DNSRecordName { - return []DNSRecordName{ - {fmt.Sprintf("cs.%s.", baseDomain), "A"}, - {fmt.Sprintf("*.cs.%s.", baseDomain), "A"}, - {fmt.Sprintf("ws.%s.", baseDomain), "A"}, - {fmt.Sprintf("*.ws.%s.", baseDomain), "A"}, - {fmt.Sprintf("*.ssh.cs.%s.", baseDomain), "A"}, - } -} - -// DataCenterDNSRecordNames returns every DNS record OMS creates for the given data center -// layout: the shared platform gateway names plus each data center's workspace and SSH names. -func DataCenterDNSRecordNames(baseDomain string, dcs []*datacenter.DataCenter) []DNSRecordName { - records := []DNSRecordName{ - {fmt.Sprintf("cs.%s.", baseDomain), "A"}, - {fmt.Sprintf("*.cs.%s.", baseDomain), "A"}, - } - for _, dc := range dcs { - records = append(records, - DNSRecordName{fmt.Sprintf("%s.", dc.WorkspaceHostingBaseDomain), "A"}, - DNSRecordName{fmt.Sprintf("*.%s.", dc.WorkspaceHostingBaseDomain), "A"}, - DNSRecordName{fmt.Sprintf("*.%s.", dc.SSHBaseDomain), "A"}, - ) - if len(dcs) > 1 { - records = append(records, - DNSRecordName{fmt.Sprintf("%s.", dc.PlatformDomain(baseDomain)), "A"}, - DNSRecordName{fmt.Sprintf("*.%s.", dc.PlatformDomain(baseDomain)), "A"}, - ) - } - } - - return records -} - // This should ALWAYS be empty. Internal flags are for internal feature // development and not intended for customer use. // Atm. it's not empty as the internal flags below are likely preview or @@ -709,20 +665,6 @@ func (b *GCPBootstrapper) validateTelemetryExportParams() error { return nil } -func (b *GCPBootstrapper) ensureDnsPermissions() error { - dnsProject := b.Env.DNSProjectID - if b.Env.DNSProjectID == "" { - dnsProject = b.Env.ProjectID - } - - err := b.ensureIAMRoleWithRetry(dnsProject, "cloud-controller", b.Env.ProjectID, []string{"roles/dns.admin"}) - if err != nil { - return err - } - - return nil -} - func (b *GCPBootstrapper) EnsureVPC() error { networkName := fmt.Sprintf("%s-vpc", b.Env.ProjectID) subnetName := fmt.Sprintf("%s-%s-subnet", b.Env.ProjectID, b.Env.Region) @@ -1054,71 +996,6 @@ func (b *GCPBootstrapper) EnsureHostsConfigured() error { return nil } -func (b *GCPBootstrapper) EnsureDNSRecords() error { - if err := b.ensureDataCenters(); err != nil { - return err - } - - gcpProject := b.Env.DNSProjectID - if b.Env.DNSProjectID == "" { - gcpProject = b.Env.ProjectID - } - - zoneName := b.Env.DNSZoneName - - err := b.GCPClient.EnsureDNSManagedZone(gcpProject, zoneName, b.Env.BaseDomain+".", "Codesphere DNS zone") - if err != nil { - return fmt.Errorf("failed to ensure DNS managed zone: %w", err) - } - - // The platform is served from one domain shared by all data centers, pointing at the - // primary data center's gateway. - records := []*dns.ResourceRecordSet{ - dnsARecord(fmt.Sprintf("cs.%s.", b.Env.BaseDomain), b.primaryDC().GatewayIP), - dnsARecord(fmt.Sprintf("*.cs.%s.", b.Env.BaseDomain), b.primaryDC().GatewayIP), - } - // Workspaces and their SSH endpoints resolve per data center, so each one gets its own - // names pointing at its own public gateway and SSH proxy. - for _, dc := range b.Env.DataCenters { - records = append(records, - dnsARecord(fmt.Sprintf("%s.", dc.WorkspaceHostingBaseDomain), dc.PublicGatewayIP), - dnsARecord(fmt.Sprintf("*.%s.", dc.WorkspaceHostingBaseDomain), dc.PublicGatewayIP), - dnsARecord(fmt.Sprintf("*.%s.", dc.SSHBaseDomain), dc.SSHProxyIP), - ) - // The platform calls each data center's own services at .cs., which - // the wildcard above would send to the primary data center's gateway. A single data - // center is that primary, so it needs no record of its own. - if len(b.Env.DataCenters) > 1 { - platformDomain := dc.PlatformDomain(b.Env.BaseDomain) - records = append(records, - dnsARecord(fmt.Sprintf("%s.", platformDomain), dc.GatewayIP), - dnsARecord(fmt.Sprintf("*.%s.", platformDomain), dc.GatewayIP), - ) - } - } - - err = b.GCPClient.EnsureDNSRecordSets(gcpProject, zoneName, records) - if err != nil { - return fmt.Errorf("failed to ensure DNS record sets: %w", err) - } - - // Record what was created so cleanup deletes exactly these records instead of recomputing - // the list from the base domain. - b.Env.DNSRecords = DataCenterDNSRecordNames(b.Env.BaseDomain, b.Env.DataCenters) - - return nil -} - -// dnsARecord builds a short-TTL A record set, as used during initial setup. -func dnsARecord(name, ip string) *dns.ResourceRecordSet { - return &dns.ResourceRecordSet{ - Name: name, - Type: "A", - Ttl: 300, - Rrdatas: []string{ip}, - } -} - // InstallCodesphere installs Codesphere into every data center from the shared jumpbox, in // ascending data center order. The order matters: the primary data center's install creates the // database, roles and schema that the secondary ones reuse. diff --git a/push-multi-dc-stack.sh b/push-multi-dc-stack.sh deleted file mode 100755 index cdb767a28..000000000 --- a/push-multi-dc-stack.sh +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env bash -# Copyright (c) Codesphere Inc. -# SPDX-License-Identifier: Apache-2.0 - -# Force-push the rebased multi-dc stack (06..11) and retarget each PR onto its new base. -# The stack was rebased onto main after multi-dc-05 was squash-merged as #627. -# -# Usage: -# ./push-multi-dc-stack.sh --dry-run # verify and print what would happen -# ./push-multi-dc-stack.sh # push and retarget -# -# Written for bash 3.2 (the /bin/bash macOS ships), so no associative arrays. -set -eu - -cd "$(dirname "$0")" - -DRY_RUN=0 -if [ "${1:-}" = "--dry-run" ]; then - DRY_RUN=1 -fi - -run() { - if [ "$DRY_RUN" -eq 1 ]; then - echo " would run: $*" - else - "$@" - fi -} - -# branch:expected-tip:pr-base — in stack order, bottom first. -STACK=" -multi-dc-06-per-datacenter-dns:4923161:main -multi-dc-07-thread-datacenter-config:79b6465:multi-dc-06-per-datacenter-dns -multi-dc-08-shared-registry:d333fe2:multi-dc-07-thread-datacenter-config -multi-dc-09-multi-dc-bootstrap:2e8f44b:multi-dc-08-shared-registry -multi-dc-10-multi-dc-tests-docs:c170bc4:multi-dc-09-multi-dc-bootstrap -multi-dc-11-drop-legacy-env-mirror:0c170b3:multi-dc-10-multi-dc-tests-docs -" - -echo "==> Verifying local state" -prev=main -echo "$STACK" | while IFS=: read -r branch want base; do - [ -n "$branch" ] || continue - - have=$(git rev-parse --short "$branch" 2>/dev/null || echo MISSING) - if [ "$have" != "$want" ]; then - echo "ABORT: $branch is at $have, expected $want" >&2 - exit 1 - fi - - # Each branch must contain the one below it, or the PRs show each other's commits. - if ! git merge-base --is-ancestor "$base" "$branch"; then - echo "ABORT: $branch does not contain $base" >&2 - exit 1 - fi - - printf " %-38s %s on %s\n" "$branch" "$have" "$base" -done - -echo "==> Pushing (force-with-lease), bottom of the stack first" -echo "$STACK" | while IFS=: read -r branch want base; do - [ -n "$branch" ] || continue - echo "--- $branch" - run git push --force-with-lease origin "$branch" -done - -echo "==> Retargeting PR bases" -# multi-dc-05 is merged, so 06 now sits on main; the rest keep their predecessor. -echo "$STACK" | while IFS=: read -r branch want base; do - [ -n "$branch" ] || continue - echo "--- $branch -> base $base" - run gh pr edit "$branch" --base "$base" -done - -echo "==> Done" -if [ "$DRY_RUN" -eq 0 ]; then - gh pr list --state open --search "multi-dc" \ - --json number,headRefName,baseRefName,mergeable \ - --template '{{range .}}{{printf "#%v %s <- %s (%s)\n" .number .baseRefName .headRefName .mergeable}}{{end}}' -fi From 14a90136bcb40581f66ac4402c362fb86fecfd1a Mon Sep 17 00:00:00 2001 From: Jona Neef Date: Wed, 16 Sep 2026 15:43:34 +0200 Subject: [PATCH 4/5] fix(gcp): satisfy revive on the moved DNS code Rename ensureDnsPermissions to ensureDNSPermissions and document EnsureDNSRecords; moving them into dns.go put them in the linter's changed-files scope. Co-Authored-By: Claude Fable 5.1 Signed-off-by: Jona Neef --- internal/bootstrap/gcp/dns.go | 7 ++++++- internal/bootstrap/gcp/iam_admin.go | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/internal/bootstrap/gcp/dns.go b/internal/bootstrap/gcp/dns.go index cd82e9141..6d9b1dc00 100644 --- a/internal/bootstrap/gcp/dns.go +++ b/internal/bootstrap/gcp/dns.go @@ -53,7 +53,9 @@ func DataCenterDNSRecordNames(baseDomain string, dcs []*datacenter.DataCenter) [ return records } -func (b *GCPBootstrapper) ensureDnsPermissions() error { +// ensureDNSPermissions grants the cloud-controller service account DNS admin on the project that +// hosts the managed zone, which is the DNS project when one is configured. +func (b *GCPBootstrapper) ensureDNSPermissions() error { dnsProject := b.Env.DNSProjectID if b.Env.DNSProjectID == "" { dnsProject = b.Env.ProjectID @@ -67,6 +69,9 @@ func (b *GCPBootstrapper) ensureDnsPermissions() error { return nil } +// EnsureDNSRecords creates the managed zone and the A records for the platform, every data +// center's workspace and SSH names and, with more than one data center, each one's own platform +// host. It records the created names in the environment so cleanup deletes exactly those. func (b *GCPBootstrapper) EnsureDNSRecords() error { if err := b.ensureDataCenters(); err != nil { return err diff --git a/internal/bootstrap/gcp/iam_admin.go b/internal/bootstrap/gcp/iam_admin.go index 1e1a7306f..6687a40b7 100644 --- a/internal/bootstrap/gcp/iam_admin.go +++ b/internal/bootstrap/gcp/iam_admin.go @@ -239,7 +239,7 @@ func (b *GCPBootstrapper) EnsureIAMRoles() error { return fmt.Errorf("failed to ensure cloud-controller role bindings: %w", err) } - err = b.ensureDnsPermissions() + err = b.ensureDNSPermissions() if err != nil { return fmt.Errorf("failed to ensure DNS permissions: %w", err) } From 143f7275b3758ec11cc1e245b89cef674098928e Mon Sep 17 00:00:00 2001 From: NJona <25478046+NJona@users.noreply.github.com> Date: Tue, 22 Sep 2026 06:22:49 +0000 Subject: [PATCH 5/5] chore(docs): Auto-update docs and licenses Signed-off-by: NJona <25478046+NJona@users.noreply.github.com> --- docs/oms_install_k0s.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/oms_install_k0s.md b/docs/oms_install_k0s.md index 160cf18bb..c3fc12167 100644 --- a/docs/oms_install_k0s.md +++ b/docs/oms_install_k0s.md @@ -48,7 +48,7 @@ $ oms install k0s --no-download -f, --force Force new download and installation -h, --help help for k0s --install-config string Path to Codesphere install-config file (required) - --k0sctl-version string Version of k0sctl to use (default "v0.31.1") + --k0sctl-version string Version of k0sctl to use (default "v0.33.1") --no-download Skip downloading k0s binary -p, --package string Package file (e.g. codesphere-v1.2.3-installer-lite.tar.gz) to load k0s from --ssh-key-path string SSH private key path for remote installation