diff --git a/.github/workflows/image.yaml b/.github/workflows/image.yaml index be9536707..f982763e8 100644 --- a/.github/workflows/image.yaml +++ b/.github/workflows/image.yaml @@ -169,7 +169,30 @@ jobs: mkdir build mv $ISO build/kairos.iso ./earthly.sh +datasource-iso --CLOUD_CONFIG=tests/assets/autoinstall.yaml - ./earthly.sh +run-qemu-tests --FLAVOR=${{ matrix.flavor }} --FROM_ARTIFACTS=true + ./earthly.sh +run-qemu-datasource-tests --FLAVOR=${{ matrix.flavor }} + + qemu-bundles-tests: + needs: + - build + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - flavor: "opensuse" + steps: + - uses: actions/checkout@v2 + - name: Download artifacts + uses: actions/download-artifact@v2 + with: + name: kairos-${{ matrix.flavor }}.iso.zip + - run: | + ls -liah + export ISO=$PWD/$(ls *.iso) + mkdir build + mv $ISO build/kairos.iso + ./earthly.sh +prepare-bundles-tests + ./earthly.sh +run-qemu-bundles-tests --FLAVOR=${{ matrix.flavor }} qemu-reset-tests: needs: @@ -194,7 +217,7 @@ jobs: mkdir build mv $ISO build/kairos.iso ./earthly.sh +datasource-iso --CLOUD_CONFIG=tests/assets/autoinstall.yaml - ./earthly.sh +run-qemu-tests --TEST_SUITE=reset-test --FLAVOR=${{ matrix.flavor }} --FROM_ARTIFACTS=true + ./earthly.sh +run-qemu-datasource-tests --TEST_SUITE=reset-test --FLAVOR=${{ matrix.flavor }} upgrade-with-cli-test: needs: @@ -250,7 +273,7 @@ jobs: export ISO=$PWD/$(ls *.iso) sudo mkdir build sudo mv $ISO build/ - ./earthly.sh +run-qemu-upgrade-test --FLAVOR=${{ matrix.flavor }} --CONTAINER_IMAGE=ttl.sh/kairos-${{ matrix.flavor }}-${{ github.sha }}:8h --TEST_SUITE=upgrade-with-cli + ./earthly.sh +run-qemu-test --FLAVOR=${{ matrix.flavor }} --CONTAINER_IMAGE=ttl.sh/kairos-${{ matrix.flavor }}-${{ github.sha }}:8h --TEST_SUITE=upgrade-with-cli - uses: actions/upload-artifact@v2 if: failure() with: @@ -313,7 +336,7 @@ jobs: export ISO=$PWD/$(ls kairos-${{matrix.flavor}}-*.iso) sudo mkdir build sudo mv $ISO build/ - ./earthly.sh +run-qemu-upgrade-test --FLAVOR=${{ matrix.flavor }} --CONTAINER_IMAGE=ttl.sh/kairos-${{ matrix.flavor }}-${{ github.sha }}:8h --TEST_SUITE=upgrade-latest-with-cli + ./earthly.sh +run-qemu-test --FLAVOR=${{ matrix.flavor }} --CONTAINER_IMAGE=ttl.sh/kairos-${{ matrix.flavor }}-${{ github.sha }}:8h --TEST_SUITE=upgrade-latest-with-cli - uses: actions/upload-artifact@v2 if: failure() with: diff --git a/Earthfile b/Earthfile index 1eb6c8541..bad5d0ee1 100644 --- a/Earthfile +++ b/Earthfile @@ -84,6 +84,16 @@ BUILD_GOLANG: RUN go build -ldflags "-s -w" -o ${BIN} ./cmd/${SRC} && upx ${BIN} SAVE ARTIFACT ${BIN} ${BIN} AS LOCAL build/${BIN} +uuidgen: + FROM alpine + RUN apk add uuidgen + + COPY . ./ + + RUN echo $(uuidgen) > UUIDGEN + + SAVE ARTIFACT UUIDGEN UUIDGEN + version: FROM alpine RUN apk add git @@ -126,6 +136,10 @@ luet: FROM quay.io/luet/base:$LUET_VERSION SAVE ARTIFACT /usr/bin/luet /luet +### +### Image Build targets +### + framework: ARG COSIGN_SKIP ARG REPOSITORIES_FILE @@ -254,6 +268,10 @@ docker-rootfs: FROM +docker SAVE ARTIFACT /. rootfs +### +### Artifacts targets (ISO, netboot, ARM) +### + iso: ARG OSBUILDER_IMAGE ARG ISO_NAME=${OS_ID} @@ -327,8 +345,23 @@ ipxe-iso: SAVE ARTIFACT /build/ipxe/src/bin/ipxe.iso iso AS LOCAL build/${ISO_NAME}-ipxe.iso.ipxe SAVE ARTIFACT /build/ipxe/src/bin/ipxe.usb usb AS LOCAL build/${ISO_NAME}-ipxe-usb.img.ipxe +# Generic targets +# usage e.g. ./earthly.sh +datasource-iso --CLOUD_CONFIG=tests/assets/qrcode.yaml +datasource-iso: + ARG ELEMENTAL_IMAGE + ARG CLOUD_CONFIG + FROM $ELEMENTAL_IMAGE + RUN zypper in -y mkisofs + WORKDIR /build + RUN touch meta-data + COPY ${CLOUD_CONFIG} user-data + RUN cat user-data + RUN mkisofs -output ci.iso -volid cidata -joliet -rock user-data meta-data + SAVE ARTIFACT /build/ci.iso iso.iso AS LOCAL build/datasource.iso -## Security targets +### +### Security target scan +### trivy: FROM aquasec/trivy SAVE ARTIFACT /usr/local/bin/trivy /trivy @@ -356,62 +389,53 @@ linux-bench-scan: COPY +linux-bench/linux-bench /build/linux-bench/linux-bench RUN /build/linux-bench/linux-bench -# Generic targets -# usage e.g. ./earthly.sh +datasource-iso --CLOUD_CONFIG=tests/assets/qrcode.yaml -datasource-iso: - ARG ELEMENTAL_IMAGE - ARG CLOUD_CONFIG - FROM $ELEMENTAL_IMAGE - RUN zypper in -y mkisofs - WORKDIR /build - RUN touch meta-data - COPY ./${CLOUD_CONFIG} user-data - RUN cat user-data - RUN mkisofs -output ci.iso -volid cidata -joliet -rock user-data meta-data - SAVE ARTIFACT /build/ci.iso iso.iso AS LOCAL build/datasource.iso -# usage e.g. ./earthly.sh +run-qemu-tests --FLAVOR=alpine --FROM_ARTIFACTS=true -run-qemu-tests: +### +### Test targets +### +# usage e.g. ./earthly.sh +run-qemu-datasource-tests --FLAVOR=alpine --FROM_ARTIFACTS=true +run-qemu-datasource-tests: FROM opensuse/leap WORKDIR /test RUN zypper in -y qemu-x86 qemu-arm qemu-tools go ARG FLAVOR ARG TEST_SUITE=autoinstall-test - ARG FROM_ARTIFACTS ENV FLAVOR=$FLAVOR ENV SSH_PORT=60022 ENV CREATE_VM=true - ARG CLOUD_CONFIG="/tests/tests/assets/autoinstall.yaml" + ARG CLOUD_CONFIG="./tests/assets/autoinstall.yaml" ENV USE_QEMU=true ENV GOPATH="/go" RUN go install -mod=mod github.com/onsi/ginkgo/v2/ginkgo ENV CLOUD_CONFIG=$CLOUD_CONFIG - - IF [ "$FROM_ARTIFACTS" = "true" ] - COPY . . + COPY . . + RUN ls -liah + IF [ -e /test/build/kairos.iso ] ENV ISO=/test/build/kairos.iso - ENV DATASOURCE=/test/build/datasource.iso ELSE - COPY ./tests . COPY +iso/kairos.iso kairos.iso - COPY ( +datasource-iso/iso.iso --CLOUD_CONFIG=$CLOUD_CONFIG) datasource.iso ENV ISO=/test/kairos.iso + END + + IF [ ! -e /test/build/datasource.iso ] + COPY ( +datasource-iso/iso.iso --CLOUD_CONFIG=$CLOUD_CONFIG) datasource.iso ENV DATASOURCE=/test/datasource.iso + ELSE + ENV DATASOURCE=/test/build/datasource.iso END - ENV CLOUD_INIT=$CLOUD_CONFIG + ENV CLOUD_INIT=/tests/tests/$CLOUD_CONFIG RUN PATH=$PATH:$GOPATH/bin ginkgo --label-filter "$TEST_SUITE" --fail-fast -r ./tests/ -run-qemu-upgrade-test: +run-qemu-test: FROM opensuse/leap WORKDIR /test RUN zypper in -y qemu-x86 qemu-arm qemu-tools go ARG FLAVOR ARG TEST_SUITE=upgrade-with-cli - ARG FROM_ARTIFACTS ARG CONTAINER_IMAGE ENV CONTAINER_IMAGE=$CONTAINER_IMAGE ENV FLAVOR=$FLAVOR @@ -428,3 +452,55 @@ run-qemu-upgrade-test: ENV ISO=$ISO RUN PATH=$PATH:$GOPATH/bin ginkgo --label-filter "$TEST_SUITE" --fail-fast -r ./tests/ + +# bundles tests needs to run in sequence: +# +prepare-bundles-tests +# +run-bundles-tests +prepare-bundles-tests: + ARG OSBUILDER_IMAGE + FROM $OSBUILDER_IMAGE + RUN zypper in -y jq docker + COPY +uuidgen/UUIDGEN ./ + COPY +version/VERSION ./ + ARG UUIDGEN=$(cat UUIDGEN) + ARG BUNDLE_IMAGE=ttl.sh/$UUIDGEN:8h + # BUILD +examples-bundle --BUNDLE_IMAGE=$BUNDLE_IMAGE + ARG VERSION=$(cat VERSION) + RUN echo "version ${VERSION}" + WITH DOCKER --load $IMG=(+examples-bundle --BUNDLE_IMAGE=$BUNDLE_IMAGE --VERSION=$VERSION) + RUN docker push $BUNDLE_IMAGE + END + BUILD +examples-bundle-config --BUNDLE_IMAGE=$BUNDLE_IMAGE + +run-qemu-bundles-tests: + ARG FLAVOR + BUILD +run-qemu-datasource-tests --CLOUD_CONFIG=./bundles-config.yaml --TEST_SUITE="bundles-test" --FLAVOR=$FLAVOR + +### +### Examples +### +### ./earthly.sh +examples-bundle --BUNDLE_IMAGE=ttl.sh/testfoobar:8h +examples-bundle: + ARG BUNDLE_IMAGE + ARG VERSION + FROM DOCKERFILE --build-arg VERSION=$VERSION -f examples/bundle/Dockerfile . + SAVE IMAGE $BUNDLE_IMAGE + +## ./earthly.sh +examples-bundle-config --BUNDLE_IMAGE=ttl.sh/testfoobar:8h +## cat bundles-config.yaml +examples-bundle-config: + ARG BUNDLE_IMAGE + FROM alpine + COPY . . + RUN echo "" >> tests/assets/live-overlay.yaml + RUN echo "install:" >> tests/assets/live-overlay.yaml + RUN echo " auto: true" >> tests/assets/live-overlay.yaml + RUN echo " reboot: true" >> tests/assets/live-overlay.yaml + RUN echo " device: auto" >> tests/assets/live-overlay.yaml + RUN echo " grub_options:" >> tests/assets/live-overlay.yaml + RUN echo " extra_cmdline: foobarzz" >> tests/assets/live-overlay.yaml + RUN echo " bundles:" >> tests/assets/live-overlay.yaml + RUN echo " - rootfs_path: /usr/local/lib/extensions/kubo" >> tests/assets/live-overlay.yaml + RUN echo " targets:" >> tests/assets/live-overlay.yaml + RUN echo " - container://${BUNDLE_IMAGE}" >> tests/assets/live-overlay.yaml + SAVE ARTIFACT tests/assets/live-overlay.yaml AS LOCAL bundles-config.yaml diff --git a/examples/bundle/Dockerfile b/examples/bundle/Dockerfile new file mode 100644 index 000000000..c512f5dd5 --- /dev/null +++ b/examples/bundle/Dockerfile @@ -0,0 +1,17 @@ +FROM alpine as build +# Install a binary +ARG VERSION +ENV VERSION=$VERSION + +RUN wget https://github.com/ipfs/kubo/releases/download/v0.15.0/kubo_v0.15.0_linux-amd64.tar.gz -O kubo.tar.gz +RUN tar xvf kubo.tar.gz +RUN mv kubo/ipfs /usr/bin/ipfs +RUN mkdir -p /usr/lib/extension-release.d/ +RUN echo ID=kairos > /usr/lib/extension-release.d/extension-release.kubo +RUN echo VERSION_ID=$VERSION >> /usr/lib/extension-release.d/extension-release.kubo + + +FROM scratch + +COPY --from=build /usr/bin/ipfs /usr/bin/ipfs +COPY --from=build /usr/lib/extension-release.d /usr/lib/extension-release.d \ No newline at end of file diff --git a/internal/agent/agent.go b/internal/agent/agent.go index 8a61c52b8..7a1b573f8 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -63,14 +63,14 @@ func Run(opts ...Option) error { if !machine.SentinelExist("bundles") { opts := c.Bundles.Options() err := bundles.RunBundles(opts...) - if !c.IgnoreBundleErrors && err != nil { + if c.FailOnBundleErrors && err != nil { return err } // Re-load providers bus.Reload() err = machine.CreateSentinel("bundles") - if !c.IgnoreBundleErrors && err != nil { + if c.FailOnBundleErrors && err != nil { return err } } diff --git a/internal/agent/hooks/bundles.go b/internal/agent/hooks/bundles.go new file mode 100644 index 000000000..fe132a94f --- /dev/null +++ b/internal/agent/hooks/bundles.go @@ -0,0 +1,30 @@ +package hook + +import ( + config "github.com/kairos-io/kairos/pkg/config" + "github.com/kairos-io/kairos/pkg/machine" + "github.com/kairos-io/kairos/sdk/bundles" +) + +type BundleOption struct{} + +func (b BundleOption) Run(c config.Config) error { + + machine.Mount("COS_PERSISTENT", "/usr/local") //nolint:errcheck + defer func() { + machine.Umount("/usr/local") + }() + + machine.Mount("COS_OEM", "/oem") //nolint:errcheck + defer func() { + machine.Umount("/oem") + }() + + opts := c.Install.Bundles.Options() + err := bundles.RunBundles(opts...) + if c.FailOnBundleErrors && err != nil { + return err + } + + return nil +} diff --git a/internal/agent/hooks/gruboptions.go b/internal/agent/hooks/gruboptions.go index d3df2f080..6ee29bdea 100644 --- a/internal/agent/hooks/gruboptions.go +++ b/internal/agent/hooks/gruboptions.go @@ -2,29 +2,20 @@ package hook import ( "fmt" - "strings" config "github.com/kairos-io/kairos/pkg/config" + "github.com/kairos-io/kairos/pkg/machine" "github.com/kairos-io/kairos/pkg/utils" ) type GrubOptions struct{} func (b GrubOptions) Run(c config.Config) error { - oem, _ := utils.SH("blkid -L COS_OEM") - if oem == "" { - fmt.Println("OEM partition not found") - return nil // do not error out - } - - oem = strings.TrimSuffix(oem, "\n") - - oemMount, err := utils.SH(fmt.Sprintf("mkdir /tmp/oem && mount %s /tmp/oem", oem)) - if err != nil { - fmt.Printf("could not mount oem: %s\n", oemMount+err.Error()) - return nil // do not error out - } + machine.Mount("COS_OEM", "/tmp/oem") //nolint:errcheck + defer func() { + machine.Umount("/tmp/oem") + }() for k, v := range c.Install.GrubOptions { out, err := utils.SH(fmt.Sprintf("grub2-editenv /tmp/oem/grubenv set %s=%s", k, v)) if err != nil { @@ -33,6 +24,5 @@ func (b GrubOptions) Run(c config.Config) error { } } - utils.SH("umount /tmp/oem") //nolint:errcheck return nil } diff --git a/internal/agent/hooks/hook.go b/internal/agent/hooks/hook.go index 3be002aed..8baa4d136 100644 --- a/internal/agent/hooks/hook.go +++ b/internal/agent/hooks/hook.go @@ -11,7 +11,8 @@ type Interface interface { var All = []Interface{ &RunStage{}, // Shells out to stages defined from the container image &GrubOptions{}, // Set custom GRUB options - &Lifecycle{}, // Handles poweroff/reboot by config options + &BundleOption{}, + &Lifecycle{}, // Handles poweroff/reboot by config options } func Run(c config.Config, hooks ...Interface) error { diff --git a/pkg/config/config.go b/pkg/config/config.go index f79f5f435..ce9b01b8c 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -23,6 +23,7 @@ type Install struct { Device string `yaml:"device,omitempty"` Poweroff bool `yaml:"poweroff,omitempty"` GrubOptions map[string]string `yaml:"grub_options,omitempty"` + Bundles Bundles `yaml:"bundles,omitempty"` } type Config struct { @@ -33,7 +34,7 @@ type Config struct { header string ConfigURL string `yaml:"config_url,omitempty"` Options map[string]string `yaml:"options,omitempty"` - IgnoreBundleErrors bool `yaml:"ignore_bundles_errors,omitempty"` + FailOnBundleErrors bool `yaml:"fail_on_bundles_errors,omitempty"` Bundles Bundles `yaml:"bundles,omitempty"` } diff --git a/pkg/machine/file.go b/pkg/machine/file.go new file mode 100644 index 000000000..b966e2634 --- /dev/null +++ b/pkg/machine/file.go @@ -0,0 +1,9 @@ +package machine + +import "os" + +func Exists(path string) bool { + _, err := os.Stat(path) + + return !os.IsNotExist(err) +} diff --git a/pkg/machine/partitions.go b/pkg/machine/partitions.go new file mode 100644 index 000000000..b75d0ad7e --- /dev/null +++ b/pkg/machine/partitions.go @@ -0,0 +1,36 @@ +package machine + +import ( + "fmt" + "os" + "strings" + + "github.com/kairos-io/kairos/pkg/utils" +) + +func Umount(path string) { + utils.SH(fmt.Sprintf("umount %s", path)) //nolint:errcheck +} + +func Mount(label, mountpoint string) error { + part, _ := utils.SH(fmt.Sprintf("blkid -L %s", label)) + if part == "" { + fmt.Printf("%s partition not found\n", label) + return fmt.Errorf("partition not found") + } + + part = strings.TrimSuffix(part, "\n") + + if !Exists(mountpoint) { + err := os.MkdirAll(mountpoint, 0755) + if err != nil { + return err + } + } + mount, err := utils.SH(fmt.Sprintf("mount %s %s", part, mountpoint)) + if err != nil { + fmt.Printf("could not mount: %s\n", mount+err.Error()) + return err + } + return nil +} diff --git a/tests/assets/live-overlay.yaml b/tests/assets/live-overlay.yaml new file mode 100644 index 000000000..381b94eb3 --- /dev/null +++ b/tests/assets/live-overlay.yaml @@ -0,0 +1,9 @@ +#node-config + +stages: + initramfs: + - name: "Set user and password" + users: + kairos: + passwd: "kairos" + hostname: kairos-{{ trunc 4 .Random }} \ No newline at end of file diff --git a/tests/bundles_test.go b/tests/bundles_test.go new file mode 100644 index 000000000..15318c359 --- /dev/null +++ b/tests/bundles_test.go @@ -0,0 +1,119 @@ +package mos_test + +import ( + "fmt" + "os" + "path/filepath" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "github.com/spectrocloud/peg/matcher" +) + +var _ = Describe("kairos bundles test", Label("bundles-test"), func() { + BeforeEach(func() { + if os.Getenv("CLOUD_INIT") == "" || !filepath.IsAbs(os.Getenv("CLOUD_INIT")) { + Fail("CLOUD_INIT must be set and must be pointing to a file as an absolute path") + } + + EventuallyConnects() + }) + + AfterEach(func() { + if CurrentGinkgoTestDescription().Failed { + gatherLogs() + } + }) + + Context("live cd", func() { + It("has default service active", func() { + if os.Getenv("FLAVOR") == "alpine" { + out, _ := Sudo("rc-status") + Expect(out).Should(ContainSubstring("kairos")) + Expect(out).Should(ContainSubstring("kairos-agent")) + fmt.Println(out) + } else { + // Eventually(func() string { + // out, _ := machine.Command("sudo systemctl status kairososososos-agent") + // return out + // }, 30*time.Second, 10*time.Second).Should(ContainSubstring("no network token")) + + out, _ := Machine.Command("sudo systemctl status kairos") + Expect(out).Should(ContainSubstring("loaded (/etc/systemd/system/kairos.service; enabled;")) + fmt.Println(out) + } + + // Debug output + out, _ := Sudo("ls -liah /oem") + fmt.Println(out) + // Expect(out).To(ContainSubstring("userdata.yaml")) + out, _ = Sudo("cat /oem/userdata") + fmt.Println(out) + out, _ = Sudo("sudo ps aux") + fmt.Println(out) + + out, _ = Sudo("sudo lsblk") + fmt.Println(out) + + }) + }) + + Context("auto installs", func() { + It("to disk with custom config", func() { + Eventually(func() string { + out, _ := Sudo("ps aux") + return out + }, 30*time.Minute, 1*time.Second).Should( + Or( + ContainSubstring("elemental install"), + )) + }) + }) + + Context("reboots and passes functional tests", func() { + + It("has grubenv file", func() { + By("checking after-install hook triggered") + + Eventually(func() string { + out, _ := Sudo("sudo cat /oem/grubenv") + return out + }, 40*time.Minute, 1*time.Second).Should( + Or( + ContainSubstring("foobarzz"), + )) + }) + + It("has custom cmdline", func() { + By("waiting reboot and checking cmdline is present") + Eventually(func() string { + out, _ := Sudo("sudo cat /proc/cmdline") + return out + }, 30*time.Minute, 1*time.Second).Should( + Or( + ContainSubstring("foobarzz"), + )) + }) + + It("has kubo extension", func() { + // Eventually(func() string { + // out, _ := Sudo("systemd-sysext") + // return out + // }, 40*time.Minute, 1*time.Second).Should( + // Or( + // ContainSubstring("kubo"), + // )) + syset, err := Sudo("systemd-sysext") + ls, _ := Sudo("ls -liah /usr/local/lib/extensions") + fmt.Println("LS:", ls) + Expect(err).ToNot(HaveOccurred()) + Expect(syset).To(ContainSubstring("kubo")) + + ipfsV, err := Sudo("ipfs version") + Expect(err).ToNot(HaveOccurred()) + + Expect(ipfsV).To(ContainSubstring("0.15.0")) + }) + }) +})