github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/cli/demo/k3d.go (about)

     1  package demo
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"strings"
    10  	"sync"
    11  
    12  	"github.com/distribution/reference"
    13  	"github.com/docker/docker/api/types/mount"
    14  
    15  	"github.com/tilt-dev/tilt/internal/docker"
    16  	"github.com/tilt-dev/tilt/pkg/logger"
    17  )
    18  
    19  const defaultK3dImage = "docker.io/rancher/k3d:v4.4.7"
    20  
    21  type cluster struct {
    22  	Name string `json:"name"`
    23  }
    24  
    25  type K3dClient struct {
    26  	cli      docker.Client
    27  	k3dImage reference.Named
    28  
    29  	ensurePulled sync.Once
    30  }
    31  
    32  func NewK3dClient(cli docker.Client) *K3dClient {
    33  	ref, err := reference.ParseNamed(defaultK3dImage)
    34  	if err != nil {
    35  		panic(fmt.Errorf("invalid image ref %q: %v", defaultK3dImage, err))
    36  	}
    37  
    38  	return &K3dClient{
    39  		cli:      cli,
    40  		k3dImage: ref,
    41  	}
    42  }
    43  
    44  func (k *K3dClient) ListClusters(ctx context.Context) ([]string, error) {
    45  	cmd := []string{"cluster", "list", "-ojson"}
    46  	var clusterListJson bytes.Buffer
    47  	stderr := logger.Get(ctx).Writer(logger.WarnLvl)
    48  	if err := k.command(ctx, cmd, &clusterListJson, stderr, true); err != nil {
    49  		return nil, err
    50  	}
    51  
    52  	var clusters []cluster
    53  	if err := json.Unmarshal(clusterListJson.Bytes(), &clusters); err != nil {
    54  		return nil, fmt.Errorf("invalid JSON output from cluster list: %v", err)
    55  	}
    56  
    57  	clusterNames := make([]string, len(clusters))
    58  	for i := range clusters {
    59  		clusterNames[i] = clusters[i].Name
    60  	}
    61  	return clusterNames, nil
    62  }
    63  
    64  func (k *K3dClient) CreateCluster(ctx context.Context, clusterName string) error {
    65  	cmd := []string{
    66  		"cluster",
    67  		"create", clusterName,
    68  		"--registry-create",
    69  		"--kubeconfig-switch-context",
    70  		"--kubeconfig-update-default",
    71  		"--no-hostip",
    72  		"--no-image-volume",
    73  		"--no-lb",
    74  		// k3d has a special label syntax which accepts a node filter so you can tag server/agent/LB differently
    75  		// since we're launching a cluster with no load balancer, there's only a single node named `server[0]`,
    76  		// but k3d will emit a confusing warning if we don't specify it explicitly, so this will be
    77  		// `dev.tilt.built=true@server[0]`
    78  		"--label", fmt.Sprintf("%s=true@%s", docker.BuiltLabel, "server[0]"),
    79  	}
    80  	stdout := logger.Get(ctx).Writer(logger.DebugLvl)
    81  	stderr := logger.Get(ctx).Writer(logger.WarnLvl)
    82  	if err := k.command(ctx, cmd, stdout, stderr, true); err != nil {
    83  		return err
    84  	}
    85  	return nil
    86  }
    87  
    88  func (k *K3dClient) DeleteCluster(ctx context.Context, clusterName string, wait bool) error {
    89  	cmd := []string{
    90  		"cluster",
    91  		"delete", clusterName,
    92  	}
    93  	var stdout, stderr io.Writer
    94  	if wait {
    95  		log := logger.Get(ctx)
    96  		stdout = log.Writer(logger.DebugLvl)
    97  		stderr = logger.NewFuncLogger(log.SupportsColor(), log.Level(),
    98  			func(level logger.Level, fields logger.Fields, b []byte) error {
    99  				// there's no kubeconfig in the container so k3d will emit confusing warnings
   100  				// note: no kubeconfig cleanup is necessary since k3d's execution is isolated
   101  				// 	via docker, so is never touching the host filesystem, but it's a weird
   102  				// 	use case so k3d doesn't have a flag to disable kubeconfig cleanup on delete
   103  				if bytes.Contains(b, []byte("Failed to remove cluster details")) ||
   104  					bytes.Contains(b, []byte("no such file or directory")) {
   105  					return nil
   106  				}
   107  				log.Write(logger.WarnLvl, b)
   108  				return nil
   109  			}).Writer(logger.WarnLvl)
   110  	}
   111  	if err := k.command(ctx, cmd, stdout, stderr, wait); err != nil {
   112  		return err
   113  	}
   114  	return nil
   115  }
   116  
   117  func (k *K3dClient) GenerateKubeconfig(ctx context.Context, clusterName string) ([]byte, error) {
   118  	var kubeconfigBuf bytes.Buffer
   119  	stderr := logger.Get(ctx).Writer(logger.WarnLvl)
   120  	err := k.command(ctx, []string{"kubeconfig", "get", clusterName}, &kubeconfigBuf, stderr, true)
   121  	if err != nil {
   122  		return nil, fmt.Errorf("failed to get kubeconfig: %v", err)
   123  	}
   124  	return kubeconfigBuf.Bytes(), nil
   125  }
   126  
   127  func (k *K3dClient) command(ctx context.Context, cmd []string, stdout io.Writer, stderr io.Writer, wait bool) error {
   128  	// lazily pull the image the first time a command is run to avoid network-induced latency checking for an
   129  	// up-to-date image on each command
   130  	k.ensurePulled.Do(func() {
   131  		ref, err := k.cli.ImagePull(ctx, k.k3dImage)
   132  		if err != nil {
   133  			logger.Get(ctx).Errorf("failed to pull %q image: %v", k.k3dImage, err)
   134  		} else {
   135  			k.k3dImage = ref
   136  		}
   137  	})
   138  
   139  	runConfig := docker.RunConfig{
   140  		Pull:   false,
   141  		Stdout: stdout,
   142  		Stderr: stderr,
   143  		Image:  k.k3dImage,
   144  		Cmd:    cmd,
   145  		Mounts: []mount.Mount{
   146  			{
   147  				Type:   mount.TypeBind,
   148  				Source: "/var/run/docker.sock",
   149  				Target: "/var/run/docker.sock",
   150  			},
   151  		},
   152  	}
   153  	runResult, err := k.cli.Run(ctx, runConfig)
   154  	if err != nil {
   155  		return fmt.Errorf("failed to run `k3d %s`: %v", strings.Join(cmd, " "), err)
   156  	}
   157  	if wait {
   158  		defer func() {
   159  			if err := runResult.Close(); err != nil {
   160  				logger.Get(ctx).Debugf("Failed to clean up container %q: %v", runResult.ContainerID, err)
   161  			}
   162  		}()
   163  		status, err := runResult.Wait()
   164  		if err != nil {
   165  			return err
   166  		}
   167  		if status != 0 {
   168  			return fmt.Errorf("k3d exited with code: %d", status)
   169  		}
   170  	}
   171  	return nil
   172  }