go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/infra/cluster/main.go (about) 1 package main 2 3 import ( 4 "context" 5 "flag" 6 "fmt" 7 "log/slog" 8 "os/signal" 9 "time" 10 11 container "cloud.google.com/go/container/apiv1" 12 "cloud.google.com/go/container/apiv1/containerpb" 13 "google.golang.org/grpc/codes" 14 "google.golang.org/grpc/status" 15 16 "go.charczuk.com/sdk/errutil" 17 ) 18 19 const nodePoolName = "inverse-tech-prod-00" 20 const nodePoolInitialNodeCount = 3 21 const nodePoolDiskSizeGb = 20 22 23 var flagSkipCreateNodePool = flag.Bool("skip-create-node-pool", false, "If we should skip the create node pool step.") 24 var flagSkipCreateCluster = flag.Bool("skip-create-cluster", false, "If we should skip the create cluster step.") 25 var flagDryRun = flag.Bool("dry-run", true, "If we should skip actual creation steps and just print the outcome.") 26 27 func main() { 28 flag.Parse() 29 run(func(ctx context.Context) { 30 client := must(container.NewClusterManagerClient(ctx)) 31 defer client.Close() 32 33 // ensure the node pool (private, protected off) 34 if !*flagSkipCreateNodePool { 35 if !nodePoolExists(ctx, client) { 36 if *flagDryRun { 37 slog.Info("[DRY-RUN] creating node pool") 38 } else { 39 slog.Info("creating node pool") 40 op := must(client.CreateNodePool(ctx, &containerpb.CreateNodePoolRequest{ 41 NodePool: &containerpb.NodePool{ 42 Name: nodePoolName, 43 InitialNodeCount: nodePoolInitialNodeCount, 44 Config: &containerpb.NodeConfig{ 45 DiskSizeGb: nodePoolDiskSizeGb, 46 }, 47 NetworkConfig: &containerpb.NodeNetworkConfig{ 48 EnablePrivateNodes: ref(true), 49 CreatePodRange: true, 50 }, 51 }, 52 })) 53 waitForOperationToComplete(ctx, client, op, 5*60*time.Second) 54 } 55 } else { 56 slog.Info("node pool already exists") 57 } 58 } else { 59 slog.Info("skipping node pool") 60 } 61 62 // ensure the cluster (with the node pool) 63 // ensure tailscale 64 // ensure tailscale works (pings??) 65 // node pool protected on 66 }) 67 } 68 69 func nodePoolExists(ctx context.Context, client *container.ClusterManagerClient) bool { 70 _, err := client.GetNodePool(ctx, &containerpb.GetNodePoolRequest{ 71 Name: nodePoolName, 72 }) 73 if err != nil && status.Code(err) == codes.NotFound { 74 return false 75 } 76 return true 77 } 78 79 func waitForOperationToComplete(ctx context.Context, client *container.ClusterManagerClient, op *containerpb.Operation, timeoutAfter time.Duration) { 80 t := time.NewTicker(time.Second) 81 defer t.Stop() 82 83 startedAt := must(time.Parse(time.RFC3339, op.StartTime)) 84 elapsedSoFar := time.Now().UTC().Sub(startedAt.UTC()) 85 86 if elapsedSoFar > timeoutAfter { 87 panic(fmt.Errorf("timed out after %v", elapsedSoFar)) 88 } 89 deadline := time.After(timeoutAfter - elapsedSoFar) 90 for { 91 select { 92 case <-ctx.Done(): 93 return 94 case <-deadline: 95 panic(fmt.Errorf("timed out after %v", elapsedSoFar)) 96 case <-t.C: 97 status := must(client.GetOperation(ctx, &containerpb.GetOperationRequest{ 98 Name: op.GetName(), 99 })) 100 if status.Status != containerpb.Operation_PENDING { 101 return 102 } 103 } 104 } 105 } 106 107 func ref[A any](v A) *A { 108 return &v 109 } 110 111 func void(err error) { 112 if err != nil { 113 panic(err) 114 } 115 } 116 117 func must[A any](v A, err error) A { 118 if err != nil { 119 panic(err) 120 } 121 return v 122 } 123 124 func run(fn func(context.Context)) { 125 done := make(chan struct{}) 126 _, cancel := signal.NotifyContext(context.Background()) 127 defer func() { 128 cancel() 129 }() 130 go func() { 131 defer func() { 132 if r := recover(); r != nil { 133 slog.Error(fmt.Sprintf("%+v", errutil.New(r).Error())) 134 } 135 close(done) 136 }() 137 // fn(ctx) 138 fn(context.Background()) 139 }() 140 <-done 141 }