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  }