Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
40 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
d8eff57
ref: simplify code
OliverTrautvetter Mar 4, 2026
f2e7e1b
chore(docs): Auto-update docs and licenses
OliverTrautvetter Mar 4, 2026
06569c1
fix: clean
OliverTrautvetter Mar 4, 2026
c9bccc2
Merge branch 'main' into cleanup_bootstrapped_infra
OliverTrautvetter Mar 4, 2026
e5c59a0
chore(docs): Auto-update docs and licenses
OliverTrautvetter Mar 4, 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 @@ -94,6 +94,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
172 changes: 172 additions & 0 deletions cli/cmd/bootstrap_gcp_cleanup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// Copyright (c) Codesphere Inc.
// SPDX-License-Identifier: Apache-2.0

package cmd

import (
"bufio"
"encoding/json"
"fmt"
"log"
"os"
"strings"

"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
}

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()

projectID := c.Opts.ProjectID
var codesphereEnv gcp.CodesphereEnvironment

// If no project ID provided, try to load from infra file
infraFilePath := gcp.GetInfraFilePath()
if projectID == "" {
if fw.Exists(infraFilePath) {
envFileContent, err := fw.ReadFile(infraFilePath)
if err != nil {
return fmt.Errorf("failed to read gcp infra file: %w", err)
}

err = json.Unmarshal(envFileContent, &codesphereEnv)
if err != nil {
return fmt.Errorf("failed to unmarshal gcp infra file: %w", err)
}
projectID = codesphereEnv.ProjectID
log.Printf("Using project ID from infra file: %s", projectID)
} else {
return fmt.Errorf("no project ID provided and no infra file found at %s", infraFilePath)
}
} else if fw.Exists(infraFilePath) {
envFileContent, err := fw.ReadFile(infraFilePath)
if err == nil {
_ = json.Unmarshal(envFileContent, &codesphereEnv)
}
}

// Verify the project was bootstrapped by OMS (skip if --force is used)
if !c.Opts.Force {
isOMSManaged, err := 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)
}
} else {
log.Printf("Skipping OMS-managed verification (--force flag used)")
}

// Confirm deletion unless force flag is set
if !c.Opts.Force {
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(os.Stdin)
confirmation, err := reader.ReadString('\n')
if err != nil {
return fmt.Errorf("failed to read confirmation: %w", err)
}

confirmation = strings.TrimSpace(confirmation)
if confirmation != projectID {
return fmt.Errorf("confirmation did not match project ID, aborting cleanup")
}
}

// Clean up DNS records if infra file has the necessary information
if !c.Opts.SkipDNSCleanup && codesphereEnv.BaseDomain != "" && codesphereEnv.DNSZoneName != "" {
dnsProjectID := codesphereEnv.DNSProjectID
if dnsProjectID == "" {
dnsProjectID = projectID
}

err := stlog.Step("Cleaning up DNS records", func() error {
return gcpClient.DeleteDNSRecordSets(dnsProjectID, codesphereEnv.DNSZoneName, codesphereEnv.BaseDomain)
})
if 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", codesphereEnv.BaseDomain, dnsProjectID)
}
} else if !c.Opts.SkipDNSCleanup && codesphereEnv.BaseDomain == "" {
log.Printf("Skipping DNS cleanup: no infrastructure information available (provide infra file or use --skip-dns-cleanup)")
}

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

// Remove the local infra file if it exists
if fw.Exists(infraFilePath) {
err = os.Remove(infraFilePath)
if err != nil {
log.Printf("Warning: failed to remove local infra file: %v", err)
} else {
log.Printf("Removed local infra file: %s", 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: io.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 beta bootstrap-gcp cleanup

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

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

# Skip DNS record cleanup
oms beta bootstrap-gcp cleanup --skip-dns-cleanup`,
},
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")

cleanup.cmd.RunE = cleanup.RunE
bootstrapGcp.AddCommand(cleanup.cmd)
}
135 changes: 135 additions & 0 deletions cli/cmd/bootstrap_gcp_cleanup_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// Copyright (c) Codesphere Inc.
// SPDX-License-Identifier: Apache-2.0

package cmd_test

import (
"encoding/json"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/spf13/cobra"

"github.com/codesphere-cloud/oms/cli/cmd"
"github.com/codesphere-cloud/oms/internal/bootstrap/gcp"
)

var _ = Describe("BootstrapGcpCleanupCmd", func() {
var (
opts *cmd.BootstrapGcpCleanupOpts
globalOpts *cmd.GlobalOptions
)

BeforeEach(func() {
globalOpts = &cmd.GlobalOptions{}
opts = &cmd.BootstrapGcpCleanupOpts{
GlobalOptions: globalOpts,
ProjectID: "",
Force: false,
SkipDNSCleanup: false,
}
})

Describe("BootstrapGcpCleanupOpts structure", func() {
Context("when initialized", func() {
It("should have correct default values", func() {
Expect(opts.ProjectID).To(Equal(""))
Expect(opts.Force).To(BeFalse())
Expect(opts.SkipDNSCleanup).To(BeFalse())
Expect(opts.GlobalOptions).ToNot(BeNil())
})
})

Context("when flags are set", func() {
It("should store flag values correctly", func() {
opts.ProjectID = "test-project-123"
opts.Force = true
opts.SkipDNSCleanup = true

Expect(opts.ProjectID).To(Equal("test-project-123"))
Expect(opts.Force).To(BeTrue())
Expect(opts.SkipDNSCleanup).To(BeTrue())
})
})
})

Describe("CodesphereEnvironment JSON marshaling", func() {
Context("when environment is complete", func() {
It("should marshal and unmarshal correctly", func() {
env := gcp.CodesphereEnvironment{
ProjectID: "test-project",
BaseDomain: "example.com",
DNSZoneName: "test-zone",
DNSProjectID: "dns-project",
}

data, err := json.Marshal(env)
Expect(err).NotTo(HaveOccurred())

var decoded gcp.CodesphereEnvironment
err = json.Unmarshal(data, &decoded)
Expect(err).NotTo(HaveOccurred())

Expect(decoded.ProjectID).To(Equal("test-project"))
Expect(decoded.BaseDomain).To(Equal("example.com"))
Expect(decoded.DNSZoneName).To(Equal("test-zone"))
Expect(decoded.DNSProjectID).To(Equal("dns-project"))
})
})

Context("when environment is minimal", func() {
It("should handle missing DNS fields", func() {
env := gcp.CodesphereEnvironment{
ProjectID: "test-project",
}

data, err := json.Marshal(env)
Expect(err).NotTo(HaveOccurred())

var decoded gcp.CodesphereEnvironment
err = json.Unmarshal(data, &decoded)
Expect(err).NotTo(HaveOccurred())

Expect(decoded.ProjectID).To(Equal("test-project"))
Expect(decoded.BaseDomain).To(Equal(""))
Expect(decoded.DNSZoneName).To(Equal(""))
})
})
})

Describe("AddBootstrapGcpCleanupCmd", func() {
Context("when adding command", func() {
It("should not panic when adding to parent command", func() {
Expect(func() {
parentCmd := &cobra.Command{
Use: "bootstrap-gcp",
}
cmd.AddBootstrapGcpCleanupCmd(parentCmd, globalOpts)
}).NotTo(Panic())
})

It("should create command with correct flags", func() {
parentCmd := &cobra.Command{
Use: "bootstrap-gcp",
}
cmd.AddBootstrapGcpCleanupCmd(parentCmd, globalOpts)

// Verify the cleanup subcommand was added
cleanupCmd, _, err := parentCmd.Find([]string{"cleanup"})
Expect(err).NotTo(HaveOccurred())
Expect(cleanupCmd).NotTo(BeNil())
Expect(cleanupCmd.Use).To(Equal("cleanup"))

// Verify flags exist
projectIDFlag := cleanupCmd.Flags().Lookup("project-id")
Expect(projectIDFlag).NotTo(BeNil())

forceFlag := cleanupCmd.Flags().Lookup("force")
Expect(forceFlag).NotTo(BeNil())

skipDNSFlag := cleanupCmd.Flags().Lookup("skip-dns-cleanup")
Expect(skipDNSFlag).NotTo(BeNil())
})
})
})
})
1 change: 1 addition & 0 deletions docs/oms-cli_beta_bootstrap-gcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,6 @@ oms-cli beta bootstrap-gcp [flags]
### SEE ALSO

* [oms-cli beta](oms-cli_beta.md) - Commands for early testing
* [oms-cli beta bootstrap-gcp cleanup](oms-cli_beta_bootstrap-gcp_cleanup.md) - Clean up GCP infrastructure created by bootstrap-gcp
* [oms-cli beta bootstrap-gcp postconfig](oms-cli_beta_bootstrap-gcp_postconfig.md) - Run post-configuration steps for GCP bootstrapping

41 changes: 41 additions & 0 deletions docs/oms-cli_beta_bootstrap-gcp_cleanup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
## oms-cli beta bootstrap-gcp cleanup

Clean up GCP infrastructure created by bootstrap-gcp

### Synopsis

Deletes a GCP project that was previously created using the bootstrap-gcp command.

```
oms-cli beta bootstrap-gcp cleanup [flags]
```

### Examples

```
# Clean up using project ID from the local infra file
oms beta bootstrap-gcp cleanup

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

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

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

### Options

```
--force Skip confirmation prompt and OMS-managed check
-h, --help help for cleanup
--project-id string GCP Project ID to delete (optional, will use infra file if not provided)
--skip-dns-cleanup Skip cleaning up DNS records
```

### SEE ALSO

* [oms-cli beta bootstrap-gcp](oms-cli_beta_bootstrap-gcp.md) - Bootstrap GCP infrastructure for Codesphere

Loading
Loading