Compare commits

...

58 Commits

Author SHA1 Message Date
Itxaka
c7373a5fa6 Merge pull request #90 from Itxaka/bump_deps_toolimage 2023-08-16 16:39:22 +02:00
Itxaka
de5488867c Bump repos
Brings the newest grub packages to support secureboot

Signed-off-by: Itxaka <itxaka@kairos.io>
2023-08-16 16:16:40 +02:00
Itxaka
f2831462f3 Bump repos to get newer livecd/grub packages
New versions restore secureboot from cd and bundle the default grub.cfg
for livecd for both bios and efi

Signed-off-by: Itxaka <itxaka@kairos.io>
2023-08-16 14:37:03 +02:00
Itxaka
80c53e0d7f Merge pull request #85 from Itxaka/rpi3_rpi4
Support rpi3 and rpi 4 boards
2023-08-09 16:59:50 +02:00
Itxaka
0019c9cbb6 Merge branch 'master' into rpi3_rpi4 2023-08-08 15:20:12 +02:00
Itxaka
714323816b Merge pull request #82 from ci-forks/create-pull-request/patch 2023-08-08 15:20:01 +02:00
Itxaka
92193abe84 Dont use local flag for pull-image
not supported and done automatically

Signed-off-by: Itxaka <itxaka@kairos.io>
2023-08-08 15:06:32 +02:00
Itxaka
c813e546d0 Support rpi3 and rpi 4 boards
Due to rpi3 not supporting GPT disks, we need to separate the building
into rpi3 and 4 so rpi4 gets GPT disks and can auto-expand the last
partition properly (and add extra partitions and so on)

This also includes some cleanup for the luet cache dirs, includes the
kairos-agent which was missing from the image for pulling images, and
cleans up the lvm vgs if they were left

Signed-off-by: Itxaka <itxaka@kairos.io>
2023-08-08 14:56:39 +02:00
mudler
ed8c62dae5 ⬆️ Update repositories
Signed-off-by: GitHub <noreply@github.com>
2023-08-07 20:09:12 +00:00
Mauro Morales
a549b5e9d9 Merge pull request #84 from kairos-io/pass-targetarch-instead-of-buildarch
Pass targetarch instead of buildarch for copying luet config
2023-08-07 16:03:04 +02:00
Mauro Morales
3a34887f19 Pass targetarch instead of buildarch for copying luet config
Signed-off-by: Mauro Morales <mauro.morales@spectrocloud.com>
2023-08-07 15:58:17 +02:00
Itxaka
518e8f2800 Merge pull request #83 from Itxaka/other_missing_dir 2023-07-31 22:19:37 +02:00
Itxaka
8559cd70c0 Add second missing dir for arm grub efi
Signed-off-by: Itxaka <itxaka.garcia@spectrocloud.com>
2023-07-31 22:19:11 +02:00
Itxaka
19927d0ca9 Merge pull request #81 from Itxaka/missing_dir 2023-07-31 19:24:16 +02:00
Itxaka
2996f21dfa Fix missing dir for amr artifacts
It was using the default one which provides grub artifacts for the
current arch, not specific arm64 ones for the arm images

Signed-off-by: Itxaka <itxaka.garcia@spectrocloud.com>
2023-07-31 16:30:15 +02:00
Itxaka
d77a9fe726 Merge pull request #75 from ci-forks/create-pull-request/patch 2023-07-27 19:06:23 +02:00
Itxaka
80c791c4e7 ⬆️ Update repositories
Signed-off-by: GitHub <noreply@github.com>
2023-07-27 17:05:35 +00:00
Itxaka
e824fa824d Update repo job
To take into account both files

Signed-off-by: Itxaka <itxakaserrano@gmail.com>
2023-07-27 18:48:52 +02:00
Itxaka
98f867fa04 Merge pull request #80 from Itxaka/provider_cross_arch_artifacts
Restore cross-arch build of arm images
2023-07-27 18:27:51 +02:00
Itxaka
758e97a775 Fix tests for cleanstack
Is no longer provided by enki but by the sdk, so tests do not need to be
here anymore

Signed-off-by: Itxaka <itxaka.garcia@spectrocloud.com>
2023-07-27 12:08:45 +02:00
Itxaka
422bfa0c95 Restore cross-arch build of arm images
By playing with the buildarch and having separated luet repo files we
can install the proper artifacts for each arch AND also teh arm64
artifacts for arm images under x86

Signed-off-by: Itxaka <itxaka.garcia@spectrocloud.com>
2023-07-27 11:29:13 +02:00
Itxaka
f94e1de47b Merge pull request #74 from Itxaka/master 2023-07-20 09:43:30 +02:00
Itxaka
646993b5be Add missing overlayfs on default grub config
Signed-off-by: Itxaka <itxaka.garcia@spectrocloud.com>
2023-07-20 09:42:37 +02:00
Itxaka
5171e7f349 Merge pull request #73 from Itxaka/concurrency 2023-07-20 09:04:26 +02:00
Itxaka
2c0bb11afd 🤖 Add concurrency to jobs
Signed-off-by: Itxaka <itxaka.garcia@spectrocloud.com>
2023-07-20 09:04:01 +02:00
Itxaka
105f8dec5f Merge pull request #39 from kairos-io/renovate/quay.io-luet-base-0.x 2023-07-20 09:01:45 +02:00
renovate[bot]
0dba931224 Update quay.io/luet/base Docker tag to v0.34.0 2023-07-20 07:00:44 +00:00
Itxaka
48c2880e57 Merge pull request #52 from ci-forks/create-pull-request/patch 2023-07-20 09:00:28 +02:00
Itxaka
abde07894a Merge pull request #49 from kairos-io/renovate/docker-login-action-2.x 2023-07-20 09:00:12 +02:00
Itxaka
fdfae05bcc Merge pull request #48 from kairos-io/renovate/docker-build-push-action-4.x 2023-07-20 08:59:55 +02:00
Itxaka
205991a206 Merge pull request #38 from kairos-io/renovate/opensuse-leap-15.x 2023-07-20 08:59:40 +02:00
Itxaka
ce8fe1fbb2 Merge pull request #70 from Itxaka/osbuilder_go 2023-07-20 08:58:50 +02:00
Itxaka
de9b7bbb7c Use cleanstack from sdk
Signed-off-by: Itxaka <itxaka.garcia@spectrocloud.com>
2023-07-20 08:57:54 +02:00
mudler
979abef912 ⬆️ Update repositories
Signed-off-by: GitHub <noreply@github.com>
2023-07-19 20:09:01 +00:00
Itxaka
e6071f8764 Fix coverage
Signed-off-by: Itxaka <itxaka.garcia@spectrocloud.com>
2023-07-19 14:12:05 +02:00
Itxaka
3938a66c56 Drop uneeded stuff
Signed-off-by: Itxaka <itxaka.garcia@spectrocloud.com>
2023-07-19 14:03:37 +02:00
Itxaka
a296b96af5 Add more testing
Signed-off-by: Itxaka <itxaka.garcia@spectrocloud.com>
2023-07-19 13:51:29 +02:00
Itxaka
9ea3d77bb9 Rework cmds so they are inline with cobra examples
Signed-off-by: Itxaka <itxaka.garcia@spectrocloud.com>
2023-07-19 12:14:44 +02:00
Mauro Morales
f2294c25a3 Merge pull request #71 from kairos-io/add-linters
Add lint workflow
2023-07-19 09:15:16 +02:00
Itxaka
338b9eed08 Fix script location
Signed-off-by: Itxaka <itxaka.garcia@spectrocloud.com>
2023-07-18 19:33:49 +02:00
Mauro Morales
9437413a24 ignore checks and fix some
Signed-off-by: Mauro Morales <mauro.morales@spectrocloud.com>
2023-07-18 16:02:06 +02:00
Mauro Morales
1b345623bc ignore checks and fix some
Signed-off-by: Mauro Morales <mauro.morales@spectrocloud.com>
2023-07-18 15:31:46 +02:00
Itxaka
71f106920b Merge branch 'master' into osbuilder_go 2023-07-18 15:17:05 +02:00
Mauro Morales
9f933175ef add shellcheckrc
Signed-off-by: Mauro Morales <mauro.morales@spectrocloud.com>
2023-07-18 15:11:40 +02:00
Mauro Morales
1544361d6d fix yaml linter errors
Signed-off-by: Mauro Morales <mauro.morales@spectrocloud.com>
2023-07-18 14:59:32 +02:00
Mauro Morales
892beef6cc fix yaml linter errors
Signed-off-by: Mauro Morales <mauro.morales@spectrocloud.com>
2023-07-18 14:50:37 +02:00
Mauro Morales
adba7de85a Add yaml lint config file
Signed-off-by: Mauro Morales <mauro.morales@spectrocloud.com>
2023-07-18 14:28:59 +02:00
Itxaka
664c8d163d Install packages by luet and merge them
Install all packages via luet, converges the rpi packages into one,
makes arm use the same generic packages as x86 as they now come from the
proper architecture and reduces dockerfile complexity

Signed-off-by: Itxaka <itxaka.garcia@spectrocloud.com>
2023-07-18 14:14:51 +02:00
Mauro Morales
e62fa977e5 Add lint workflow
Signed-off-by: Mauro Morales <mauro.morales@spectrocloud.com>
2023-07-18 13:28:08 +02:00
Itxaka
2be9cfce66 Add iso builder
This adds a new package for the iso builder run directly on go.
This is extracted from the original elemental-cli and then from the now
build-less kairos-agent

This uses no deps on elemental, only deps are on kairos-agent for the
config stuff mainly.

Signed-off-by: Itxaka <itxaka.garcia@spectrocloud.com>
2023-07-18 12:51:03 +02:00
Itxaka
d8a56badc1 Merge pull request #69 from Itxaka/revert_gpt 2023-07-17 10:22:36 +02:00
Itxaka
f8353326fb Revert "Drop uneeded sfdisk call"
This reverts commit e5f563c4db.
2023-07-17 10:19:41 +02:00
Itxaka
eb6171b08c Revert "Really make it gpt"
This reverts commit a923f2e558.
2023-07-17 10:19:32 +02:00
Itxaka
a2b1ee4536 Revert "Use GPT for raw images"
This reverts commit 6b69df91a5.
2023-07-17 10:19:25 +02:00
Itxaka
e5f563c4db Drop uneeded sfdisk call
Signed-off-by: Itxaka <itxaka.garcia@spectrocloud.com>
2023-07-13 14:38:50 +02:00
renovate[bot]
48939327d6 Update docker/login-action action to v2 2023-05-16 20:01:29 +00:00
renovate[bot]
f38d261b93 Update docker/build-push-action action to v4 2023-05-16 20:01:24 +00:00
renovate[bot]
851ad65aaa Update opensuse/leap Docker tag to v15.5 2023-04-14 04:12:50 +00:00
45 changed files with 3733 additions and 195 deletions

24
.github/workflows/enki.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
---
name: 'run enki unit tests'
on:
pull_request:
concurrency:
group: enki-${{ github.ref || github.head_ref }}
cancel-in-progress: true
env:
FORCE_COLOR: 1
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: earthly/actions-setup@v1.0.7
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Build
run: cd tools-image/enki && earthly -P +test

View File

@@ -8,6 +8,10 @@ on:
tags:
- '*'
concurrency:
group: image-${{ github.ref || github.head_ref }}
cancel-in-progress: true
jobs:
docker:
runs-on: ubuntu-latest
@@ -46,14 +50,14 @@ jobs:
- name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
registry: quay.io
username: ${{ secrets.QUAY_USERNAME }}
password: ${{ secrets.QUAY_PASSWORD }}
- name: Build
uses: docker/build-push-action@v2
uses: docker/build-push-action@v4
with:
builder: ${{ steps.buildx.outputs.name }}
context: .

21
.github/workflows/lint.yaml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: Lint
on:
push:
branches:
- master
pull_request:
paths:
- '**'
concurrency:
group: lint-${{ github.ref || github.head_ref }}
cancel-in-progress: true
env:
FORCE_COLOR: 1
jobs:
call-workflow:
uses: kairos-io/linting-composite-action/.github/workflows/reusable-linting.yaml@v0.0.6
with:
yamldirs: ".github/workflows/ config/ tools-image/"
is-go: true

View File

@@ -9,6 +9,10 @@ on:
- '*'
pull_request:
concurrency:
group: test-${{ github.ref || github.head_ref }}
cancel-in-progress: true
jobs:
docker:
runs-on: ubuntu-latest

View File

@@ -7,6 +7,9 @@ on:
- master
tags:
- '*'
concurrency:
group: tool-image-${{ github.ref || github.head_ref }}
cancel-in-progress: true
jobs:
docker:
@@ -46,14 +49,14 @@ jobs:
- name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
registry: quay.io
username: ${{ secrets.QUAY_USERNAME }}
password: ${{ secrets.QUAY_PASSWORD }}
- name: Build
uses: docker/build-push-action@v2
uses: docker/build-push-action@v4
with:
builder: ${{ steps.buildx.outputs.name }}
context: ./tools-image

5
.shellcheckrc Normal file
View File

@@ -0,0 +1,5 @@
disable=SC2086
disable=SC2034
disable=SC2046
disable=SC2068
disable=SC2154

21
.yamllint Normal file
View File

@@ -0,0 +1,21 @@
extends: default
rules:
# 80 chars should be enough, but don't fail if a line is longer
line-length:
max: 150
level: warning
# accept both key:
# - item
#
# and key:
# - item
indentation:
indent-sequences: whatever
truthy:
check-keys: false
document-start:
present: false

View File

@@ -16,7 +16,10 @@ bump-repositories:
COPY +last-commit-packages/REPO_ARM64 REPO_ARM64
ARG REPO_AMD64=$(cat REPO_AMD64)
ARG REPO_ARM64=$(cat REPO_ARM64)
COPY tools-image/luet.yaml luet.yaml
RUN yq eval ".repositories[0] |= . * { \"reference\": \"${REPO_AMD64}\" }" -i luet.yaml
RUN yq eval ".repositories[1] |= . * { \"reference\": \"${REPO_ARM64}\" }" -i luet.yaml
SAVE ARTIFACT luet.yaml AS LOCAL tools-image/luet.yaml
COPY tools-image/luet-amd64.yaml luet-amd64.yaml
COPY tools-image/luet-arm64.yaml luet-arm64.yaml
RUN yq eval ".repositories[0] |= . * { \"reference\": \"${REPO_AMD64}\" }" -i luet-amd64.yaml
RUN yq eval ".repositories[0] |= . * { \"reference\": \"${REPO_ARM64}\" }" -i luet-arm64.yaml
SAVE ARTIFACT luet-arm64.yaml AS LOCAL tools-image/luet-arm64.yaml
SAVE ARTIFACT luet-amd64.yaml AS LOCAL tools-image/luet-amd64.yaml

View File

@@ -73,13 +73,11 @@ vars:
# kind: Service
# version: v1
# name: webhook-service
vars:
- name: NGINX_NAMESPACE
objref:
kind: Namespace
name: system
apiVersion: v1
apiVersion: v1
- name: ARTIFACT_COPIER_ROLE
objref:

View File

@@ -9,13 +9,13 @@ webhook:
leaderElection:
leaderElect: true
resourceName: 98ca89ca.kairos.io
# leaderElectionReleaseOnCancel defines if the leader should step down volume
# leaderElectionReleaseOnCancel defines if the leader should step down volume
# when the Manager ends. This requires the binary to immediately end when the
# Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
# speeds up voluntary leader transitions as the new leader don't have to wait
# LeaseDuration time first.
# In the default scaffold provided, the program ends immediately after
# the manager stops, so would be fine to enable this option. However,
# if you are doing or is intended to do any operation such as perform cleanups
# In the default scaffold provided, the program ends immediately after
# the manager stops, so would be fine to enable this option. However,
# if you are doing or is intended to do any operation such as perform cleanups
# after the manager stops then its usage might be unsafe.
# leaderElectionReleaseOnCancel: true

View File

@@ -1,4 +1,3 @@
# Prometheus Monitor Service (Metrics)
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor

View File

@@ -26,7 +26,8 @@ set -e
kubectl cluster-info --context kind-$CLUSTER_NAME
echo "Sleep to give times to node to populate with all info"
kubectl wait --for=condition=Ready node/$CLUSTER_NAME-control-plane
export EXTERNAL_IP=$(kubectl get nodes -o jsonpath='{.items[].status.addresses[?(@.type == "InternalIP")].address}')
EXTERNAL_IP=$(kubectl get nodes -o jsonpath='{.items[].status.addresses[?(@.type == "InternalIP")].address}')
export EXTERNAL_IP
export BRIDGE_IP="172.18.0.1"
kubectl get nodes -o wide
cd $ROOT_DIR/tests && $GINKGO -r -v ./e2e

View File

@@ -1,86 +1,96 @@
# https://quay.io/repository/kairos/packages?tab=tags&tag=latest
ARG LEAP_VERSION=15.4
ARG LUET_VERSION=0.33.0
ARG LEAP_VERSION=15.5
ARG LUET_VERSION=0.34.0
FROM quay.io/luet/base:$LUET_VERSION AS luet
FROM opensuse/leap:$LEAP_VERSION as luet-install
FROM golang:1.20 as enki
ENV CGO_ENABLED=0
COPY ./enki /src/enki
WORKDIR /src/enki
RUN go mod download
# Set arg/env after go mod download, otherwise we invalidate the cached layers due to the commit changing easily
ARG ENKI_VERSION=0.0.1
ARG ENKI_COMMIT=""
ENV ENKI_VERSION=${ENKI_VERSION}
ENV ENKI_COMMIT=${ENKI_COMMIT}
RUN go build \
-ldflags "-w -s \
-X github.com/kairos-io/enki/internal/version.version=$ENKI_VERSION \
-X github.com/kairos-io/enki/internal/version.gitCommit=$ENKI_COMMIT" \
-o /usr/bin/enki
FROM opensuse/leap:$LEAP_VERSION as default
RUN zypper ref && zypper dup -y
## ISO+ Arm image + Netboot + cloud images Build depedencies
RUN zypper ref && zypper in -y bc qemu-tools jq cdrtools docker git curl gptfdisk kpartx sudo xfsprogs parted util-linux-systemd e2fsprogs curl util-linux udev rsync grub2 dosfstools grub2-x86_64-efi squashfs mtools xorriso lvm2 zstd
COPY --from=luet /usr/bin/luet /usr/bin/luet
ENV LUET_NOLOCK=true
ENV TMPDIR=/tmp
COPY luet.yaml /etc/luet/luet.yaml
RUN luet install -y system/elemental-cli
ARG TARGETARCH
# copy both arches
COPY luet-arm64.yaml /tmp/luet-arm64.yaml
COPY luet-amd64.yaml /tmp/luet-amd64.yaml
# Set the default luet config to the current build arch
RUN mkdir -p /etc/luet/
RUN cp /tmp/luet-${TARGETARCH}.yaml /etc/luet/luet.yaml
## Live CD artifacts
RUN luet install -y livecd/grub2 --system-target /grub2
RUN luet install -y livecd/grub2-efi-image --system-target /efi
# remove luet tmp files. Side effect of setting the system-target is that it treats it as a root fs
RUN rm -Rf /grub2/var
RUN rm -Rf /efi/var
## amd64 Live CD artifacts
FROM quay.io/kairos/packages:grub2-livecd-0.0.6 AS grub2
FROM quay.io/kairos/packages:grub2-efi-image-livecd-0.0.6 AS efi
## RPI64
## Firmware is in the amd64 repo (noarch)
FROM quay.io/kairos/packages:u-boot-rpi64-firmware-2021.01-5.1 AS rpi-u-boot
FROM quay.io/kairos/packages:raspberrypi-firmware-firmware-2021.03.10-2.1 AS rpi-firmware
FROM quay.io/kairos/packages:raspberrypi-firmware-config-firmware-2021.03.10-2.1 AS rpi-firmware-config
FROM quay.io/kairos/packages:raspberrypi-firmware-dt-firmware-2021.03.15-2.1 AS rpi-firmware-dt
RUN luet install -y firmware/u-boot-rpi64 firmware/raspberrypi-firmware firmware/raspberrypi-firmware-config firmware/raspberrypi-firmware-dt --system-target /rpi/
## PineBook64 Pro
FROM quay.io/kairos/packages:u-boot-rockchip-arm-vendor-blob-0.1 AS pinebook-u-boot
RUN luet install -y arm-vendor-blob/u-boot-rockchip --system-target /pinebookpro/u-boot
## Generic ARM artifacts
FROM quay.io/kairos/packages-arm64:grub-efi-static-0.2 AS grub-efi
FROM quay.io/kairos/packages-arm64:grub-config-static-0.3 AS grub-config
FROM quay.io/kairos/packages-arm64:grub-artifacts-static-0.2 AS grub-artifacts
## Odroid fw
RUN luet install -y firmware/odroid-c2 --system-target /firmware/odroid-c2
## RAW images
FROM quay.io/kairos/packages:grub-efi-static-0.1 AS grub-raw-efi
FROM quay.io/kairos/packages:grub-config-static-0.1 AS grub-raw-config
FROM quay.io/kairos/packages:grub-artifacts-static-0.1 AS grub-raw-artifacts
## RAW images for current arch
RUN luet install -y static/grub-efi --system-target /raw/grub
RUN luet install -y static/grub-config --system-target /raw/grubconfig
RUN luet install -y static/grub-artifacts --system-target /raw/grubartifacts
FROM opensuse/leap:$LEAP_VERSION
COPY --from=luet-install /usr/bin/elemental /usr/bin/elemental
COPY --from=luet /usr/bin/luet /usr/bin/luet
## RAW images for arm64
# Luet will install this artifacts from the current arch repo, so in x86 it will
# get them from the x86 repo and we want it to do it from the arm64 repo, even on x86
# so we use the arm64 luet config and use that to install those on x86
# This is being used by the prepare_arm_images.sh and build-arch-image.sh scripts
RUN luet install --config /tmp/luet-arm64.yaml -y static/grub-efi --system-target /arm/raw/grubefi
RUN luet install --config /tmp/luet-arm64.yaml -y static/grub-config --system-target /arm/raw/grubconfig
RUN luet install --config /tmp/luet-arm64.yaml -y static/grub-artifacts --system-target /arm/raw/grubartifacts
# ISO files
COPY --from=luet-install /grub2 /grub2
COPY --from=luet-install /efi /efi
# kairos-agent so we can use the pull-image
RUN luet install -y system/kairos-agent
# RAW images
COPY --from=grub-raw-efi / /raw/grub
COPY --from=grub-raw-config / /raw/grubconfig
COPY --from=grub-raw-artifacts / /raw/grubartifacts
# remove luet tmp files. Side effect of setting the system-target is that it treats it as a root fs
# so temporal files are stored in each dir
RUN rm -Rf /grub2/var/tmp
RUN rm -Rf /grub2/var/cache
RUN rm -Rf /efi/var/tmp
RUN rm -Rf /efi/var/cache
RUN rm -Rf /rpi/var/tmp
RUN rm -Rf /rpi/var/cache
RUN rm -Rf /pinebookpro/u-boot/var/tmp
RUN rm -Rf /pinebookpro/u-boot/var/cache
RUN rm -Rf /firmware/odroid-c2/var/tmp
RUN rm -Rf /firmware/odroid-c2/var/cache
RUN rm -Rf /raw/grub/var/tmp
RUN rm -Rf /raw/grub/var/cache
RUN rm -Rf /raw/grubconfig/var/tmp
RUN rm -Rf /raw/grubconfig/var/cache
RUN rm -Rf /raw/grubartifacts/var/tmp
RUN rm -Rf /raw/grubartifacts/var/cache
RUN rm -Rf /arm/raw/grubefi/var/tmp
RUN rm -Rf /arm/raw/grubefi/var/cache
RUN rm -Rf /arm/raw/grubconfig/var/tmp
RUN rm -Rf /arm/raw/grubconfig/var/cache
RUN rm -Rf /arm/raw/grubartifacts/var/tmp
RUN rm -Rf /arm/raw/grubartifacts/var/cache
# RPI64
COPY --from=rpi-u-boot / /rpi/u-boot
COPY --from=rpi-firmware / /rpi/rpi-firmware
COPY --from=rpi-firmware-config / /rpi/rpi-firmware-config
COPY --from=rpi-firmware-dt / /rpi/rpi-firmware-dt
# Pinebook
COPY --from=pinebook-u-boot / /pinebookpro/u-boot
# Generic
COPY --from=grub-efi / /arm/grub/efi
COPY --from=grub-config / /arm/grub/config
COPY --from=grub-artifacts / /arm/grub/artifacts
RUN zypper ref && zypper dup -y
## ISO Build depedencies
RUN zypper ref && zypper in -y xfsprogs parted util-linux-systemd e2fsprogs curl util-linux udev rsync grub2 dosfstools grub2-x86_64-efi squashfs mtools xorriso lvm2 zstd
RUN mkdir /config
# Arm image build deps
RUN zypper in -y jq docker git curl gptfdisk kpartx sudo
# Netboot
RUN zypper in -y cdrtools
# cloud images
RUN zypper in -y bc qemu-tools
# ISO build config
COPY ./config.yaml /config/manifest.yaml
COPY ./entrypoint.sh /entrypoint.sh
@@ -101,5 +111,6 @@ COPY ./netboot.sh /netboot.sh
COPY defaults.yaml /defaults.yaml
COPY --from=enki /usr/bin/enki /usr/bin/enki
ENTRYPOINT [ "/entrypoint.sh" ]

View File

@@ -7,15 +7,7 @@ if [ -z "$image" ]; then
exit 1
fi
if [ ! -e "$WORKDIR/luet.yaml" ]; then
ls -liah $WORKDIR
echo "No valid config file"
cat "$WORKDIR/luet.yaml"
exit 1
fi
sudo luet install --config $WORKDIR/luet.yaml -y --system-target $WORKDIR firmware/odroid-c2
# conv=notrunc ?
dd if=$WORKDIR/bl1.bin.hardkernel of=$image conv=fsync bs=1 count=442
dd if=$WORKDIR/bl1.bin.hardkernel of=$image conv=fsync bs=512 skip=1 seek=1
dd if=$WORKDIR/u-boot.odroidc2 of=$image conv=fsync bs=512 seek=97
dd if=/firmware/odroid-c2/bl1.bin.hardkernel of=$image conv=fsync bs=1 count=442
dd if=/firmware/odroid-c2/bl1.bin.hardkernel of=$image conv=fsync bs=512 skip=1 seek=1
dd if=/firmware/odroid-c2/u-boot.odroidc2 of=$image conv=fsync bs=512 seek=97

View File

@@ -16,9 +16,7 @@ TEMPDIR="$(mktemp -d)"
echo $TEMPDIR
mount "${device}p1" "${TEMPDIR}"
for dir in /rpi/u-boot /rpi/rpi-firmware /rpi/rpi-firmware-config /rpi/rpi-firmware-dt
do
cp -rfv ${dir}/* $TEMPDIR
done
# Copy all rpi files
cp -rfv /rpi/* $TEMPDIR
umount "${TEMPDIR}"

22
tools-image/arm/boards/rpi4.sh Executable file
View File

@@ -0,0 +1,22 @@
#!/bin/bash
partprobe
kpartx -va $DRIVE
image=$1
if [ -z "$image" ]; then
echo "No image specified"
exit 1
fi
set -ax
TEMPDIR="$(mktemp -d)"
echo $TEMPDIR
mount "${device}p1" "${TEMPDIR}"
# Copy all rpi files
cp -rfv /rpi/* $TEMPDIR
umount "${TEMPDIR}"

View File

@@ -73,6 +73,8 @@ cleanup() {
fi
losetup -D "${LOOP}" || true;
dmsetup remove KairosVG-oem || true;
dmsetup remove KairosVG-recovery || true;
}
ensure_dir_structure() {
@@ -131,7 +133,7 @@ get_url()
esac
}
trap "cleanup" 1 2 3 6 9 14 15 EXIT
trap "cleanup" 1 2 3 6 14 15 EXIT
load_vars
@@ -215,7 +217,12 @@ while [ "$#" -gt 0 ]; do
shift 1
done
if [ "$model" == "rpi64" ]; then
if [ "$model" == "rpi64" ];then
echo "rpi64 model not supported anymore, please select either rpi3 or rpi4"
exit 1
fi
if [ "$model" == "rpi3" ] || [ "$model" == "rpi4" ]; then
container_image=${CONTAINER_IMAGE:-quay.io/costoolkit/examples:rpi-latest}
else
# Odroid C2 image contains kernel-default-extra, might have broader support
@@ -223,6 +230,7 @@ else
fi
if [ -n "$cos_config" ] && [ -e "$cos_config" ]; then
# shellcheck source=/dev/null
source "$cos_config"
fi
@@ -283,7 +291,7 @@ ensure_dir_structure $TARGET
# Download the container image
if [ -z "$directory" ]; then
echo ">>> Downloading container image"
elemental pull-image $( (( $local_build == 'true')) && printf %s '--local' ) $container_image $TARGET
kairos-agent pull-image $container_image $TARGET
else
echo ">>> Copying files from $directory"
rsync -axq --exclude='host' --exclude='mnt' --exclude='proc' --exclude='sys' --exclude='dev' --exclude='tmp' ${directory}/ $TARGET
@@ -320,9 +328,9 @@ cp -rfv ${STATEDIR}/cOS/active.img ${RECOVERY}/cOS/recovery.img
tune2fs -L ${SYSTEM_LABEL} ${RECOVERY}/cOS/recovery.img
# Install real grub config to recovery
cp -rfv /arm/grub/config/* $RECOVERY
cp -rfv /arm/raw/grubconfig/* $RECOVERY
mkdir -p $RECOVERY/grub2/fonts
cp -rfv /arm/grub/artifacts/* $RECOVERY/grub2
cp -rfv /arm/raw/grubartifacts/* $RECOVERY/grub2
mv $RECOVERY/grub2/*pf2 $RECOVERY/grub2/fonts
sync
@@ -335,7 +343,7 @@ if [ -z "$EFI" ]; then
exit 1
fi
cp -rfv /arm/grub/efi/* $EFI
cp -rfv /arm/raw/grubefi/* $EFI
if [ -n "$EFI" ] && [ -n "$efi_dir" ]; then
echo "Copy $efi_dir to EFI directory"
cp -rfv $efi_dir/* $EFI
@@ -345,31 +353,37 @@ partprobe
echo ">> Writing image and partition table"
dd if=/dev/zero of="${output_image}" bs=1024000 count="${size}" || exit 1
# make it gpt
echo "label: gpt" | sfdisk "${output_image}"
if [ "$model" == "rpi64" ]; then
# Image partitions
# only rpi4 supports gpt
if [ "$model" == "rpi3" ]; then
sgdisk -n 1:8192:+96M -c 1:EFI -t 1:0c00 ${output_image}
sgdisk -n 2:0:+${state_size}M -c 2:state -t 2:8300 ${output_image}
sgdisk -n 3:0:+$(( recovery_size + oem_size ))M -c 3:lvm -t 3:8e00 ${output_image}
sgdisk -n 4:0:+64M -c 4:persistent -t 4:8300 ${output_image}
sgdisk -m 1:2:3:4 ${output_image}
sfdisk --part-type ${output_image} 1 c
elif [ "$model" == "rpi4" ]; then
echo "label: gpt" | sfdisk "${output_image}"
sgdisk -n 1:8192:+96M -c 1:EFI -t 1:0c00 ${output_image}
sgdisk -n 2:0:+${state_size}M -c 2:state -t 2:8300 ${output_image}
sgdisk -n 3:0:+${recovery_size}M -c 3:recovery -t 3:8300 ${output_image}
sgdisk -n 4:0:+${oem_size}M -c 4:oem -t 4:8300 ${output_image}
sgdisk -n 5:0:+64M -c 5:persistent -t 5:8300 ${output_image}
sgdisk -g ${output_image}
sgdisk -m 1:2:3:4:5 ${output_image}
else
sgdisk -n 1:8192:+16M -c 1:EFI -t 1:0700 ${output_image}
fi
sgdisk -n 2:0:+${state_size}M -c 2:state -t 2:8300 ${output_image}
if [ "$disable_lvm" == 'true' ]; then
sgdisk -n 3:0:+${recovery_size}M -c 3:recovery -t 3:8300 ${output_image}
else
sgdisk -n 3:0:+$(( ${recovery_size} + ${oem_size} ))M -c 3:lvm -t 3:8e00 ${output_image}
fi
sgdisk -n 4:0:+64M -c 4:persistent -t 4:8300 ${output_image}
# Make the disk GPT
sgdisk -g ${output_image}
if [ "$model" == "rpi64" ]; then
sfdisk --part-type ${output_image} 1 c
sgdisk -n 2:0:+${state_size}M -c 2:state -t 2:8300 ${output_image}
sgdisk -n 3:0:+$(( recovery_size + oem_size ))M -c 3:lvm -t 3:8e00 ${output_image}
sgdisk -n 4:0:+64M -c 4:persistent -t 4:8300 ${output_image}
sgdisk -m 1:2:3:4 ${output_image}
fi
# Prepare the image and copy over the files
export DRIVE=$(losetup -f "${output_image}" --show)
DRIVE=$(losetup -f "${output_image}" --show)
export DRIVE
if [ -z "${DRIVE}" ]; then
echo "Cannot execute losetup for $output_image"
exit 1
@@ -386,64 +400,68 @@ export device="/dev/mapper/${device}"
partprobe
kpartx -vag $DRIVE
if [ "$model" == 'rpi4' ]; then
kpartx -vag $DRIVE
else
kpartx -va $DRIVE
fi
echo ">> Populating partitions"
efi=${device}p1
state=${device}p2
recovery=${device}p3
persistent=${device}p4
oem_lv=/dev/mapper/KairosVG-oem
recovery_lv=/dev/mapper/KairosVG-recovery
if [ "$model" == 'rpi4' ]; then
oem=${device}p4
persistent=${device}p5
else
persistent=${device}p4
oem_lv=/dev/mapper/KairosVG-oem
recovery_lv=/dev/mapper/KairosVG-recovery
fi
# Create partitions (RECOVERY, STATE, COS_PERSISTENT)
mkfs.vfat -F 32 ${efi}
fatlabel ${efi} COS_GRUB
if [ "$disable_lvm" == 'true' ]; then
mkfs.ext4 -F -L ${RECOVERY_LABEL} $recovery
else
pvcreate $recovery
vgcreate KairosVG $recovery
lvcreate -Z n -n oem -L ${oem_size} KairosVG
lvcreate -Z n -n recovery -l 100%FREE KairosVG
vgchange -ay
vgmknodes
mkfs.ext4 -F -L ${OEM_LABEL} $oem_lv
mkfs.ext4 -F -L ${RECOVERY_LABEL} $recovery_lv
fi
mkfs.ext4 -F -L ${STATE_LABEL} $state
mkfs.ext4 -F -L ${PERSISTENT_LABEL} $persistent
if [ "$model" == 'rpi4' ]; then
mkfs.ext4 -F -L ${RECOVERY_LABEL} $recovery
mkfs.ext4 -F -L ${OEM_LABEL} $oem
else
pvcreate $recovery
vgcreate KairosVG $recovery
lvcreate -Z n -n oem -L ${oem_size} KairosVG
lvcreate -Z n -n recovery -l 100%FREE KairosVG
vgchange -ay
vgmknodes
mkfs.ext4 -F -L ${OEM_LABEL} $oem_lv
mkfs.ext4 -F -L ${RECOVERY_LABEL} $recovery_lv
fi
mkdir $WORKDIR/state
mkdir $WORKDIR/recovery
mkdir $WORKDIR/efi
mkdir $WORKDIR/oem
if [ "$disable_lvm" == 'true' ]; then
mount $recovery $WORKDIR/recovery
else
mount $recovery_lv $WORKDIR/recovery
fi
mount $state $WORKDIR/state
mount $efi $WORKDIR/efi
if [ "$disable_lvm" == "false" ]; then
mkdir $WORKDIR/oem
mount $oem_lv $WORKDIR/oem
cp -rfv /defaults.yaml $WORKDIR/oem/01_defaults.yaml
# Set a OEM config file if specified
if [ -n "$config" ]; then
echo ">> Copying $config OEM config file"
get_url $config $WORKDIR/oem/99_custom.yaml
fi
umount $WORKDIR/oem
if [ "$model" == 'rpi4' ]; then
mount $recovery $WORKDIR/recovery
mount $oem $WORKDIR/oem
else
echo "LVM disabled: Not adding default config with default user/pass and custom config file"
echo "Enable LVM to copy those files into /oem"
mount $recovery_lv $WORKDIR/recovery
mount $oem_lv $WORKDIR/oem
fi
cp -rfv /defaults.yaml $WORKDIR/oem/01_defaults.yaml
# Set a OEM config file if specified
if [ -n "$config" ]; then
echo ">> Copying $config OEM config file"
get_url $config $WORKDIR/oem/99_custom.yaml
fi
grub2-editenv $WORKDIR/state/grub_oem_env set "default_menu_entry=$menu_entry"
@@ -451,7 +469,6 @@ grub2-editenv $WORKDIR/state/grub_oem_env set "default_menu_entry=$menu_entry"
# We copy the file we saved earier to the STATE partition
cp -rfv "${tmpgrubconfig}" $WORKDIR/state/grubmenu
# Copy over content
cp -arf $EFI/* $WORKDIR/efi
cp -arf $RECOVERY/* $WORKDIR/recovery
@@ -460,10 +477,13 @@ cp -arf $STATEDIR/* $WORKDIR/state
umount $WORKDIR/recovery
umount $WORKDIR/state
umount $WORKDIR/efi
umount $WORKDIR/oem
if [ "$disable_lvm" == 'false' ]; then
vgchange -an
if [ "$model" != 'rpi4' ]; then
vgchange -an
fi
sync
# Flash uboot and vendor-specific bits
@@ -474,7 +494,11 @@ sync
sleep 5
sync
kpartx -dvg $DRIVE || true
if [ "$model" == 'rpi4' ]; then
kpartx -dvg $DRIVE
else
kpartx -dv $DRIVE || true
fi
umount $DRIVE || true

View File

@@ -3,4 +3,4 @@ iso:
- dir:/efi
image:
- dir:/efi
- dir:/grub2
- dir:/grub2

View File

@@ -1,8 +1,8 @@
#cloud-config
name: "Default user"
stages:
initramfs:
- name: "Set default user/pass"
users:
initramfs:
- name: "Set default user/pass"
users:
kairos:
passwd: "kairos"
passwd: "kairos"

View File

@@ -0,0 +1,19 @@
VERSION 0.7
# renovate: datasource=docker depName=golang
ARG --global GO_VERSION=1.20-alpine3.18
test:
FROM golang:$GO_VERSION
RUN apk add rsync gcc musl-dev docker jq
WORKDIR /build
COPY . .
RUN go mod download
ARG TEST_PATHS=./...
ARG LABEL_FILTER=
ENV CGO_ENABLED=1
# Some test require the docker sock exposed
WITH DOCKER
RUN go run github.com/onsi/ginkgo/v2/ginkgo run --label-filter "$LABEL_FILTER" -v --fail-fast --race --covermode=atomic --coverprofile=coverage.out --coverpkg=github.com/kairos-io/enki/... -p -r $TEST_PATHS
END
SAVE ARTIFACT coverage.out AS LOCAL coverage.out

View File

@@ -0,0 +1,122 @@
package cmd
import (
"fmt"
"os/exec"
"github.com/kairos-io/enki/pkg/action"
"github.com/kairos-io/enki/pkg/config"
"github.com/kairos-io/enki/pkg/utils"
v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"k8s.io/mount-utils"
)
// NewBuildISOCmd returns a new instance of the build-iso subcommand and appends it to
// the root command.
func NewBuildISOCmd() *cobra.Command {
c := &cobra.Command{
Use: "build-iso SOURCE",
Short: "Build bootable installation media ISOs",
Long: "Build bootable installation media ISOs\n\n" +
"SOURCE - should be provided as uri in following format <sourceType>:<sourceName>\n" +
" * <sourceType> - might be [\"dir\", \"file\", \"oci\", \"docker\"], as default is \"docker\"\n" +
" * <sourceName> - is path to file or directory, image name with tag version",
Args: cobra.MaximumNArgs(1),
PreRunE: func(cmd *cobra.Command, args []string) error {
return CheckRoot()
},
RunE: func(cmd *cobra.Command, args []string) error {
path, err := exec.LookPath("mount")
if err != nil {
return err
}
mounter := mount.New(path)
cfg, err := config.ReadConfigBuild(viper.GetString("config-dir"), cmd.Flags(), mounter)
if err != nil {
cfg.Logger.Errorf("Error reading config: %s\n", err)
}
flags := cmd.Flags()
// Set this after parsing of the flags, so it fails on parsing and prints usage properly
cmd.SilenceUsage = true
cmd.SilenceErrors = true // Do not propagate errors down the line, we control them
spec, err := config.ReadBuildISO(cfg, flags)
if err != nil {
cfg.Logger.Errorf("invalid install command setup %v", err)
return err
}
if len(args) == 1 {
imgSource, err := v1.NewSrcFromURI(args[0])
if err != nil {
cfg.Logger.Errorf("not a valid rootfs source image argument: %s", args[0])
return err
}
spec.RootFS = []*v1.ImageSource{imgSource}
} else if len(spec.RootFS) == 0 {
errmsg := "rootfs source image for building ISO was not provided"
cfg.Logger.Errorf(errmsg)
return fmt.Errorf(errmsg)
}
// Repos and overlays can't be unmarshaled directly as they require
// to be merged on top and flags do not match any config value key
oRootfs, _ := flags.GetString("overlay-rootfs")
oUEFI, _ := flags.GetString("overlay-uefi")
oISO, _ := flags.GetString("overlay-iso")
if oRootfs != "" {
if ok, err := utils.Exists(cfg.Fs, oRootfs); ok {
spec.RootFS = append(spec.RootFS, v1.NewDirSrc(oRootfs))
} else {
cfg.Logger.Errorf("Invalid value for overlay-rootfs")
return fmt.Errorf("Invalid path '%s': %v", oRootfs, err)
}
}
if oUEFI != "" {
if ok, err := utils.Exists(cfg.Fs, oUEFI); ok {
spec.UEFI = append(spec.UEFI, v1.NewDirSrc(oUEFI))
} else {
cfg.Logger.Errorf("Invalid value for overlay-uefi")
return fmt.Errorf("Invalid path '%s': %v", oUEFI, err)
}
}
if oISO != "" {
if ok, err := utils.Exists(cfg.Fs, oISO); ok {
spec.Image = append(spec.Image, v1.NewDirSrc(oISO))
} else {
cfg.Logger.Errorf("Invalid value for overlay-iso")
return fmt.Errorf("Invalid path '%s': %v", oISO, err)
}
}
buildISO := action.NewBuildISOAction(cfg, spec)
err = buildISO.ISORun()
if err != nil {
cfg.Logger.Errorf(err.Error())
return err
}
return nil
},
}
c.Flags().StringP("name", "n", "", "Basename of the generated ISO file")
c.Flags().StringP("output", "o", "", "Output directory (defaults to current directory)")
c.Flags().Bool("date", false, "Adds a date suffix into the generated ISO file")
c.Flags().String("overlay-rootfs", "", "Path of the overlayed rootfs data")
c.Flags().String("overlay-uefi", "", "Path of the overlayed uefi data")
c.Flags().String("overlay-iso", "", "Path of the overlayed iso data")
c.Flags().String("label", "", "Label of the ISO volume")
archType := newEnumFlag([]string{"x86_64", "arm64"}, "x86_64")
c.Flags().Bool("squash-no-compression", true, "Disable squashfs compression.")
c.Flags().VarP(archType, "arch", "a", "Arch to build the image for")
return c
}
func init() {
rootCmd.AddCommand(NewBuildISOCmd())
}

View File

@@ -0,0 +1,70 @@
/*
Copyright © 2022 SUSE LLC
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 cmd
import (
"bytes"
"fmt"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/spf13/viper"
)
var _ = Describe("BuildISO", Label("iso", "cmd"), func() {
var buf *bytes.Buffer
BeforeEach(func() {
buf = new(bytes.Buffer)
rootCmd.SetOut(buf)
rootCmd.SetErr(buf)
})
AfterEach(func() {
viper.Reset()
})
It("Errors out if no rootfs sources are defined", Label("flags"), func() {
_, _, err := executeCommandC(rootCmd, "build-iso")
fmt.Println(buf)
Expect(err).ToNot(BeNil())
Expect(err.Error()).To(ContainSubstring("rootfs source image for building ISO was not provided"))
})
It("Errors out if rootfs is a non valid argument", Label("flags"), func() {
_, _, err := executeCommandC(rootCmd, "build-iso", "/no/image/reference")
Expect(err).ToNot(BeNil())
Expect(err.Error()).To(ContainSubstring("invalid image reference"))
})
It("Errors out if overlay roofs path does not exist", Label("flags"), func() {
_, _, err := executeCommandC(
rootCmd, "build-iso", "system/cos", "--overlay-rootfs", "/nonexistingpath",
)
Expect(err).ToNot(BeNil())
Expect(err.Error()).To(ContainSubstring("Invalid path"))
})
It("Errors out if overlay uefi path does not exist", Label("flags"), func() {
_, _, err := executeCommandC(
rootCmd, "build-iso", "someimage:latest", "--overlay-uefi", "/nonexistingpath",
)
Expect(err).ToNot(BeNil())
Expect(err.Error()).To(ContainSubstring("Invalid path"))
})
It("Errors out if overlay iso path does not exist", Label("flags"), func() {
_, _, err := executeCommandC(
rootCmd, "build-iso", "some/image:latest", "--overlay-iso", "/nonexistingpath",
)
Expect(err).ToNot(BeNil())
Expect(err.Error()).To(ContainSubstring("Invalid path"))
})
})

View File

@@ -0,0 +1,29 @@
/*
Copyright © 2021 SUSE LLC
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 cmd
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestWhitebox(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "CLI whitebox test suite")
}

View File

@@ -0,0 +1,53 @@
/*
Copyright © 2021 SUSE LLC
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 cmd
import (
"io/ioutil"
"os"
"github.com/spf13/cobra"
)
func executeCommandC(cmd *cobra.Command, args ...string) (c *cobra.Command, output string, err error) {
// Set args to command
cmd.SetArgs(args)
// store old stdout
oldStdout := os.Stdout
r, w, _ := os.Pipe()
// Change stdout to our pipe
os.Stdout = w
// run the command
c, err = cmd.ExecuteC()
if err != nil {
// Remember to restore stdout!
os.Stdout = oldStdout
return nil, "", err
}
err = w.Close()
if err != nil {
// Remember to restore stdout!
os.Stdout = oldStdout
return nil, "", err
}
// Read output from our pipe
out, _ := ioutil.ReadAll(r)
// restore stdout
os.Stdout = oldStdout
return c, string(out), nil
}

View File

@@ -0,0 +1,90 @@
package cmd
import (
"errors"
"fmt"
"os"
"strings"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func NewRootCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "enki",
Short: "enki",
}
cmd.PersistentFlags().Bool("debug", false, "Enable debug output")
cmd.PersistentFlags().String("config-dir", "/etc/elemental", "Set config dir (default is /etc/elemental)")
cmd.PersistentFlags().String("logfile", "", "Set logfile")
cmd.PersistentFlags().Bool("quiet", false, "Do not output to stdout")
_ = viper.BindPFlag("debug", cmd.PersistentFlags().Lookup("debug"))
_ = viper.BindPFlag("config-dir", cmd.PersistentFlags().Lookup("config-dir"))
_ = viper.BindPFlag("logfile", cmd.PersistentFlags().Lookup("logfile"))
_ = viper.BindPFlag("quiet", cmd.PersistentFlags().Lookup("quiet"))
if viper.GetBool("debug") {
logrus.SetLevel(logrus.DebugLevel)
}
return cmd
}
// rootCmd represents the base command when called without any subcommands
var rootCmd = NewRootCmd()
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
// CheckRoot is a helper to return on PreRunE, so we can add it to commands that require root
func CheckRoot() error {
if os.Geteuid() != 0 {
return errors.New("this command requires root privileges")
}
return nil
}
type enum struct {
Allowed []string
Value string
}
func (a enum) String() string {
return a.Value
}
func (a *enum) Set(p string) error {
isIncluded := func(opts []string, val string) bool {
for _, opt := range opts {
if val == opt {
return true
}
}
return false
}
if !isIncluded(a.Allowed, p) {
return fmt.Errorf("%s is not included in %s", p, strings.Join(a.Allowed, ","))
}
a.Value = p
return nil
}
func (a *enum) Type() string {
return "string"
}
// newEnum give a list of allowed flag parameters, where the second argument is the default
func newEnumFlag(allowed []string, d string) *enum {
return &enum{
Allowed: allowed,
Value: d,
}
}

154
tools-image/enki/go.mod Normal file
View File

@@ -0,0 +1,154 @@
module github.com/kairos-io/enki
go 1.20
require (
github.com/kairos-io/kairos-agent/v2 v2.1.11-0.20230713071318-9a16b94e2af6
github.com/kairos-io/kairos-sdk v0.0.9-0.20230719194412-fe26d1de9166
github.com/mitchellh/mapstructure v1.5.0
github.com/onsi/ginkgo/v2 v2.9.7
github.com/onsi/gomega v1.27.8
github.com/sanity-io/litter v1.5.5
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.7.0
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.16.0
github.com/twpayne/go-vfs v1.7.2
k8s.io/mount-utils v0.27.3
)
require (
atomicgo.dev/cursor v0.1.3 // indirect
atomicgo.dev/keyboard v0.2.9 // indirect
atomicgo.dev/schedule v0.0.2 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.1 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/Microsoft/hcsshim v0.10.0-rc.8 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230117203413-a47887b8f098 // indirect
github.com/StackExchange/wmi v1.2.1 // indirect
github.com/acomagu/bufpipe v1.0.3 // indirect
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 // indirect
github.com/cavaliergopher/grab v2.0.0+incompatible // indirect
github.com/cavaliergopher/grab/v3 v3.0.1 // indirect
github.com/cloudflare/circl v1.3.1 // indirect
github.com/containerd/cgroups v1.1.0 // indirect
github.com/containerd/console v1.0.3 // indirect
github.com/containerd/containerd v1.7.1 // indirect
github.com/containerd/continuity v0.3.0 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect
github.com/denisbrodbeck/machineid v1.0.1 // indirect
github.com/diskfs/go-diskfs v1.3.0 // indirect
github.com/distribution/distribution v2.8.2+incompatible // indirect
github.com/docker/cli v23.0.5+incompatible // indirect
github.com/docker/distribution v2.8.2+incompatible // indirect
github.com/docker/docker v23.0.6+incompatible // indirect
github.com/docker/docker-credential-helpers v0.7.0 // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-git/gcfg v1.5.0 // indirect
github.com/go-git/go-billy/v5 v5.3.1 // indirect
github.com/go-git/go-git/v5 v5.4.2 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/go-containerregistry v0.15.2 // indirect
github.com/google/pprof v0.0.0-20230228050547-1710fef4ab10 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gookit/color v1.5.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/huandu/xstrings v1.4.0 // indirect
github.com/imdario/mergo v0.3.15 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/itchyny/gojq v0.12.12 // indirect
github.com/itchyny/timefmt-go v0.1.5 // indirect
github.com/jaypipes/ghw v0.10.0 // indirect
github.com/jaypipes/pcidb v1.0.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/kendru/darwin/go/depgraph v0.0.0-20221105232959-877d6a81060c // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/compress v1.16.5 // indirect
github.com/lithammer/fuzzysearch v1.1.8 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/moby v23.0.4+incompatible // indirect
github.com/moby/sys/mountinfo v0.6.2 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/mudler/entities v0.0.0-20220905203055-68348bae0f49 // indirect
github.com/mudler/yip v1.3.1-0.20230704124832-e5812d0f5890 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc3 // indirect
github.com/packethost/packngo v0.29.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee // indirect
github.com/pierrec/lz4 v2.6.1+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pkg/xattr v0.4.9 // indirect
github.com/pterm/pterm v0.12.63 // indirect
github.com/qeesung/image2ascii v1.0.1 // indirect
github.com/rancher-sandbox/linuxkit v1.0.1-0.20230517173613-432a87ba3e09 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/samber/lo v1.37.0 // indirect
github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b // indirect
github.com/sergi/go-diff v1.3.1 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/spectrocloud-labs/herd v0.4.2 // indirect
github.com/spf13/afero v1.9.5 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/tredoe/osutil/v2 v2.0.0-rc.16 // indirect
github.com/ulikunitz/xz v0.5.11 // indirect
github.com/vbatts/tar-split v0.11.3 // indirect
github.com/vishvananda/netlink v1.2.1-beta.2 // indirect
github.com/vishvananda/netns v0.0.4 // indirect
github.com/vmware/vmw-guestinfo v0.0.0-20220317130741-510905f0efa3 // indirect
github.com/wayneashleyberry/terminal-dimensions v1.1.0 // indirect
github.com/willdonnelly/passwd v0.0.0-20141013001024-7935dab3074c // indirect
github.com/xanzy/ssh-agent v0.3.1 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/zcalusic/sysinfo v0.9.5 // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/crypto v0.9.0 // indirect
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
golang.org/x/mod v0.10.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sync v0.2.0 // indirect
golang.org/x/sys v0.10.0 // indirect
golang.org/x/term v0.10.0 // indirect
golang.org/x/text v0.11.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.9.1 // indirect
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
google.golang.org/grpc v1.55.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/djherbis/times.v1 v1.3.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
howett.net/plist v1.0.0 // indirect
k8s.io/klog/v2 v2.90.1 // indirect
k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5 // indirect
pault.ag/go/modprobe v0.1.2 // indirect
pault.ag/go/topsort v0.1.1 // indirect
)

1008
tools-image/enki/go.sum Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,36 @@
package version
import (
"runtime"
)
var (
version = "v0.0.1"
// gitCommit is the git sha1
gitCommit = ""
)
// BuildInfo describes the compile time information.
type BuildInfo struct {
// Version is the current semver.
Version string `json:"version,omitempty"`
// GitCommit is the git sha1.
GitCommit string `json:"git_commit,omitempty"`
// GoVersion is the version of the Go compiler used.
GoVersion string `json:"go_version,omitempty"`
}
func GetVersion() string {
return version
}
// Get returns build info
func Get() BuildInfo {
v := BuildInfo{
Version: GetVersion(),
GitCommit: gitCommit,
GoVersion: runtime.Version(),
}
return v
}

7
tools-image/enki/main.go Normal file
View File

@@ -0,0 +1,7 @@
package main
import "github.com/kairos-io/enki/cmd"
func main() {
cmd.Execute()
}

View File

@@ -0,0 +1,28 @@
/*
Copyright © 2022 SUSE LLC
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 action_test
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestActionSuite(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Actions test suite")
}

View File

@@ -0,0 +1,360 @@
package action
import (
"fmt"
"path/filepath"
"strings"
"time"
"github.com/kairos-io/enki/pkg/constants"
"github.com/kairos-io/enki/pkg/utils"
"github.com/kairos-io/kairos-agent/v2/pkg/elemental"
v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1"
sdk "github.com/kairos-io/kairos-sdk/utils"
)
type BuildISOAction struct {
cfg *v1.BuildConfig
spec *v1.LiveISO
e *elemental.Elemental
}
type BuildISOActionOption func(a *BuildISOAction)
func NewBuildISOAction(cfg *v1.BuildConfig, spec *v1.LiveISO, opts ...BuildISOActionOption) *BuildISOAction {
b := &BuildISOAction{
cfg: cfg,
e: elemental.NewElemental(&cfg.Config),
spec: spec,
}
for _, opt := range opts {
opt(b)
}
return b
}
// ISORun will install the system from a given configuration
func (b *BuildISOAction) ISORun() (err error) {
cleanup := sdk.NewCleanStack()
defer func() { err = cleanup.Cleanup(err) }()
isoTmpDir, err := utils.TempDir(b.cfg.Fs, "", "enki-iso")
if err != nil {
return err
}
cleanup.Push(func() error { return b.cfg.Fs.RemoveAll(isoTmpDir) })
rootDir := filepath.Join(isoTmpDir, "rootfs")
err = utils.MkdirAll(b.cfg.Fs, rootDir, constants.DirPerm)
if err != nil {
return err
}
uefiDir := filepath.Join(isoTmpDir, "uefi")
err = utils.MkdirAll(b.cfg.Fs, uefiDir, constants.DirPerm)
if err != nil {
return err
}
isoDir := filepath.Join(isoTmpDir, "iso")
err = utils.MkdirAll(b.cfg.Fs, isoDir, constants.DirPerm)
if err != nil {
return err
}
if b.cfg.OutDir != "" {
err = utils.MkdirAll(b.cfg.Fs, b.cfg.OutDir, constants.DirPerm)
if err != nil {
b.cfg.Logger.Errorf("Failed creating output folder: %s", b.cfg.OutDir)
return err
}
}
b.cfg.Logger.Infof("Preparing squashfs root...")
err = b.applySources(rootDir, b.spec.RootFS...)
if err != nil {
b.cfg.Logger.Errorf("Failed installing OS packages: %v", err)
return err
}
err = utils.CreateDirStructure(b.cfg.Fs, rootDir)
if err != nil {
b.cfg.Logger.Errorf("Failed creating root directory structure: %v", err)
return err
}
b.cfg.Logger.Infof("Preparing EFI image...")
err = b.applySources(uefiDir, b.spec.UEFI...)
if err != nil {
b.cfg.Logger.Errorf("Failed installing EFI packages: %v", err)
return err
}
b.cfg.Logger.Infof("Preparing ISO image root tree...")
err = b.applySources(isoDir, b.spec.Image...)
if err != nil {
b.cfg.Logger.Errorf("Failed installing ISO image packages: %v", err)
return err
}
err = b.prepareISORoot(isoDir, rootDir, uefiDir)
if err != nil {
b.cfg.Logger.Errorf("Failed preparing ISO's root tree: %v", err)
return err
}
b.cfg.Logger.Infof("Creating ISO image...")
err = b.burnISO(isoDir)
if err != nil {
b.cfg.Logger.Errorf("Failed preparing ISO's root tree: %v", err)
return err
}
return err
}
func (b BuildISOAction) prepareISORoot(isoDir string, rootDir string, uefiDir string) error {
kernel, initrd, err := b.e.FindKernelInitrd(rootDir)
if err != nil {
b.cfg.Logger.Error("Could not find kernel and/or initrd")
return err
}
err = utils.MkdirAll(b.cfg.Fs, filepath.Join(isoDir, "boot"), constants.DirPerm)
if err != nil {
return err
}
//TODO document boot/kernel and boot/initrd expectation in bootloader config
b.cfg.Logger.Debugf("Copying Kernel file %s to iso root tree", kernel)
err = utils.CopyFile(b.cfg.Fs, kernel, filepath.Join(isoDir, constants.IsoKernelPath))
if err != nil {
return err
}
b.cfg.Logger.Debugf("Copying initrd file %s to iso root tree", initrd)
err = utils.CopyFile(b.cfg.Fs, initrd, filepath.Join(isoDir, constants.IsoInitrdPath))
if err != nil {
return err
}
b.cfg.Logger.Info("Creating squashfs...")
err = utils.CreateSquashFS(b.cfg.Runner, b.cfg.Logger, rootDir, filepath.Join(isoDir, constants.IsoRootFile), constants.GetDefaultSquashfsOptions())
if err != nil {
return err
}
b.cfg.Logger.Info("Creating EFI image...")
err = b.createEFI(uefiDir, filepath.Join(isoDir, constants.IsoEFIPath))
if err != nil {
return err
}
return nil
}
func (b BuildISOAction) createEFI(root string, img string) error {
efiSize, err := utils.DirSize(b.cfg.Fs, root)
if err != nil {
return err
}
// align efiSize to the next 4MB slot
align := int64(4 * 1024 * 1024)
efiSizeMB := (efiSize/align*align + align) / (1024 * 1024)
err = b.e.CreateFileSystemImage(&v1.Image{
File: img,
Size: uint(efiSizeMB),
FS: constants.EfiFs,
Label: constants.EfiLabel,
})
if err != nil {
return err
}
files, err := b.cfg.Fs.ReadDir(root)
if err != nil {
return err
}
for _, f := range files {
_, err = b.cfg.Runner.Run("mcopy", "-s", "-i", img, filepath.Join(root, f.Name()), "::")
if err != nil {
return err
}
}
return nil
}
func (b BuildISOAction) burnISO(root string) error {
cmd := "xorriso"
var outputFile string
var isoFileName string
if b.cfg.Date {
currTime := time.Now()
isoFileName = fmt.Sprintf("%s.%s.iso", b.cfg.Name, currTime.Format("20060102"))
} else {
isoFileName = fmt.Sprintf("%s.iso", b.cfg.Name)
}
outputFile = isoFileName
if b.cfg.OutDir != "" {
outputFile = filepath.Join(b.cfg.OutDir, outputFile)
}
if exists, _ := utils.Exists(b.cfg.Fs, outputFile); exists {
b.cfg.Logger.Warnf("Overwriting already existing %s", outputFile)
err := b.cfg.Fs.Remove(outputFile)
if err != nil {
return err
}
}
args := []string{
"-volid", b.spec.Label, "-joliet", "on", "-padding", "0",
"-outdev", outputFile, "-map", root, "/", "-chmod", "0755", "--",
}
args = append(args, constants.GetXorrisoBooloaderArgs(root)...)
out, err := b.cfg.Runner.Run(cmd, args...)
b.cfg.Logger.Debugf("Xorriso: %s", string(out))
if err != nil {
return err
}
checksum, err := utils.CalcFileChecksum(b.cfg.Fs, outputFile)
if err != nil {
return fmt.Errorf("checksum computation failed: %w", err)
}
err = b.cfg.Fs.WriteFile(fmt.Sprintf("%s.sha256", outputFile), []byte(fmt.Sprintf("%s %s\n", checksum, isoFileName)), 0644)
if err != nil {
return fmt.Errorf("cannot write checksum file: %w", err)
}
return nil
}
func (b BuildISOAction) applySources(target string, sources ...*v1.ImageSource) error {
for _, src := range sources {
_, err := b.e.DumpSource(target, src)
if err != nil {
return err
}
}
return nil
}
func (g *BuildISOAction) PrepareEFI(rootDir, uefiDir string) error {
err := utils.MkdirAll(g.cfg.Fs, filepath.Join(uefiDir, constants.EfiBootPath), constants.DirPerm)
if err != nil {
return err
}
switch g.cfg.Arch {
case constants.ArchAmd64, constants.Archx86:
err = utils.CopyFile(
g.cfg.Fs,
filepath.Join(rootDir, constants.GrubEfiImagex86),
filepath.Join(uefiDir, constants.GrubEfiImagex86Dest),
)
case constants.ArchArm64:
err = utils.CopyFile(
g.cfg.Fs,
filepath.Join(rootDir, constants.GrubEfiImageArm64),
filepath.Join(uefiDir, constants.GrubEfiImageArm64Dest),
)
default:
err = fmt.Errorf("Not supported architecture: %v", g.cfg.Arch)
}
if err != nil {
return err
}
return g.cfg.Fs.WriteFile(filepath.Join(uefiDir, constants.EfiBootPath, constants.GrubCfg), []byte(constants.GrubEfiCfg), constants.FilePerm)
}
func (g *BuildISOAction) PrepareISO(rootDir, imageDir string) error {
err := utils.MkdirAll(g.cfg.Fs, filepath.Join(imageDir, constants.GrubPrefixDir), constants.DirPerm)
if err != nil {
return err
}
switch g.cfg.Arch {
case constants.ArchAmd64, constants.Archx86:
// Create eltorito image
eltorito, err := g.BuildEltoritoImg(rootDir)
if err != nil {
return err
}
// Inlude loaders in expected paths
loaderDir := filepath.Join(imageDir, constants.IsoLoaderPath)
err = utils.MkdirAll(g.cfg.Fs, loaderDir, constants.DirPerm)
if err != nil {
return err
}
loaderFiles := []string{eltorito, constants.GrubBootHybridImg}
loaderFiles = append(loaderFiles, strings.Split(constants.SyslinuxFiles, " ")...)
for _, f := range loaderFiles {
err = utils.CopyFile(g.cfg.Fs, filepath.Join(rootDir, f), loaderDir)
if err != nil {
return err
}
}
fontsDir := filepath.Join(loaderDir, "/grub2/fonts")
err = utils.MkdirAll(g.cfg.Fs, fontsDir, constants.DirPerm)
if err != nil {
return err
}
err = utils.CopyFile(g.cfg.Fs, filepath.Join(rootDir, constants.GrubFont), fontsDir)
if err != nil {
return err
}
case constants.ArchArm64:
// TBC
default:
return fmt.Errorf("Not supported architecture: %v", g.cfg.Arch)
}
// Write grub.cfg file
err = g.cfg.Fs.WriteFile(
filepath.Join(imageDir, constants.GrubPrefixDir, constants.GrubCfg),
[]byte(fmt.Sprintf(constants.GrubCfgTemplate, g.spec.GrubEntry, g.spec.Label)),
constants.FilePerm,
)
if err != nil {
return err
}
// Include EFI contents in iso root too
return g.PrepareEFI(rootDir, imageDir)
}
func (g *BuildISOAction) BuildEltoritoImg(rootDir string) (string, error) {
var args []string
args = append(args, "-O", constants.GrubBiosTarget)
args = append(args, "-o", constants.GrubBiosImg)
args = append(args, "-p", constants.GrubPrefixDir)
args = append(args, "-d", constants.GrubI386BinDir)
args = append(args, strings.Split(constants.GrubModules, " ")...)
chRoot := utils.NewChroot(rootDir, &g.cfg.Config)
out, err := chRoot.Run("grub2-mkimage", args...)
if err != nil {
g.cfg.Logger.Errorf("grub2-mkimage failed: %s", string(out))
g.cfg.Logger.Errorf("Error: %v", err)
return "", err
}
concatFiles := func() error {
return utils.ConcatFiles(
g.cfg.Fs, []string{constants.GrubBiosCDBoot, constants.GrubBiosImg},
constants.GrubEltoritoImg,
)
}
err = chRoot.RunCallback(concatFiles)
if err != nil {
return "", err
}
return constants.GrubEltoritoImg, nil
}

View File

@@ -0,0 +1,195 @@
/*
Copyright © 2022 SUSE LLC
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 action_test
import (
"bytes"
"errors"
"fmt"
"github.com/kairos-io/enki/pkg/action"
"github.com/kairos-io/enki/pkg/config"
"github.com/kairos-io/enki/pkg/constants"
"github.com/kairos-io/enki/pkg/utils"
v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1"
v1mock "github.com/kairos-io/kairos-agent/v2/tests/mocks"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/sirupsen/logrus"
"github.com/twpayne/go-vfs"
"github.com/twpayne/go-vfs/vfst"
"path/filepath"
)
var _ = Describe("Runtime Actions", func() {
var cfg *v1.BuildConfig
var runner *v1mock.FakeRunner
var fs vfs.FS
var logger v1.Logger
var mounter *v1mock.ErrorMounter
var syscall *v1mock.FakeSyscall
var client *v1mock.FakeHTTPClient
var cloudInit *v1mock.FakeCloudInitRunner
var cleanup func()
var memLog *bytes.Buffer
var imageExtractor *v1mock.FakeImageExtractor
BeforeEach(func() {
runner = v1mock.NewFakeRunner()
syscall = &v1mock.FakeSyscall{}
mounter = v1mock.NewErrorMounter()
client = &v1mock.FakeHTTPClient{}
memLog = &bytes.Buffer{}
logger = v1.NewBufferLogger(memLog)
logger.SetLevel(logrus.DebugLevel)
cloudInit = &v1mock.FakeCloudInitRunner{}
fs, cleanup, _ = vfst.NewTestFS(map[string]interface{}{})
imageExtractor = v1mock.NewFakeImageExtractor(logger)
cfg = config.NewBuildConfig(
config.WithFs(fs),
config.WithRunner(runner),
config.WithLogger(logger),
config.WithMounter(mounter),
config.WithSyscall(syscall),
config.WithClient(client),
config.WithCloudInitRunner(cloudInit),
config.WithImageExtractor(imageExtractor),
)
})
AfterEach(func() {
cleanup()
})
Describe("Build ISO", Label("iso"), func() {
var iso *v1.LiveISO
BeforeEach(func() {
iso = config.NewISO()
tmpDir, err := utils.TempDir(fs, "", "test")
Expect(err).ShouldNot(HaveOccurred())
cfg.Date = false
cfg.OutDir = tmpDir
runner.SideEffect = func(cmd string, args ...string) ([]byte, error) {
switch cmd {
case "xorriso":
err := fs.WriteFile(filepath.Join(tmpDir, "elemental.iso"), []byte("profound thoughts"), constants.FilePerm)
return []byte{}, err
default:
return []byte{}, nil
}
}
})
It("Successfully builds an ISO from a Docker image", func() {
rootSrc, _ := v1.NewSrcFromURI("oci:image:version")
iso.RootFS = []*v1.ImageSource{rootSrc}
uefiSrc, _ := v1.NewSrcFromURI("oci:image:version")
iso.UEFI = []*v1.ImageSource{uefiSrc}
imageSrc, _ := v1.NewSrcFromURI("oci:image:version")
iso.Image = []*v1.ImageSource{imageSrc}
// Create kernel and vmlinuz
// Thanks to the testfs stuff in utils.TempDir we know what the temp fs is gonna be as
// its predictable
bootDir := filepath.Join("/tmp/enki-iso/rootfs", "boot")
err := utils.MkdirAll(fs, bootDir, constants.DirPerm)
Expect(err).ShouldNot(HaveOccurred())
_, err = fs.Create(filepath.Join(bootDir, "vmlinuz"))
Expect(err).ShouldNot(HaveOccurred())
_, err = fs.Create(filepath.Join(bootDir, "initrd"))
Expect(err).ShouldNot(HaveOccurred())
buildISO := action.NewBuildISOAction(cfg, iso)
err = buildISO.ISORun()
Expect(err).ShouldNot(HaveOccurred())
})
It("Fails if kernel or initrd is not found in rootfs", func() {
rootSrc, _ := v1.NewSrcFromURI("oci:image:version")
iso.RootFS = []*v1.ImageSource{rootSrc}
uefiSrc, _ := v1.NewSrcFromURI("oci:image:version")
iso.UEFI = []*v1.ImageSource{uefiSrc}
imageSrc, _ := v1.NewSrcFromURI("oci:image:version")
iso.Image = []*v1.ImageSource{imageSrc}
By("fails without kernel")
buildISO := action.NewBuildISOAction(cfg, iso)
err := buildISO.ISORun()
Expect(err).Should(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("No file found with prefixes"))
Expect(err.Error()).To(ContainSubstring("uImage Image zImage vmlinuz image"))
bootDir := filepath.Join("/tmp/enki-iso/rootfs", "boot")
err = utils.MkdirAll(fs, bootDir, constants.DirPerm)
Expect(err).ShouldNot(HaveOccurred())
_, err = fs.Create(filepath.Join(bootDir, "vmlinuz"))
Expect(err).ShouldNot(HaveOccurred())
By("fails without initrd")
buildISO = action.NewBuildISOAction(cfg, iso)
err = buildISO.ISORun()
Expect(err).Should(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("No file found with prefixes"))
Expect(err.Error()).To(ContainSubstring("initrd initramfs"))
})
It("Fails installing image sources", func() {
rootSrc, _ := v1.NewSrcFromURI("oci:image:version")
iso.RootFS = []*v1.ImageSource{rootSrc}
uefiSrc, _ := v1.NewSrcFromURI("oci:image:version")
iso.UEFI = []*v1.ImageSource{uefiSrc}
imageSrc, _ := v1.NewSrcFromURI("oci:image:version")
iso.Image = []*v1.ImageSource{imageSrc}
imageExtractor.SideEffect = func(imageRef, destination, platformRef string) error {
return fmt.Errorf("uh oh")
}
buildISO := action.NewBuildISOAction(cfg, iso)
err := buildISO.ISORun()
Expect(err).Should(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("uh oh"))
})
It("Fails on ISO filesystem creation", func() {
rootSrc, _ := v1.NewSrcFromURI("oci:image:version")
iso.RootFS = []*v1.ImageSource{rootSrc}
uefiSrc, _ := v1.NewSrcFromURI("oci:image:version")
iso.UEFI = []*v1.ImageSource{uefiSrc}
imageSrc, _ := v1.NewSrcFromURI("oci:image:version")
iso.Image = []*v1.ImageSource{imageSrc}
bootDir := filepath.Join("/tmp/enki-iso/rootfs", "boot")
err := utils.MkdirAll(fs, bootDir, constants.DirPerm)
Expect(err).ShouldNot(HaveOccurred())
_, err = fs.Create(filepath.Join(bootDir, "vmlinuz"))
Expect(err).ShouldNot(HaveOccurred())
_, err = fs.Create(filepath.Join(bootDir, "initrd"))
Expect(err).ShouldNot(HaveOccurred())
runner.SideEffect = func(command string, args ...string) ([]byte, error) {
if command == "xorriso" {
return []byte{}, errors.New("Burn ISO error")
}
return []byte{}, nil
}
buildISO := action.NewBuildISOAction(cfg, iso)
err = buildISO.ISORun()
Expect(err).Should(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("Burn ISO error"))
})
})
})

View File

@@ -0,0 +1,303 @@
package config
import (
"github.com/kairos-io/enki/internal/version"
"github.com/kairos-io/enki/pkg/constants"
"github.com/kairos-io/enki/pkg/utils"
"github.com/kairos-io/kairos-agent/v2/pkg/cloudinit"
"github.com/kairos-io/kairos-agent/v2/pkg/http"
v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1"
"github.com/mitchellh/mapstructure"
"github.com/sanity-io/litter"
"github.com/sirupsen/logrus"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"github.com/twpayne/go-vfs"
"io"
"io/fs"
"k8s.io/mount-utils"
"os"
"reflect"
"runtime"
)
var decodeHook = viper.DecodeHook(
mapstructure.ComposeDecodeHookFunc(
UnmarshalerHook(),
mapstructure.StringToTimeDurationHookFunc(),
mapstructure.StringToSliceHookFunc(","),
),
)
func WithFs(fs v1.FS) func(r *v1.Config) error {
return func(r *v1.Config) error {
r.Fs = fs
return nil
}
}
func WithLogger(logger v1.Logger) func(r *v1.Config) error {
return func(r *v1.Config) error {
r.Logger = logger
return nil
}
}
func WithSyscall(syscall v1.SyscallInterface) func(r *v1.Config) error {
return func(r *v1.Config) error {
r.Syscall = syscall
return nil
}
}
func WithMounter(mounter mount.Interface) func(r *v1.Config) error {
return func(r *v1.Config) error {
r.Mounter = mounter
return nil
}
}
func WithRunner(runner v1.Runner) func(r *v1.Config) error {
return func(r *v1.Config) error {
r.Runner = runner
return nil
}
}
func WithClient(client v1.HTTPClient) func(r *v1.Config) error {
return func(r *v1.Config) error {
r.Client = client
return nil
}
}
func WithCloudInitRunner(ci v1.CloudInitRunner) func(r *v1.Config) error {
return func(r *v1.Config) error {
r.CloudInitRunner = ci
return nil
}
}
func WithArch(arch string) func(r *v1.Config) error {
return func(r *v1.Config) error {
r.Arch = arch
return nil
}
}
func WithImageExtractor(extractor v1.ImageExtractor) func(r *v1.Config) error {
return func(r *v1.Config) error {
r.ImageExtractor = extractor
return nil
}
}
type GenericOptions func(a *v1.Config) error
func ReadConfigBuild(configDir string, flags *pflag.FlagSet, mounter mount.Interface) (*v1.BuildConfig, error) {
logger := v1.NewLogger()
if configDir == "" {
configDir = "."
}
cfg := NewBuildConfig(
WithLogger(logger),
WithMounter(mounter),
)
configLogger(cfg.Logger, cfg.Fs)
viper.AddConfigPath(configDir)
viper.SetConfigType("yaml")
viper.SetConfigName("manifest.yaml")
// If a config file is found, read it in.
_ = viper.MergeInConfig()
// Bind buildconfig flags
bindGivenFlags(viper.GetViper(), flags)
// unmarshal all the vars into the config object
err := viper.Unmarshal(cfg, setDecoder, decodeHook)
if err != nil {
cfg.Logger.Warnf("error unmarshalling config: %s", err)
}
err = cfg.Sanitize()
cfg.Logger.Debugf("Full config loaded: %s", litter.Sdump(cfg))
return cfg, err
}
func ReadBuildISO(b *v1.BuildConfig, flags *pflag.FlagSet) (*v1.LiveISO, error) {
iso := NewISO()
vp := viper.Sub("iso")
if vp == nil {
vp = viper.New()
}
// Bind build-iso cmd flags
bindGivenFlags(vp, flags)
err := vp.Unmarshal(iso, setDecoder, decodeHook)
if err != nil {
b.Logger.Warnf("error unmarshalling LiveISO: %s", err)
}
err = iso.Sanitize()
b.Logger.Debugf("Loaded LiveISO: %s", litter.Sdump(iso))
return iso, err
}
func NewISO() *v1.LiveISO {
return &v1.LiveISO{
Label: constants.ISOLabel,
GrubEntry: constants.GrubDefEntry,
UEFI: []*v1.ImageSource{},
Image: []*v1.ImageSource{},
}
}
func NewBuildConfig(opts ...GenericOptions) *v1.BuildConfig {
b := &v1.BuildConfig{
Config: *NewConfig(opts...),
Name: constants.BuildImgName,
}
return b
}
func NewConfig(opts ...GenericOptions) *v1.Config {
log := v1.NewLogger()
arch, err := utils.GolangArchToArch(runtime.GOARCH)
if err != nil {
log.Errorf("invalid arch: %s", err.Error())
return nil
}
c := &v1.Config{
Fs: vfs.OSFS,
Logger: log,
Syscall: &v1.RealSyscall{},
Client: http.NewClient(),
Repos: []v1.Repository{},
Arch: arch,
SquashFsNoCompression: true,
}
for _, o := range opts {
err := o(c)
if err != nil {
log.Errorf("error applying config option: %s", err.Error())
return nil
}
}
// delay runner creation after we have run over the options in case we use WithRunner
if c.Runner == nil {
c.Runner = &v1.RealRunner{Logger: c.Logger}
}
// Now check if the runner has a logger inside, otherwise point our logger into it
// This can happen if we set the WithRunner option as that doesn't set a logger
if c.Runner.GetLogger() == nil {
c.Runner.SetLogger(c.Logger)
}
// Delay the yip runner creation, so we set the proper logger instead of blindly setting it to the logger we create
// at the start of NewRunConfig, as WithLogger can be passed on init, and that would result in 2 different logger
// instances, on the config.Logger and the other on config.CloudInitRunner
if c.CloudInitRunner == nil {
c.CloudInitRunner = cloudinit.NewYipCloudInitRunner(c.Logger, c.Runner, vfs.OSFS)
}
if c.Mounter == nil {
c.Mounter = mount.New(constants.MountBinary)
}
return c
}
func configLogger(log v1.Logger, vfs v1.FS) {
// Set debug level
if viper.GetBool("debug") {
log.SetLevel(v1.DebugLevel())
}
// Set formatter so both file and stdout format are equal
log.SetFormatter(&logrus.TextFormatter{
ForceColors: true,
DisableColors: false,
DisableTimestamp: false,
FullTimestamp: true,
})
// Logfile
logfile := viper.GetString("logfile")
if logfile != "" {
o, err := vfs.OpenFile(logfile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, fs.ModePerm)
if err != nil {
log.Errorf("Could not open %s for logging to file: %s", logfile, err.Error())
}
// else set it to both stdout and the file
mw := io.MultiWriter(os.Stdout, o)
log.SetOutput(mw)
} else { // no logfile
if viper.GetBool("quiet") { // quiet is enabled so discard all logging
log.SetOutput(io.Discard)
} else { // default to stdout
log.SetOutput(os.Stdout)
}
}
log.Infof("Starting enki version %s", version.GetVersion())
if log.GetLevel() == logrus.DebugLevel {
log.Debugf("%+v\n", version.Get())
}
}
// BindGivenFlags binds to viper only passed flags, ignoring any non provided flag
func bindGivenFlags(vp *viper.Viper, flagSet *pflag.FlagSet) {
if flagSet != nil {
flagSet.VisitAll(func(f *pflag.Flag) {
if f.Changed {
_ = vp.BindPFlag(f.Name, f)
}
})
}
}
// setDecoder sets ZeroFields mastructure attribute to true
func setDecoder(config *mapstructure.DecoderConfig) {
// Make sure we zero fields before applying them, this is relevant for slices
// so we do not merge with any already present value and directly apply whatever
// we got form configs.
config.ZeroFields = true
}
type Unmarshaler interface {
CustomUnmarshal(interface{}) (bool, error)
}
func UnmarshalerHook() mapstructure.DecodeHookFunc {
return func(from reflect.Value, to reflect.Value) (interface{}, error) {
// get the destination object address if it is not passed by reference
if to.CanAddr() {
to = to.Addr()
}
// If the destination implements the unmarshaling interface
u, ok := to.Interface().(Unmarshaler)
if !ok {
return from.Interface(), nil
}
// If it is nil and a pointer, create and assign the target value first
if to.IsNil() && to.Type().Kind() == reflect.Ptr {
to.Set(reflect.New(to.Type().Elem()))
u = to.Interface().(Unmarshaler)
}
// Call the custom unmarshaling method
cont, err := u.CustomUnmarshal(from.Interface())
if cont {
// Continue with the decoding stack
return from.Interface(), err
}
// Decoding finalized
return to.Interface(), err
}
}

View File

@@ -0,0 +1,124 @@
package constants
import (
"fmt"
"os"
"path/filepath"
)
const (
GrubDefEntry = "Kairos"
EfiLabel = "COS_GRUB"
ISOLabel = "COS_LIVE"
MountBinary = "/usr/bin/mount"
EfiFs = "vfat"
IsoRootFile = "rootfs.squashfs"
IsoEFIPath = "/boot/uefi.img"
BuildImgName = "elemental"
EfiBootPath = "/EFI/BOOT"
GrubEfiImagex86 = "/usr/share/grub2/x86_64-efi/grub.efi"
GrubEfiImageArm64 = "/usr/share/grub2/arm64-efi/grub.efi"
GrubEfiImagex86Dest = EfiBootPath + "/bootx64.efi"
GrubEfiImageArm64Dest = EfiBootPath + "/bootaa64.efi"
GrubCfg = "grub.cfg"
GrubPrefixDir = "/boot/grub2"
GrubEfiCfg = "search --no-floppy --file --set=root " + IsoKernelPath +
"\nset prefix=($root)" + GrubPrefixDir +
"\nconfigfile $prefix/" + GrubCfg
GrubFont = "/usr/share/grub2/unicode.pf2"
GrubBootHybridImg = "/usr/share/grub2/i386-pc/boot_hybrid.img"
SyslinuxFiles = "/usr/share/syslinux/isolinux.bin " +
"/usr/share/syslinux/menu.c32 " +
"/usr/share/syslinux/chain.c32 " +
"/usr/share/syslinux/mboot.c32"
IsoLoaderPath = "/boot/x86_64/loader"
GrubCfgTemplate = `search --no-floppy --file --set=root /boot/kernel
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 "%s" --class os --unrestricted {
echo Loading kernel...
$linux ($root)/boot/kernel cdroot root=live:CDLABEL=%s rd.live.dir=/ rd.live.squashimg=rootfs.squashfs rd.live.overlay.overlayfs console=tty1 console=ttyS0 rd.cos.disable
echo Loading initrd...
$initrd ($root)/boot/initrd
}
if [ "${grub_platform}" = "efi" ]; then
hiddenentry "Text mode" --hotkey "t" {
set textmode=true
terminal_output console
}
fi`
GrubBiosTarget = "i386-pc"
GrubI386BinDir = "/usr/share/grub2/i386-pc"
GrubBiosImg = GrubI386BinDir + "/core.img"
GrubBiosCDBoot = GrubI386BinDir + "/cdboot.img"
GrubEltoritoImg = GrubI386BinDir + "/eltorito.img"
//TODO this list could be optimized
GrubModules = "ext2 iso9660 linux echo configfile search_label search_fs_file search search_fs_uuid " +
"ls normal gzio png fat gettext font minicmd gfxterm gfxmenu all_video xfs btrfs lvm luks " +
"gcry_rijndael gcry_sha256 gcry_sha512 crypto cryptodisk test true loadenv part_gpt " +
"part_msdos biosdisk vga vbe chain boot"
IsoHybridMBR = "/boot/x86_64/loader/boot_hybrid.img"
IsoBootCatalog = "/boot/x86_64/boot.catalog"
IsoBootFile = "/boot/x86_64/loader/eltorito.img"
// These paths are arbitrary but coupled to grub.cfg
IsoKernelPath = "/boot/kernel"
IsoInitrdPath = "/boot/initrd"
// Default directory and file fileModes
DirPerm = os.ModeDir | os.ModePerm
FilePerm = 0666
NoWriteDirPerm = 0555 | os.ModeDir
TempDirPerm = os.ModePerm | os.ModeSticky | os.ModeDir
ArchAmd64 = "amd64"
Archx86 = "x86_64"
ArchArm64 = "arm64"
)
// GetDefaultSquashfsOptions returns the default options to use when creating a squashfs
func GetDefaultSquashfsOptions() []string {
return []string{"-b", "1024k"}
}
func GetXorrisoBooloaderArgs(root string) []string {
args := []string{
"-boot_image", "grub", fmt.Sprintf("bin_path=%s", IsoBootFile),
"-boot_image", "grub", fmt.Sprintf("grub2_mbr=%s/%s", root, IsoHybridMBR),
"-boot_image", "grub", "grub2_boot_info=on",
"-boot_image", "any", "partition_offset=16",
"-boot_image", "any", fmt.Sprintf("cat_path=%s", IsoBootCatalog),
"-boot_image", "any", "cat_hidden=on",
"-boot_image", "any", "boot_info_table=on",
"-boot_image", "any", "platform_id=0x00",
"-boot_image", "any", "emul_type=no_emulation",
"-boot_image", "any", "load_size=2048",
"-append_partition", "2", "0xef", filepath.Join(root, IsoEFIPath),
"-boot_image", "any", "next",
"-boot_image", "any", "efi_path=--interval:appended_partition_2:all::",
"-boot_image", "any", "platform_id=0xef",
"-boot_image", "any", "emul_type=no_emulation",
}
return args
}

View File

@@ -0,0 +1,218 @@
/*
Copyright © 2022 SUSE LLC
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 utils
import (
"errors"
"fmt"
"os"
"sort"
"strings"
"github.com/kairos-io/enki/pkg/constants"
v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1"
)
// Chroot represents the struct that will allow us to run commands inside a given chroot
type Chroot struct {
path string
defaultMounts []string
extraMounts map[string]string
activeMounts []string
config *v1.Config
}
func NewChroot(path string, config *v1.Config) *Chroot {
return &Chroot{
path: path,
defaultMounts: []string{"/dev", "/dev/pts", "/proc", "/sys"},
extraMounts: map[string]string{},
activeMounts: []string{},
config: config,
}
}
// ChrootedCallback runs the given callback in a chroot environment
func ChrootedCallback(cfg *v1.Config, path string, bindMounts map[string]string, callback func() error) error {
chroot := NewChroot(path, cfg)
chroot.SetExtraMounts(bindMounts)
return chroot.RunCallback(callback)
}
// Sets additional bind mounts for the chroot enviornment. They are represented
// in a map where the key is the path outside the chroot and the value is the
// path inside the chroot.
func (c *Chroot) SetExtraMounts(extraMounts map[string]string) {
c.extraMounts = extraMounts
}
// Prepare will mount the defaultMounts as bind mounts, to be ready when we run chroot
func (c *Chroot) Prepare() error {
var err error
keys := []string{}
mountOptions := []string{"bind"}
if len(c.activeMounts) > 0 {
return errors.New("There are already active mountpoints for this instance")
}
defer func() {
if err != nil {
c.Close()
}
}()
for _, mnt := range c.defaultMounts {
mountPoint := fmt.Sprintf("%s%s", strings.TrimSuffix(c.path, "/"), mnt)
err = MkdirAll(c.config.Fs, mountPoint, constants.DirPerm)
if err != nil {
return err
}
err = c.config.Mounter.Mount(mnt, mountPoint, "bind", mountOptions)
if err != nil {
return err
}
c.activeMounts = append(c.activeMounts, mountPoint)
}
for k := range c.extraMounts {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
mountPoint := fmt.Sprintf("%s%s", strings.TrimSuffix(c.path, "/"), c.extraMounts[k])
err = MkdirAll(c.config.Fs, mountPoint, constants.DirPerm)
if err != nil {
return err
}
err = c.config.Mounter.Mount(k, mountPoint, "bind", mountOptions)
if err != nil {
return err
}
c.activeMounts = append(c.activeMounts, mountPoint)
}
return nil
}
// Close will unmount all active mounts created in Prepare on reverse order
func (c *Chroot) Close() error {
failures := []string{}
for len(c.activeMounts) > 0 {
curr := c.activeMounts[len(c.activeMounts)-1]
c.config.Logger.Debugf("Unmounting %s from chroot", curr)
c.activeMounts = c.activeMounts[:len(c.activeMounts)-1]
err := c.config.Mounter.Unmount(curr)
if err != nil {
c.config.Logger.Errorf("Error unmounting %s: %s", curr, err)
failures = append(failures, curr)
}
}
if len(failures) > 0 {
c.activeMounts = failures
return fmt.Errorf("failed closing chroot environment. Unmount failures: %v", failures)
}
return nil
}
// RunCallback runs the given callback in a chroot environment
func (c *Chroot) RunCallback(callback func() error) (err error) {
var currentPath string
var oldRootF *os.File
// Store current path
currentPath, err = os.Getwd()
if err != nil {
c.config.Logger.Error("Failed to get current path")
return err
}
defer func() {
tmpErr := os.Chdir(currentPath)
if err == nil && tmpErr != nil {
err = tmpErr
}
}()
// Store current root
oldRootF, err = c.config.Fs.Open("/")
if err != nil {
c.config.Logger.Errorf("Can't open current root")
return err
}
defer oldRootF.Close()
if len(c.activeMounts) == 0 {
err = c.Prepare()
if err != nil {
c.config.Logger.Errorf("Can't mount default mounts")
return err
}
defer func() {
tmpErr := c.Close()
if err == nil {
err = tmpErr
}
}()
}
// Change to new dir before running chroot!
err = c.config.Syscall.Chdir(c.path)
if err != nil {
c.config.Logger.Errorf("Can't chdir %s: %s", c.path, err)
return err
}
err = c.config.Syscall.Chroot(c.path)
if err != nil {
c.config.Logger.Errorf("Can't chroot %s: %s", c.path, err)
return err
}
// Restore to old root
defer func() {
tmpErr := oldRootF.Chdir()
if tmpErr != nil {
c.config.Logger.Errorf("Can't change to old root dir")
if err == nil {
err = tmpErr
}
} else {
tmpErr = c.config.Syscall.Chroot(".")
if tmpErr != nil {
c.config.Logger.Errorf("Can't chroot back to old root")
if err == nil {
err = tmpErr
}
}
}
}()
return callback()
}
// Run executes a command inside a chroot
func (c *Chroot) Run(command string, args ...string) (out []byte, err error) {
callback := func() error {
out, err = c.config.Runner.Run(command, args...)
return err
}
err = c.RunCallback(callback)
if err != nil {
c.config.Logger.Errorf("Cant run command %s with args %v on chroot: %s", command, args, err)
c.config.Logger.Debugf("Output from command: %s", out)
}
return out, err
}

View File

@@ -0,0 +1,40 @@
package utils
import (
"fmt"
"github.com/kairos-io/enki/pkg/constants"
v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1"
"strings"
)
// CreateSquashFS creates a squash file at destination from a source, with options
// TODO: Check validity of source maybe?
func CreateSquashFS(runner v1.Runner, logger v1.Logger, source string, destination string, options []string) error {
// create args
args := []string{source, destination}
// append options passed to args in order to have the correct order
// protect against options passed together in the same string , i.e. "-x add" instead of "-x", "add"
var optionsExpanded []string
for _, op := range options {
optionsExpanded = append(optionsExpanded, strings.Split(op, " ")...)
}
args = append(args, optionsExpanded...)
out, err := runner.Run("mksquashfs", args...)
if err != nil {
logger.Debugf("Error running squashfs creation, stdout: %s", out)
logger.Errorf("Error while creating squashfs from %s to %s: %s", source, destination, err)
return err
}
return nil
}
func GolangArchToArch(arch string) (string, error) {
switch strings.ToLower(arch) {
case constants.ArchAmd64:
return constants.Archx86, nil
case constants.ArchArm64:
return constants.ArchArm64, nil
default:
return "", fmt.Errorf("invalid arch")
}
}

View File

@@ -0,0 +1,223 @@
package utils
import (
"crypto/sha256"
"fmt"
"io"
"os"
"path/filepath"
"strconv"
"sync"
"syscall"
"time"
"github.com/kairos-io/enki/pkg/constants"
v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1"
"github.com/twpayne/go-vfs"
"github.com/twpayne/go-vfs/vfst"
)
// MkdirAll directory and all parents if not existing
func MkdirAll(fs v1.FS, name string, mode os.FileMode) (err error) {
if _, isReadOnly := fs.(*vfs.ReadOnlyFS); isReadOnly {
return permError("mkdir", name)
}
if name, err = fs.RawPath(name); err != nil {
return &os.PathError{Op: "mkdir", Path: name, Err: err}
}
return os.MkdirAll(name, mode)
}
// permError returns an *os.PathError with Err syscall.EPERM.
func permError(op, path string) error {
return &os.PathError{
Op: op,
Path: path,
Err: syscall.EPERM,
}
}
// Copies source file to target file using Fs interface
func CreateDirStructure(fs v1.FS, target string) error {
for _, dir := range []string{"/run", "/dev", "/boot", "/usr/local", "/oem"} {
err := MkdirAll(fs, filepath.Join(target, dir), constants.DirPerm)
if err != nil {
return err
}
}
for _, dir := range []string{"/proc", "/sys"} {
err := MkdirAll(fs, filepath.Join(target, dir), constants.NoWriteDirPerm)
if err != nil {
return err
}
}
err := MkdirAll(fs, filepath.Join(target, "/tmp"), constants.DirPerm)
if err != nil {
return err
}
// Set /tmp permissions regardless the umask setup
err = fs.Chmod(filepath.Join(target, "/tmp"), constants.TempDirPerm)
if err != nil {
return err
}
return nil
}
// TempDir creates a temp file in the virtual fs
// Took from afero.FS code and adapted
func TempDir(fs v1.FS, dir, prefix string) (name string, err error) {
if dir == "" {
dir = os.TempDir()
}
// This skips adding random stuff to the created temp dir so the temp dir created is predictable for testing
if _, isTestFs := fs.(*vfst.TestFS); isTestFs {
err = MkdirAll(fs, filepath.Join(dir, prefix), 0700)
if err != nil {
return "", err
}
name = filepath.Join(dir, prefix)
return
}
nconflict := 0
for i := 0; i < 10000; i++ {
try := filepath.Join(dir, prefix+nextRandom())
err = MkdirAll(fs, try, 0700)
if os.IsExist(err) {
if nconflict++; nconflict > 10 {
randmu.Lock()
rand = reseed()
randmu.Unlock()
}
continue
}
if err == nil {
name = try
}
break
}
return
}
// Random number state.
// We generate random temporary file names so that there's a good
// chance the file doesn't exist yet - keeps the number of tries in
// TempFile to a minimum.
var rand uint32
var randmu sync.Mutex
func reseed() uint32 {
return uint32(time.Now().UnixNano() + int64(os.Getpid()))
}
func nextRandom() string {
randmu.Lock()
r := rand
if r == 0 {
r = reseed()
}
r = r*1664525 + 1013904223 // constants from Numerical Recipes
rand = r
randmu.Unlock()
return strconv.Itoa(int(1e9 + r%1e9))[1:]
}
// CopyFile Copies source file to target file using Fs interface. If target
// is directory source is copied into that directory using source name file.
func CopyFile(fs v1.FS, source string, target string) (err error) {
return ConcatFiles(fs, []string{source}, target)
}
// IsDir check if the path is a dir
func IsDir(fs v1.FS, path string) (bool, error) {
fi, err := fs.Stat(path)
if err != nil {
return false, err
}
return fi.IsDir(), nil
}
// ConcatFiles Copies source files to target file using Fs interface.
// Source files are concatenated into target file in the given order.
// If target is a directory source is copied into that directory using
// 1st source name file.
func ConcatFiles(fs v1.FS, sources []string, target string) (err error) {
if len(sources) == 0 {
return fmt.Errorf("Empty sources list")
}
if dir, _ := IsDir(fs, target); dir {
target = filepath.Join(target, filepath.Base(sources[0]))
}
targetFile, err := fs.Create(target)
if err != nil {
return err
}
defer func() {
if err == nil {
err = targetFile.Close()
} else {
_ = fs.Remove(target)
}
}()
var sourceFile *os.File
for _, source := range sources {
sourceFile, err = fs.Open(source)
if err != nil {
break
}
_, err = io.Copy(targetFile, sourceFile)
if err != nil {
break
}
err = sourceFile.Close()
if err != nil {
break
}
}
return err
}
// DirSize returns the accumulated size of all files in folder
func DirSize(fs v1.FS, path string) (int64, error) {
var size int64
err := vfs.Walk(fs, path, func(_ string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
size += info.Size()
}
return err
})
return size, err
}
// Check if a file or directory exists.
func Exists(fs v1.FS, path string) (bool, error) {
_, err := fs.Stat(path)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
// CalcFileChecksum opens the given file and returns the sha256 checksum of it.
func CalcFileChecksum(fs v1.FS, fileName string) (string, error) {
f, err := fs.Open(fileName)
if err != nil {
return "", err
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return "", err
}
return fmt.Sprintf("%x", h.Sum(nil)), nil
}

View File

@@ -0,0 +1,29 @@
/*
Copyright © 2021 SUSE LLC
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 utils_test
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestWhitebox(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Utils test suite")
}

View File

@@ -0,0 +1,294 @@
/*
Copyright © 2021 SUSE LLC
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 utils_test
import (
"errors"
"fmt"
conf "github.com/kairos-io/enki/pkg/config"
"github.com/kairos-io/enki/pkg/constants"
"github.com/kairos-io/enki/pkg/utils"
v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1"
v1mock "github.com/kairos-io/kairos-agent/v2/tests/mocks"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/twpayne/go-vfs"
"github.com/twpayne/go-vfs/vfst"
"os"
"strings"
)
var _ = Describe("Utils", Label("utils"), func() {
var config *v1.Config
var runner *v1mock.FakeRunner
var logger v1.Logger
var syscall *v1mock.FakeSyscall
var client *v1mock.FakeHTTPClient
var mounter *v1mock.ErrorMounter
var fs vfs.FS
var cleanup func()
BeforeEach(func() {
runner = v1mock.NewFakeRunner()
syscall = &v1mock.FakeSyscall{}
mounter = v1mock.NewErrorMounter()
client = &v1mock.FakeHTTPClient{}
logger = v1.NewNullLogger()
// Ensure /tmp exists in the VFS
fs, cleanup, _ = vfst.NewTestFS(nil)
fs.Mkdir("/tmp", constants.DirPerm)
fs.Mkdir("/run", constants.DirPerm)
fs.Mkdir("/etc", constants.DirPerm)
config = conf.NewConfig(
conf.WithFs(fs),
conf.WithRunner(runner),
conf.WithLogger(logger),
conf.WithMounter(mounter),
conf.WithSyscall(syscall),
conf.WithClient(client),
)
})
AfterEach(func() { cleanup() })
Describe("Chroot", Label("chroot"), func() {
var chroot *utils.Chroot
BeforeEach(func() {
chroot = utils.NewChroot(
"/whatever",
config,
)
})
Describe("ChrootedCallback method", func() {
It("runs a callback in a chroot", func() {
err := utils.ChrootedCallback(config, "/somepath", map[string]string{}, func() error {
return nil
})
Expect(err).ShouldNot(HaveOccurred())
err = utils.ChrootedCallback(config, "/somepath", map[string]string{}, func() error {
return fmt.Errorf("callback error")
})
Expect(err).Should(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("callback error"))
})
})
Describe("on success", func() {
It("command should be called in the chroot", func() {
_, err := chroot.Run("chroot-command")
Expect(err).To(BeNil())
Expect(syscall.WasChrootCalledWith("/whatever")).To(BeTrue())
})
It("commands should be called with a customized chroot", func() {
chroot.SetExtraMounts(map[string]string{"/real/path": "/in/chroot/path"})
Expect(chroot.Prepare()).To(BeNil())
defer chroot.Close()
_, err := chroot.Run("chroot-command")
Expect(err).To(BeNil())
Expect(syscall.WasChrootCalledWith("/whatever")).To(BeTrue())
_, err = chroot.Run("chroot-another-command")
Expect(err).To(BeNil())
})
It("runs a callback in a custom chroot", func() {
called := false
callback := func() error {
called = true
return nil
}
err := chroot.RunCallback(callback)
Expect(err).To(BeNil())
Expect(syscall.WasChrootCalledWith("/whatever")).To(BeTrue())
Expect(called).To(BeTrue())
})
})
Describe("on failure", func() {
It("should return error if chroot-command fails", func() {
runner.ReturnError = errors.New("run error")
_, err := chroot.Run("chroot-command")
Expect(err).NotTo(BeNil())
Expect(syscall.WasChrootCalledWith("/whatever")).To(BeTrue())
})
It("should return error if callback fails", func() {
called := false
callback := func() error {
called = true
return errors.New("Callback error")
}
err := chroot.RunCallback(callback)
Expect(err).NotTo(BeNil())
Expect(syscall.WasChrootCalledWith("/whatever")).To(BeTrue())
Expect(called).To(BeTrue())
})
It("should return error if preparing twice before closing", func() {
Expect(chroot.Prepare()).To(BeNil())
defer chroot.Close()
Expect(chroot.Prepare()).NotTo(BeNil())
Expect(chroot.Close()).To(BeNil())
Expect(chroot.Prepare()).To(BeNil())
})
It("should return error if failed to chroot", func() {
syscall.ErrorOnChroot = true
_, err := chroot.Run("chroot-command")
Expect(err).ToNot(BeNil())
Expect(syscall.WasChrootCalledWith("/whatever")).To(BeTrue())
Expect(err.Error()).To(ContainSubstring("chroot error"))
})
It("should return error if failed to mount on prepare", Label("mount"), func() {
mounter.ErrorOnMount = true
_, err := chroot.Run("chroot-command")
Expect(err).ToNot(BeNil())
Expect(err.Error()).To(ContainSubstring("mount error"))
})
It("should return error if failed to unmount on close", Label("unmount"), func() {
mounter.ErrorOnUnmount = true
_, err := chroot.Run("chroot-command")
Expect(err).ToNot(BeNil())
Expect(err.Error()).To(ContainSubstring("failed closing chroot"))
})
})
})
Describe("CopyFile", Label("CopyFile"), func() {
It("Copies source file to target file", func() {
err := utils.MkdirAll(fs, "/some", constants.DirPerm)
Expect(err).ShouldNot(HaveOccurred())
_, err = fs.Create("/some/file")
Expect(err).ShouldNot(HaveOccurred())
_, err = fs.Stat("/some/otherfile")
Expect(err).Should(HaveOccurred())
Expect(utils.CopyFile(fs, "/some/file", "/some/otherfile")).ShouldNot(HaveOccurred())
e, err := utils.Exists(fs, "/some/otherfile")
Expect(err).ShouldNot(HaveOccurred())
Expect(e).To(BeTrue())
})
It("Copies source file to target folder", func() {
err := utils.MkdirAll(fs, "/some", constants.DirPerm)
Expect(err).ShouldNot(HaveOccurred())
err = utils.MkdirAll(fs, "/someotherfolder", constants.DirPerm)
Expect(err).ShouldNot(HaveOccurred())
_, err = fs.Create("/some/file")
Expect(err).ShouldNot(HaveOccurred())
_, err = fs.Stat("/someotherfolder/file")
Expect(err).Should(HaveOccurred())
Expect(utils.CopyFile(fs, "/some/file", "/someotherfolder")).ShouldNot(HaveOccurred())
e, err := utils.Exists(fs, "/someotherfolder/file")
Expect(err).ShouldNot(HaveOccurred())
Expect(e).To(BeTrue())
})
It("Fails to open non existing file", func() {
err := utils.MkdirAll(fs, "/some", constants.DirPerm)
Expect(err).ShouldNot(HaveOccurred())
Expect(utils.CopyFile(fs, "/some/file", "/some/otherfile")).NotTo(BeNil())
_, err = fs.Stat("/some/otherfile")
Expect(err).NotTo(BeNil())
})
It("Fails to copy on non writable target", func() {
err := utils.MkdirAll(fs, "/some", constants.DirPerm)
Expect(err).ShouldNot(HaveOccurred())
fs.Create("/some/file")
_, err = fs.Stat("/some/otherfile")
Expect(err).NotTo(BeNil())
fs = vfs.NewReadOnlyFS(fs)
Expect(utils.CopyFile(fs, "/some/file", "/some/otherfile")).NotTo(BeNil())
_, err = fs.Stat("/some/otherfile")
Expect(err).NotTo(BeNil())
})
})
Describe("CreateDirStructure", Label("CreateDirStructure"), func() {
It("Creates essential directories", func() {
dirList := []string{"sys", "proc", "dev", "tmp", "boot", "usr/local", "oem"}
for _, dir := range dirList {
_, err := fs.Stat(fmt.Sprintf("/my/root/%s", dir))
Expect(err).NotTo(BeNil())
}
Expect(utils.CreateDirStructure(fs, "/my/root")).To(BeNil())
for _, dir := range dirList {
fi, err := fs.Stat(fmt.Sprintf("/my/root/%s", dir))
Expect(err).To(BeNil())
if fi.Name() == "tmp" {
Expect(fmt.Sprintf("%04o", fi.Mode().Perm())).To(Equal("0777"))
Expect(fi.Mode() & os.ModeSticky).NotTo(Equal(0))
}
if fi.Name() == "sys" {
Expect(fmt.Sprintf("%04o", fi.Mode().Perm())).To(Equal("0555"))
}
}
})
It("Fails on non writable target", func() {
fs = vfs.NewReadOnlyFS(fs)
Expect(utils.CreateDirStructure(fs, "/my/root")).NotTo(BeNil())
})
})
Describe("DirSize", Label("fs"), func() {
BeforeEach(func() {
err := utils.MkdirAll(fs, "/folder/subfolder", constants.DirPerm)
Expect(err).ShouldNot(HaveOccurred())
f, err := fs.Create("/folder/file")
Expect(err).ShouldNot(HaveOccurred())
err = f.Truncate(1024)
Expect(err).ShouldNot(HaveOccurred())
f, err = fs.Create("/folder/subfolder/file")
Expect(err).ShouldNot(HaveOccurred())
err = f.Truncate(2048)
Expect(err).ShouldNot(HaveOccurred())
})
It("Returns the expected size of a test folder", func() {
size, err := utils.DirSize(fs, "/folder")
Expect(err).ShouldNot(HaveOccurred())
Expect(size).To(Equal(int64(3072)))
})
})
Describe("CalcFileChecksum", Label("checksum"), func() {
It("compute correct sha256 checksum", func() {
testData := strings.Repeat("abcdefghilmnopqrstuvz\n", 20)
testDataSHA256 := "7f182529f6362ae9cfa952ab87342a7180db45d2c57b52b50a68b6130b15a422"
err := fs.Mkdir("/iso", constants.DirPerm)
Expect(err).ShouldNot(HaveOccurred())
err = fs.WriteFile("/iso/test.iso", []byte(testData), 0644)
Expect(err).ShouldNot(HaveOccurred())
checksum, err := utils.CalcFileChecksum(fs, "/iso/test.iso")
Expect(err).ShouldNot(HaveOccurred())
Expect(checksum).To(Equal(testDataSHA256))
})
})
Describe("CreateSquashFS", Label("CreateSquashFS"), func() {
It("runs with no options if none given", func() {
err := utils.CreateSquashFS(runner, logger, "source", "dest", []string{})
Expect(runner.IncludesCmds([][]string{
{"mksquashfs", "source", "dest"},
})).To(BeNil())
Expect(err).ToNot(HaveOccurred())
})
It("runs with options if given", func() {
err := utils.CreateSquashFS(runner, logger, "source", "dest", constants.GetDefaultSquashfsOptions())
cmd := []string{"mksquashfs", "source", "dest"}
cmd = append(cmd, constants.GetDefaultSquashfsOptions()...)
Expect(runner.IncludesCmds([][]string{
cmd,
})).To(BeNil())
Expect(err).ToNot(HaveOccurred())
})
It("returns an error if it fails", func() {
runner.ReturnError = errors.New("error")
err := utils.CreateSquashFS(runner, logger, "source", "dest", []string{})
Expect(runner.IncludesCmds([][]string{
{"mksquashfs", "source", "dest"},
})).To(BeNil())
Expect(err).To(HaveOccurred())
})
})
})

View File

@@ -2,4 +2,4 @@
set -ex
elemental --config-dir /config $@
enki --config-dir /config $@

View File

@@ -0,0 +1,15 @@
general:
debug: false
spinner_charset: 9
logging:
enable_emoji: false
repositories:
- name: "kairos"
description: "kairos repository"
type: "docker"
cached: true
enable: true
priority: 2
urls:
- "quay.io/kairos/packages"
reference: 20230816140633-repository.yaml

View File

@@ -0,0 +1,15 @@
general:
debug: false
spinner_charset: 9
logging:
enable_emoji: false
repositories:
- name: "kairos-arm64"
description: "kairos repository arm64"
type: "docker"
cached: true
enable: true
priority: 2
urls:
- "quay.io/kairos/packages-arm64"
reference: 20230816141243-repository.yaml

View File

@@ -1,24 +0,0 @@
general:
debug: false
spinner_charset: 9
logging:
enable_emoji: false
repositories:
- name: "kairos"
description: "kairos repository"
type: "docker"
arch: amd64
cached: true
priority: 2
urls:
- "quay.io/kairos/packages"
reference: 20230512113938-repository.yaml
- name: "kairos-arm64"
description: "kairos repository arm64"
type: "docker"
arch: arm64
cached: true
priority: 2
urls:
- "quay.io/kairos/packages-arm64"
reference: 20230512115044-repository.yaml

View File

@@ -38,7 +38,7 @@ mkdir -p $WORKDIR/tmpefi
# Create the EFI partition FAT16 and include the EFI image and a basic grub.cfg
truncate -s $((20*1024*1024)) bootloader/efi.img
cp -rfv /arm/grub/efi/* $WORKDIR/tmpefi
cp -rfv /arm/raw/grubefi/* $WORKDIR/tmpefi
mkfs.fat -F16 -n COS_GRUB bootloader/efi.img
mcopy -s -i bootloader/efi.img $WORKDIR/tmpefi/EFI ::EFI
@@ -93,9 +93,9 @@ cp -rfv ${STATEDIR}/cOS/active.img ${RECOVERY}/cOS/recovery.img
tune2fs -L ${SYSTEM_LABEL} ${RECOVERY}/cOS/recovery.img
# Install real grub config to recovery
cp -rfv /arm/grub/config/* $RECOVERY
cp -rfv /arm/raw/grubconfig/* $RECOVERY
mkdir -p $RECOVERY/grub2/fonts
cp -rfv /arm/grub/artifacts/* $RECOVERY/grub2
cp -rfv /arm/raw/grubartifacts/* $RECOVERY/grub2
mv $RECOVERY/grub2/*pf2 $RECOVERY/grub2/fonts
dd if=/dev/zero of=recovery_partition.img bs=1M count=$recovery_size

View File

@@ -71,7 +71,7 @@ truncate -s "+$((1024*1024))" $OUT
if [ -n "$EXTEND" ]; then
echo "Extending image of $EXTEND MB"
truncate -s "+$(($EXTEND*1024*1024))" $OUT
truncate -s "+$((EXTEND*1024*1024))" $OUT
fi
# Create the partition table in $OUT (assumes sectors of 512 bytes)