Merge pull request #22 from kairos-io/515-central-server

515 central server
This commit is contained in:
Ettore Di Giacinto 2022-12-15 17:25:12 +01:00 committed by GitHub
commit e6e5cddbfe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 892 additions and 570 deletions

View File

@ -7,6 +7,7 @@ on:
- master
tags:
- '*'
pull_request:
jobs:
docker:
@ -16,4 +17,4 @@ jobs:
uses: actions/checkout@v2
- name: Test
run: |
make kind-e2e-tests
make kind-e2e-tests

View File

@ -147,13 +147,12 @@ uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified
.PHONY: deploy
deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config.
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
# TODO: No need to build and then apply. `kubectl apply -k config/default` does the trick
$(KUSTOMIZE) build config/default | kubectl apply -f -
.PHONY: deploy-dev
deploy-dev: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config.
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
$(KUSTOMIZE) build config/dev | kubectl apply -f -
kubectl apply -k config/dev
.PHONY: undeploy
undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
@ -282,4 +281,4 @@ kind-e2e-tests: ginkgo kind-setup install undeploy-dev deploy-dev e2e-tests
kubesplit: manifests kustomize
rm -rf helm-chart
mkdir helm-chart
$(KUSTOMIZE) build config/default | kubesplit -helm helm-chart
$(KUSTOMIZE) build config/default | kubesplit -helm helm-chart

View File

@ -16,6 +16,7 @@ bases:
- ../crd
- ../rbac
- ../manager
- ../nginx
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
# crd/kustomization.yaml
#- ../webhook
@ -72,3 +73,16 @@ vars:
# kind: Service
# version: v1
# name: webhook-service
vars:
- name: NGINX_NAMESPACE
objref:
kind: Namespace
name: system
apiVersion: v1
- name: ARTIFACT_COPIER_ROLE
objref:
kind: Role
name: artifactCopier
apiVersion: rbac.authorization.k8s.io/v1

View File

@ -38,3 +38,7 @@ spec:
- "--health-probe-bind-address=:8081"
- "--metrics-bind-address=127.0.0.1:8080"
- "--leader-elect"
- "--copy-to-namespace=$(NGINX_NAMESPACE)"
- "--copy-role=$(ARTIFACT_COPIER_ROLE)"
- --copy-to-pod-label=app.kubernetes.io/name=osbuilder-nginx
- --copy-to-path="/usr/share/nginx/html"

View File

@ -1,74 +1,7 @@
# Adds namespace to all resources.
namespace: osartifactbuilder-operator-system
# Value of this field is prepended to the
# names of all resources, e.g. a deployment named
# "wordpress" becomes "alices-wordpress".
# Note that it should also match with the prefix (text before '-') of the namespace
# field above.
namePrefix: osartifactbuilder-operator-
# Labels to add to all resources and selectors.
#commonLabels:
# someName: someValue
bases:
- ../crd
- ../rbac
- ../manager
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
# crd/kustomization.yaml
#- ../webhook
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required.
#- ../certmanager
# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
#- ../prometheus
- ../default
patchesStrategicMerge:
# Protect the /metrics endpoint by putting it behind auth.
# If you want your controller-manager to expose the /metrics
# endpoint w/o any authn/z, please comment the following line.
- manager_auth_proxy_patch.yaml
# Mount the controller config file for loading manager configurations
# through a ComponentConfig type
#- manager_config_patch.yaml
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
# crd/kustomization.yaml
#- manager_webhook_patch.yaml
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'.
# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks.
# 'CERTMANAGER' needs to be enabled to use ca injection
#- webhookcainjection_patch.yaml
# the following config is for teaching kustomize how to do var substitution
vars:
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix.
#- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR
# objref:
# kind: Certificate
# group: cert-manager.io
# version: v1
# name: serving-cert # this name should match the one in certificate.yaml
# fieldref:
# fieldpath: metadata.namespace
#- name: CERTIFICATE_NAME
# objref:
# kind: Certificate
# group: cert-manager.io
# version: v1
# name: serving-cert # this name should match the one in certificate.yaml
#- name: SERVICE_NAMESPACE # namespace of the service
# objref:
# kind: Service
# version: v1
# name: webhook-service
# fieldref:
# fieldpath: metadata.namespace
#- name: SERVICE_NAME
# objref:
# kind: Service
# version: v1
# name: webhook-service
images:
- name: quay.io/kairos/osbuilder
newName: quay.io/kairos/osbuilder
newTag: test

View File

@ -1,40 +0,0 @@
# This patch inject a sidecar container which is a HTTP proxy for the
# controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews.
apiVersion: apps/v1
kind: Deployment
metadata:
name: controller-manager
namespace: system
spec:
template:
spec:
containers:
- name: kube-rbac-proxy
securityContext:
allowPrivilegeEscalation: false
# TODO(user): uncomment for common cases that do not require escalating privileges
# capabilities:
# drop:
# - "ALL"
image: gcr.io/kubebuilder/kube-rbac-proxy:v0.11.0
args:
- "--secure-listen-address=0.0.0.0:8443"
- "--upstream=http://127.0.0.1:8080/"
- "--logtostderr=true"
- "--v=0"
ports:
- containerPort: 8443
protocol: TCP
name: https
resources:
limits:
cpu: 500m
memory: 128Mi
requests:
cpu: 5m
memory: 64Mi
- name: manager
args:
- "--health-probe-bind-address=:8081"
- "--metrics-bind-address=127.0.0.1:8080"
- "--leader-elect"

View File

@ -1,21 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: controller-manager
namespace: system
spec:
template:
spec:
containers:
- name: manager
imagePullPolicy: Never
args:
- "--config=controller_manager_config.yaml"
volumeMounts:
- name: manager-config
mountPath: /controller_manager_config.yaml
subPath: controller_manager_config.yaml
volumes:
- name: manager-config
configMap:
name: manager-config

View File

@ -1,6 +1,8 @@
resources:
- manager.yaml
namespace: system
generatorOptions:
disableNameSuffixHash: true

View File

@ -9,7 +9,6 @@ apiVersion: apps/v1
kind: Deployment
metadata:
name: controller-manager
namespace: system
labels:
control-plane: controller-manager
spec:

View File

@ -0,0 +1,41 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nginx-public
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 3Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
labels:
app.kubernetes.io/name: osbuilder-nginx
spec:
selector:
matchLabels:
app.kubernetes.io/name: osbuilder-nginx
replicas: 1
template:
metadata:
labels:
app.kubernetes.io/name: osbuilder-nginx
spec:
containers:
- image: nginx
name: nginx
volumeMounts:
- mountPath: "/usr/share/nginx/html"
name: nginx-public
ports:
- containerPort: 80
serviceAccountName: controller-manager
terminationGracePeriodSeconds: 10
volumes:
- name: nginx-public
persistentVolumeClaim:
claimName: nginx-public

View File

@ -0,0 +1,4 @@
resources:
- deployment.yaml
- service.yaml
- role.yaml

18
config/nginx/role.yaml Normal file
View File

@ -0,0 +1,18 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: artifactCopier
rules:
- apiGroups:
- ""
resources:
- pods
verbs:
- list
- get
- apiGroups:
- ""
resources:
- pods/exec
verbs:
- create

12
config/nginx/service.yaml Normal file
View File

@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: osbuilder-nginx
spec:
type: NodePort
selector:
app.kubernetes.io/name: osbuilder-nginx
ports:
- protocol: TCP
port: 80
targetPort: 80

View File

@ -5,6 +5,31 @@ metadata:
creationTimestamp: null
name: manager-role
rules:
- apiGroups:
- ""
resources:
- serviceaccounts
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- "rbac.authorization.k8s.io"
resources:
- roles
- rolebindings
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- build.kairos.io
resources:
@ -40,17 +65,30 @@ rules:
- apiGroups:
- ""
resources:
- services
- configmaps
verbs:
- get
- create
- update
- apiGroups:
- "apps"
- "batch"
resources:
- deployments
- jobs
verbs:
- get
- create
- update
# Temporary so that it can grant these permissions to the created role
- apiGroups:
- ""
resources:
- pods
verbs:
- list
- get
- apiGroups:
- ""
resources:
- pods/exec
verbs:
- create

View File

@ -1,338 +0,0 @@
/*
Copyright 2022.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controllers
import (
"fmt"
buildv1alpha1 "github.com/kairos-io/osbuilder/api/v1alpha1"
appsv1 "k8s.io/api/apps/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func genDeploymentLabel(s string) map[string]string {
return map[string]string{
"osbuild": "workload" + s,
}
}
// TODO: Handle registry auth
// TODO: This shells out, but needs ENV_VAR with key refs mapping
func unpackContainer(id, containerImage, pullImage string, pullOptions buildv1alpha1.Pull) v1.Container {
return v1.Container{
ImagePullPolicy: v1.PullAlways,
Name: fmt.Sprintf("pull-image-%s", id),
Image: containerImage,
Command: []string{"/bin/bash", "-cxe"},
Args: []string{
fmt.Sprintf(
"luet util unpack %s %s",
pullImage,
"/rootfs",
),
},
VolumeMounts: []v1.VolumeMount{
{
Name: "rootfs",
MountPath: "/rootfs",
},
},
}
}
func createImageContainer(containerImage string, pushOptions buildv1alpha1.Push) v1.Container {
return v1.Container{
ImagePullPolicy: v1.PullAlways,
Name: "create-image",
Image: containerImage,
Command: []string{"/bin/bash", "-cxe"},
Args: []string{
fmt.Sprintf(
"tar -czvpf test.tar -C /rootfs . && luet util pack %s test.tar image.tar && mv image.tar /public",
pushOptions.ImageName,
),
},
VolumeMounts: []v1.VolumeMount{
{
Name: "rootfs",
MountPath: "/rootfs",
},
{
Name: "public",
MountPath: "/public",
},
},
}
}
func osReleaseContainer(containerImage string) v1.Container {
return v1.Container{
ImagePullPolicy: v1.PullAlways,
Name: "os-release",
Image: containerImage,
Command: []string{"/bin/bash", "-cxe"},
Args: []string{
"cp -rfv /etc/os-release /rootfs/etc/os-release",
},
VolumeMounts: []v1.VolumeMount{
{
Name: "config",
MountPath: "/etc/os-release",
SubPath: "os-release",
},
{
Name: "rootfs",
MountPath: "/rootfs",
},
},
}
}
func (r *OSArtifactReconciler) genDeployment(artifact buildv1alpha1.OSArtifact, svc *v1.Service) *appsv1.Deployment {
// TODO: svc is unused, but could be used in the future to generate the Netboot URL
objMeta := metav1.ObjectMeta{
Name: artifact.Name,
Namespace: artifact.Namespace,
OwnerReferences: genOwner(artifact),
}
pushImage := artifact.Spec.PushOptions.Push
privileged := false
serviceAccount := false
cmd := fmt.Sprintf(
"/entrypoint.sh --debug --name %s build-iso --date=false --output /public dir:/rootfs",
artifact.Name,
)
volumeMounts := []v1.VolumeMount{
{
Name: "public",
MountPath: "/public",
},
{
Name: "rootfs",
MountPath: "/rootfs",
},
}
if artifact.Spec.GRUBConfig != "" {
volumeMounts = append(volumeMounts, v1.VolumeMount{
Name: "config",
MountPath: "/iso/iso-overlay/boot/grub2/grub.cfg",
SubPath: "grub.cfg",
})
}
cloudImgCmd := fmt.Sprintf(
"/raw-images.sh /rootfs /public/%s.raw",
artifact.Name,
)
if artifact.Spec.CloudConfig != "" {
volumeMounts = append(volumeMounts, v1.VolumeMount{
Name: "config",
MountPath: "/iso/iso-overlay/cloud_config.yaml",
SubPath: "config",
})
cloudImgCmd += " /iso/iso-overlay/cloud_config.yaml"
}
if artifact.Spec.CloudConfig != "" || artifact.Spec.GRUBConfig != "" {
cmd = fmt.Sprintf(
"/entrypoint.sh --debug --name %s build-iso --date=false --overlay-iso /iso/iso-overlay --output /public dir:/rootfs",
artifact.Name,
)
}
buildIsoContainer := v1.Container{
ImagePullPolicy: v1.PullAlways,
SecurityContext: &v1.SecurityContext{Privileged: &privileged},
Name: "build-iso",
Image: r.ToolImage,
Command: []string{"/bin/bash", "-cxe"},
Args: []string{
cmd,
},
VolumeMounts: volumeMounts,
}
buildCloudImageContainer := v1.Container{
ImagePullPolicy: v1.PullAlways,
SecurityContext: &v1.SecurityContext{Privileged: &privileged},
Name: "build-cloud-image",
Image: r.ToolImage,
Command: []string{"/bin/bash", "-cxe"},
Args: []string{
cloudImgCmd,
},
VolumeMounts: volumeMounts,
}
if artifact.Spec.DiskSize != "" {
buildCloudImageContainer.Env = []v1.EnvVar{{
Name: "EXTEND",
Value: artifact.Spec.DiskSize,
}}
}
extractNetboot := v1.Container{
ImagePullPolicy: v1.PullAlways,
SecurityContext: &v1.SecurityContext{Privileged: &privileged},
Name: "build-netboot",
Image: r.ToolImage,
Command: []string{"/bin/bash", "-cxe"},
Env: []v1.EnvVar{{
Name: "URL",
Value: artifact.Spec.NetbootURL,
}},
Args: []string{
fmt.Sprintf(
"/netboot.sh /public/%s.iso /public/%s",
artifact.Name,
artifact.Name,
),
},
VolumeMounts: volumeMounts,
}
buildAzureCloudImageContainer := v1.Container{
ImagePullPolicy: v1.PullAlways,
SecurityContext: &v1.SecurityContext{Privileged: &privileged},
Name: "build-azure-cloud-image",
Image: r.ToolImage,
Command: []string{"/bin/bash", "-cxe"},
Args: []string{
fmt.Sprintf(
"/azure.sh /public/%s.raw /public/%s.vhd",
artifact.Name,
artifact.Name,
),
},
VolumeMounts: volumeMounts,
}
buildGCECloudImageContainer := v1.Container{
ImagePullPolicy: v1.PullAlways,
SecurityContext: &v1.SecurityContext{Privileged: &privileged},
Name: "build-gce-cloud-image",
Image: r.ToolImage,
Command: []string{"/bin/bash", "-cxe"},
Args: []string{
fmt.Sprintf(
"/gce.sh /public/%s.raw /public/%s.gce.raw",
artifact.Name,
artifact.Name,
),
},
VolumeMounts: volumeMounts,
}
servingContainer := v1.Container{
ImagePullPolicy: v1.PullAlways,
SecurityContext: &v1.SecurityContext{Privileged: &privileged},
Name: "serve",
Ports: []v1.ContainerPort{v1.ContainerPort{Name: "http", ContainerPort: 80}},
Image: r.ServingImage,
VolumeMounts: []v1.VolumeMount{
{
Name: "public",
MountPath: "/usr/share/nginx/html",
},
},
}
pod := v1.PodSpec{
AutomountServiceAccountToken: &serviceAccount,
Volumes: []v1.Volume{
{
Name: "public",
VolumeSource: v1.VolumeSource{EmptyDir: &v1.EmptyDirVolumeSource{}},
},
{
Name: "rootfs",
VolumeSource: v1.VolumeSource{EmptyDir: &v1.EmptyDirVolumeSource{}},
},
{
Name: "config",
VolumeSource: v1.VolumeSource{
ConfigMap: &v1.ConfigMapVolumeSource{
LocalObjectReference: v1.LocalObjectReference{Name: artifact.Name}}},
},
},
}
pod.InitContainers = []v1.Container{unpackContainer("baseimage", r.ToolImage, artifact.Spec.ImageName, artifact.Spec.PullOptions)}
for i, bundle := range artifact.Spec.Bundles {
pod.InitContainers = append(pod.InitContainers, unpackContainer(fmt.Sprint(i), r.ToolImage, bundle, artifact.Spec.PullOptions))
}
if artifact.Spec.OSRelease != "" {
pod.InitContainers = append(pod.InitContainers, osReleaseContainer(r.ToolImage))
}
if artifact.Spec.ISO || artifact.Spec.Netboot {
pod.InitContainers = append(pod.InitContainers, buildIsoContainer)
}
if artifact.Spec.Netboot {
pod.InitContainers = append(pod.InitContainers, extractNetboot)
}
if artifact.Spec.CloudImage || artifact.Spec.AzureImage || artifact.Spec.GCEImage {
pod.InitContainers = append(pod.InitContainers, buildCloudImageContainer)
}
if artifact.Spec.AzureImage {
pod.InitContainers = append(pod.InitContainers, buildAzureCloudImageContainer)
}
if artifact.Spec.GCEImage {
pod.InitContainers = append(pod.InitContainers, buildGCECloudImageContainer)
}
if pushImage {
pod.InitContainers = append(pod.InitContainers, createImageContainer(r.ToolImage, artifact.Spec.PushOptions))
}
pod.Containers = []v1.Container{servingContainer}
deploymentLabels := genDeploymentLabel(artifact.Name)
replicas := int32(1)
return &appsv1.Deployment{
ObjectMeta: objMeta,
Spec: appsv1.DeploymentSpec{
Selector: &metav1.LabelSelector{MatchLabels: deploymentLabels},
Replicas: &replicas,
Template: v1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: deploymentLabels,
},
Spec: pod,
},
},
}
}

528
controllers/job.go Normal file
View File

@ -0,0 +1,528 @@
/*
Copyright 2022.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controllers
import (
"bytes"
"context"
"fmt"
buildv1alpha1 "github.com/kairos-io/osbuilder/api/v1alpha1"
"github.com/pkg/errors"
batchv1 "k8s.io/api/batch/v1"
v1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/tools/remotecommand"
)
func genJobLabel(s string) map[string]string {
return map[string]string{
"osbuild": "workload" + s,
}
}
// TODO: Handle registry auth
// TODO: This shells out, but needs ENV_VAR with key refs mapping
// TODO: Cache downloaded images?
func unpackContainer(id, containerImage, pullImage string, pullOptions buildv1alpha1.Pull) v1.Container {
return v1.Container{
ImagePullPolicy: v1.PullAlways,
Name: fmt.Sprintf("pull-image-%s", id),
Image: containerImage,
Command: []string{"/bin/bash", "-cxe"},
Args: []string{
fmt.Sprintf(
"luet util unpack %s %s",
pullImage,
"/rootfs",
),
},
VolumeMounts: []v1.VolumeMount{
{
Name: "rootfs",
MountPath: "/rootfs",
},
},
}
}
func createImageContainer(containerImage string, pushOptions buildv1alpha1.Push) v1.Container {
return v1.Container{
ImagePullPolicy: v1.PullAlways,
Name: "create-image",
Image: containerImage,
Command: []string{"/bin/bash", "-cxe"},
Args: []string{
fmt.Sprintf(
"tar -czvpf test.tar -C /rootfs . && luet util pack %s test.tar image.tar && mv image.tar /artifacts",
pushOptions.ImageName,
),
},
VolumeMounts: []v1.VolumeMount{
{
Name: "rootfs",
MountPath: "/rootfs",
},
{
Name: "artifacts",
MountPath: "/artifacts",
},
},
}
}
func createPushToServerImageContainer(containerImage string, artifactPodInfo ArtifactPodInfo) v1.Container {
command := fmt.Sprintf("tar cf - -C artifacts/ . | kubectl exec -i -n %s $(kubectl get pods -l %s -n %s --no-headers -o custom-columns=\":metadata.name\" | head -n1) -- tar xf - -C %s", artifactPodInfo.Namespace, artifactPodInfo.Label, artifactPodInfo.Namespace, artifactPodInfo.Path)
fmt.Printf("command = %+v\n", command)
return v1.Container{
ImagePullPolicy: v1.PullAlways,
Name: "push-to-server",
Image: containerImage,
Command: []string{"/bin/bash", "-cxe"},
Args: []string{command},
VolumeMounts: []v1.VolumeMount{
{
Name: "rootfs",
MountPath: "/rootfs",
},
{
Name: "artifacts",
MountPath: "/artifacts",
},
},
}
}
func osReleaseContainer(containerImage string) v1.Container {
return v1.Container{
ImagePullPolicy: v1.PullAlways,
Name: "os-release",
Image: containerImage,
Command: []string{"/bin/bash", "-cxe"},
Args: []string{
"cp -rfv /etc/os-release /rootfs/etc/os-release",
},
VolumeMounts: []v1.VolumeMount{
{
Name: "config",
MountPath: "/etc/os-release",
SubPath: "os-release",
},
{
Name: "rootfs",
MountPath: "/rootfs",
},
},
}
}
func (r *OSArtifactReconciler) genJob(artifact buildv1alpha1.OSArtifact) *batchv1.Job {
objMeta := genObjectMeta(artifact)
pushImage := artifact.Spec.PushOptions.Push
privileged := false
serviceAccount := true
cmd := fmt.Sprintf(
"/entrypoint.sh --debug --name %s build-iso --date=false --output /artifacts dir:/rootfs",
artifact.Name,
)
volumeMounts := []v1.VolumeMount{
{
Name: "artifacts",
MountPath: "/artifacts",
},
{
Name: "rootfs",
MountPath: "/rootfs",
},
}
if artifact.Spec.GRUBConfig != "" {
volumeMounts = append(volumeMounts, v1.VolumeMount{
Name: "config",
MountPath: "/iso/iso-overlay/boot/grub2/grub.cfg",
SubPath: "grub.cfg",
})
}
cloudImgCmd := fmt.Sprintf(
"/raw-images.sh /rootfs /artifacts/%s.raw",
artifact.Name,
)
if artifact.Spec.CloudConfig != "" {
volumeMounts = append(volumeMounts, v1.VolumeMount{
Name: "config",
MountPath: "/iso/iso-overlay/cloud_config.yaml",
SubPath: "config",
})
cloudImgCmd += " /iso/iso-overlay/cloud_config.yaml"
}
if artifact.Spec.CloudConfig != "" || artifact.Spec.GRUBConfig != "" {
cmd = fmt.Sprintf(
"/entrypoint.sh --debug --name %s build-iso --date=false --overlay-iso /iso/iso-overlay --output /artifacts dir:/rootfs",
artifact.Name,
)
}
buildIsoContainer := v1.Container{
ImagePullPolicy: v1.PullAlways,
SecurityContext: &v1.SecurityContext{Privileged: &privileged},
Name: "build-iso",
Image: r.ToolImage,
Command: []string{"/bin/bash", "-cxe"},
Args: []string{
cmd,
},
VolumeMounts: volumeMounts,
}
buildCloudImageContainer := v1.Container{
ImagePullPolicy: v1.PullAlways,
SecurityContext: &v1.SecurityContext{Privileged: &privileged},
Name: "build-cloud-image",
Image: r.ToolImage,
Command: []string{"/bin/bash", "-cxe"},
Args: []string{
cloudImgCmd,
},
VolumeMounts: volumeMounts,
}
if artifact.Spec.DiskSize != "" {
buildCloudImageContainer.Env = []v1.EnvVar{{
Name: "EXTEND",
Value: artifact.Spec.DiskSize,
}}
}
extractNetboot := v1.Container{
ImagePullPolicy: v1.PullAlways,
SecurityContext: &v1.SecurityContext{Privileged: &privileged},
Name: "build-netboot",
Image: r.ToolImage,
Command: []string{"/bin/bash", "-cxe"},
Env: []v1.EnvVar{{
Name: "URL",
Value: artifact.Spec.NetbootURL,
}},
Args: []string{
fmt.Sprintf(
"/netboot.sh /artifacts/%s.iso /artifacts/%s",
artifact.Name,
artifact.Name,
),
},
VolumeMounts: volumeMounts,
}
buildAzureCloudImageContainer := v1.Container{
ImagePullPolicy: v1.PullAlways,
SecurityContext: &v1.SecurityContext{Privileged: &privileged},
Name: "build-azure-cloud-image",
Image: r.ToolImage,
Command: []string{"/bin/bash", "-cxe"},
Args: []string{
fmt.Sprintf(
"/azure.sh /artifacts/%s.raw /artifacts/%s.vhd",
artifact.Name,
artifact.Name,
),
},
VolumeMounts: volumeMounts,
}
buildGCECloudImageContainer := v1.Container{
ImagePullPolicy: v1.PullAlways,
SecurityContext: &v1.SecurityContext{Privileged: &privileged},
Name: "build-gce-cloud-image",
Image: r.ToolImage,
Command: []string{"/bin/bash", "-cxe"},
Args: []string{
fmt.Sprintf(
"/gce.sh /artifacts/%s.raw /artifacts/%s.gce.raw",
artifact.Name,
artifact.Name,
),
},
VolumeMounts: volumeMounts,
}
pod := v1.PodSpec{
AutomountServiceAccountToken: &serviceAccount,
ServiceAccountName: objMeta.Name,
RestartPolicy: v1.RestartPolicyNever,
Volumes: []v1.Volume{
{
Name: "artifacts",
VolumeSource: v1.VolumeSource{EmptyDir: &v1.EmptyDirVolumeSource{}},
},
{
Name: "rootfs",
VolumeSource: v1.VolumeSource{EmptyDir: &v1.EmptyDirVolumeSource{}},
},
{
Name: "config",
VolumeSource: v1.VolumeSource{
ConfigMap: &v1.ConfigMapVolumeSource{
LocalObjectReference: v1.LocalObjectReference{Name: artifact.Name}}},
},
},
}
pod.InitContainers = []v1.Container{unpackContainer("baseimage", r.ToolImage, artifact.Spec.ImageName, artifact.Spec.PullOptions)}
for i, bundle := range artifact.Spec.Bundles {
pod.InitContainers = append(pod.InitContainers, unpackContainer(fmt.Sprint(i), r.ToolImage, bundle, artifact.Spec.PullOptions))
}
if artifact.Spec.OSRelease != "" {
pod.InitContainers = append(pod.InitContainers, osReleaseContainer(r.ToolImage))
}
if artifact.Spec.ISO || artifact.Spec.Netboot {
pod.InitContainers = append(pod.InitContainers, buildIsoContainer)
}
if artifact.Spec.Netboot {
pod.InitContainers = append(pod.InitContainers, extractNetboot)
}
if artifact.Spec.CloudImage || artifact.Spec.AzureImage || artifact.Spec.GCEImage {
pod.InitContainers = append(pod.InitContainers, buildCloudImageContainer)
}
if artifact.Spec.AzureImage {
pod.InitContainers = append(pod.InitContainers, buildAzureCloudImageContainer)
}
if artifact.Spec.GCEImage {
pod.InitContainers = append(pod.InitContainers, buildGCECloudImageContainer)
}
// TODO: Does it make sense to build the image and not push it? Maybe remove
// this flag?
if pushImage {
pod.InitContainers = append(pod.InitContainers, createImageContainer(r.ToolImage, artifact.Spec.PushOptions))
}
pod.Containers = []v1.Container{
createPushToServerImageContainer(r.CopierImage, r.ArtifactPodInfo),
}
jobLabels := genJobLabel(artifact.Name)
job := batchv1.Job{
ObjectMeta: objMeta,
Spec: batchv1.JobSpec{
Template: v1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: jobLabels,
},
Spec: pod,
},
},
}
return &job
}
// createServiceAccount creates a service account that has the permissions to
// copy the artifacts to the http server Pod. This service account is used for
// the "push to server" container.
func (r *OSArtifactReconciler) createCopierServiceAccount(ctx context.Context, objMeta metav1.ObjectMeta) error {
sa, err := r.clientSet.CoreV1().
ServiceAccounts(objMeta.Namespace).Get(ctx, objMeta.Name, metav1.GetOptions{})
if sa == nil || apierrors.IsNotFound(err) {
t := true
_, err := r.clientSet.CoreV1().ServiceAccounts(objMeta.Namespace).Create(ctx,
&v1.ServiceAccount{
ObjectMeta: objMeta,
AutomountServiceAccountToken: &t,
}, metav1.CreateOptions{})
if err != nil {
return err
}
}
return err
}
// func (r *OSArtifactReconciler) createCopierRole(ctx context.Context, objMeta metav1.ObjectMeta) error {
// role, err := r.clientSet.RbacV1().
// Roles(objMeta.Namespace).
// Get(ctx, objMeta.Name, metav1.GetOptions{})
// if role == nil || apierrors.IsNotFound(err) {
// _, err := r.clientSet.RbacV1().Roles(objMeta.Namespace).Create(ctx,
// &rbacv1.Role{
// ObjectMeta: objMeta,
// Rules: []rbacv1.PolicyRule{
// // TODO: The actual permissions we need is that to copy to a Pod.
// // The Pod is on another namespace, so we need a cluster wide permission.
// // This can get viral because the controller needs to have the permissions
// // if it is to grant them to the Job.
// {
// Verbs: []string{"list"},
// APIGroups: []string{""},
// Resources: []string{"pods"},
// },
// },
// },
// metav1.CreateOptions{},
// )
// if err != nil {
// return err
// }
// }
// return err
// }
func (r *OSArtifactReconciler) createCopierRoleBinding(ctx context.Context, objMeta metav1.ObjectMeta) error {
newrb := &rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: objMeta.Name,
Namespace: r.ArtifactPodInfo.Namespace,
// TODO: We can't have cross-namespace owners. The role binding will have to deleted explicitly by the reconciler (finalizer?)
// OwnerReferences: objMeta.OwnerReferences,
},
RoleRef: rbacv1.RoleRef{
APIGroup: "rbac.authorization.k8s.io",
Kind: "Role",
Name: r.ArtifactPodInfo.Role,
},
Subjects: []rbacv1.Subject{
{
Kind: "ServiceAccount",
Name: objMeta.Name,
Namespace: objMeta.Namespace,
},
},
}
rb, err := r.clientSet.RbacV1().
RoleBindings(r.ArtifactPodInfo.Namespace).
Get(ctx, objMeta.Name, metav1.GetOptions{})
if rb == nil || apierrors.IsNotFound(err) {
_, err := r.clientSet.RbacV1().
RoleBindings(r.ArtifactPodInfo.Namespace).
Create(ctx, newrb, metav1.CreateOptions{})
if err != nil {
return err
}
}
return err
}
// createRBAC creates a ServiceAccount, and a binding to the CopierRole so that
// the container that copies the artifacts to the http server Pod has the
// permissions to do so.
func (r *OSArtifactReconciler) createRBAC(ctx context.Context, artifact buildv1alpha1.OSArtifact) error {
objMeta := genObjectMeta(artifact)
err := r.createCopierServiceAccount(ctx, objMeta)
if err != nil {
return errors.Wrap(err, "creating a service account")
}
err = r.createCopierRoleBinding(ctx, objMeta)
if err != nil {
return errors.Wrap(err, "creating a role binding for the copy-role")
}
return err
}
// removeRBAC deletes the role binding between the service account of this artifact
// and the CopierRole. The ServiceAccount is removed automatically through the Owner
// relationship with the OSArtifact. The RoleBinding can't have it as an owner
// because it is in a different Namespace.
func (r *OSArtifactReconciler) removeRBAC(ctx context.Context, artifact buildv1alpha1.OSArtifact) error {
err := r.clientSet.RbacV1().RoleBindings(r.ArtifactPodInfo.Namespace).
Delete(ctx, artifact.Name, metav1.DeleteOptions{})
// Ignore not found. No need to do anything.
if err != nil && apierrors.IsNotFound(err) {
return nil
}
return err
}
func (r *OSArtifactReconciler) removeArtifacts(ctx context.Context, artifact buildv1alpha1.OSArtifact) error {
//Finding Pods using labels
fmt.Printf("r.ArtifactPodInfo = %+v\n", r.ArtifactPodInfo.Label)
pods, err := r.clientSet.CoreV1().Pods(r.ArtifactPodInfo.Namespace).
List(ctx, metav1.ListOptions{LabelSelector: r.ArtifactPodInfo.Label})
if err != nil {
return errors.Wrap(err, fmt.Sprintf("listing pods with label %s in namespace %s", r.ArtifactPodInfo.Label, r.ArtifactPodInfo.Namespace))
}
if len(pods.Items) < 1 {
return errors.New("No artifact pod found")
}
pod := pods.Items[0]
stdout, stderr, err := r.executeRemoteCommand(r.ArtifactPodInfo.Namespace, pod.Name, fmt.Sprintf("rm -rf %s/%s.*", r.ArtifactPodInfo.Path, artifact.Name))
if err != nil {
return errors.Wrap(err, fmt.Sprintf("%s\n%s", stdout, stderr))
}
return nil
}
func (r *OSArtifactReconciler) executeRemoteCommand(namespace, podName, command string) (string, string, error) {
buf := &bytes.Buffer{}
errBuf := &bytes.Buffer{}
request := r.clientSet.CoreV1().RESTClient().
Post().
Namespace(namespace).
Resource("pods").
Name(podName).
SubResource("exec").
VersionedParams(&v1.PodExecOptions{
Command: []string{"/bin/sh", "-c", command},
Stdin: false,
Stdout: true,
Stderr: true,
TTY: true,
}, scheme.ParameterCodec)
exec, err := remotecommand.NewSPDYExecutor(r.restConfig, "POST", request.URL())
if err != nil {
return "", "", err
}
err = exec.Stream(remotecommand.StreamOptions{
Stdout: buf,
Stderr: errBuf,
})
if err != nil {
return "", "", fmt.Errorf("%w Failed executing command %s on %v/%v", err, command, namespace, podName)
}
return buf.String(), errBuf.String(), nil
}

View File

@ -25,22 +25,42 @@ import (
"github.com/pkg/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"sigs.k8s.io/cluster-api/util/patch"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/log"
)
const FinalizerName = "build.kairos.io/osbuilder-finalizer"
type ArtifactPodInfo struct {
Label string
Namespace string
Path string
Role string
}
// OSArtifactReconciler reconciles a OSArtifact object
type OSArtifactReconciler struct {
client.Client
Scheme *runtime.Scheme
clientSet *kubernetes.Clientset
ServingImage, ToolImage string
Scheme *runtime.Scheme
restConfig *rest.Config
clientSet *kubernetes.Clientset
ServingImage, ToolImage, CopierImage string
ArtifactPodInfo ArtifactPodInfo
}
func genObjectMeta(artifact buildv1alpha1.OSArtifact) metav1.ObjectMeta {
return metav1.ObjectMeta{
Name: artifact.Name,
Namespace: artifact.Namespace,
OwnerReferences: genOwner(artifact),
}
}
func genOwner(artifact buildv1alpha1.OSArtifact) []metav1.OwnerReference {
@ -57,6 +77,12 @@ func genOwner(artifact buildv1alpha1.OSArtifact) []metav1.OwnerReference {
//+kubebuilder:rbac:groups=build.kairos.io,resources=osartifacts/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=build.kairos.io,resources=osartifacts/finalizers,verbs=update
// TODO: Is this ^ how I should have created rbac permissions for the controller?
// - git commit all changes
// - generate code with kubebuilder
// - check if my permissions were removed
// - do it properly
// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
@ -79,17 +105,22 @@ func (r *OSArtifactReconciler) Reconcile(ctx context.Context, req ctrl.Request)
logger.Info(fmt.Sprintf("Reconciling %v", osbuild))
stop, err := r.handleFinalizer(ctx, &osbuild)
if err != nil || stop {
return ctrl.Result{}, err
}
// generate configmap required for building a custom image
desiredConfigMap := r.genConfigMap(osbuild)
logger.Info(fmt.Sprintf("Checking configmap %v", osbuild))
cfgMap, err := r.clientSet.CoreV1().ConfigMaps(req.Namespace).Get(ctx, desiredConfigMap.Name, v1.GetOptions{})
cfgMap, err := r.clientSet.CoreV1().ConfigMaps(req.Namespace).Get(ctx, desiredConfigMap.Name, metav1.GetOptions{})
if cfgMap == nil || apierrors.IsNotFound(err) {
logger.Info(fmt.Sprintf("Creating service %v", desiredConfigMap))
logger.Info(fmt.Sprintf("Creating config map %v", desiredConfigMap))
cfgMap, err = r.clientSet.CoreV1().ConfigMaps(req.Namespace).Create(ctx, desiredConfigMap, v1.CreateOptions{})
_, err = r.clientSet.CoreV1().ConfigMaps(req.Namespace).Create(ctx, desiredConfigMap, metav1.CreateOptions{})
if err != nil {
logger.Error(err, "Failed while creating svc")
logger.Error(err, "Failed while creating config map")
return ctrl.Result{}, err
}
return ctrl.Result{Requeue: true}, err
@ -98,34 +129,21 @@ func (r *OSArtifactReconciler) Reconcile(ctx context.Context, req ctrl.Request)
return ctrl.Result{Requeue: true}, err
}
desiredService := genService(osbuild)
logger.Info(fmt.Sprintf("Checking service %v", osbuild))
svc, err := r.clientSet.CoreV1().Services(req.Namespace).Get(ctx, desiredService.Name, v1.GetOptions{})
if svc == nil || apierrors.IsNotFound(err) {
logger.Info(fmt.Sprintf("Creating service %v", desiredService))
svc, err = r.clientSet.CoreV1().Services(req.Namespace).Create(ctx, desiredService, v1.CreateOptions{})
if err != nil {
logger.Error(err, "Failed while creating svc")
return ctrl.Result{}, err
}
return ctrl.Result{Requeue: true}, err
}
if err != nil {
return ctrl.Result{Requeue: true}, err
}
logger.Info(fmt.Sprintf("Checking deployment %v", osbuild))
desiredDeployment := r.genDeployment(osbuild, svc)
deployment, err := r.clientSet.AppsV1().Deployments(req.Namespace).Get(ctx, desiredDeployment.Name, v1.GetOptions{})
if deployment == nil || apierrors.IsNotFound(err) {
logger.Info(fmt.Sprintf("Creating Deployment %v", deployment))
err = r.createRBAC(ctx, osbuild)
if err != nil {
return ctrl.Result{Requeue: true}, err
}
deployment, err = r.clientSet.AppsV1().Deployments(req.Namespace).Create(ctx, desiredDeployment, v1.CreateOptions{})
desiredJob := r.genJob(osbuild)
job, err := r.clientSet.BatchV1().Jobs(req.Namespace).Get(ctx, desiredJob.Name, metav1.GetOptions{})
if job == nil || apierrors.IsNotFound(err) {
logger.Info(fmt.Sprintf("Creating Job %v", job))
_, err = r.clientSet.BatchV1().Jobs(req.Namespace).Create(ctx, desiredJob, metav1.CreateOptions{})
if err != nil {
logger.Error(err, "Failed while creating deployment")
logger.Error(err, "Failed while creating job")
return ctrl.Result{}, nil
}
@ -143,7 +161,7 @@ func (r *OSArtifactReconciler) Reconcile(ctx context.Context, req ctrl.Request)
if err != nil {
return ctrl.Result{}, err
}
if deployment.Status.ReadyReplicas == deployment.Status.Replicas {
if job.Status.Succeeded > 0 {
copy.Status.Phase = "Ready"
} else if copy.Status.Phase != "Building" {
copy.Status.Phase = "Building"
@ -166,13 +184,66 @@ func (r *OSArtifactReconciler) Reconcile(ctx context.Context, req ctrl.Request)
// SetupWithManager sets up the controller with the Manager.
func (r *OSArtifactReconciler) SetupWithManager(mgr ctrl.Manager) error {
clientset, err := kubernetes.NewForConfig(mgr.GetConfig())
cfg := mgr.GetConfig()
clientset, err := kubernetes.NewForConfig(cfg)
if err != nil {
return err
}
r.restConfig = cfg
r.clientSet = clientset
return ctrl.NewControllerManagedBy(mgr).
For(&buildv1alpha1.OSArtifact{}).
Complete(r)
}
// Returns true if reconciliation should stop or false otherwise
func (r *OSArtifactReconciler) handleFinalizer(ctx context.Context, osbuild *buildv1alpha1.OSArtifact) (bool, error) {
// examine DeletionTimestamp to determine if object is under deletion
if osbuild.DeletionTimestamp.IsZero() {
// The object is not being deleted, so if it does not have our finalizer,
// then lets add the finalizer and update the object. This is equivalent
// registering our finalizer.
if !controllerutil.ContainsFinalizer(osbuild, FinalizerName) {
controllerutil.AddFinalizer(osbuild, FinalizerName)
if err := r.Update(ctx, osbuild); err != nil {
return true, err
}
}
} else {
// The object is being deleted
if controllerutil.ContainsFinalizer(osbuild, FinalizerName) {
// our finalizer is present, so lets handle any external dependency
if err := r.finalize(ctx, osbuild); err != nil {
// if fail to delete the external dependency here, return with error
// so that it can be retried
return true, err
}
// remove our finalizer from the list and update it.
controllerutil.RemoveFinalizer(osbuild, FinalizerName)
if err := r.Update(ctx, osbuild); err != nil {
return true, err
}
}
// Stop reconciliation as the item is being deleted
return true, nil
}
return false, nil
}
// - Remove artifacts from the server Pod
// - Delete role-binding (because it doesn't have the OSArtifact as an owner and won't be deleted automatically)
func (r *OSArtifactReconciler) finalize(ctx context.Context, osbuild *buildv1alpha1.OSArtifact) error {
if err := r.removeRBAC(ctx, *osbuild); err != nil {
return err
}
if err := r.removeArtifacts(ctx, *osbuild); err != nil {
return err
}
return nil
}

View File

@ -1,39 +0,0 @@
/*
Copyright 2022.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controllers
import (
buildv1alpha1 "github.com/kairos-io/osbuilder/api/v1alpha1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func genService(artifact buildv1alpha1.OSArtifact) *v1.Service {
objMeta := metav1.ObjectMeta{
Name: artifact.Name,
Namespace: artifact.Namespace,
OwnerReferences: genOwner(artifact),
}
return &v1.Service{
ObjectMeta: objMeta,
Spec: v1.ServiceSpec{
Type: v1.ServiceTypeNodePort,
Ports: []v1.ServicePort{{Name: "http", Port: int32(80)}},
Selector: genDeploymentLabel(artifact.Name),
},
}
}

1
go.mod
View File

@ -51,6 +51,7 @@ require (
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/moby/spdystream v0.2.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect

3
go.sum
View File

@ -82,6 +82,7 @@ github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210826220005-b48c857c3a0e/go.m
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
@ -136,6 +137,7 @@ github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc=
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk=
@ -375,6 +377,7 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.4.2 h1:6h7AQ0yhTcIsmFmnAwQls75jp2Gzs4iB8W7pjMO+rqo=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8=
github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=

21
main.go
View File

@ -52,12 +52,22 @@ func main() {
var metricsAddr string
var enableLeaderElection bool
var probeAddr string
var serveImage, toolImage string
var serveImage, toolImage, copierImage string
var copyToPodLabel, copyToNamespace, copyToPath, copierRole string
flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.")
flag.StringVar(&copierImage, "copier-image", "quay.io/kairos/kubectl", "The image that is used to copy artifacts to the server pod.")
flag.StringVar(&serveImage, "serve-image", "nginx", "Serve image.")
// It needs luet inside
flag.StringVar(&toolImage, "tool-image", "quay.io/kairos/osbuilder-tools:latest", "Tool image.")
// Information on where to copy the artifacts
flag.StringVar(&copyToPodLabel, "copy-to-pod-label", "", "The label of the Pod to which artifacts should be copied.")
flag.StringVar(&copyToNamespace, "copy-to-namespace", "", "The namespace of the copy-to-pod-label Pod.")
flag.StringVar(&copyToPath, "copy-to-path", "", "The path under which to copy artifacts in the copy-to-pod-label Pod.")
flag.StringVar(&copierRole, "copy-role", "", "The name or the Kubernetes Role that has the permissions to copy artifacts to the copy-to-pod-label Pod")
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
"Enable leader election for controller manager. "+
@ -98,7 +108,14 @@ func main() {
Client: mgr.GetClient(),
ServingImage: serveImage,
ToolImage: toolImage,
Scheme: mgr.GetScheme(),
CopierImage: copierImage,
ArtifactPodInfo: controllers.ArtifactPodInfo{
Label: copyToPodLabel,
Namespace: copyToNamespace,
Path: copyToPath,
Role: copierRole,
},
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "OSArtifact")
os.Exit(1)

View File

@ -1,6 +1,10 @@
package e2e_test
import (
"bytes"
"fmt"
"os/exec"
"strings"
"time"
. "github.com/onsi/ginkgo/v2"
@ -20,20 +24,91 @@ var _ = Describe("ISO build test", func() {
err := kubectl.Apply("", "../../tests/fixtures/simple.yaml")
Expect(err).ToNot(HaveOccurred())
Eventually(func() string {
b, _ := kubectl.GetData("default", "osartifacts", "hello-kairos", "jsonpath={.spec.imageName}")
return string(b)
}, 2*time.Minute, 2*time.Second).Should(Equal("quay.io/kairos/core-opensuse:latest"))
itHasTheCorrectImage()
itHasTheCorrectLabels()
itCopiesTheArtifacts()
Eventually(func() string {
b, _ := kubectl.GetData("default", "deployments", "hello-kairos", "jsonpath={.spec.template.metadata.labels.osbuild}")
return string(b)
}, 2*time.Minute, 2*time.Second).Should(Equal("workloadhello-kairos"))
Eventually(func() string {
b, _ := kubectl.GetData("default", "deployments", "hello-kairos", "jsonpath={.spec.status.unavailableReplicas}")
return string(b)
}, 15*time.Minute, 2*time.Second).ShouldNot(Equal("1"))
By("deleting the custom resource", func() {
err = kubectl.New().Delete("osartifacts", "-n", "default", "hello-kairos")
Expect(err).ToNot(HaveOccurred())
})
itCleansUpRoleBindings()
itDeletesTheArtifacts()
})
})
})
func itHasTheCorrectImage() {
Eventually(func() string {
b, _ := kubectl.GetData("default", "osartifacts", "hello-kairos", "jsonpath={.spec.imageName}")
fmt.Printf("looking for image core-opensuse:latest = %+v\n", string(b))
return string(b)
}, 2*time.Minute, 2*time.Second).Should(Equal("quay.io/kairos/core-opensuse:latest"))
}
func itHasTheCorrectLabels() {
Eventually(func() string {
b, _ := kubectl.GetData("default", "jobs", "hello-kairos", "jsonpath={.spec.template.metadata.labels.osbuild}")
fmt.Printf("looking for label workloadhello-kairos = %+v\n", string(b))
return string(b)
}, 2*time.Minute, 2*time.Second).Should(Equal("workloadhello-kairos"))
}
func itCopiesTheArtifacts() {
nginxNamespace := "osartifactbuilder-operator-system"
Eventually(func() string {
podName := strings.TrimSpace(findPodsWithLabel(nginxNamespace, "app.kubernetes.io/name=osbuilder-nginx"))
out, _ := kubectl.RunCommandWithOutput(nginxNamespace, podName, "ls /usr/share/nginx/html")
return out
}, 15*time.Minute, 2*time.Second).Should(MatchRegexp("hello-kairos.iso"))
}
func itCleansUpRoleBindings() {
nginxNamespace := "osartifactbuilder-operator-system"
Eventually(func() string {
rb := findRoleBindings(nginxNamespace)
return rb
}, 3*time.Minute, 2*time.Second).ShouldNot(MatchRegexp("hello-kairos"))
}
func itDeletesTheArtifacts() {
nginxNamespace := "osartifactbuilder-operator-system"
Eventually(func() string {
podName := findPodsWithLabel(nginxNamespace, "app.kubernetes.io/name=osbuilder-nginx")
out, err := kubectl.RunCommandWithOutput(nginxNamespace, podName, "ls /usr/share/nginx/html")
Expect(err).ToNot(HaveOccurred(), out)
return out
}, 3*time.Minute, 2*time.Second).ShouldNot(MatchRegexp("hello-kairos.iso"))
}
func findPodsWithLabel(namespace, label string) string {
kubectlCommand := fmt.Sprintf("kubectl get pods -n %s -l %s --no-headers -o custom-columns=\":metadata.name\" | head -n1", namespace, label)
cmd := exec.Command("bash", "-c", kubectlCommand)
var out bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &stderr
err := cmd.Run()
Expect(err).ToNot(HaveOccurred(), stderr.String())
return strings.TrimSpace(out.String())
}
func findRoleBindings(namespace string) string {
kubectlCommand := fmt.Sprintf("kubectl get rolebindings -n %s --no-headers -o custom-columns=\":metadata.name\"", namespace)
cmd := exec.Command("bash", "-c", kubectlCommand)
var out bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &stderr
err := cmd.Run()
Expect(err).ToNot(HaveOccurred(), stderr.String())
return strings.TrimSpace(out.String())
}

View File

@ -6,7 +6,7 @@ spec:
imageName: "quay.io/kairos/core-opensuse:latest"
iso: true
bundles:
- quay.io/kairos/packages:goreleaser-utils-1.11.1
- quay.io/kairos/packages:goreleaser-utils-1.13.1
grubConfig: |
search --file --set=root /boot/kernel.xz
set default=0
@ -49,4 +49,4 @@ spec:
device: "/dev/sda"
reboot: true
poweroff: true
auto: true # Required, for automated installations
auto: true # Required, for automated installations