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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ internal/util/testdata/
config.yaml
prod.vault.yaml
configure-k0s.sh
# ... and their per-data-center variants written by a --multi-dc bootstrap
config-dc*.yaml
prod-dc*.vault.yaml
configure-k0s-dc*.sh

# Debugger files
__debug*
18 changes: 15 additions & 3 deletions cli/cmd/bootstrap_gcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ func AddBootstrapGcpCmd(parent *cobra.Command, opts *util.GlobalOptions) {
flags.BoolVar(&bootstrapGcpCmd.CodesphereEnv.SpotVMs, "spot-vms", false, "Use Spot VMs for Codesphere infrastructure. Falls back to standard VMs if spot capacity unavailable. Mutually exclusive with --preemptible (default: false)")
flags.IntVar(&bootstrapGcpCmd.CodesphereEnv.DatacenterID, "datacenter-id", 1, "Datacenter ID (default: 1)")
flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.DatacenterName, "datacenter-name", "dev", "Datacenter name (default: dev)")
flags.BoolVar(&bootstrapGcpCmd.CodesphereEnv.MultiDC, "multi-dc", false, "Bootstrap two data centers that share one PostgreSQL server but run separate Kubernetes and Ceph clusters. Doubles the Ceph and k0s nodes to 14 VMs (~100 vCPUs) and reserves 6 static IPs, so the region's CPU quota may need raising. Cannot be combined with --datacenter-id. (default: false)")
flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.CustomPgIP, "custom-pg-ip", "", "Custom PostgreSQL IP (optional)")
flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.Region, "region", "europe-west4", "GCP Region (default: europe-west4)")
flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.Zone, "zone", "europe-west4-a", "GCP Zone (default: europe-west4-a)")
Expand Down Expand Up @@ -174,6 +175,8 @@ func (c *BootstrapGcpCmd) BootstrapGcp() error {

c.CodesphereEnv.RegistryType = gcp.RegistryType(c.InputRegistryType)
c.CodesphereEnv.OmsWorkdir = c.Env.GetOmsWorkdir()
// The value alone cannot distinguish the default 1 from an explicit --datacenter-id=1.
c.CodesphereEnv.DatacenterIDExplicit = c.cmd.Flags().Changed("datacenter-id")
if c.CodesphereEnv.GitHubPAT != "" {
c.CodesphereEnv.RegistryType = gcp.RegistryTypeGitHub
if c.CodesphereEnv.RegistryUser == "" {
Expand Down Expand Up @@ -208,18 +211,27 @@ func (c *BootstrapGcpCmd) BootstrapGcp() error {

if bs.Env.InstallVersion != "" {
log.Printf("Access Codesphere in your web browser at https://cs.%s", bs.Env.BaseDomain)
for _, dc := range bs.Env.DataCenters {
log.Printf("Data center %d hosts workspaces under %s", dc.ID, dc.WorkspaceHostingBaseDomain)
}

return nil
}

packageName := "<package-name>-installer"
installCmd := "oms install codesphere -c /etc/codesphere/config.yaml -k /etc/codesphere/secrets/age_key.txt --vault /etc/codesphere/secrets/prod.vault.yaml"
if gcp.RegistryType(bs.Env.RegistryType) == gcp.RegistryTypeGitHub {
log.Printf("You set a GitHub PAT for direct image access. Make sure to use a lite package, as VM root disk sizes are reduced.")
installCmd += " -s load-container-images"
packageName += "-lite"
}
log.Printf("example install command (run from jumpbox):\n%s -p %s.tar.gz", installCmd, packageName)
packageFile := packageName + ".tar.gz"
if len(bs.Env.DataCenters) > 1 {
log.Printf("example install commands (run from jumpbox). Run the data center 1 command to completion first — the other data centers share its database:")
} else {
log.Printf("example install command (run from jumpbox):")
}
for _, dc := range bs.Env.DataCenters {
log.Printf("# data center %d\n%s", dc.ID, bs.InstallCommand(dc, packageFile))
}

return nil
}
33 changes: 32 additions & 1 deletion cli/cmd/bootstrap_gcp_cleanup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,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)

Expand Down
53 changes: 32 additions & 21 deletions cli/cmd/bootstrap_gcp_restart_vms.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,51 +28,61 @@ type BootstrapGcpRestartVMsOpts struct {
Name string
}

// resolveProjectAndZone returns the project ID and zone from flags or the infra file.
// If both flags are set they are used directly; if neither is set, the infra file is read.
// Providing only one of --project-id / --zone is an error.
func (c *BootstrapGcpRestartVMsCmd) resolveProjectAndZone(fw intutil.FileIO) (string, string, error) {
// resolveEnvironment returns the environment to restart VMs in. Project ID and zone come from
// the flags or, when neither is set, from the infra file. Providing only one of
// --project-id / --zone is an error.
//
// The data center layout always comes from the infra file, since it determines the VM names. It
// is read best-effort when the flags supply project and zone, in which case a missing file just
// means single-data-center names.
func (c *BootstrapGcpRestartVMsCmd) resolveEnvironment(fw intutil.FileIO) (*gcp.CodesphereEnvironment, error) {
projectID := c.Opts.ProjectID
zone := c.Opts.Zone

if (projectID == "") != (zone == "") {
return "", "", fmt.Errorf("--project-id and --zone must be provided together")
}
if projectID != "" {
return projectID, zone, nil
return nil, fmt.Errorf("--project-id and --zone must be provided together")
}

infraFilePath := gcp.GetInfraFilePath()
infraEnv, exists, err := gcp.LoadInfraFile(fw, infraFilePath)
if err != nil {
return "", "", fmt.Errorf("failed to load infra file: %w", err)
}
if !exists {
return "", "", fmt.Errorf("infra file not found at %s; use --project-id and --zone flags", infraFilePath)
if projectID == "" {
return nil, fmt.Errorf("failed to load infra file: %w", err)
}
log.Printf("Warning: %v", err)
}
if infraEnv.ProjectID == "" || infraEnv.Zone == "" {
return "", "", fmt.Errorf("infra file is missing project ID or zone; use --project-id and --zone flags")

if projectID == "" {
if !exists {
return nil, fmt.Errorf("infra file not found at %s; use --project-id and --zone flags", infraFilePath)
}
if infraEnv.ProjectID == "" || infraEnv.Zone == "" {
return nil, fmt.Errorf("infra file is missing project ID or zone; use --project-id and --zone flags")
}
projectID, zone = infraEnv.ProjectID, infraEnv.Zone
}
return infraEnv.ProjectID, infraEnv.Zone, nil

return &gcp.CodesphereEnvironment{
ProjectID: projectID,
Zone: zone,
MultiDC: infraEnv.MultiDC,
DataCenters: infraEnv.DataCenters,
}, nil
}

func (c *BootstrapGcpRestartVMsCmd) RunE(_ *cobra.Command, _ []string) error {
ctx := c.cmd.Context()
stlog := bootstrap.NewStepLogger(false)
fw := intutil.NewFilesystemWriter()

projectID, zone, err := c.resolveProjectAndZone(fw)
csEnv, err := c.resolveEnvironment(fw)
if err != nil {
return err
}
projectID, zone := csEnv.ProjectID, csEnv.Zone

gcpClient := gcp.NewGCPClient(ctx, stlog, os.Getenv("GOOGLE_APPLICATION_CREDENTIALS"))

csEnv := &gcp.CodesphereEnvironment{
ProjectID: projectID,
Zone: zone,
}

bs, err := gcp.NewGCPBootstrapper(
ctx,
nil, stlog, csEnv, nil, gcpClient, fw, nil, nil, intutil.NewTime(), nil,
Expand Down Expand Up @@ -112,6 +122,7 @@ func AddBootstrapGcpRestartVMsCmd(bootstrapGcp *cobra.Command, opts *util.Global
{Desc: "Restart all VMs using project info from the local infra file"},
{Cmd: "--name jumpbox", Desc: "Restart only the jumpbox VM"},
{Cmd: "--name k0s-1", Desc: "Restart a specific k0s node"},
{Cmd: "--name k0s-1-dc2", Desc: "Restart a node of the second data center of a --multi-dc bootstrap"},
{Cmd: "--project-id my-project --zone us-central1-a", Desc: "Restart all VMs with explicit project and zone"},
{Cmd: "--project-id my-project --zone us-central1-a --name ceph-1", Desc: "Restart a specific VM with explicit project and zone"},
}),
Expand Down
1 change: 1 addition & 0 deletions docs/oms_beta_bootstrap-gcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ oms beta bootstrap-gcp [flags]
--install-version string Codesphere version to install (default: none)
--internal-flags stringArray Internal flags to enable in Codesphere installation (optional) (default [headless-services,vcluster,custom-service-image,ms-in-ls])
--local-trace-endpoint string Endpoint for exporting traces to an in-cluster storage (optional)
--multi-dc Bootstrap two data centers that share one PostgreSQL server but run separate Kubernetes and Ceph clusters. Doubles the Ceph and k0s nodes to 14 VMs (~100 vCPUs) and reserves 6 static IPs, so the region's CPU quota may need raising. Cannot be combined with --datacenter-id. (default: false)
--oidc-client-id string OIDC OAuth provider Client ID (optional)
--oidc-client-secret string OIDC OAuth provider Client Secret (optional)
--oidc-issuer-url string OIDC OAuth provider issuer URL (optional)
Expand Down
3 changes: 3 additions & 0 deletions docs/oms_beta_bootstrap-gcp_restart-vms.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ $ oms beta bootstrap-gcp restart-vms --name jumpbox
# Restart a specific k0s node
$ oms beta bootstrap-gcp restart-vms --name k0s-1

# Restart a node of the second data center of a --multi-dc bootstrap
$ oms beta bootstrap-gcp restart-vms --name k0s-1-dc2

# Restart all VMs with explicit project and zone
$ oms beta bootstrap-gcp restart-vms --project-id my-project --zone us-central1-a

Expand Down
15 changes: 14 additions & 1 deletion internal/bootstrap/gcp/cleanup.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,20 @@ func (e *CleanupExecutor) CleanupDNSRecords() error {
log.Printf("Skipping DNS cleanup: missing base domain or DNS zone name (provide --base-domain/--dns-zone-name or use --skip-dns-cleanup)")
return nil
}
return e.Deps.GCPClient.DeleteDNSRecordSets(e.DNSProjectID, e.DNSZoneName, e.BaseDomain)
return e.Deps.GCPClient.DeleteDNSRecordSets(e.DNSProjectID, e.DNSZoneName, e.dnsRecords())
}

// 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
Expand Down
Loading
Loading