diff --git a/api/v1alpha1/osartifact_types.go b/api/v1alpha1/osartifact_types.go index 34f18c1..773e4a0 100644 --- a/api/v1alpha1/osartifact_types.go +++ b/api/v1alpha1/osartifact_types.go @@ -35,7 +35,30 @@ type OSArtifactSpec struct { CloudConfig string `json:"cloudConfig,omitempty"` GRUBConfig string `json:"grubConfig,omitempty"` - PullFromKube bool `json:"pullFromKube,omitempty"` + Bundles []string `json:"bundles,omitempty"` + PullOptions Pull `json:"pull,omitempty"` + PushOptions Push `json:"push,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"` +} + +type SecretKeySelector struct { + LocalObjectReference `json:",inline"` + // +optional + Namespace string `json:"namespace,omitempty"` + // +optional + Key string `json:"key,omitempty"` } // OSArtifactStatus defines the observed state of OSArtifact diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index e55c55e..52bab9d 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -25,12 +25,27 @@ import ( 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 out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) out.Status = in.Status } @@ -87,6 +102,13 @@ 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.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) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OSArtifactSpec. @@ -113,3 +135,59 @@ func (in *OSArtifactStatus) DeepCopy() *OSArtifactStatus { in.DeepCopyInto(out) 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. +func (in *SecretKeySelector) DeepCopy() *SecretKeySelector { + if in == nil { + return nil + } + out := new(SecretKeySelector) + in.DeepCopyInto(out) + return out +} diff --git a/config/crd/bases/build.c3os-x.io_osartifacts.yaml b/config/crd/bases/build.c3os-x.io_osartifacts.yaml index 5a1199d..d8776f1 100644 --- a/config/crd/bases/build.c3os-x.io_osartifacts.yaml +++ b/config/crd/bases/build.c3os-x.io_osartifacts.yaml @@ -35,7 +35,15 @@ spec: spec: description: OSArtifactSpec defines the desired state of OSArtifact properties: + bundles: + items: + type: string + type: array cloudConfig: + description: 'TODO: treat cloudconfig as a secret, and take a secretRef + where to store it (optionally)' + type: string + grubConfig: type: string imageName: description: Foo is an example field of OSArtifact. Edit osartifact_types.go @@ -43,6 +51,38 @@ spec: type: string iso: type: boolean + pull: + properties: + containerRegistryCredentials: + properties: + key: + type: string + name: + type: string + namespace: + type: string + required: + - name + type: object + type: object + push: + properties: + containerRegistryCredentials: + properties: + key: + type: string + name: + type: string + namespace: + type: string + required: + - name + type: object + imageName: + type: string + push: + type: boolean + type: object type: object status: description: OSArtifactStatus defines the observed state of OSArtifact diff --git a/controllers/deployment.go b/controllers/deployment.go index 0237f35..792fda6 100644 --- a/controllers/deployment.go +++ b/controllers/deployment.go @@ -31,6 +31,78 @@ func genDeploymentLabel(s string) 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 pushImageContainer(containerImage string, pushOptions buildv1alpha1.Push) v1.Container { + return v1.Container{ + ImagePullPolicy: v1.PullAlways, + Name: "push-image", + Image: containerImage, + Command: []string{"/bin/bash", "-cxe"}, + Args: []string{ + fmt.Sprintf( + "skopeo /public/image.tar %s", + pushOptions.ImageName, + ), + }, + VolumeMounts: []v1.VolumeMount{ + + { + Name: "public", + MountPath: "/public", + }, + }, + } +} + func (r *OSArtifactReconciler) genDeployment(artifact buildv1alpha1.OSArtifact) *appsv1.Deployment { objMeta := metav1.ObjectMeta{ Name: artifact.Name, @@ -38,8 +110,11 @@ func (r *OSArtifactReconciler) genDeployment(artifact buildv1alpha1.OSArtifact) OwnerReferences: genOwner(artifact), } + pushImage := artifact.Spec.PushOptions.Push + privileged := false serviceAccount := false + buildIsoContainer := v1.Container{ ImagePullPolicy: v1.PullAlways, SecurityContext: &v1.SecurityContext{Privileged: &privileged}, @@ -48,9 +123,8 @@ func (r *OSArtifactReconciler) genDeployment(artifact buildv1alpha1.OSArtifact) Command: []string{"/bin/bash", "-cxe"}, Args: []string{ fmt.Sprintf( - "elemental --debug --name %s build-iso --date=false --overlay-iso /iso/iso-overlay %s --output /public", + "elemental --debug --name %s build-iso --date=false --overlay-iso /iso/iso-overlay --output /public dir:/rootfs", artifact.Name, - artifact.Spec.ImageName, ), }, VolumeMounts: []v1.VolumeMount{ @@ -64,35 +138,10 @@ func (r *OSArtifactReconciler) genDeployment(artifact buildv1alpha1.OSArtifact) SubPath: "config", }, { - Name: "grub", + Name: "config", MountPath: "/iso/iso-overlay/boot/grub2/grub.cfg", SubPath: "grub.cfg", }, - }, - } - - if artifact.Spec.PullFromKube { - buildIsoContainer.Args = []string{ - fmt.Sprintf( - "elemental --debug --name %s build-iso --date=false --overlay-iso /iso/iso-overlay --output /public /rootfs", - artifact.Name, - ), - } - } - - pullContainer := v1.Container{ - ImagePullPolicy: v1.PullAlways, - Name: "build-iso", - Image: artifact.Spec.ImageName, - Command: []string{"/bin/bash", "-cxe"}, - Args: []string{ - fmt.Sprintf( - "rsync -aqAX --exclude='mnt' --exclude='proc' --exclude='sys' --exclude='dev' --exclude='tmp' %s %s", - "/", - "/rootfs", - ), - }, - VolumeMounts: []v1.VolumeMount{ { Name: "rootfs", MountPath: "/rootfs", @@ -134,11 +183,19 @@ func (r *OSArtifactReconciler) genDeployment(artifact buildv1alpha1.OSArtifact) }, } - pod.InitContainers = []v1.Container{buildIsoContainer} - if artifact.Spec.PullFromKube { - // pull first - pod.InitContainers = append([]v1.Container{pullContainer}, pod.InitContainers...) + 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)) } + + pod.InitContainers = append(pod.InitContainers, buildIsoContainer) + + if pushImage { + pod.InitContainers = append(pod.InitContainers, createImageContainer(r.ToolImage, artifact.Spec.PushOptions)) + + } + pod.Containers = []v1.Container{servingContainer} deploymentLabels := genDeploymentLabel(artifact.Name) diff --git a/controllers/osartifact_controller.go b/controllers/osartifact_controller.go index cbcad05..09f4449 100644 --- a/controllers/osartifact_controller.go +++ b/controllers/osartifact_controller.go @@ -38,9 +38,9 @@ import ( // OSArtifactReconciler reconciles a OSArtifact object type OSArtifactReconciler struct { client.Client - Scheme *runtime.Scheme - clientSet *kubernetes.Clientset - ServingImage, BuildImage string + Scheme *runtime.Scheme + clientSet *kubernetes.Clientset + ServingImage, BuildImage, ToolImage string } func genOwner(artifact buildv1alpha1.OSArtifact) []metav1.OwnerReference { diff --git a/main.go b/main.go index b8f4820..0a1736c 100644 --- a/main.go +++ b/main.go @@ -52,10 +52,12 @@ func main() { var metricsAddr string var enableLeaderElection bool var probeAddr string - var buildImage, serveImage string + var buildImage, serveImage, toolImage string flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") - flag.StringVar(&buildImage, "build-image", "quay.io/costoolkit/elemental-cli:v0.0.15-8a78e6b", "Build image.") + flag.StringVar(&buildImage, "build-image", "quay.io/costoolkit/elemental-cli:v0.0.15-ae4f000", "Build image.") flag.StringVar(&serveImage, "serve-image", "nginx", "Serve image.") + // It needs luet inside + flag.StringVar(&toolImage, "tool-image", "quay.io/c3os/core-alpine:v0.57.0", "Tool image.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") flag.BoolVar(&enableLeaderElection, "leader-elect", false, @@ -96,6 +98,7 @@ func main() { if err = (&controllers.OSArtifactReconciler{ Client: mgr.GetClient(), ServingImage: serveImage, + ToolImage: toolImage, BuildImage: buildImage, Scheme: mgr.GetScheme(), }).SetupWithManager(mgr); err != nil { diff --git a/tests/fixtures/simple.yaml b/tests/fixtures/simple.yaml index 7b730f4..99a5460 100644 --- a/tests/fixtures/simple.yaml +++ b/tests/fixtures/simple.yaml @@ -4,4 +4,49 @@ metadata: name: hello-c3os spec: imageName: "quay.io/c3os/c3os:opensuse-latest" - iso: true \ No newline at end of file + iso: true + bundles: + - quay.io/c3os/packages:goreleaser-utils-1.11.1 + grubConfig: | + search --file --set=root /boot/kernel.xz + set default=0 + set timeout=10 + set timeout_style=menu + set linux=linux + set initrd=initrd + if [ "${grub_cpu}" = "x86_64" -o "${grub_cpu}" = "i386" -o "${grub_cpu}" = "arm64" ];then + if [ "${grub_platform}" = "efi" ]; then + if [ "${grub_cpu}" != "arm64" ]; then + set linux=linuxefi + set initrd=initrdefi + fi + fi + fi + if [ "${grub_platform}" = "efi" ]; then + echo "Please press 't' to show the boot menu on this console" + fi + set font=($root)/boot/${grub_cpu}/loader/grub2/fonts/unicode.pf2 + if [ -f ${font} ];then + loadfont ${font} + fi + menuentry "install" --class os --unrestricted { + echo Loading kernel... + $linux ($root)/boot/kernel.xz cdroot root=live:CDLABEL=COS_LIVE rd.live.dir=/ rd.live.squashimg=rootfs.squashfs console=tty1 console=ttyS0 rd.cos.disable vga=795 nomodeset nodepair.enable + echo Loading initrd... + $initrd ($root)/boot/rootfs.xz + } + + if [ "${grub_platform}" = "efi" ]; then + hiddenentry "Text mode" --hotkey "t" { + set textmode=true + terminal_output console + } + fi + + cloudConfig: | + #node-config + install: + device: "/dev/sda" + reboot: true + poweroff: true + auto: true # Required, for automated installations \ No newline at end of file