Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
9f1eee8
feat: add cleanup command to bootstrap
OliverTrautvetter Feb 3, 2026
f1a09cb
feat: update cleanup command
OliverTrautvetter Feb 10, 2026
539a590
Merge branch 'main' into cleanup_bootstrapped_infra
OliverTrautvetter Feb 10, 2026
382e896
ref: mockery
OliverTrautvetter Feb 10, 2026
9cfbd1e
chore(docs): Auto-update docs and licenses
OliverTrautvetter Feb 10, 2026
f20991b
test: add tests for new cleanup
OliverTrautvetter Feb 10, 2026
5b7220f
Merge branch 'cleanup_bootstrapped_infra' of https://github.com/codes…
OliverTrautvetter Feb 10, 2026
5508d73
ref: enhance cleanup command with dependency injection and error hand…
OliverTrautvetter Feb 10, 2026
4f32c3f
ref: add utility functions for OMS-managed label checks and DNS recor…
OliverTrautvetter Feb 10, 2026
ec12723
ref: enhance GCP cleanup command with project ID handling
OliverTrautvetter Feb 10, 2026
bb57d56
chore(docs): Auto-update docs and licenses
OliverTrautvetter Feb 10, 2026
3e923af
ref: remove redundant file existence check in cleanup command
OliverTrautvetter Feb 10, 2026
f362c2a
fix: error handling in EnableAPIs method for GCPClient
OliverTrautvetter Feb 10, 2026
8f23960
ref: streamline GCP cleanup process with improved infra file handling…
OliverTrautvetter Feb 16, 2026
22df9dd
ref: improve error messages for GCP infra file handling
OliverTrautvetter Feb 16, 2026
e2e1117
Merge branch 'main' into cleanup_bootstrapped_infra
OliverTrautvetter Feb 16, 2026
424cb1e
chore(docs): Auto-update docs and licenses
OliverTrautvetter Feb 16, 2026
af14d9c
fix: improve cleanup to revoke set permissions
OliverTrautvetter Feb 18, 2026
84adde4
feat: enhance GCP cleanup command with DNS settings and error handlin…
OliverTrautvetter Feb 20, 2026
6c12790
Merge remote-tracking branch 'origin/main' into cleanup_bootstrapped_…
OliverTrautvetter Feb 23, 2026
bd05188
fix: merge error
OliverTrautvetter Feb 23, 2026
deef455
chore(docs): Auto-update docs and licenses
OliverTrautvetter Feb 23, 2026
ca4b2c3
ref: consolidate infrastructure file loading into a single function
OliverTrautvetter Feb 23, 2026
9d89e67
Merge branch 'cleanup_bootstrapped_infra' of https://github.com/codes…
OliverTrautvetter Feb 23, 2026
8c95a6c
fix: add DNSProjectID option for GCP cleanup command
OliverTrautvetter Feb 24, 2026
285b82a
chore(docs): Auto-update docs and licenses
OliverTrautvetter Feb 24, 2026
11efdea
Merge branch 'main' into cleanup_bootstrapped_infra
OliverTrautvetter Feb 24, 2026
328063a
fix: typo
OliverTrautvetter Feb 26, 2026
92a9b24
chore(docs): Auto-update docs and licenses
OliverTrautvetter Feb 26, 2026
ae0cca0
Update cli/cmd/bootstrap_gcp_cleanup.go
OliverTrautvetter Feb 26, 2026
94a8952
Update cli/cmd/bootstrap_gcp_cleanup.go
OliverTrautvetter Feb 26, 2026
91018a9
Update internal/bootstrap/gcp/gcp_client_cleanup_test.go
OliverTrautvetter Feb 26, 2026
37b4390
Update cli/cmd/bootstrap_gcp_cleanup_test.go
OliverTrautvetter Feb 26, 2026
8dd311b
fix: cleanup
OliverTrautvetter Feb 26, 2026
a528af5
chore(docs): Auto-update docs and licenses
OliverTrautvetter Feb 26, 2026
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
1 change: 1 addition & 0 deletions cli/cmd/bootstrap_gcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ func AddBootstrapGcpCmd(parent *cobra.Command, opts *GlobalOptions) {

parent.AddCommand(bootstrapGcpCmd.cmd)
AddBootstrapGcpPostconfigCmd(bootstrapGcpCmd.cmd, opts)
AddBootstrapGcpCleanupCmd(bootstrapGcpCmd.cmd, opts)
}

func (c *BootstrapGcpCmd) BootstrapGcp() error {
Expand Down
222 changes: 222 additions & 0 deletions cli/cmd/bootstrap_gcp_cleanup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
// Copyright (c) Codesphere Inc.
// SPDX-License-Identifier: Apache-2.0

package cmd

import (
"bufio"
"fmt"
"io"
"log"
"os"
"strings"

csio "github.com/codesphere-cloud/cs-go/pkg/io"
"github.com/codesphere-cloud/oms/internal/bootstrap"
"github.com/codesphere-cloud/oms/internal/bootstrap/gcp"
"github.com/codesphere-cloud/oms/internal/util"
"github.com/spf13/cobra"
)

type BootstrapGcpCleanupCmd struct {
cmd *cobra.Command
Opts *BootstrapGcpCleanupOpts
}

type BootstrapGcpCleanupOpts struct {
*GlobalOptions
ProjectID string
Force bool
SkipDNSCleanup bool
BaseDomain string
DNSZoneName string
DNSProjectID string
}

type CleanupDeps struct {
GCPClient gcp.GCPClientManager
FileIO util.FileIO
StepLogger *bootstrap.StepLogger
ConfirmReader io.Reader
InfraFilePath string
}

func (c *BootstrapGcpCleanupCmd) RunE(_ *cobra.Command, args []string) error {
ctx := c.cmd.Context()
stlog := bootstrap.NewStepLogger(false)
gcpClient := gcp.NewGCPClient(ctx, stlog, os.Getenv("GOOGLE_APPLICATION_CREDENTIALS"))
fw := util.NewFilesystemWriter()

deps := &CleanupDeps{
GCPClient: gcpClient,
FileIO: fw,
StepLogger: stlog,
ConfirmReader: os.Stdin,
InfraFilePath: gcp.GetInfraFilePath(),
}

return c.ExecuteCleanup(deps)
}

func (c *BootstrapGcpCleanupCmd) confirmDeletion(deps *CleanupDeps, projectID string) error {
log.Printf("WARNING: This will permanently delete the GCP project '%s' and all its resources.", projectID)
log.Printf("This action cannot be undone.\n")
log.Println("Type the project ID to confirm deletion: ")

reader := bufio.NewReader(deps.ConfirmReader)
confirmation, err := reader.ReadString('\n')
if err != nil {
return fmt.Errorf("failed to read confirmation: %w", err)
}
if strings.TrimSpace(confirmation) != projectID {
return fmt.Errorf("confirmation did not match project ID, aborting cleanup")
}
return nil
}

// ExecuteCleanup performs the cleanup operation with the provided dependencies.
func (c *BootstrapGcpCleanupCmd) ExecuteCleanup(deps *CleanupDeps) error {
projectID := c.Opts.ProjectID
infraFileLoaded := false
infraFileExists := false
var infraEnv gcp.CodesphereEnvironment

// Only load infra file if we need information from it (project ID or DNS info)
missingDNSInfo := c.Opts.BaseDomain == "" || c.Opts.DNSZoneName == "" || c.Opts.DNSProjectID == ""
needsInfraFile := projectID == "" || (!c.Opts.SkipDNSCleanup && missingDNSInfo)
if needsInfraFile {
var err error
infraEnv, infraFileExists, err = gcp.LoadInfraFile(deps.FileIO, deps.InfraFilePath)
if err != nil {
if projectID == "" {
return fmt.Errorf("failed to load infra file: %w", err)
}
log.Printf("Warning: %v", err)
infraEnv = gcp.CodesphereEnvironment{}
} else if infraEnv.ProjectID != "" {
infraFileLoaded = true
}
}

// Determine project ID to use
if projectID == "" {
if infraFileExists && infraEnv.ProjectID == "" {
return fmt.Errorf("infra file at %s contains empty project ID", deps.InfraFilePath)
}
if infraEnv.ProjectID == "" {
return fmt.Errorf("no project ID provided and no infra file found at %s", deps.InfraFilePath)
}
projectID = infraEnv.ProjectID
log.Printf("Using project ID from infra file: %s", projectID)
} else if infraFileLoaded && infraEnv.ProjectID != projectID {
log.Printf("Warning: infra file contains project ID '%s' but deleting '%s'; ignoring infra file for DNS cleanup", infraEnv.ProjectID, projectID)
infraEnv = gcp.CodesphereEnvironment{}
infraFileLoaded = false
}

// Apply command-line overrides for DNS settings
baseDomain := c.Opts.BaseDomain
if baseDomain == "" {
baseDomain = infraEnv.BaseDomain
}
dnsZoneName := c.Opts.DNSZoneName
if dnsZoneName == "" {
dnsZoneName = infraEnv.DNSZoneName
}

// Verify project is OMS-managed
if c.Opts.Force {
log.Printf("Skipping OMS-managed verification and deletion confirmation (--force flag used)")
} else {
isOMSManaged, err := deps.GCPClient.IsOMSManagedProject(projectID)
if err != nil {
return fmt.Errorf("failed to verify project: %w", err)
}
if !isOMSManaged {
return fmt.Errorf("project %s was not bootstrapped by OMS (missing 'oms-managed' label). Use --force to override this check", projectID)
}

if err := c.confirmDeletion(deps, projectID); err != nil {
return fmt.Errorf("deletion confirmation failed: %w", err)
}
}

// Clean up DNS records
if !c.Opts.SkipDNSCleanup && baseDomain != "" && dnsZoneName != "" {
dnsProjectID := c.Opts.DNSProjectID
if dnsProjectID == "" {
dnsProjectID = infraEnv.DNSProjectID
}
if dnsProjectID == "" {
dnsProjectID = projectID
}
if err := deps.StepLogger.Step("Cleaning up DNS records", func() error {
return deps.GCPClient.DeleteDNSRecordSets(dnsProjectID, dnsZoneName, baseDomain)
}); err != nil {
log.Printf("Warning: failed to clean up DNS records: %v", err)
log.Printf("You may need to manually delete DNS records for %s in project %s", baseDomain, dnsProjectID)
}
} else if !c.Opts.SkipDNSCleanup && baseDomain == "" {
log.Printf("Skipping DNS cleanup: no base domain available (provide --base-domain or infra file, or use --skip-dns-cleanup)")
}

// Delete the project
if err := deps.StepLogger.Step("Deleting GCP project", func() error {
return deps.GCPClient.DeleteProject(projectID)
}); err != nil {
return fmt.Errorf("failed to delete project: %w", err)
}

// Clean up local infra file only if it matches the deleted project
if infraFileLoaded && infraEnv.ProjectID == projectID {
if err := deps.FileIO.Remove(deps.InfraFilePath); err != nil {
log.Printf("Warning: failed to remove local infra file: %v", err)
} else {
log.Printf("Removed local infra file: %s", deps.InfraFilePath)
}
}

log.Println("\nGCP project cleanup completed successfully!")
log.Printf("Project '%s' has been scheduled for deletion.", projectID)
log.Printf("Note: GCP projects are retained for 30 days before permanent deletion. You can restore the project within this period from the GCP Console.")

return nil
}

func AddBootstrapGcpCleanupCmd(bootstrapGcp *cobra.Command, opts *GlobalOptions) {
cleanup := BootstrapGcpCleanupCmd{
cmd: &cobra.Command{
Use: "cleanup",
Short: "Clean up GCP infrastructure created by bootstrap-gcp",
Long: csio.Long(`Deletes a GCP project that was previously created using the bootstrap-gcp command.`),
Example: ` # Clean up using project ID from the local infra file
oms-cli beta bootstrap-gcp cleanup

# Clean up a specific project
oms-cli beta bootstrap-gcp cleanup --project-id my-project-abc123

# Force cleanup without confirmation (skips OMS-managed check)
oms-cli beta bootstrap-gcp cleanup --project-id my-project-abc123 --force

# Skip DNS record cleanup
oms-cli beta bootstrap-gcp cleanup --skip-dns-cleanup

# Clean up with manual DNS settings (when infra file is not available)
oms-cli beta bootstrap-gcp cleanup --project-id my-project --base-domain example.com --dns-zone-name my-zone --dns-project-id dns-project`,
},
Opts: &BootstrapGcpCleanupOpts{
GlobalOptions: opts,
},
}

flags := cleanup.cmd.Flags()
flags.StringVar(&cleanup.Opts.ProjectID, "project-id", "", "GCP Project ID to delete (optional, will use infra file if not provided)")
flags.BoolVar(&cleanup.Opts.Force, "force", false, "Skip confirmation prompt and OMS-managed check")
flags.BoolVar(&cleanup.Opts.SkipDNSCleanup, "skip-dns-cleanup", false, "Skip cleaning up DNS records")
flags.StringVar(&cleanup.Opts.BaseDomain, "base-domain", "", "Base domain for DNS cleanup (optional, will use infra file if not provided)")
flags.StringVar(&cleanup.Opts.DNSZoneName, "dns-zone-name", "", "DNS zone name for DNS cleanup (optional, will use infra file if not provided)")
flags.StringVar(&cleanup.Opts.DNSProjectID, "dns-project-id", "", "GCP Project ID for DNS zone (optional, will use infra file if not provided)")

cleanup.cmd.RunE = cleanup.RunE
bootstrapGcp.AddCommand(cleanup.cmd)
}
Loading