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 }