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

     1  package manifest
     2  
     3  import (
     4  	"fmt"
     5  	"path"
     6  	"strings"
     7  
     8  	"github.com/docker/buildx/util/platformutil"
     9  	v1 "github.com/opencontainers/image-spec/specs-go/v1"
    10  	"github.com/pkg/errors"
    11  	appsv1 "k8s.io/api/apps/v1"
    12  	corev1 "k8s.io/api/core/v1"
    13  	"k8s.io/apimachinery/pkg/api/resource"
    14  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    15  )
    16  
    17  type DeploymentOpt struct {
    18  	Namespace          string
    19  	Name               string
    20  	Image              string
    21  	Replicas           int
    22  	ServiceAccountName string
    23  	SchedulerName      string
    24  
    25  	// Qemu
    26  	Qemu struct {
    27  		// when true, will install binfmt
    28  		Install bool
    29  		Image   string
    30  	}
    31  
    32  	BuildkitFlags []string
    33  	// files mounted at /etc/buildkitd
    34  	ConfigFiles map[string][]byte
    35  
    36  	Rootless                 bool
    37  	NodeSelector             map[string]string
    38  	CustomAnnotations        map[string]string
    39  	CustomLabels             map[string]string
    40  	Tolerations              []corev1.Toleration
    41  	RequestsCPU              string
    42  	RequestsMemory           string
    43  	RequestsEphemeralStorage string
    44  	LimitsCPU                string
    45  	LimitsMemory             string
    46  	LimitsEphemeralStorage   string
    47  	Platforms                []v1.Platform
    48  }
    49  
    50  const (
    51  	containerName      = "buildkitd"
    52  	AnnotationPlatform = "buildx.docker.com/platform"
    53  	LabelApp           = "app"
    54  )
    55  
    56  var (
    57  	ErrReservedAnnotationPlatform = errors.Errorf("the annotation \"%s\" is reserved and cannot be customized", AnnotationPlatform)
    58  	ErrReservedLabelApp           = errors.Errorf("the label \"%s\" is reserved and cannot be customized", LabelApp)
    59  )
    60  
    61  func NewDeployment(opt *DeploymentOpt) (d *appsv1.Deployment, c []*corev1.ConfigMap, err error) {
    62  	labels := map[string]string{
    63  		LabelApp: opt.Name,
    64  	}
    65  	annotations := map[string]string{}
    66  	replicas := int32(opt.Replicas)
    67  	privileged := true
    68  	args := opt.BuildkitFlags
    69  
    70  	if len(opt.Platforms) > 0 {
    71  		annotations[AnnotationPlatform] = strings.Join(platformutil.Format(opt.Platforms), ",")
    72  	}
    73  
    74  	for k, v := range opt.CustomAnnotations {
    75  		if k == AnnotationPlatform {
    76  			return nil, nil, ErrReservedAnnotationPlatform
    77  		}
    78  		annotations[k] = v
    79  	}
    80  
    81  	for k, v := range opt.CustomLabels {
    82  		if k == LabelApp {
    83  			return nil, nil, ErrReservedLabelApp
    84  		}
    85  		labels[k] = v
    86  	}
    87  
    88  	d = &appsv1.Deployment{
    89  		TypeMeta: metav1.TypeMeta{
    90  			APIVersion: appsv1.SchemeGroupVersion.String(),
    91  			Kind:       "Deployment",
    92  		},
    93  		ObjectMeta: metav1.ObjectMeta{
    94  			Namespace:   opt.Namespace,
    95  			Name:        opt.Name,
    96  			Labels:      labels,
    97  			Annotations: annotations,
    98  		},
    99  		Spec: appsv1.DeploymentSpec{
   100  			Replicas: &replicas,
   101  			Selector: &metav1.LabelSelector{
   102  				MatchLabels: labels,
   103  			},
   104  			Template: corev1.PodTemplateSpec{
   105  				ObjectMeta: metav1.ObjectMeta{
   106  					Labels:      labels,
   107  					Annotations: annotations,
   108  				},
   109  				Spec: corev1.PodSpec{
   110  					ServiceAccountName: opt.ServiceAccountName,
   111  					SchedulerName:      opt.SchedulerName,
   112  					Containers: []corev1.Container{
   113  						{
   114  							Name:  containerName,
   115  							Image: opt.Image,
   116  							Args:  args,
   117  							SecurityContext: &corev1.SecurityContext{
   118  								Privileged: &privileged,
   119  							},
   120  							ReadinessProbe: &corev1.Probe{
   121  								ProbeHandler: corev1.ProbeHandler{
   122  									Exec: &corev1.ExecAction{
   123  										Command: []string{"buildctl", "debug", "workers"},
   124  									},
   125  								},
   126  							},
   127  							Resources: corev1.ResourceRequirements{
   128  								Requests: corev1.ResourceList{},
   129  								Limits:   corev1.ResourceList{},
   130  							},
   131  						},
   132  					},
   133  				},
   134  			},
   135  		},
   136  	}
   137  	for _, cfg := range splitConfigFiles(opt.ConfigFiles) {
   138  		cc := &corev1.ConfigMap{
   139  			TypeMeta: metav1.TypeMeta{
   140  				APIVersion: corev1.SchemeGroupVersion.String(),
   141  				Kind:       "ConfigMap",
   142  			},
   143  			ObjectMeta: metav1.ObjectMeta{
   144  				Namespace:   opt.Namespace,
   145  				Name:        opt.Name + "-" + cfg.name,
   146  				Annotations: annotations,
   147  			},
   148  			Data: cfg.files,
   149  		}
   150  
   151  		d.Spec.Template.Spec.Containers[0].VolumeMounts = []corev1.VolumeMount{{
   152  			Name:      cfg.name,
   153  			MountPath: path.Join("/etc/buildkit", cfg.path),
   154  		}}
   155  
   156  		d.Spec.Template.Spec.Volumes = []corev1.Volume{{
   157  			Name: "config",
   158  			VolumeSource: corev1.VolumeSource{
   159  				ConfigMap: &corev1.ConfigMapVolumeSource{
   160  					LocalObjectReference: corev1.LocalObjectReference{
   161  						Name: cc.Name,
   162  					},
   163  				},
   164  			},
   165  		}}
   166  		c = append(c, cc)
   167  	}
   168  
   169  	if opt.Qemu.Install {
   170  		d.Spec.Template.Spec.InitContainers = []corev1.Container{
   171  			{
   172  				Name:  "qemu",
   173  				Image: opt.Qemu.Image,
   174  				Args:  []string{"--install", "all"},
   175  				SecurityContext: &corev1.SecurityContext{
   176  					Privileged: &privileged,
   177  				},
   178  			},
   179  		}
   180  	}
   181  
   182  	if opt.Rootless {
   183  		if err := toRootless(d); err != nil {
   184  			return nil, nil, err
   185  		}
   186  	}
   187  
   188  	if len(opt.NodeSelector) > 0 {
   189  		d.Spec.Template.Spec.NodeSelector = opt.NodeSelector
   190  	}
   191  
   192  	if len(opt.Tolerations) > 0 {
   193  		d.Spec.Template.Spec.Tolerations = opt.Tolerations
   194  	}
   195  
   196  	if opt.RequestsCPU != "" {
   197  		reqCPU, err := resource.ParseQuantity(opt.RequestsCPU)
   198  		if err != nil {
   199  			return nil, nil, err
   200  		}
   201  		d.Spec.Template.Spec.Containers[0].Resources.Requests[corev1.ResourceCPU] = reqCPU
   202  	}
   203  
   204  	if opt.RequestsMemory != "" {
   205  		reqMemory, err := resource.ParseQuantity(opt.RequestsMemory)
   206  		if err != nil {
   207  			return nil, nil, err
   208  		}
   209  		d.Spec.Template.Spec.Containers[0].Resources.Requests[corev1.ResourceMemory] = reqMemory
   210  	}
   211  
   212  	if opt.RequestsEphemeralStorage != "" {
   213  		reqEphemeralStorage, err := resource.ParseQuantity(opt.RequestsEphemeralStorage)
   214  		if err != nil {
   215  			return nil, nil, err
   216  		}
   217  		d.Spec.Template.Spec.Containers[0].Resources.Requests[corev1.ResourceEphemeralStorage] = reqEphemeralStorage
   218  	}
   219  
   220  	if opt.LimitsCPU != "" {
   221  		limCPU, err := resource.ParseQuantity(opt.LimitsCPU)
   222  		if err != nil {
   223  			return nil, nil, err
   224  		}
   225  		d.Spec.Template.Spec.Containers[0].Resources.Limits[corev1.ResourceCPU] = limCPU
   226  	}
   227  
   228  	if opt.LimitsMemory != "" {
   229  		limMemory, err := resource.ParseQuantity(opt.LimitsMemory)
   230  		if err != nil {
   231  			return nil, nil, err
   232  		}
   233  		d.Spec.Template.Spec.Containers[0].Resources.Limits[corev1.ResourceMemory] = limMemory
   234  	}
   235  
   236  	if opt.LimitsEphemeralStorage != "" {
   237  		limEphemeralStorage, err := resource.ParseQuantity(opt.LimitsEphemeralStorage)
   238  		if err != nil {
   239  			return nil, nil, err
   240  		}
   241  		d.Spec.Template.Spec.Containers[0].Resources.Limits[corev1.ResourceEphemeralStorage] = limEphemeralStorage
   242  	}
   243  
   244  	return
   245  }
   246  
   247  func toRootless(d *appsv1.Deployment) error {
   248  	d.Spec.Template.Spec.Containers[0].Args = append(
   249  		d.Spec.Template.Spec.Containers[0].Args,
   250  		"--oci-worker-no-process-sandbox",
   251  	)
   252  	d.Spec.Template.Spec.Containers[0].SecurityContext = &corev1.SecurityContext{
   253  		SeccompProfile: &corev1.SeccompProfile{
   254  			Type: corev1.SeccompProfileTypeUnconfined,
   255  		},
   256  	}
   257  	if d.Spec.Template.ObjectMeta.Annotations == nil {
   258  		d.Spec.Template.ObjectMeta.Annotations = make(map[string]string, 1)
   259  	}
   260  	d.Spec.Template.ObjectMeta.Annotations["container.apparmor.security.beta.kubernetes.io/"+containerName] = "unconfined"
   261  
   262  	// Dockerfile has `VOLUME /home/user/.local/share/buildkit` by default too,
   263  	// but the default VOLUME does not work with rootless on Google's Container-Optimized OS
   264  	// as it is mounted with `nosuid,nodev`.
   265  	// https://github.com/moby/buildkit/issues/879#issuecomment-1240347038
   266  	// https://github.com/moby/buildkit/pull/3097
   267  	const emptyDirVolName = "buildkitd"
   268  	d.Spec.Template.Spec.Containers[0].VolumeMounts = append(d.Spec.Template.Spec.Containers[0].VolumeMounts, corev1.VolumeMount{
   269  		Name:      emptyDirVolName,
   270  		MountPath: "/home/user/.local/share/buildkit",
   271  	})
   272  	d.Spec.Template.Spec.Volumes = append(d.Spec.Template.Spec.Volumes, corev1.Volume{
   273  		Name: emptyDirVolName,
   274  		VolumeSource: corev1.VolumeSource{
   275  			EmptyDir: &corev1.EmptyDirVolumeSource{},
   276  		},
   277  	})
   278  
   279  	return nil
   280  }
   281  
   282  type config struct {
   283  	name  string
   284  	path  string
   285  	files map[string]string
   286  }
   287  
   288  func splitConfigFiles(m map[string][]byte) []config {
   289  	var c []config
   290  	idx := map[string]int{}
   291  	nameIdx := 0
   292  	for k, v := range m {
   293  		dir := path.Dir(k)
   294  		i, ok := idx[dir]
   295  		if !ok {
   296  			idx[dir] = len(c)
   297  			i = len(c)
   298  			name := "config"
   299  			if dir != "." {
   300  				nameIdx++
   301  				name = fmt.Sprintf("%s-%d", name, nameIdx)
   302  			}
   303  			c = append(c, config{
   304  				path:  dir,
   305  				name:  name,
   306  				files: map[string]string{},
   307  			})
   308  		}
   309  		c[i].files[path.Base(k)] = string(v)
   310  	}
   311  	return c
   312  }