github.com/pdmccormick/importable-docker-buildx@v0.0.0-20240426161518-e47091289030/driver/kubernetes/driver.go (about)

     1  package kubernetes
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/docker/buildx/driver"
    11  	"github.com/docker/buildx/driver/kubernetes/execconn"
    12  	"github.com/docker/buildx/driver/kubernetes/manifest"
    13  	"github.com/docker/buildx/driver/kubernetes/podchooser"
    14  	"github.com/docker/buildx/store"
    15  	"github.com/docker/buildx/util/platformutil"
    16  	"github.com/docker/buildx/util/progress"
    17  	"github.com/moby/buildkit/client"
    18  	"github.com/pkg/errors"
    19  	appsv1 "k8s.io/api/apps/v1"
    20  	corev1 "k8s.io/api/core/v1"
    21  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    22  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    23  	"k8s.io/client-go/kubernetes"
    24  	clientappsv1 "k8s.io/client-go/kubernetes/typed/apps/v1"
    25  	clientcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
    26  )
    27  
    28  const (
    29  	DriverName = "kubernetes"
    30  )
    31  
    32  const (
    33  	// valid values for driver-opt loadbalance
    34  	LoadbalanceRandom = "random"
    35  	LoadbalanceSticky = "sticky"
    36  )
    37  
    38  type Driver struct {
    39  	driver.InitConfig
    40  	factory driver.Factory
    41  
    42  	// if you add fields, remember to update docs:
    43  	// https://github.com/docker/docs/blob/main/content/build/drivers/kubernetes.md
    44  	minReplicas      int
    45  	deployment       *appsv1.Deployment
    46  	configMaps       []*corev1.ConfigMap
    47  	clientset        *kubernetes.Clientset
    48  	deploymentClient clientappsv1.DeploymentInterface
    49  	podClient        clientcorev1.PodInterface
    50  	configMapClient  clientcorev1.ConfigMapInterface
    51  	podChooser       podchooser.PodChooser
    52  	defaultLoad      bool
    53  }
    54  
    55  func (d *Driver) IsMobyDriver() bool {
    56  	return false
    57  }
    58  
    59  func (d *Driver) Config() driver.InitConfig {
    60  	return d.InitConfig
    61  }
    62  
    63  func (d *Driver) Bootstrap(ctx context.Context, l progress.Logger) error {
    64  	return progress.Wrap("[internal] booting buildkit", l, func(sub progress.SubLogger) error {
    65  		_, err := d.deploymentClient.Get(ctx, d.deployment.Name, metav1.GetOptions{})
    66  		if err != nil {
    67  			if !apierrors.IsNotFound(err) {
    68  				return errors.Wrapf(err, "error for bootstrap %q", d.deployment.Name)
    69  			}
    70  
    71  			for _, cfg := range d.configMaps {
    72  				// create ConfigMap first if exists
    73  				_, err = d.configMapClient.Create(ctx, cfg, metav1.CreateOptions{})
    74  				if err != nil {
    75  					if !apierrors.IsAlreadyExists(err) {
    76  						return errors.Wrapf(err, "error while calling configMapClient.Create for %q", cfg.Name)
    77  					}
    78  					_, err = d.configMapClient.Update(ctx, cfg, metav1.UpdateOptions{})
    79  					if err != nil {
    80  						return errors.Wrapf(err, "error while calling configMapClient.Update for %q", cfg.Name)
    81  					}
    82  				}
    83  			}
    84  
    85  			_, err = d.deploymentClient.Create(ctx, d.deployment, metav1.CreateOptions{})
    86  			if err != nil {
    87  				return errors.Wrapf(err, "error while calling deploymentClient.Create for %q", d.deployment.Name)
    88  			}
    89  		}
    90  		return sub.Wrap(
    91  			fmt.Sprintf("waiting for %d pods to be ready", d.minReplicas),
    92  			func() error {
    93  				return d.wait(ctx)
    94  			})
    95  	})
    96  }
    97  
    98  func (d *Driver) wait(ctx context.Context) error {
    99  	// TODO: use watch API
   100  	var (
   101  		err  error
   102  		depl *appsv1.Deployment
   103  	)
   104  	for try := 0; try < 100; try++ {
   105  		depl, err = d.deploymentClient.Get(ctx, d.deployment.Name, metav1.GetOptions{})
   106  		if err == nil {
   107  			if depl.Status.ReadyReplicas >= int32(d.minReplicas) {
   108  				return nil
   109  			}
   110  			err = errors.Errorf("expected %d replicas to be ready, got %d",
   111  				d.minReplicas, depl.Status.ReadyReplicas)
   112  		}
   113  		select {
   114  		case <-ctx.Done():
   115  			return ctx.Err()
   116  		case <-time.After(time.Duration(100+try*20) * time.Millisecond):
   117  		}
   118  	}
   119  	return err
   120  }
   121  
   122  func (d *Driver) Info(ctx context.Context) (*driver.Info, error) {
   123  	depl, err := d.deploymentClient.Get(ctx, d.deployment.Name, metav1.GetOptions{})
   124  	if err != nil {
   125  		// TODO: return err if err != ErrNotFound
   126  		return &driver.Info{
   127  			Status: driver.Inactive,
   128  		}, nil
   129  	}
   130  	if depl.Status.ReadyReplicas <= 0 {
   131  		return &driver.Info{
   132  			Status: driver.Stopped,
   133  		}, nil
   134  	}
   135  	pods, err := podchooser.ListRunningPods(ctx, d.podClient, depl)
   136  	if err != nil {
   137  		return nil, err
   138  	}
   139  	var dynNodes []store.Node
   140  	for _, p := range pods {
   141  		node := store.Node{
   142  			Name: p.Name,
   143  			// Other fields are unset (TODO: detect real platforms)
   144  		}
   145  
   146  		if p.Annotations != nil {
   147  			if p, ok := p.Annotations[manifest.AnnotationPlatform]; ok {
   148  				ps, err := platformutil.Parse(strings.Split(p, ","))
   149  				if err == nil {
   150  					node.Platforms = ps
   151  				}
   152  			}
   153  		}
   154  
   155  		dynNodes = append(dynNodes, node)
   156  	}
   157  	return &driver.Info{
   158  		Status:       driver.Running,
   159  		DynamicNodes: dynNodes,
   160  	}, nil
   161  }
   162  
   163  func (d *Driver) Version(ctx context.Context) (string, error) {
   164  	return "", nil
   165  }
   166  
   167  func (d *Driver) Stop(ctx context.Context, force bool) error {
   168  	// future version may scale the replicas to zero here
   169  	return nil
   170  }
   171  
   172  func (d *Driver) Rm(ctx context.Context, force, rmVolume, rmDaemon bool) error {
   173  	if !rmDaemon {
   174  		return nil
   175  	}
   176  
   177  	if err := d.deploymentClient.Delete(ctx, d.deployment.Name, metav1.DeleteOptions{}); err != nil {
   178  		if !apierrors.IsNotFound(err) {
   179  			return errors.Wrapf(err, "error while calling deploymentClient.Delete for %q", d.deployment.Name)
   180  		}
   181  	}
   182  	for _, cfg := range d.configMaps {
   183  		if err := d.configMapClient.Delete(ctx, cfg.Name, metav1.DeleteOptions{}); err != nil {
   184  			if !apierrors.IsNotFound(err) {
   185  				return errors.Wrapf(err, "error while calling configMapClient.Delete for %q", cfg.Name)
   186  			}
   187  		}
   188  	}
   189  	return nil
   190  }
   191  
   192  func (d *Driver) Dial(ctx context.Context) (net.Conn, error) {
   193  	restClient := d.clientset.CoreV1().RESTClient()
   194  	restClientConfig, err := d.KubeClientConfig.ClientConfig()
   195  	if err != nil {
   196  		return nil, err
   197  	}
   198  	pod, err := d.podChooser.ChoosePod(ctx)
   199  	if err != nil {
   200  		return nil, err
   201  	}
   202  	if len(pod.Spec.Containers) == 0 {
   203  		return nil, errors.Errorf("pod %s does not have any container", pod.Name)
   204  	}
   205  	containerName := pod.Spec.Containers[0].Name
   206  	cmd := []string{"buildctl", "dial-stdio"}
   207  	conn, err := execconn.ExecConn(ctx, restClient, restClientConfig, pod.Namespace, pod.Name, containerName, cmd)
   208  	if err != nil {
   209  		return nil, err
   210  	}
   211  	return conn, nil
   212  }
   213  
   214  func (d *Driver) Client(ctx context.Context, opts ...client.ClientOpt) (*client.Client, error) {
   215  	opts = append([]client.ClientOpt{
   216  		client.WithContextDialer(func(context.Context, string) (net.Conn, error) {
   217  			return d.Dial(ctx)
   218  		}),
   219  	}, opts...)
   220  	return client.New(ctx, "", opts...)
   221  }
   222  
   223  func (d *Driver) Factory() driver.Factory {
   224  	return d.factory
   225  }
   226  
   227  func (d *Driver) Features(_ context.Context) map[driver.Feature]bool {
   228  	return map[driver.Feature]bool{
   229  		driver.OCIExporter:    true,
   230  		driver.DockerExporter: d.DockerAPI != nil,
   231  		driver.CacheExport:    true,
   232  		driver.MultiPlatform:  true, // Untested (needs multiple Driver instances)
   233  		driver.DefaultLoad:    d.defaultLoad,
   234  	}
   235  }
   236  
   237  func (d *Driver) HostGatewayIP(_ context.Context) (net.IP, error) {
   238  	return nil, errors.New("host-gateway is not supported by the kubernetes driver")
   239  }