feat: purge cluster CSI storage on rebuild
This commit is contained in:
parent
00cb4ead63
commit
def841532b
|
|
@ -26,6 +26,7 @@ var bootstrapRotateWebhookAuthorization bool
|
||||||
var bootstrapMergeBootstrapPR bool
|
var bootstrapMergeBootstrapPR bool
|
||||||
var bootstrapManageNetworkBridges bool
|
var bootstrapManageNetworkBridges bool
|
||||||
var bootstrapEnableDelivery bool
|
var bootstrapEnableDelivery bool
|
||||||
|
var bootstrapDestroyDemocraticCSIStorage bool
|
||||||
|
|
||||||
var upsertOperationalSecret = bootstrap.UpsertOperationalSecret
|
var upsertOperationalSecret = bootstrap.UpsertOperationalSecret
|
||||||
var bootstrapCmd = &cobra.Command{
|
var bootstrapCmd = &cobra.Command{
|
||||||
|
|
@ -51,6 +52,7 @@ func init() {
|
||||||
bootstrapCmd.Flags().BoolVar(&bootstrapMergeBootstrapPR, "merge-bootstrap-pr", false, "Merge the generated Flux repository migration PR before bootstrapping")
|
bootstrapCmd.Flags().BoolVar(&bootstrapMergeBootstrapPR, "merge-bootstrap-pr", false, "Merge the generated Flux repository migration PR before bootstrapping")
|
||||||
bootstrapCmd.Flags().BoolVar(&bootstrapManageNetworkBridges, "manage-network-bridges", false, "Persist Terraform management for existing Talos network bridges")
|
bootstrapCmd.Flags().BoolVar(&bootstrapManageNetworkBridges, "manage-network-bridges", false, "Persist Terraform management for existing Talos network bridges")
|
||||||
bootstrapCmd.Flags().BoolVar(&bootstrapEnableDelivery, "enable-delivery", false, "Resolve delivery defaults and reconcile the configured app delivery source")
|
bootstrapCmd.Flags().BoolVar(&bootstrapEnableDelivery, "enable-delivery", false, "Resolve delivery defaults and reconcile the configured app delivery source")
|
||||||
|
bootstrapCmd.Flags().BoolVar(&bootstrapDestroyDemocraticCSIStorage, "destroy-democratic-csi-storage", false, "Delete only TrueNAS datasets under this cluster's configured Democratic CSI parent during rebuild")
|
||||||
}
|
}
|
||||||
|
|
||||||
func runBootstrap(cmd *cobra.Command, args []string) error {
|
func runBootstrap(cmd *cobra.Command, args []string) error {
|
||||||
|
|
@ -191,7 +193,7 @@ func runBootstrap(cmd *cobra.Command, args []string) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
runner := bootstrap.Runner{Config: cfg, Mode: bootstrap.Mode(bootstrapMode), ConfirmRebuild: bootstrapYes, RegisterWebhook: bootstrapRegisterWebhook, EnableDelivery: bootstrapEnableDelivery}
|
runner := bootstrap.Runner{Config: cfg, Mode: bootstrap.Mode(bootstrapMode), ConfirmRebuild: bootstrapYes, RegisterWebhook: bootstrapRegisterWebhook, EnableDelivery: bootstrapEnableDelivery, DestroyDemocraticCSIStorage: bootstrapDestroyDemocraticCSIStorage}
|
||||||
return runner.Run()
|
return runner.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ type Runner struct {
|
||||||
EnableDelivery bool
|
EnableDelivery bool
|
||||||
SkipDeliveryScaffolding bool
|
SkipDeliveryScaffolding bool
|
||||||
AutoMergeBootstrapMigration bool
|
AutoMergeBootstrapMigration bool
|
||||||
|
DestroyDemocraticCSIStorage bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type operationalSecrets struct {
|
type operationalSecrets struct {
|
||||||
|
|
@ -186,6 +187,9 @@ func (r Runner) Run() error {
|
||||||
if r.Mode, err = resolveLifecycleMode(r.Mode, r.ConfirmRebuild); err != nil {
|
if r.Mode, err = resolveLifecycleMode(r.Mode, r.ConfirmRebuild); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if r.DestroyDemocraticCSIStorage && r.Mode != Rebuild {
|
||||||
|
return errors.New("--destroy-democratic-csi-storage requires --mode=rebuild --yes")
|
||||||
|
}
|
||||||
if r.RegisterWebhook {
|
if r.RegisterWebhook {
|
||||||
return r.reconcileWebhook(filepath.Join(r.Config.Git.CloneParent, r.Config.Talos.RepoDirName, r.Config.Talos.GeneratedDir))
|
return r.reconcileWebhook(filepath.Join(r.Config.Git.CloneParent, r.Config.Talos.RepoDirName, r.Config.Talos.GeneratedDir))
|
||||||
}
|
}
|
||||||
|
|
@ -294,6 +298,11 @@ func (r Runner) Run() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.Config.Talos.AutoRunTerraform {
|
if r.Config.Talos.AutoRunTerraform {
|
||||||
|
if r.DestroyDemocraticCSIStorage {
|
||||||
|
if err := destroyDemocraticCSIStorage(r.Config.DemocraticCSI); err != nil {
|
||||||
|
return fmt.Errorf("destroy Democratic CSI storage: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
if err := r.reconcileTerraform(terraformDir); err != nil {
|
if err := r.reconcileTerraform(terraformDir); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
66
internal/bootstrap/democratic_csi.go
Normal file
66
internal/bootstrap/democratic_csi.go
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
package bootstrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Pingu-Studio/MaidnCLI/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
var democraticCSIHTTPClient = &http.Client{Timeout: 30 * time.Second}
|
||||||
|
|
||||||
|
func destroyDemocraticCSIStorage(csi config.DemocraticCSIConfig) error {
|
||||||
|
parent := strings.Trim(csi.DatasetParentNFS, "/")
|
||||||
|
if parent == "" || strings.Contains(parent, "..") {
|
||||||
|
return fmt.Errorf("invalid Democratic CSI dataset parent")
|
||||||
|
}
|
||||||
|
base := "http://" + strings.TrimPrefix(strings.TrimPrefix(csi.TrueNASHost, "http://"), "https://") + ":80/api/v2.0/pool/dataset"
|
||||||
|
request, err := http.NewRequest(http.MethodGet, base+"?parent="+url.QueryEscape(parent), nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
request.Header.Set("Authorization", "Bearer "+csi.TrueNASAPIKey)
|
||||||
|
response, err := democraticCSIHTTPClient.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
if response.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("list datasets: %s", response.Status)
|
||||||
|
}
|
||||||
|
var datasets []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(response.Body).Decode(&datasets); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
prefix := parent + "/"
|
||||||
|
children := make([]string, 0, len(datasets))
|
||||||
|
for _, dataset := range datasets {
|
||||||
|
if strings.HasPrefix(dataset.Name, prefix) && !strings.Contains(strings.TrimPrefix(dataset.Name, prefix), "/") {
|
||||||
|
children = append(children, dataset.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(children)
|
||||||
|
for _, child := range children {
|
||||||
|
request, err := http.NewRequest(http.MethodDelete, base+"/id/"+url.PathEscape(child)+"?recursive=true&force=true", nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
request.Header.Set("Authorization", "Bearer "+csi.TrueNASAPIKey)
|
||||||
|
response, err := democraticCSIHTTPClient.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
response.Body.Close()
|
||||||
|
if response.StatusCode != http.StatusNoContent {
|
||||||
|
return fmt.Errorf("delete dataset %q: %s", child, response.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
33
internal/bootstrap/democratic_csi_test.go
Normal file
33
internal/bootstrap/democratic_csi_test.go
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
package bootstrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/Pingu-Studio/MaidnCLI/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type roundTripper func(*http.Request) (*http.Response, error)
|
||||||
|
|
||||||
|
func (f roundTripper) RoundTrip(request *http.Request) (*http.Response, error) { return f(request) }
|
||||||
|
|
||||||
|
func TestDestroyDemocraticCSIStorageDeletesOnlyDirectChildren(t *testing.T) {
|
||||||
|
original := democraticCSIHTTPClient
|
||||||
|
t.Cleanup(func() { democraticCSIHTTPClient = original })
|
||||||
|
var deleted []string
|
||||||
|
democraticCSIHTTPClient = &http.Client{Transport: roundTripper(func(request *http.Request) (*http.Response, error) {
|
||||||
|
if request.Method == http.MethodGet {
|
||||||
|
return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(`[{"name":"pool/cluster/one"},{"name":"pool/cluster/one/child"},{"name":"pool/other"}]`))}, nil
|
||||||
|
}
|
||||||
|
deleted = append(deleted, request.URL.EscapedPath())
|
||||||
|
return &http.Response{StatusCode: http.StatusNoContent, Body: io.NopCloser(strings.NewReader(""))}, nil
|
||||||
|
})}
|
||||||
|
if err := destroyDemocraticCSIStorage(config.DemocraticCSIConfig{TrueNASHost: "truenas.test", TrueNASAPIKey: "token", DatasetParentNFS: "pool/cluster"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(deleted) != 1 || !strings.Contains(deleted[0], "pool%2Fcluster%2Fone") {
|
||||||
|
t.Fatalf("deleted %v", deleted)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue