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 }