mirror of
https://github.com/kairos-io/osbuilder.git
synced 2025-04-27 11:11:44 +00:00
allow for custom artifact export logic
Signed-off-by: Jacob Payne <jacob@spectrocloud.com>
This commit is contained in:
parent
95dd24d549
commit
8d67aafa9c
@ -17,7 +17,7 @@ limitations under the License.
|
||||
// Package v1alpha1 contains API Schema definitions for the build v1alpha1 API group
|
||||
// +kubebuilder:object:generate=true
|
||||
// +groupName=build.kairos.io
|
||||
package v1alpha1
|
||||
package v1alpha2
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
@ -26,7 +26,7 @@ import (
|
||||
|
||||
var (
|
||||
// GroupVersion is group version used to register these objects
|
||||
GroupVersion = schema.GroupVersion{Group: "build.kairos.io", Version: "v1alpha1"}
|
||||
GroupVersion = schema.GroupVersion{Group: "build.kairos.io", Version: "v1alpha2"}
|
||||
|
||||
// SchemeBuilder is used to add go types to the GroupVersionKind scheme
|
||||
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
|
@ -14,23 +14,18 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package v1alpha1
|
||||
package v1alpha2
|
||||
|
||||
import (
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
|
||||
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
|
||||
|
||||
// OSArtifactSpec defines the desired state of OSArtifact
|
||||
type OSArtifactSpec struct {
|
||||
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
|
||||
// Important: Run "make" to regenerate code after modifying this file
|
||||
|
||||
// Foo is an example field of OSArtifact. Edit osartifact_types.go to remove/update
|
||||
ImageName string `json:"imageName,omitempty"`
|
||||
// This needs to be revisited
|
||||
|
||||
ISO bool `json:"iso,omitempty"`
|
||||
|
||||
//Disk-only stuff
|
||||
@ -42,47 +37,42 @@ type OSArtifactSpec struct {
|
||||
Netboot bool `json:"netboot,omitempty"`
|
||||
NetbootURL string `json:"netbootURL,omitempty"`
|
||||
|
||||
// TODO: treat cloudconfig as a secret, and take a secretRef where to store it (optionally)
|
||||
CloudConfig string `json:"cloudConfig,omitempty"`
|
||||
GRUBConfig string `json:"grubConfig,omitempty"`
|
||||
CloudConfigRef *SecretKeySelector `json:"cloudConfigRef,omitempty"`
|
||||
GRUBConfig string `json:"grubConfig,omitempty"`
|
||||
|
||||
Bundles []string `json:"bundles,omitempty"`
|
||||
PullOptions Pull `json:"pull,omitempty"`
|
||||
OSRelease string `json:"osRelease,omitempty"`
|
||||
// TODO: Currently not used. Reserved to be used when we have a way to push to registries.
|
||||
PushOptions Push `json:"push,omitempty"`
|
||||
}
|
||||
Bundles []string `json:"bundles,omitempty"`
|
||||
OSRelease string `json:"osRelease,omitempty"`
|
||||
|
||||
type Push struct {
|
||||
Push bool `json:"push,omitempty"`
|
||||
ImageName string `json:"imageName,omitempty"`
|
||||
ContainerRegistryCredentials *SecretKeySelector `json:"containerRegistryCredentials,omitempty"`
|
||||
}
|
||||
|
||||
type Pull struct {
|
||||
ContainerRegistryCredentials *SecretKeySelector `json:"containerRegistryCredentials,omitempty"`
|
||||
}
|
||||
type LocalObjectReference struct {
|
||||
Name string `json:"name"`
|
||||
ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"`
|
||||
Exporters []batchv1.JobSpec `json:"exporter,omitempty"`
|
||||
Volume *corev1.PersistentVolumeClaimSpec `json:"volume,omitempty"`
|
||||
}
|
||||
|
||||
type SecretKeySelector struct {
|
||||
LocalObjectReference `json:",inline"`
|
||||
// +optional
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
Name string `json:"name"`
|
||||
// +optional
|
||||
Key string `json:"key,omitempty"`
|
||||
}
|
||||
|
||||
type ArtifactPhase string
|
||||
|
||||
const (
|
||||
Pending = "Pending"
|
||||
Building = "Building"
|
||||
Exporting = "Exporting"
|
||||
Ready = "Ready"
|
||||
Error = "Error"
|
||||
)
|
||||
|
||||
// OSArtifactStatus defines the observed state of OSArtifact
|
||||
type OSArtifactStatus struct {
|
||||
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
|
||||
// Important: Run "make" to regenerate code after modifying this file
|
||||
Phase string `json:"phase,omitempty"`
|
||||
// +kubebuilder:default=Pending
|
||||
Phase ArtifactPhase `json:"phase,omitempty"`
|
||||
}
|
||||
|
||||
//+kubebuilder:object:root=true
|
||||
//+kubebuilder:subresource:status
|
||||
// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase`
|
||||
|
||||
// OSArtifact is the Schema for the osartifacts API
|
||||
type OSArtifact struct {
|
@ -19,27 +19,14 @@ limitations under the License.
|
||||
|
||||
// Code generated by controller-gen. DO NOT EDIT.
|
||||
|
||||
package v1alpha1
|
||||
package v1alpha2
|
||||
|
||||
import (
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
"k8s.io/api/core/v1"
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *LocalObjectReference) DeepCopyInto(out *LocalObjectReference) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LocalObjectReference.
|
||||
func (in *LocalObjectReference) DeepCopy() *LocalObjectReference {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(LocalObjectReference)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *OSArtifact) DeepCopyInto(out *OSArtifact) {
|
||||
*out = *in
|
||||
@ -102,13 +89,33 @@ func (in *OSArtifactList) DeepCopyObject() runtime.Object {
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *OSArtifactSpec) DeepCopyInto(out *OSArtifactSpec) {
|
||||
*out = *in
|
||||
if in.CloudConfigRef != nil {
|
||||
in, out := &in.CloudConfigRef, &out.CloudConfigRef
|
||||
*out = new(SecretKeySelector)
|
||||
**out = **in
|
||||
}
|
||||
if in.Bundles != nil {
|
||||
in, out := &in.Bundles, &out.Bundles
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
in.PullOptions.DeepCopyInto(&out.PullOptions)
|
||||
in.PushOptions.DeepCopyInto(&out.PushOptions)
|
||||
if in.ImagePullSecrets != nil {
|
||||
in, out := &in.ImagePullSecrets, &out.ImagePullSecrets
|
||||
*out = make([]v1.LocalObjectReference, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Exporters != nil {
|
||||
in, out := &in.Exporters, &out.Exporters
|
||||
*out = make([]batchv1.JobSpec, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.Volume != nil {
|
||||
in, out := &in.Volume, &out.Volume
|
||||
*out = new(v1.PersistentVolumeClaimSpec)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OSArtifactSpec.
|
||||
@ -136,50 +143,9 @@ func (in *OSArtifactStatus) DeepCopy() *OSArtifactStatus {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Pull) DeepCopyInto(out *Pull) {
|
||||
*out = *in
|
||||
if in.ContainerRegistryCredentials != nil {
|
||||
in, out := &in.ContainerRegistryCredentials, &out.ContainerRegistryCredentials
|
||||
*out = new(SecretKeySelector)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Pull.
|
||||
func (in *Pull) DeepCopy() *Pull {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Pull)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Push) DeepCopyInto(out *Push) {
|
||||
*out = *in
|
||||
if in.ContainerRegistryCredentials != nil {
|
||||
in, out := &in.ContainerRegistryCredentials, &out.ContainerRegistryCredentials
|
||||
*out = new(SecretKeySelector)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Push.
|
||||
func (in *Push) DeepCopy() *Push {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Push)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SecretKeySelector) DeepCopyInto(out *SecretKeySelector) {
|
||||
*out = *in
|
||||
out.LocalObjectReference = in.LocalObjectReference
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretKeySelector.
|
File diff suppressed because it is too large
Load Diff
@ -16,6 +16,3 @@ resources:
|
||||
- auth_proxy_role.yaml
|
||||
- auth_proxy_role_binding.yaml
|
||||
- auth_proxy_client_clusterrole.yaml
|
||||
|
||||
patchesStrategicMerge:
|
||||
- role_custom.yaml
|
@ -5,6 +5,49 @@ metadata:
|
||||
creationTimestamp: null
|
||||
name: manager-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- configmaps
|
||||
verbs:
|
||||
- create
|
||||
- get
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- persistentvolumeclaims
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- pods
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- secrets
|
||||
verbs:
|
||||
- get
|
||||
- apiGroups:
|
||||
- batch
|
||||
resources:
|
||||
- jobs
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- build.kairos.io
|
||||
resources:
|
||||
|
@ -1,94 +0,0 @@
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
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:
|
||||
- osartifacts
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- build.kairos.io
|
||||
resources:
|
||||
- osartifacts/finalizers
|
||||
verbs:
|
||||
- update
|
||||
- apiGroups:
|
||||
- build.kairos.io
|
||||
resources:
|
||||
- osartifacts/status
|
||||
verbs:
|
||||
- get
|
||||
- patch
|
||||
- update
|
||||
- apiGroups:
|
||||
- build.kairos.io
|
||||
resources:
|
||||
- osartifacts/finalizers
|
||||
verbs:
|
||||
- update
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- configmaps
|
||||
verbs:
|
||||
- get
|
||||
- create
|
||||
- update
|
||||
- apiGroups:
|
||||
- "batch"
|
||||
resources:
|
||||
- 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
|
@ -17,22 +17,19 @@ limitations under the License.
|
||||
package controllers
|
||||
|
||||
import (
|
||||
buildv1alpha1 "github.com/kairos-io/osbuilder/api/v1alpha1"
|
||||
osbuilder "github.com/kairos-io/osbuilder/api/v1alpha2"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func (r *OSArtifactReconciler) genConfigMap(artifact buildv1alpha1.OSArtifact) *v1.ConfigMap {
|
||||
func (r *OSArtifactReconciler) genConfigMap(artifact *osbuilder.OSArtifact) *v1.ConfigMap {
|
||||
return &v1.ConfigMap{
|
||||
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: artifact.Name,
|
||||
Namespace: artifact.Namespace,
|
||||
OwnerReferences: genOwner(artifact),
|
||||
Name: artifact.Name,
|
||||
Namespace: artifact.Namespace,
|
||||
},
|
||||
Data: map[string]string{
|
||||
"config": artifact.Spec.CloudConfig,
|
||||
"grub.cfg": artifact.Spec.GRUBConfig,
|
||||
"os-release": artifact.Spec.OSRelease,
|
||||
}}
|
||||
|
@ -17,35 +17,17 @@ 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"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/tools/remotecommand"
|
||||
osbuilder "github.com/kairos-io/osbuilder/api/v1alpha2"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
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,
|
||||
func unpackContainer(id, containerImage, pullImage string) corev1.Container {
|
||||
return corev1.Container{
|
||||
ImagePullPolicy: corev1.PullAlways,
|
||||
Name: fmt.Sprintf("pull-image-%s", id),
|
||||
Image: containerImage,
|
||||
Command: []string{"/bin/bash", "-cxe"},
|
||||
@ -56,7 +38,7 @@ func unpackContainer(id, containerImage, pullImage string, pullOptions buildv1al
|
||||
"/rootfs",
|
||||
),
|
||||
},
|
||||
VolumeMounts: []v1.VolumeMount{
|
||||
VolumeMounts: []corev1.VolumeMount{
|
||||
{
|
||||
Name: "rootfs",
|
||||
MountPath: "/rootfs",
|
||||
@ -65,19 +47,19 @@ func unpackContainer(id, containerImage, pullImage string, pullOptions buildv1al
|
||||
}
|
||||
}
|
||||
|
||||
func pushImageName(artifact buildv1alpha1.OSArtifact) string {
|
||||
pushName := artifact.Spec.PushOptions.ImageName
|
||||
func pushImageName(artifact *osbuilder.OSArtifact) string {
|
||||
pushName := artifact.Spec.ImageName
|
||||
if pushName != "" {
|
||||
return pushName
|
||||
}
|
||||
return artifact.Name
|
||||
}
|
||||
|
||||
func createImageContainer(containerImage string, artifact buildv1alpha1.OSArtifact) v1.Container {
|
||||
func createImageContainer(containerImage string, artifact *osbuilder.OSArtifact) corev1.Container {
|
||||
imageName := pushImageName(artifact)
|
||||
|
||||
return v1.Container{
|
||||
ImagePullPolicy: v1.PullAlways,
|
||||
return corev1.Container{
|
||||
ImagePullPolicy: corev1.PullAlways,
|
||||
Name: "create-image",
|
||||
Image: containerImage,
|
||||
Command: []string{"/bin/bash", "-cxe"},
|
||||
@ -88,7 +70,7 @@ func createImageContainer(containerImage string, artifact buildv1alpha1.OSArtifa
|
||||
artifact.Name,
|
||||
),
|
||||
},
|
||||
VolumeMounts: []v1.VolumeMount{
|
||||
VolumeMounts: []corev1.VolumeMount{
|
||||
{
|
||||
Name: "rootfs",
|
||||
MountPath: "/rootfs",
|
||||
@ -101,39 +83,16 @@ func createImageContainer(containerImage string, artifact buildv1alpha1.OSArtifa
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
func osReleaseContainer(containerImage string) corev1.Container {
|
||||
return corev1.Container{
|
||||
ImagePullPolicy: corev1.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{
|
||||
VolumeMounts: []corev1.VolumeMount{
|
||||
{
|
||||
Name: "config",
|
||||
MountPath: "/etc/os-release",
|
||||
@ -147,18 +106,38 @@ func osReleaseContainer(containerImage string) v1.Container {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *OSArtifactReconciler) genJob(artifact buildv1alpha1.OSArtifact) *batchv1.Job {
|
||||
objMeta := genObjectMeta(artifact)
|
||||
func (r *OSArtifactReconciler) newArtifactPVC(artifact *osbuilder.OSArtifact) *corev1.PersistentVolumeClaim {
|
||||
if artifact.Spec.Volume == nil {
|
||||
artifact.Spec.Volume = &corev1.PersistentVolumeClaimSpec{
|
||||
AccessModes: []corev1.PersistentVolumeAccessMode{
|
||||
corev1.ReadWriteOnce,
|
||||
},
|
||||
Resources: corev1.ResourceRequirements{
|
||||
Requests: map[corev1.ResourceName]resource.Quantity{
|
||||
"storage": resource.MustParse("2Gi"),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
privileged := false
|
||||
serviceAccount := true
|
||||
pvc := &corev1.PersistentVolumeClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: artifact.Name + "-artifacts",
|
||||
Namespace: artifact.Namespace,
|
||||
},
|
||||
Spec: *artifact.Spec.Volume,
|
||||
}
|
||||
|
||||
return pvc
|
||||
}
|
||||
|
||||
func (r *OSArtifactReconciler) newBuilderPod(pvcName string, artifact *osbuilder.OSArtifact) *corev1.Pod {
|
||||
cmd := fmt.Sprintf(
|
||||
"/entrypoint.sh --debug --name %s build-iso --date=false --output /artifacts dir:/rootfs",
|
||||
artifact.Name,
|
||||
)
|
||||
|
||||
volumeMounts := []v1.VolumeMount{
|
||||
volumeMounts := []corev1.VolumeMount{
|
||||
{
|
||||
Name: "artifacts",
|
||||
MountPath: "/artifacts",
|
||||
@ -170,7 +149,7 @@ func (r *OSArtifactReconciler) genJob(artifact buildv1alpha1.OSArtifact) *batchv
|
||||
}
|
||||
|
||||
if artifact.Spec.GRUBConfig != "" {
|
||||
volumeMounts = append(volumeMounts, v1.VolumeMount{
|
||||
volumeMounts = append(volumeMounts, corev1.VolumeMount{
|
||||
Name: "config",
|
||||
MountPath: "/iso/iso-overlay/boot/grub2/grub.cfg",
|
||||
SubPath: "grub.cfg",
|
||||
@ -182,26 +161,26 @@ func (r *OSArtifactReconciler) genJob(artifact buildv1alpha1.OSArtifact) *batchv
|
||||
artifact.Name,
|
||||
)
|
||||
|
||||
if artifact.Spec.CloudConfig != "" {
|
||||
volumeMounts = append(volumeMounts, v1.VolumeMount{
|
||||
Name: "config",
|
||||
if artifact.Spec.CloudConfigRef != nil {
|
||||
volumeMounts = append(volumeMounts, corev1.VolumeMount{
|
||||
Name: "cloudconfig",
|
||||
MountPath: "/iso/iso-overlay/cloud_config.yaml",
|
||||
SubPath: "config",
|
||||
SubPath: artifact.Spec.CloudConfigRef.Key,
|
||||
})
|
||||
|
||||
cloudImgCmd += " /iso/iso-overlay/cloud_config.yaml"
|
||||
}
|
||||
|
||||
if artifact.Spec.CloudConfig != "" || artifact.Spec.GRUBConfig != "" {
|
||||
if artifact.Spec.CloudConfigRef != nil || 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},
|
||||
buildIsoContainer := corev1.Container{
|
||||
ImagePullPolicy: corev1.PullAlways,
|
||||
SecurityContext: &corev1.SecurityContext{Privileged: ptr(true)},
|
||||
Name: "build-iso",
|
||||
Image: r.ToolImage,
|
||||
Command: []string{"/bin/bash", "-cxe"},
|
||||
@ -211,9 +190,9 @@ func (r *OSArtifactReconciler) genJob(artifact buildv1alpha1.OSArtifact) *batchv
|
||||
VolumeMounts: volumeMounts,
|
||||
}
|
||||
|
||||
buildCloudImageContainer := v1.Container{
|
||||
ImagePullPolicy: v1.PullAlways,
|
||||
SecurityContext: &v1.SecurityContext{Privileged: &privileged},
|
||||
buildCloudImageContainer := corev1.Container{
|
||||
ImagePullPolicy: corev1.PullAlways,
|
||||
SecurityContext: &corev1.SecurityContext{Privileged: ptr(true)},
|
||||
Name: "build-cloud-image",
|
||||
Image: r.ToolImage,
|
||||
|
||||
@ -225,19 +204,19 @@ func (r *OSArtifactReconciler) genJob(artifact buildv1alpha1.OSArtifact) *batchv
|
||||
}
|
||||
|
||||
if artifact.Spec.DiskSize != "" {
|
||||
buildCloudImageContainer.Env = []v1.EnvVar{{
|
||||
buildCloudImageContainer.Env = []corev1.EnvVar{{
|
||||
Name: "EXTEND",
|
||||
Value: artifact.Spec.DiskSize,
|
||||
}}
|
||||
}
|
||||
|
||||
extractNetboot := v1.Container{
|
||||
ImagePullPolicy: v1.PullAlways,
|
||||
SecurityContext: &v1.SecurityContext{Privileged: &privileged},
|
||||
extractNetboot := corev1.Container{
|
||||
ImagePullPolicy: corev1.PullAlways,
|
||||
SecurityContext: &corev1.SecurityContext{Privileged: ptr(true)},
|
||||
Name: "build-netboot",
|
||||
Image: r.ToolImage,
|
||||
Command: []string{"/bin/bash", "-cxe"},
|
||||
Env: []v1.EnvVar{{
|
||||
Env: []corev1.EnvVar{{
|
||||
Name: "URL",
|
||||
Value: artifact.Spec.NetbootURL,
|
||||
}},
|
||||
@ -251,9 +230,9 @@ func (r *OSArtifactReconciler) genJob(artifact buildv1alpha1.OSArtifact) *batchv
|
||||
VolumeMounts: volumeMounts,
|
||||
}
|
||||
|
||||
buildAzureCloudImageContainer := v1.Container{
|
||||
ImagePullPolicy: v1.PullAlways,
|
||||
SecurityContext: &v1.SecurityContext{Privileged: &privileged},
|
||||
buildAzureCloudImageContainer := corev1.Container{
|
||||
ImagePullPolicy: corev1.PullAlways,
|
||||
SecurityContext: &corev1.SecurityContext{Privileged: ptr(true)},
|
||||
Name: "build-azure-cloud-image",
|
||||
Image: r.ToolImage,
|
||||
Command: []string{"/bin/bash", "-cxe"},
|
||||
@ -267,9 +246,9 @@ func (r *OSArtifactReconciler) genJob(artifact buildv1alpha1.OSArtifact) *batchv
|
||||
VolumeMounts: volumeMounts,
|
||||
}
|
||||
|
||||
buildGCECloudImageContainer := v1.Container{
|
||||
ImagePullPolicy: v1.PullAlways,
|
||||
SecurityContext: &v1.SecurityContext{Privileged: &privileged},
|
||||
buildGCECloudImageContainer := corev1.Container{
|
||||
ImagePullPolicy: corev1.PullAlways,
|
||||
SecurityContext: &corev1.SecurityContext{Privileged: ptr(true)},
|
||||
Name: "build-gce-cloud-image",
|
||||
Image: r.ToolImage,
|
||||
Command: []string{"/bin/bash", "-cxe"},
|
||||
@ -283,251 +262,93 @@ func (r *OSArtifactReconciler) genJob(artifact buildv1alpha1.OSArtifact) *batchv
|
||||
VolumeMounts: volumeMounts,
|
||||
}
|
||||
|
||||
pod := v1.PodSpec{
|
||||
AutomountServiceAccountToken: &serviceAccount,
|
||||
ServiceAccountName: objMeta.Name,
|
||||
RestartPolicy: v1.RestartPolicyNever,
|
||||
Volumes: []v1.Volume{
|
||||
podSpec := corev1.PodSpec{
|
||||
AutomountServiceAccountToken: ptr(false),
|
||||
RestartPolicy: corev1.RestartPolicyNever,
|
||||
Volumes: []corev1.Volume{
|
||||
{
|
||||
Name: "artifacts",
|
||||
VolumeSource: v1.VolumeSource{EmptyDir: &v1.EmptyDirVolumeSource{}},
|
||||
Name: "artifacts",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
|
||||
ClaimName: pvcName,
|
||||
ReadOnly: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "rootfs",
|
||||
VolumeSource: v1.VolumeSource{EmptyDir: &v1.EmptyDirVolumeSource{}},
|
||||
VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}},
|
||||
},
|
||||
{
|
||||
Name: "config",
|
||||
VolumeSource: v1.VolumeSource{
|
||||
ConfigMap: &v1.ConfigMapVolumeSource{
|
||||
LocalObjectReference: v1.LocalObjectReference{Name: artifact.Name}}},
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
ConfigMap: &corev1.ConfigMapVolumeSource{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: artifact.Name,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
pod.InitContainers = []v1.Container{unpackContainer("baseimage", r.ToolImage, artifact.Spec.ImageName, artifact.Spec.PullOptions)}
|
||||
if artifact.Spec.CloudConfigRef != nil {
|
||||
podSpec.Volumes = append(podSpec.Volumes, corev1.Volume{
|
||||
Name: "cloudconfig",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
Secret: &corev1.SecretVolumeSource{
|
||||
SecretName: artifact.Spec.CloudConfigRef.Name,
|
||||
Optional: ptr(true),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
for i := range artifact.Spec.ImagePullSecrets {
|
||||
podSpec.ImagePullSecrets = append(podSpec.ImagePullSecrets, artifact.Spec.ImagePullSecrets[i])
|
||||
}
|
||||
|
||||
podSpec.InitContainers = []corev1.Container{unpackContainer("baseimage", r.ToolImage, artifact.Spec.ImageName)}
|
||||
|
||||
for i, bundle := range artifact.Spec.Bundles {
|
||||
pod.InitContainers = append(pod.InitContainers, unpackContainer(fmt.Sprint(i), r.ToolImage, bundle, artifact.Spec.PullOptions))
|
||||
podSpec.InitContainers = append(podSpec.InitContainers, unpackContainer(fmt.Sprint(i), r.ToolImage, bundle))
|
||||
}
|
||||
|
||||
if artifact.Spec.OSRelease != "" {
|
||||
pod.InitContainers = append(pod.InitContainers, osReleaseContainer(r.ToolImage))
|
||||
podSpec.InitContainers = append(podSpec.InitContainers, osReleaseContainer(r.ToolImage))
|
||||
}
|
||||
|
||||
if artifact.Spec.ISO || artifact.Spec.Netboot {
|
||||
pod.InitContainers = append(pod.InitContainers, buildIsoContainer)
|
||||
podSpec.Containers = append(podSpec.Containers, buildIsoContainer)
|
||||
}
|
||||
|
||||
if artifact.Spec.Netboot {
|
||||
pod.InitContainers = append(pod.InitContainers, extractNetboot)
|
||||
podSpec.Containers = append(podSpec.Containers, extractNetboot)
|
||||
}
|
||||
|
||||
if artifact.Spec.CloudImage || artifact.Spec.AzureImage || artifact.Spec.GCEImage {
|
||||
pod.InitContainers = append(pod.InitContainers, buildCloudImageContainer)
|
||||
if artifact.Spec.CloudImage {
|
||||
podSpec.Containers = append(podSpec.Containers, buildCloudImageContainer)
|
||||
}
|
||||
|
||||
if artifact.Spec.AzureImage {
|
||||
pod.InitContainers = append(pod.InitContainers, buildAzureCloudImageContainer)
|
||||
podSpec.Containers = append(podSpec.Containers, buildAzureCloudImageContainer)
|
||||
}
|
||||
|
||||
if artifact.Spec.GCEImage {
|
||||
pod.InitContainers = append(pod.InitContainers, buildGCECloudImageContainer)
|
||||
podSpec.Containers = append(podSpec.Containers, buildGCECloudImageContainer)
|
||||
}
|
||||
|
||||
pod.InitContainers = append(pod.InitContainers, createImageContainer(r.ToolImage, artifact))
|
||||
podSpec.Containers = append(podSpec.Containers, createImageContainer(r.ToolImage, artifact))
|
||||
|
||||
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{
|
||||
return &corev1.Pod{
|
||||
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,
|
||||
},
|
||||
GenerateName: artifact.Name + "-",
|
||||
Namespace: artifact.Namespace,
|
||||
},
|
||||
Spec: podSpec,
|
||||
}
|
||||
|
||||
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
|
||||
func ptr[T any](val T) *T {
|
||||
return &val
|
||||
}
|
||||
|
@ -19,230 +19,286 @@ package controllers
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
buildv1alpha1 "github.com/kairos-io/osbuilder/api/v1alpha1"
|
||||
"github.com/pkg/errors"
|
||||
osbuilder "github.com/kairos-io/osbuilder/api/v1alpha2"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "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"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/builder"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
"sigs.k8s.io/controller-runtime/pkg/source"
|
||||
)
|
||||
|
||||
const FinalizerName = "build.kairos.io/osbuilder-finalizer"
|
||||
|
||||
type ArtifactPodInfo struct {
|
||||
Label string
|
||||
Namespace string
|
||||
Path string
|
||||
Role string
|
||||
}
|
||||
const (
|
||||
FinalizerName = "build.kairos.io/osbuilder-finalizer"
|
||||
artifactLabel = "build.kairos.io/artifact"
|
||||
artifactExporterIndexAnnotation = "build.kairos.io/export-index"
|
||||
)
|
||||
|
||||
// OSArtifactReconciler reconciles a OSArtifact object
|
||||
type OSArtifactReconciler struct {
|
||||
client.Client
|
||||
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 (r *OSArtifactReconciler) InjectClient(c client.Client) error {
|
||||
r.Client = c
|
||||
|
||||
func genOwner(artifact buildv1alpha1.OSArtifact) []metav1.OwnerReference {
|
||||
return []metav1.OwnerReference{
|
||||
*metav1.NewControllerRef(&artifact.ObjectMeta, schema.GroupVersionKind{
|
||||
Group: buildv1alpha1.GroupVersion.Group,
|
||||
Version: buildv1alpha1.GroupVersion.Version,
|
||||
Kind: "OSArtifact",
|
||||
}),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//+kubebuilder:rbac:groups=build.kairos.io,resources=osartifacts,verbs=get;list;watch;create;update;patch;delete
|
||||
//+kubebuilder:rbac:groups=build.kairos.io,resources=osartifacts/status,verbs=get;update;patch
|
||||
//+kubebuilder:rbac:groups=build.kairos.io,resources=osartifacts/finalizers,verbs=update
|
||||
//+kubebuilder:rbac:groups="",resources=pods,verbs=get;list;watch;create;delete
|
||||
//+kubebuilder:rbac:groups="",resources=persistentvolumeclaims,verbs=get;list;create;delete;watch
|
||||
//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;
|
||||
//+kubebuilder:rbac:groups="",resources=configmaps,verbs=get;create;
|
||||
//+kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;delete
|
||||
|
||||
// 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
|
||||
// the OSArtifact object against the actual cluster state, and then
|
||||
// perform operations to make the cluster state reflect the state specified by
|
||||
// the user.
|
||||
//
|
||||
// For more details, check Reconcile and its Result here:
|
||||
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.12.1/pkg/reconcile
|
||||
func (r *OSArtifactReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
var osbuild buildv1alpha1.OSArtifact
|
||||
if err := r.Get(ctx, req.NamespacedName, &osbuild); err != nil {
|
||||
var artifact osbuilder.OSArtifact
|
||||
if err := r.Get(ctx, req.NamespacedName, &artifact); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
return ctrl.Result{}, err
|
||||
return ctrl.Result{Requeue: true}, err
|
||||
}
|
||||
|
||||
logger.Info(fmt.Sprintf("Reconciling %v", osbuild))
|
||||
|
||||
stop, err := r.handleFinalizer(ctx, &osbuild)
|
||||
if err != nil || stop {
|
||||
return ctrl.Result{}, err
|
||||
if artifact.DeletionTimestamp != nil {
|
||||
controllerutil.RemoveFinalizer(&artifact, FinalizerName)
|
||||
return ctrl.Result{}, r.Update(ctx, &artifact)
|
||||
}
|
||||
|
||||
// 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, metav1.GetOptions{})
|
||||
if cfgMap == nil || apierrors.IsNotFound(err) {
|
||||
logger.Info(fmt.Sprintf("Creating config map %v", desiredConfigMap))
|
||||
|
||||
_, err = r.clientSet.CoreV1().ConfigMaps(req.Namespace).Create(ctx, desiredConfigMap, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
logger.Error(err, "Failed while creating config map")
|
||||
return ctrl.Result{}, err
|
||||
if !controllerutil.ContainsFinalizer(&artifact, FinalizerName) {
|
||||
controllerutil.AddFinalizer(&artifact, FinalizerName)
|
||||
if err := r.Update(ctx, &artifact); err != nil {
|
||||
return ctrl.Result{Requeue: true}, err
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info(fmt.Sprintf("Reconciling %s/%s", artifact.Namespace, artifact.Name))
|
||||
|
||||
switch artifact.Status.Phase {
|
||||
case osbuilder.Exporting:
|
||||
return r.checkExport(ctx, &artifact)
|
||||
case osbuilder.Ready, osbuilder.Error:
|
||||
return ctrl.Result{}, nil
|
||||
default:
|
||||
return r.checkBuild(ctx, &artifact)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *OSArtifactReconciler) startBuild(ctx context.Context, artifact *osbuilder.OSArtifact) (ctrl.Result, error) {
|
||||
// generate configmap required for building a custom image
|
||||
cm := r.genConfigMap(artifact)
|
||||
if cm.Labels == nil {
|
||||
cm.Labels = map[string]string{}
|
||||
}
|
||||
cm.Labels[artifactLabel] = artifact.Name
|
||||
if err := controllerutil.SetOwnerReference(artifact, cm, r.Scheme()); err != nil {
|
||||
return ctrl.Result{Requeue: true}, err
|
||||
}
|
||||
if err != nil {
|
||||
if err := r.Create(ctx, cm); err != nil && !apierrors.IsAlreadyExists(err) {
|
||||
return ctrl.Result{Requeue: true}, err
|
||||
}
|
||||
|
||||
logger.Info(fmt.Sprintf("Checking deployment %v", osbuild))
|
||||
|
||||
err = r.createRBAC(ctx, osbuild)
|
||||
if err != nil {
|
||||
pvc := r.newArtifactPVC(artifact)
|
||||
if pvc.Labels == nil {
|
||||
pvc.Labels = map[string]string{}
|
||||
}
|
||||
pvc.Labels[artifactLabel] = artifact.Name
|
||||
if err := controllerutil.SetOwnerReference(artifact, pvc, r.Scheme()); err != nil {
|
||||
return ctrl.Result{Requeue: true}, err
|
||||
}
|
||||
if err := r.Create(ctx, pvc); err != nil {
|
||||
return ctrl.Result{Requeue: true}, err
|
||||
}
|
||||
|
||||
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))
|
||||
pod := r.newBuilderPod(pvc.Name, artifact)
|
||||
if pod.Labels == nil {
|
||||
pod.Labels = map[string]string{}
|
||||
}
|
||||
pod.Labels[artifactLabel] = artifact.Name
|
||||
if err := controllerutil.SetOwnerReference(artifact, pod, r.Scheme()); err != nil {
|
||||
return ctrl.Result{Requeue: true}, err
|
||||
}
|
||||
if err := r.Create(ctx, pod); err != nil {
|
||||
return ctrl.Result{Requeue: true}, err
|
||||
}
|
||||
|
||||
_, err = r.clientSet.BatchV1().Jobs(req.Namespace).Create(ctx, desiredJob, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
logger.Error(err, "Failed while creating job")
|
||||
artifact.Status.Phase = osbuilder.Building
|
||||
if err := r.Status().Update(ctx, artifact); err != nil {
|
||||
return ctrl.Result{Requeue: true}, err
|
||||
}
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
func (r *OSArtifactReconciler) checkBuild(ctx context.Context, artifact *osbuilder.OSArtifact) (ctrl.Result, error) {
|
||||
var pods corev1.PodList
|
||||
if err := r.List(ctx, &pods, &client.ListOptions{
|
||||
LabelSelector: labels.SelectorFromSet(labels.Set{
|
||||
artifactLabel: artifact.Name,
|
||||
}),
|
||||
}); err != nil {
|
||||
return ctrl.Result{Requeue: true}, err
|
||||
}
|
||||
|
||||
for _, pod := range pods.Items {
|
||||
switch pod.Status.Phase {
|
||||
case corev1.PodSucceeded:
|
||||
artifact.Status.Phase = osbuilder.Exporting
|
||||
return ctrl.Result{Requeue: true}, r.Status().Update(ctx, artifact)
|
||||
case corev1.PodFailed:
|
||||
artifact.Status.Phase = osbuilder.Error
|
||||
return ctrl.Result{Requeue: true}, r.Status().Update(ctx, artifact)
|
||||
case corev1.PodPending, corev1.PodRunning:
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
return ctrl.Result{Requeue: true}, nil
|
||||
}
|
||||
if err != nil {
|
||||
|
||||
return r.startBuild(ctx, artifact)
|
||||
}
|
||||
|
||||
func (r *OSArtifactReconciler) checkExport(ctx context.Context, artifact *osbuilder.OSArtifact) (ctrl.Result, error) {
|
||||
var jobs batchv1.JobList
|
||||
if err := r.List(ctx, &jobs, &client.ListOptions{
|
||||
LabelSelector: labels.SelectorFromSet(labels.Set{
|
||||
artifactLabel: artifact.Name,
|
||||
}),
|
||||
}); err != nil {
|
||||
return ctrl.Result{Requeue: true}, err
|
||||
}
|
||||
|
||||
logger.Info(fmt.Sprintf("Updating state %v", osbuild))
|
||||
|
||||
copy := osbuild.DeepCopy()
|
||||
|
||||
helper, err := patch.NewHelper(&osbuild, r.Client)
|
||||
if err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
if job.Status.Succeeded > 0 {
|
||||
copy.Status.Phase = "Ready"
|
||||
} else if copy.Status.Phase != "Building" {
|
||||
copy.Status.Phase = "Building"
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second)
|
||||
defer cancel()
|
||||
if err := helper.Patch(ctx, copy); err != nil {
|
||||
return ctrl.Result{}, errors.Wrapf(err, "couldn't patch osbuild %q", copy.Name)
|
||||
indexedJobs := make(map[string]*batchv1.Job, len(artifact.Spec.Exporters))
|
||||
for _, job := range jobs.Items {
|
||||
if job.GetAnnotations() != nil {
|
||||
if idx, ok := job.GetAnnotations()[artifactExporterIndexAnnotation]; ok {
|
||||
indexedJobs[idx] = &job
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// for _, c := range append(pod.Status.ContainerStatuses, pod.Status.InitContainerStatuses...) {
|
||||
// if c.State.Terminated != nil && c.State.Terminated.ExitCode != 0 {
|
||||
// packageBuildCopy.Status.State = "Failed"
|
||||
// }
|
||||
// }
|
||||
var pvcs corev1.PersistentVolumeClaimList
|
||||
var pvc *corev1.PersistentVolumeClaim
|
||||
if err := r.List(ctx, &pvcs, &client.ListOptions{LabelSelector: labels.SelectorFromSet(labels.Set{artifactLabel: artifact.Name})}); err != nil {
|
||||
return ctrl.Result{Requeue: true}, err
|
||||
}
|
||||
|
||||
for _, item := range pvcs.Items {
|
||||
pvc = &item
|
||||
break
|
||||
}
|
||||
|
||||
if pvc == nil {
|
||||
log.FromContext(ctx).Error(nil, "failed to locate pvc for artifact, this should not happen")
|
||||
return ctrl.Result{}, fmt.Errorf("failed to locate artifact pvc")
|
||||
}
|
||||
|
||||
var succeeded int
|
||||
for i := range artifact.Spec.Exporters {
|
||||
idx := fmt.Sprintf("%d", i)
|
||||
|
||||
job := indexedJobs[idx]
|
||||
if job == nil {
|
||||
job = &batchv1.Job{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fmt.Sprintf("%s-export-%s", artifact.Name, idx),
|
||||
Namespace: artifact.Namespace,
|
||||
Annotations: map[string]string{
|
||||
artifactExporterIndexAnnotation: idx,
|
||||
},
|
||||
Labels: map[string]string{
|
||||
artifactLabel: artifact.Name,
|
||||
},
|
||||
},
|
||||
Spec: artifact.Spec.Exporters[i],
|
||||
}
|
||||
|
||||
job.Spec.Template.Spec.Volumes = append(job.Spec.Template.Spec.Volumes, corev1.Volume{
|
||||
Name: "artifacts",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
|
||||
ClaimName: pvc.Name,
|
||||
ReadOnly: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if err := controllerutil.SetOwnerReference(artifact, job, r.Scheme()); err != nil {
|
||||
return ctrl.Result{Requeue: true}, err
|
||||
}
|
||||
|
||||
if err := r.Create(ctx, job); err != nil {
|
||||
return ctrl.Result{Requeue: true}, err
|
||||
}
|
||||
|
||||
} else if job.Spec.Completions == nil || *job.Spec.Completions == 1 {
|
||||
if job.Status.Succeeded > 0 {
|
||||
succeeded++
|
||||
}
|
||||
} else if *job.Spec.BackoffLimit <= job.Status.Failed {
|
||||
artifact.Status.Phase = osbuilder.Error
|
||||
if err := r.Status().Update(ctx, artifact); err != nil {
|
||||
return ctrl.Result{Requeue: true}, err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if succeeded == len(artifact.Spec.Exporters) {
|
||||
artifact.Status.Phase = osbuilder.Ready
|
||||
if err := r.Status().Update(ctx, artifact); err != nil {
|
||||
return ctrl.Result{Requeue: true}, err
|
||||
}
|
||||
}
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// SetupWithManager sets up the controller with the Manager.
|
||||
func (r *OSArtifactReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
|
||||
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{}).
|
||||
For(&osbuilder.OSArtifact{}).
|
||||
Owns(&osbuilder.OSArtifact{}).
|
||||
Watches(
|
||||
&source.Kind{Type: &corev1.Pod{}},
|
||||
handler.EnqueueRequestsFromMapFunc(r.findOwningArtifact),
|
||||
builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}),
|
||||
).
|
||||
Watches(
|
||||
&source.Kind{Type: &batchv1.Job{}},
|
||||
handler.EnqueueRequestsFromMapFunc(r.findOwningArtifact),
|
||||
builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}),
|
||||
).
|
||||
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
|
||||
func (r *OSArtifactReconciler) findOwningArtifact(obj client.Object) []reconcile.Request {
|
||||
if obj.GetLabels() == nil {
|
||||
return 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
|
||||
if artifactName, ok := obj.GetLabels()[artifactLabel]; ok {
|
||||
return []reconcile.Request{
|
||||
{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: artifactName,
|
||||
Namespace: obj.GetNamespace(),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
3
go.sum
3
go.sum
@ -191,7 +191,6 @@ github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh
|
||||
github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng=
|
||||
github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/gobuffalo/flect v0.2.4 h1:BSYA8+T60cdyq+vynaSUjqSVI9mDEg9ZfQUXKmfjo4I=
|
||||
github.com/gobuffalo/flect v0.2.4/go.mod h1:1ZyCLIbg0YD7sDkzvFdPoOydPtD8y9JQnrOROolUcM8=
|
||||
@ -280,7 +279,6 @@ github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLe
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
|
||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
|
||||
@ -831,7 +829,6 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.10-0.20220218145154-897bd77cd717 h1:hI3jKY4Hpf63ns040onEbB3dAkR/H/P83hw1TG8dD3Y=
|
||||
golang.org/x/tools v0.1.10-0.20220218145154-897bd77cd717/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
12
main.go
12
main.go
@ -31,7 +31,7 @@ import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/healthz"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
|
||||
buildv1alpha1 "github.com/kairos-io/osbuilder/api/v1alpha1"
|
||||
buildv1alpha2 "github.com/kairos-io/osbuilder/api/v1alpha2"
|
||||
"github.com/kairos-io/osbuilder/controllers"
|
||||
//+kubebuilder:scaffold:imports
|
||||
)
|
||||
@ -44,7 +44,7 @@ var (
|
||||
func init() {
|
||||
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
|
||||
|
||||
utilruntime.Must(buildv1alpha1.AddToScheme(scheme))
|
||||
utilruntime.Must(buildv1alpha2.AddToScheme(scheme))
|
||||
//+kubebuilder:scaffold:scheme
|
||||
}
|
||||
|
||||
@ -105,17 +105,9 @@ func main() {
|
||||
}
|
||||
|
||||
if err = (&controllers.OSArtifactReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
ServingImage: serveImage,
|
||||
ToolImage: toolImage,
|
||||
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)
|
||||
|
@ -1,114 +1,157 @@
|
||||
package e2e_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"context"
|
||||
osbuilder "github.com/kairos-io/osbuilder/api/v1alpha2"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
kubectl "github.com/rancher-sandbox/ele-testhelpers/kubectl"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/selection"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/client-go/dynamic"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
var _ = Describe("ISO build test", func() {
|
||||
//k := kubectl.New()
|
||||
Context("registration", func() {
|
||||
k8s := dynamic.NewForConfigOrDie(ctrl.GetConfigOrDie())
|
||||
scheme := runtime.NewScheme()
|
||||
_ = osbuilder.AddToScheme(scheme)
|
||||
|
||||
AfterEach(func() {
|
||||
kubectl.New().Delete("osartifacts", "-n", "default", "hello-kairos")
|
||||
var artifactName string
|
||||
artifacts := k8s.Resource(schema.GroupVersionResource{Group: osbuilder.GroupVersion.Group, Version: osbuilder.GroupVersion.Version, Resource: "osartifacts"}).Namespace("default")
|
||||
pods := k8s.Resource(schema.GroupVersionResource{Group: corev1.GroupName, Version: corev1.SchemeGroupVersion.Version, Resource: "pods"}).Namespace("default")
|
||||
pvcs := k8s.Resource(schema.GroupVersionResource{Group: corev1.GroupName, Version: corev1.SchemeGroupVersion.Version, Resource: "persistentvolumeclaims"}).Namespace("default")
|
||||
jobs := k8s.Resource(schema.GroupVersionResource{Group: batchv1.GroupName, Version: batchv1.SchemeGroupVersion.Version, Resource: "jobs"}).Namespace("default")
|
||||
|
||||
artifact := &osbuilder.OSArtifact{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "OSArtifact",
|
||||
APIVersion: osbuilder.GroupVersion.String(),
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: "simple-",
|
||||
},
|
||||
Spec: osbuilder.OSArtifactSpec{
|
||||
ImageName: "quay.io/kairos/core-opensuse:latest",
|
||||
ISO: true,
|
||||
DiskSize: "",
|
||||
Exporters: []batchv1.JobSpec{
|
||||
{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
RestartPolicy: corev1.RestartPolicyNever,
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "test",
|
||||
Image: "debian:latest",
|
||||
Command: []string{"bash"},
|
||||
Args: []string{"-xec", "[ -f /artifacts/*.iso ]"},
|
||||
VolumeMounts: []corev1.VolumeMount{
|
||||
{
|
||||
Name: "artifacts",
|
||||
ReadOnly: true,
|
||||
MountPath: "/artifacts",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
uArtifact := unstructured.Unstructured{}
|
||||
uArtifact.Object, _ = runtime.DefaultUnstructuredConverter.ToUnstructured(artifact)
|
||||
resp, err := artifacts.Create(context.TODO(), &uArtifact, metav1.CreateOptions{})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
artifactName = resp.GetName()
|
||||
|
||||
Context("simple", func() {
|
||||
artifactLabelSelectorReq, _ := labels.NewRequirement("build.kairos.io/artifact", selection.Equals, []string{artifactName})
|
||||
artifactLabelSelector := labels.NewSelector().Add(*artifactLabelSelectorReq)
|
||||
|
||||
It("starts the build", func() {
|
||||
Eventually(func(g Gomega) {
|
||||
w, err := pods.Watch(context.TODO(), metav1.ListOptions{LabelSelector: artifactLabelSelector.String()})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
var stopped bool
|
||||
for !stopped {
|
||||
event, ok := <-w.ResultChan()
|
||||
|
||||
stopped = event.Type != watch.Deleted && event.Type != watch.Error || !ok
|
||||
}
|
||||
}).Should(Succeed())
|
||||
})
|
||||
|
||||
It("creates a simple iso", func() {
|
||||
err := kubectl.Apply("", "../../tests/fixtures/simple.yaml")
|
||||
It("exports the artifacts", func() {
|
||||
Eventually(func(g Gomega) {
|
||||
w, err := jobs.Watch(context.TODO(), metav1.ListOptions{LabelSelector: artifactLabelSelector.String()})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
var stopped bool
|
||||
for !stopped {
|
||||
event, ok := <-w.ResultChan()
|
||||
|
||||
stopped = event.Type != watch.Deleted && event.Type != watch.Error || !ok
|
||||
}
|
||||
}).Should(Succeed())
|
||||
})
|
||||
|
||||
It("artifact successfully builds", func() {
|
||||
Eventually(func(g Gomega) {
|
||||
w, err := artifacts.Watch(context.TODO(), metav1.ListOptions{})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
var artifact osbuilder.OSArtifact
|
||||
var stopped bool
|
||||
for !stopped {
|
||||
event, ok := <-w.ResultChan()
|
||||
stopped = !ok
|
||||
|
||||
if event.Type == watch.Modified && event.Object.(*unstructured.Unstructured).GetName() == artifactName {
|
||||
err := scheme.Convert(event.Object, &artifact, nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
stopped = artifact.Status.Phase == osbuilder.Ready
|
||||
}
|
||||
|
||||
}
|
||||
}).Should(Succeed())
|
||||
})
|
||||
|
||||
It("cleans up resources on deleted", func() {
|
||||
err := artifacts.Delete(context.TODO(), artifactName, metav1.DeleteOptions{})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
itHasTheCorrectImage()
|
||||
itHasTheCorrectLabels()
|
||||
itCopiesTheArtifacts()
|
||||
|
||||
By("deleting the custom resource", func() {
|
||||
err = kubectl.New().Delete("osartifacts", "-n", "default", "hello-kairos")
|
||||
Eventually(func(g Gomega) int {
|
||||
res, err := artifacts.List(context.TODO(), metav1.ListOptions{})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
itCleansUpRoleBindings()
|
||||
itDeletesTheArtifacts()
|
||||
return len(res.Items)
|
||||
}).WithTimeout(time.Minute).Should(Equal(0))
|
||||
Eventually(func(g Gomega) int {
|
||||
res, err := pods.List(context.TODO(), metav1.ListOptions{LabelSelector: artifactLabelSelector.String()})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
return len(res.Items)
|
||||
}).WithTimeout(time.Minute).Should(Equal(0))
|
||||
Eventually(func(g Gomega) int {
|
||||
res, err := pvcs.List(context.TODO(), metav1.ListOptions{LabelSelector: artifactLabelSelector.String()})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
return len(res.Items)
|
||||
}).WithTimeout(time.Minute).Should(Equal(0))
|
||||
Eventually(func(g Gomega) int {
|
||||
res, err := jobs.List(context.TODO(), metav1.ListOptions{LabelSelector: artifactLabelSelector.String()})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
return len(res.Items)
|
||||
}).WithTimeout(time.Minute).Should(Equal(0))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
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())
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user