├── .dockerignore ├── .gitignore ├── .tekton ├── appliance-pull-request.yaml └── appliance-push.yaml ├── Dockerfile.openshift-appliance ├── Dockerfile.openshift-appliance-build ├── Dockerfile.openshift-appliance.ds ├── LICENSE ├── Makefile ├── OWNERS ├── OWNERS_ALIASES ├── README.md ├── cmd ├── build.go ├── clean.go ├── config.go ├── debug.go └── main.go ├── data ├── scripts │ └── bin │ │ ├── add-grub-menuitem.sh.template │ │ ├── apply-operator-crs.sh.template │ │ ├── create-pinned-image-sets.sh.template │ │ ├── create-virtual-device.sh.template │ │ ├── deploy.sh.template │ │ ├── mount-agent-data.sh.template │ │ ├── pre-install-node-zero.sh.template │ │ ├── pre-install.sh.template │ │ ├── release-image-download.sh.template │ │ ├── release-image.sh.template │ │ ├── set-env-files.sh.template │ │ ├── set-node-zero.sh.template │ │ ├── setup-local-registry-upgrade.sh.template │ │ ├── setup-local-registry.sh.template │ │ ├── start-cluster-upgrade.sh.template │ │ ├── stop-local-registry.sh.template │ │ └── update-hosts.sh.template ├── services │ ├── bootstrap │ │ ├── ironic-agent.service │ │ ├── pre-install-node-zero.service │ │ ├── pre-install.service │ │ └── update-hosts.service │ ├── common │ │ └── start-local-registry.service │ ├── deploy │ │ └── deploy.service.template │ └── install │ │ ├── add-grub-menuitem.service │ │ ├── apply-operator-crs.service │ │ ├── create-pinned-image-sets.service │ │ ├── mount-live-iso@.service │ │ ├── set-node-zero.service │ │ ├── start-cluster-upgrade@.service │ │ ├── start-local-registry-upgrade@.service │ │ └── stop-local-registry.service └── udev │ └── rules.d │ ├── 99-live-iso.rules │ └── 99-upgrade-iso.rules ├── docs ├── appliance-config.md ├── images │ ├── deploy-iso.gif │ ├── grub.png │ ├── hl-overview.png │ └── upgrade-iso.gif ├── upgrade-tool.md └── user-guide.md ├── go.mod ├── go.sum ├── hack ├── diskimage │ ├── convert_to_qcow2.sh │ ├── embed_install_ignition.sh │ ├── extract_install_ignition.sh │ └── test_install_ignition.md └── publish-codecov.sh ├── pkg ├── asset │ ├── appliance │ │ ├── appliance_diskimage.go │ │ ├── appliance_liveiso.go │ │ └── base_diskimage.go │ ├── config │ │ ├── appliance_config.go │ │ ├── deploy_config.go │ │ └── env_config.go │ ├── data │ │ └── data_iso.go │ ├── deploy │ │ └── deploy_iso.go │ ├── ignition │ │ ├── bootstrap_ignition.go │ │ ├── deploy_ignition.go │ │ ├── install_ignition.go │ │ └── recovery_ignition.go │ ├── installer │ │ └── installer_binary.go │ ├── manifests │ │ ├── agentpullsecret.go │ │ ├── clusterimageset.go │ │ ├── infraenv.go │ │ ├── operator_crs.go │ │ └── unconfigured.go │ ├── recovery │ │ ├── base_iso.go │ │ └── recovery_iso.go │ ├── registry │ │ └── registriesconf.go │ └── upgrade │ │ └── upgrade_iso.go ├── consts │ └── consts.go ├── conversions │ └── conversions.go ├── coreos │ ├── coreos.go │ └── coreos_test.go ├── executer │ ├── executer.go │ └── mock_executer.go ├── fileutil │ └── fileutil.go ├── genisoimage │ ├── genisoimage.go │ └── genisoimage_test.go ├── graph │ ├── graph.go │ └── graph_test.go ├── ignition │ ├── ignition.go │ ├── ignition_test.go │ └── mock_ignition.go ├── installer │ ├── installer.go │ └── installer_test.go ├── log │ ├── log.go │ └── spinner.go ├── registry │ ├── registry.go │ └── registry_test.go ├── release │ ├── mock_release.go │ ├── release.go │ └── release_test.go ├── skopeo │ ├── skopeo.go │ └── skopeo_test.go ├── syslinux │ ├── syslinux.go │ └── syslinux_test.go ├── templates │ ├── data.go │ ├── partitions.go │ ├── partitions_test.go │ ├── scripts │ │ ├── grub │ │ │ └── user.cfg.template │ │ ├── guestfish │ │ │ └── guestfish.sh.template │ │ └── mirror │ │ │ ├── imageset.yaml.template │ │ │ └── pinned-image-set.yaml.template │ ├── templates.go │ └── templates_suite_test.go └── types │ └── appliance_config_type.go ├── registry ├── Dockerfile.registry ├── config.yml └── main.go ├── rpm-prefetching ├── README.md ├── rpms.in.yaml └── rpms.lock.yaml ├── skipper-mac.yaml └── skipper.yaml /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | build 3 | assets 4 | **/Dockerfile.openshift-appliance* 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /reports 3 | /.idea 4 | /build 5 | /assets 6 | 7 | .DS_Store -------------------------------------------------------------------------------- /Dockerfile.openshift-appliance: -------------------------------------------------------------------------------- 1 | # Build appliance 2 | FROM registry.access.redhat.com/ubi9/go-toolset:9.6-1747333074 AS builder 3 | COPY go.mod go.mod 4 | COPY go.sum go.sum 5 | RUN go mod download 6 | COPY . . 7 | RUN cd cmd && CGO_ENABLED=1 GOFLAGS="" GO111MODULE=on go build -o /tmp/openshift-appliance 8 | 9 | # Build registry 10 | RUN cd registry && CGO_ENABLED=1 GOFLAGS="" GO111MODULE=on go build -o /tmp/registry 11 | 12 | # Create final image 13 | FROM registry.access.redhat.com/ubi9/ubi:9.6-1747219013 14 | 15 | # Create/Mount assets 16 | ARG ASSETS_DIR=/assets 17 | RUN mkdir $ASSETS_DIR && chmod 775 $ASSETS_DIR 18 | VOLUME $ASSETS_DIR 19 | ENV ASSETS_DIR=$ASSETS_DIR 20 | 21 | # Install skopeo/podman/libguestfs 22 | RUN dnf -y install skopeo podman guestfs-tools genisoimage coreos-installer syslinux && dnf clean all 23 | 24 | # Config libguestfs 25 | ENV LIBGUESTFS_BACKEND=direct 26 | 27 | # Download oc 28 | RUN curl -sL -o /tmp/openshift-client-linux.tar.gz https://mirror.openshift.com/pub/openshift-v4/clients/ocp/4.18.5/openshift-client-linux.tar.gz || true 29 | RUN tar xvzf /tmp/openshift-client-linux.tar.gz -C /usr/local/bin && chmod +x /usr/local/bin/oc 30 | 31 | # Download oc-mirror 32 | RUN curl -sL -o /tmp/oc-mirror.tar.gz https://mirror.openshift.com/pub/openshift-v4/clients/ocp/4.18.5/oc-mirror.tar.gz || true 33 | RUN tar xvzf /tmp/oc-mirror.tar.gz -C /usr/local/bin && chmod +x /usr/local/bin/oc-mirror 34 | 35 | # Copy openshift-appliance binary 36 | COPY --from=builder /tmp/openshift-appliance /openshift-appliance 37 | 38 | # Copy registry files 39 | COPY --from=builder /tmp/registry /registry 40 | COPY /registry/config.yml /config.yml 41 | COPY /registry/Dockerfile.registry /Dockerfile.registry 42 | 43 | # Copy 44 | RUN mkdir -p data 45 | COPY /data data 46 | 47 | ENTRYPOINT ["/openshift-appliance", "--dir", "assets"] 48 | 49 | LABEL summary="OpenShift-based Appliance Builder" \ 50 | name="OpenShift-based Appliance Builder" \ 51 | description="A utility for building a disk image that orchestrates OpenShift installation using the Agent-based installer." \ 52 | io.k8s.description="A utility for building a disk image that orchestrates OpenShift installation using the Agent-based installer." \ 53 | io.k8s.display-name="OpenShift-based Appliance Builder" \ 54 | io.openshift.tags="openshift,appliance,installer,agent" \ 55 | com.redhat.component="openshift-appliance" 56 | 57 | -------------------------------------------------------------------------------- /Dockerfile.openshift-appliance-build: -------------------------------------------------------------------------------- 1 | FROM registry.access.redhat.com/ubi9/go-toolset:9.6-1747333074 AS golang 2 | 3 | ENV GOFLAGS="" 4 | 5 | RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.56.0 && \ 6 | go install golang.org/x/tools/cmd/goimports@v0.1.0 && \ 7 | go install github.com/onsi/ginkgo/ginkgo@v1.16.1 && \ 8 | go install github.com/golang/mock/mockgen@v1.6.0 && \ 9 | go install github.com/vektra/mockery/v2@v2.9.6 && \ 10 | go install gotest.tools/gotestsum@v1.6.3 && \ 11 | go install github.com/axw/gocov/gocov@v1.1.0 && \ 12 | go install github.com/AlekSi/gocov-xml@v1.1.0 13 | 14 | FROM quay.io/centos/centos:stream9 15 | 16 | RUN dnf install -y make git diffutils && dnf clean all 17 | 18 | ENV GOROOT=/usr/lib/golang 19 | ENV GOPATH=/opt/app-root/src/go 20 | ENV PATH=$PATH:$GOROOT/bin:$GOPATH/bin 21 | ENV LANG=en_US.UTF-8 22 | 23 | COPY --from=golang $GOPATH $GOPATH 24 | COPY --from=golang $GOROOT $GOROOT 25 | 26 | RUN chmod 775 -R $GOPATH && chmod 775 -R $GOROOT 27 | -------------------------------------------------------------------------------- /Dockerfile.openshift-appliance.ds: -------------------------------------------------------------------------------- 1 | # Build appliance 2 | FROM registry.access.redhat.com/ubi9/go-toolset:9.6-1747333074 AS builder 3 | COPY go.mod go.mod 4 | COPY go.sum go.sum 5 | RUN go mod download 6 | COPY . . 7 | RUN cd cmd && CGO_ENABLED=1 GOFLAGS="" GO111MODULE=on go build -o /tmp/openshift-appliance 8 | 9 | # Build registry 10 | RUN cd registry && CGO_ENABLED=1 GOFLAGS="" GO111MODULE=on go build -o /tmp/registry 11 | 12 | # Set 'oc' image 13 | FROM registry.redhat.io/openshift4/ose-cli AS oc 14 | 15 | # Set 'oc-mirror' image 16 | FROM registry.redhat.io/openshift4/oc-mirror-plugin-rhel9 AS oc-mirror 17 | 18 | # Create final image 19 | FROM registry.access.redhat.com/ubi9/ubi:9.6-1747219013 20 | 21 | # Create/Mount assets 22 | ARG ASSETS_DIR=/assets 23 | RUN mkdir $ASSETS_DIR && chmod 775 $ASSETS_DIR 24 | VOLUME $ASSETS_DIR 25 | ENV ASSETS_DIR=$ASSETS_DIR 26 | 27 | # Install skopeo/podman/libguestfs 28 | RUN dnf -y install skopeo podman guestfs-tools genisoimage coreos-installer syslinux && dnf clean all 29 | 30 | # Config libguestfs 31 | ENV LIBGUESTFS_BACKEND=direct 32 | 33 | # Copy oc binary 34 | COPY --from=oc /usr/bin/oc /usr/bin/oc 35 | 36 | # Copy oc-mirror binary 37 | COPY --from=oc-mirror /usr/bin/oc-mirror /usr/bin/oc-mirror 38 | 39 | # Copy openshift-appliance binary 40 | COPY --from=builder /tmp/openshift-appliance /openshift-appliance 41 | 42 | # Copy registry files 43 | COPY --from=builder /tmp/registry /registry 44 | COPY /registry/config.yml /config.yml 45 | COPY /registry/Dockerfile.registry /Dockerfile.registry 46 | 47 | # Copy 48 | RUN mkdir -p data 49 | COPY /data data 50 | 51 | ENTRYPOINT ["/openshift-appliance", "--dir", "assets"] 52 | 53 | LABEL summary="OpenShift-based Appliance Builder" \ 54 | name="OpenShift-based Appliance Builder" \ 55 | description="A utility for building a disk image that orchestrates OpenShift installation using the Agent-based installer." \ 56 | io.k8s.description="A utility for building a disk image that orchestrates OpenShift installation using the Agent-based installer." \ 57 | io.k8s.display-name="OpenShift-based Appliance Builder" \ 58 | io.openshift.tags="openshift,appliance,installer,agent" \ 59 | com.redhat.component="openshift-appliance" 60 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | IMAGE := $(or ${IMAGE}, quay.io/edge-infrastructure/openshift-appliance:latest) 2 | PWD = $(shell pwd) 3 | LOG_LEVEL := $(or ${LOG_LEVEL}, info) 4 | CMD := $(or ${CMD}, build) 5 | ASSETS := $(or ${ASSETS}, $(PWD)/assets) 6 | 7 | CI ?= false 8 | ROOT_DIR = $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 9 | REPORTS ?= $(ROOT_DIR)/reports 10 | COVER_PROFILE := $(or ${COVER_PROFILE},$(REPORTS)/unit_coverage.out) 11 | 12 | CI ?= false 13 | VERBOSE ?= false 14 | GO_TEST_FORMAT = pkgname 15 | 16 | GOTEST_FLAGS = --format=$(GO_TEST_FORMAT) $(GOTEST_PUBLISH_FLAGS) -- -count=1 -cover -coverprofile=$(REPORTS)/$(TEST_SCENARIO)_coverage.out 17 | GINKGO_FLAGS = -ginkgo.focus="$(FOCUS)" -ginkgo.v -ginkgo.skip="$(SKIP)" -ginkgo.v -ginkgo.junit-report=./junit_$(TEST_SCENARIO)_test.xml 18 | 19 | TIMEOUT = 30m 20 | GINKGO_REPORTFILE := $(or $(GINKGO_REPORTFILE), ./junit_unit_test.xml) 21 | GO_UNITTEST_FLAGS = --format=$(GO_TEST_FORMAT) $(GOTEST_PUBLISH_FLAGS) -- -count=1 -cover -coverprofile=$(COVER_PROFILE) 22 | GINKGO_UNITTEST_FLAGS = -ginkgo.focus="$(FOCUS)" -ginkgo.v -ginkgo.skip="$(SKIP)" -ginkgo.v -ginkgo.junit-report=$(GINKGO_REPORTFILE) 23 | 24 | 25 | .PHONY: build 26 | 27 | build: 28 | podman build -f Dockerfile.openshift-appliance . -t $(IMAGE) 29 | 30 | build-appliance: 31 | mkdir -p build 32 | cd ./cmd && CGO_ENABLED=1 GOFLAGS="" go build -o ../build/openshift-appliance 33 | 34 | build-openshift-ci-test-bin: 35 | ./hack/setup_env.sh 36 | 37 | lint: 38 | golangci-lint run -v --timeout=10m --concurrency=2 --fast 39 | 40 | test: $(REPORTS) 41 | go test -count=1 -cover -coverprofile=$(COVER_PROFILE) ./... 42 | $(MAKE) _coverage 43 | 44 | _coverage: 45 | ifeq ($(CI), true) 46 | COVER_PROFILE=$(COVER_PROFILE) ./hack/publish-codecov.sh 47 | endif 48 | 49 | test-short: 50 | go test -short ./... 51 | 52 | generate: 53 | go generate $(shell go list ./...) 54 | $(MAKE) format 55 | 56 | format: 57 | @goimports -w -l main.go internal pkg || /bin/true 58 | 59 | run: 60 | podman run --rm -it \ 61 | -v $(ASSETS):/assets:Z \ 62 | --privileged \ 63 | --net=host \ 64 | $(IMAGE) $(CMD) --log-level $(LOG_LEVEL) 65 | 66 | all: lint test build run 67 | 68 | $(REPORTS): 69 | -mkdir -p $(REPORTS) 70 | 71 | clean: 72 | -rm -rf $(REPORTS) 73 | 74 | generate-mocks: 75 | find . -name 'mock_*.go' -type f -not -path './vendor/*' -delete 76 | go generate -v $(shell go list ./...) 77 | 78 | unit-test: 79 | $(MAKE) _unit_test TIMEOUT=30m TEST="$(or $(TEST),$(shell go list ./...))" 80 | 81 | _unit_test: $(REPORTS) 82 | # TODO: Add code coverage reports 83 | gotestsum $(GO_UNITTEST_FLAGS) $(TEST) $(GINKGO_UNITTEST_FLAGS) -timeout $(TIMEOUT) 84 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | # See the OWNERS docs: https://git.k8s.io/community/contributors/guide/owners.md 2 | 3 | filters: 4 | .*: 5 | approvers: 6 | - approvers 7 | reviewers: 8 | - approvers 9 | -------------------------------------------------------------------------------- /OWNERS_ALIASES: -------------------------------------------------------------------------------- 1 | # See the OWNERS docs at https://go.k8s.io/owners 2 | 3 | aliases: 4 | approvers: 5 | - romfreiman 6 | - avishayt 7 | - gamli75 8 | - danielerez 9 | - jhernand 10 | - oourfali 11 | -------------------------------------------------------------------------------- /cmd/clean.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/openshift/appliance/pkg/asset/config" 8 | "github.com/openshift/appliance/pkg/consts" 9 | "github.com/openshift/appliance/pkg/log" 10 | "github.com/pkg/errors" 11 | "github.com/sirupsen/logrus" 12 | "github.com/spf13/cobra" 13 | 14 | assetstore "github.com/openshift/installer/pkg/asset/store" 15 | ) 16 | 17 | var ( 18 | cleanCache bool 19 | ) 20 | 21 | func NewCleanCmd() *cobra.Command { 22 | cmd := &cobra.Command{ 23 | Use: "clean", 24 | Short: "Clean assets directory (exclude builder cache)", 25 | Long: "", 26 | Run: func(_ *cobra.Command, _ []string) { 27 | cleanup := log.SetupFileHook(rootOpts.dir) 28 | defer cleanup() 29 | 30 | // Remove state file 31 | if err := deleteStateFile(rootOpts.dir); err != nil { 32 | logrus.Fatal(err) 33 | } 34 | 35 | // Remove temp dir 36 | if err := os.RemoveAll(filepath.Join(rootOpts.dir, config.TempDir)); err != nil { 37 | logrus.Fatal(err) 38 | } 39 | 40 | // Remove appliance files 41 | if err := os.RemoveAll(filepath.Join(rootOpts.dir, consts.ApplianceFileName)); err != nil { 42 | logrus.Fatal(err) 43 | } 44 | if err := os.RemoveAll(filepath.Join(rootOpts.dir, consts.ApplianceLiveIsoFileName)); err != nil { 45 | logrus.Fatal(err) 46 | } 47 | 48 | if cleanCache { 49 | // Remove cache dir 50 | if err := os.RemoveAll(filepath.Join(rootOpts.dir, config.CacheDir)); err != nil { 51 | logrus.Fatal(err) 52 | } 53 | } 54 | 55 | logrus.Infof("Cleanup complete") 56 | }, 57 | } 58 | cmd.Flags().BoolVar(&cleanCache, "cache", false, "Clean also the builder cache directory") 59 | return cmd 60 | } 61 | 62 | func deleteStateFile(directory string) error { 63 | store, err := assetstore.NewStore(directory) 64 | if err != nil { 65 | return errors.Wrap(err, "failed to create asset store") 66 | } 67 | 68 | err = store.DestroyState() 69 | if err != nil { 70 | return errors.Wrap(err, "failed to remove state file") 71 | } 72 | 73 | return nil 74 | } 75 | -------------------------------------------------------------------------------- /cmd/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/openshift/appliance/pkg/asset/config" 8 | "github.com/sirupsen/logrus" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func NewGenerateConfigCmd() *cobra.Command { 13 | cmd := &cobra.Command{ 14 | Use: "generate-config", 15 | Short: "Generate a template of the appliance config manifest", 16 | Args: cobra.ExactArgs(0), 17 | PreRun: func(cmd *cobra.Command, args []string) { 18 | configFilePath := filepath.Join(rootOpts.dir, config.ApplianceConfigFilename) 19 | _, err := os.Stat(configFilePath) 20 | if !os.IsNotExist(err) { 21 | logrus.Fatal("Config file already exists at assets directory") 22 | } 23 | }, 24 | Run: func(cmd *cobra.Command, args []string) { 25 | configAppliance := config.ApplianceConfig{} 26 | if err := getAssetStore().Fetch(cmd.Context(), &configAppliance); err != nil { 27 | logrus.Fatal(err) 28 | } 29 | if err := configAppliance.PersistToFile(rootOpts.dir); err != nil { 30 | logrus.Fatal(err) 31 | } 32 | logrus.Infof("Generated config file in assets directory: %s", config.ApplianceConfigFilename) 33 | }, 34 | PostRun: func(cmd *cobra.Command, args []string) { 35 | if err := deleteStateFile(rootOpts.dir); err != nil { 36 | logrus.Fatal(err) 37 | } 38 | }, 39 | } 40 | 41 | return cmd 42 | } 43 | -------------------------------------------------------------------------------- /cmd/debug.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/openshift/appliance/pkg/asset/config" 5 | "github.com/openshift/appliance/pkg/asset/ignition" 6 | "github.com/sirupsen/logrus" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func NewGenerateInstallIgnitionCmd() *cobra.Command { 11 | cmd := &cobra.Command{ 12 | Use: "generate-install-ignition", 13 | Args: cobra.ExactArgs(0), 14 | Hidden: true, 15 | PreRun: func(cmd *cobra.Command, args []string) { 16 | if err := getAssetStore().Fetch(cmd.Context(), &config.EnvConfig{ 17 | AssetsDir: rootOpts.dir, 18 | }); err != nil { 19 | logrus.Fatal(err) 20 | } 21 | }, 22 | Run: func(cmd *cobra.Command, args []string) { 23 | installIgnition := ignition.InstallIgnition{} 24 | if err := getAssetStore().Fetch(cmd.Context(), &installIgnition); err != nil { 25 | logrus.Fatal(err) 26 | } 27 | if err := installIgnition.PersistToFile(rootOpts.dir); err != nil { 28 | logrus.Fatal(err) 29 | } 30 | logrus.Infof("Generated ignition file at assets directory: %s", ignition.InstallIgnitionPath) 31 | }, 32 | PostRun: func(cmd *cobra.Command, args []string) { 33 | if err := deleteStateFile(rootOpts.dir); err != nil { 34 | logrus.Fatal(err) 35 | } 36 | }, 37 | } 38 | 39 | return cmd 40 | } 41 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/openshift/appliance/pkg/log" 8 | "github.com/sirupsen/logrus" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var ( 13 | rootOpts struct { 14 | dir string 15 | logLevel string 16 | } 17 | ) 18 | 19 | func main() { 20 | applianceMain() 21 | } 22 | 23 | func applianceMain() { 24 | rootCmd := newRootCmd() 25 | 26 | for _, subCmd := range []*cobra.Command{ 27 | NewBuildCmd(), 28 | NewCleanCmd(), 29 | NewGenerateConfigCmd(), 30 | 31 | // Hidden commands for debug 32 | NewGenerateInstallIgnitionCmd(), 33 | } { 34 | rootCmd.AddCommand(subCmd) 35 | } 36 | 37 | if err := rootCmd.Execute(); err != nil { 38 | logrus.Fatalf("Error executing openshift-appliance: %v", err) 39 | } 40 | } 41 | 42 | func newRootCmd() *cobra.Command { 43 | cmd := &cobra.Command{ 44 | Use: filepath.Base(os.Args[0]), 45 | Short: "Builds an OpenShift-based appliance", 46 | Long: "", 47 | PersistentPreRun: runRootCmd, 48 | SilenceErrors: true, 49 | SilenceUsage: true, 50 | } 51 | cmd.PersistentFlags().StringVar(&rootOpts.dir, "dir", ".", "assets directory") 52 | cmd.PersistentFlags().StringVar(&rootOpts.logLevel, "log-level", "info", "log level (e.g. \"debug | info | warn | error\")") 53 | return cmd 54 | } 55 | 56 | func runRootCmd(cmd *cobra.Command, args []string) { 57 | log.SetupOutputHook(rootOpts.logLevel) 58 | } 59 | -------------------------------------------------------------------------------- /data/scripts/bin/add-grub-menuitem.sh.template: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mount -o remount,rw /dev/disk/by-partlabel/boot /boot 4 | 5 | # Append the content of user.cfg to grub.cfg in order to prevent duplicate menu entries. 6 | # For details see: 7 | # * https://github.com/coreos/fedora-coreos-tracker/issues/805 8 | # * https://github.com/coreos/fedora-coreos-config/blob/5c1ac4e7d4a596efac69a3eb78061dc2f59e94fb/overlay.d/40grub/usr/lib/bootupd/grub2-static/configs.d/70_coreos-user.cfg 9 | cat {{.UserCfgFilePath}} >> {{.GrubCfgFilePath}} 10 | rm -rf {{.UserCfgFilePath}} 11 | -------------------------------------------------------------------------------- /data/scripts/bin/apply-operator-crs.sh.template: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export KUBECONFIG="/etc/kubernetes/static-pod-resources/kube-apiserver-certs/secrets/node-kubeconfigs/localhost.kubeconfig" 4 | 5 | ensure_csvs_succeeded() { 6 | local namespace="$1" 7 | 8 | # Get all CSV names in the specified namespace 9 | local csv_names=$(oc get csv -n "$namespace" -o jsonpath='{.items[*].metadata.name}') 10 | if [ -z "$csv_names" ]; then 11 | echo "No CSVs found yet in namespace '$namespace'." 12 | return 1 13 | fi 14 | 15 | # Iterate through each CSV name 16 | IFS=' ' read -r -a csv_array <<< "$csv_names" 17 | for csv in "${csv_array[@]}"; do 18 | # Get the status of the current CSV 19 | local status=$(oc get csv -n "$namespace" "$csv" -o jsonpath='{.status.phase}') 20 | if [ "$status" != "Succeeded" ]; then 21 | return 1 22 | fi 23 | done 24 | 25 | return 0 26 | } 27 | 28 | # Loop over the CRs 29 | crsDir="/etc/assisted/extra-manifests/post-installation" 30 | for file in "$crsDir"/*.yaml "$crsDir"/*.yml; do 31 | echo "Processing YAML file: $file" 32 | 33 | # Get the namespace from the YAML file 34 | namespace=$(cat "$file" | grep -oP "namespace: *['\"]*\K[^'\"]+") 35 | if [ -z "$namespace" ]; then 36 | continue 37 | fi 38 | 39 | # Wait for the CSVs to succeed 40 | until ensure_csvs_succeeded "$namespace" &>/dev/null; do 41 | echo "Waiting for CSVs to succeed in namespace '$namespace'..." 42 | sleep 60 43 | done 44 | 45 | # Check if the CR is already available 46 | if oc get -f "$file" &>/dev/null; then 47 | continue 48 | fi 49 | 50 | # Apply the CR 51 | until oc apply -f "$file" &>/dev/null; do 52 | echo "Retrying to apply CR: '$file'..." 53 | sleep 60 54 | done 55 | done 56 | -------------------------------------------------------------------------------- /data/scripts/bin/create-pinned-image-sets.sh.template: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export KUBECONFIG="/etc/kubernetes/static-pod-resources/kube-apiserver-certs/secrets/node-kubeconfigs/localhost.kubeconfig" 4 | 5 | # Wait for cluster to be ready 6 | until [ "$(oc get clusterversion -o jsonpath='{.items[*].status.conditions[?(@.type=="Available")].status}')" == "True" ]; 7 | do 8 | echo "Waiting for the cluster to be ready..." 9 | sleep 60 10 | done 11 | 12 | # If the PinnedImageSet crd doesn't exist, patch the cluster FeatureGate to be tech preview 13 | if [ "$(oc get crd/pinnedimagesets.machineconfiguration.openshift.io |& grep -iE "(no resources found|not found)")" ]; 14 | then 15 | oc patch featuregate cluster --type='merge' -p '{"spec": {"featureSet": "TechPreviewNoUpgrade"}}' 16 | fi 17 | 18 | # Wait for crd pinned image sets to exist 19 | until [ "$(oc get crd/pinnedimagesets.machineconfiguration.openshift.io |& grep -ivE "(no resources found|not found)")" ]; 20 | do 21 | echo "Waiting for crd PinnedImageSet to be created..." 22 | sleep 60 23 | done 24 | 25 | echo "Waiting for crd PinnedImageSet to be enabled..." 26 | oc wait --for=condition=Established crd/pinnedimagesets.machineconfiguration.openshift.io 27 | 28 | # Create the pinned image sets 29 | oc apply -f /etc/assisted/master-pinned-image-set.yaml 30 | oc apply -f /etc/assisted/worker-pinned-image-set.yaml 31 | 32 | # Wait for pinned images file to be created (or until the node reboots) 33 | until [ -f /etc/crio/crio.conf.d/50-pinned-images ]; 34 | do 35 | echo "Waiting for the PinnedImageSet to be configured" 36 | sleep 60 37 | done 38 | -------------------------------------------------------------------------------- /data/scripts/bin/create-virtual-device.sh.template: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "{{.IsLiveISO}}" = "true" ]; then 4 | # A virtual device is not needed in Live ISO mode 5 | exit 0 6 | fi 7 | 8 | # Create a sparse-raw file for the boot sector 9 | dd if=/dev/zero of=/tmp/boot.raw bs=1M seek=1 count=0 10 | 11 | # Configure a loop device for boot.raw 12 | losetup /dev/loop2 /tmp/boot.raw 13 | 14 | # Create a virtual device for passing to coreos-installer 15 | dmsetup create agent <<. 16 | 0 2048 linear /dev/loop2 0 17 | {{.Partition0.StartSector}} {{.Partition0.Size}} linear /dev/disk/by-partlabel/BIOS-BOOT 0 18 | {{.Partition1.StartSector}} {{.Partition1.Size}} linear /dev/disk/by-partlabel/EFI-SYSTEM 0 19 | {{.Partition2.StartSector}} {{.Partition2.Size}} linear /dev/disk/by-partlabel/boot 0 20 | {{.Partition3.StartSector}} {{.Partition3.Size}} linear /dev/disk/by-partlabel/root 0 21 | . 22 | -------------------------------------------------------------------------------- /data/scripts/bin/deploy.sh.template: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o pipefail 4 | 5 | prepare_issue=/etc/issue.d/40_prepare.issue 6 | start_issue=/etc/issue.d/50_start.issue 7 | clone_issue=/etc/issue.d/60_clone.issue 8 | done_issue=/etc/issue.d/70_done.issue 9 | post_issue=/etc/issue.d/80_post.issue 10 | output_issue=/etc/issue.d/90_output.issue 11 | 12 | # Reload agetty in background to monitor progress 13 | ( 14 | while true; do 15 | agetty --reload 16 | sleep 1 17 | done 18 | ) & 19 | 20 | printf '\\e{yellow}Preparing to start appliance disk image cloning...\\e{reset}\n' | tee $prepare_issue 21 | 22 | # Load appliance image 23 | podman load -q -i /run/media/iso/deploy/{{.ApplianceImageTar}} 24 | 25 | # Create a loop device for each appliance part 26 | APPLIANCE_FILES="/run/media/iso/deploy/{{.ApplianceFileName}}*" 27 | loop_sizes=() 28 | for f in $APPLIANCE_FILES 29 | do 30 | device=$(losetup --find) 31 | losetup $device $f 32 | loop_sizes+=($device) 33 | done 34 | 35 | # Create a device map using the loop devices 36 | ( 37 | start=0 38 | for device in "${loop_sizes[@]}" 39 | do 40 | size=`blockdev --getsz $device` 41 | echo "$start $size linear $device 0" 42 | ((start+=$size)) 43 | done 44 | ) | dmsetup create appliance 45 | 46 | rm -rf $prepare_issue 47 | printf '\\e{cyan}Cloning appliance disk image to {{.TargetDevice}}...\\e{reset}\n' | tee $start_issue 48 | 49 | # Run virt-resize 50 | sparse="--no-sparse" 51 | if [ "{{.SparseClone}}" = "true" ]; then 52 | sparse="" 53 | fi 54 | podman run --rm -t --privileged --entrypoint virt-resize {{.ApplianceImageName}} --expand /dev/sda4 /dev/dm-0 {{.TargetDevice}} $sparse 2>&1 | tee $clone_issue 55 | 56 | # Handle clone failure/success 57 | if [ "$?" -eq 0 ]; then 58 | printf '\\e{lightgreen}\nAppliance disk image cloning is done!\\e{reset}\n' | tee $done_issue 59 | 60 | # Run post script 61 | if [ "{{.PostScript}}" != "" ]; then 62 | printf '\\e{lightblue}\nExecuting post deployment script: {{.PostScript}}\\e{reset}\n' | tee $post_issue 63 | "/usr/local/bin/{{.PostScript}}" | tee $output_issue 64 | fi 65 | else 66 | printf '\\e{red}\nAppliance disk image cloning failed.\\e{reset}\n' | tee $done_issue 67 | fi 68 | 69 | agetty --reload 70 | -------------------------------------------------------------------------------- /data/scripts/bin/mount-agent-data.sh.template: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ISO_DIR=/run/media/iso 4 | MNT_DIR=/mnt/agentdata 5 | 6 | create_data_device() { 7 | # Create a loop device for each data file part 8 | DATA_FILES="$ISO_DIR/data/data*" 9 | loop_sizes=() 10 | for f in $DATA_FILES 11 | do 12 | device=$(losetup --find) 13 | losetup $device $f 14 | loop_sizes+=($device) 15 | done 16 | 17 | # Create a device map using the loop devices 18 | ( 19 | start=0 20 | for device in "${loop_sizes[@]}" 21 | do 22 | size=`blockdev --getsz $device` 23 | echo "$start $size linear $device 0" 24 | ((start+=$size)) 25 | done 26 | ) | dmsetup create data 27 | } 28 | 29 | mkdir -p $MNT_DIR 30 | 31 | if [ "{{.IsLiveISO}}" = "true" ]; then 32 | # Wait for mount 33 | while ! mountpoint -q $ISO_DIR; do 34 | echo "Waiting for $ISO_DIR to be fully mounted..." 35 | sleep 5 36 | done 37 | 38 | if [ "{{.IsBootstrapStep}}" = "true" ]; then 39 | # Create virtual device for the registry data 40 | create_data_device 41 | 42 | # Mount data iso 43 | mount -o ro /dev/dm-0 $MNT_DIR 44 | else 45 | registry_data_iso=/home/core/registry_data.iso 46 | if [ ! -f "$registry_data_iso" ]; then 47 | cat $ISO_DIR/data/data* > $registry_data_iso 48 | fi 49 | mount -o ro $registry_data_iso $MNT_DIR 50 | fi 51 | else # Disk image mode 52 | # Mount agentdata partition 53 | mount -o ro /dev/disk/by-partlabel/agentdata $MNT_DIR 54 | fi 55 | -------------------------------------------------------------------------------- /data/scripts/bin/pre-install-node-zero.sh.template: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source "issue_status.sh" 4 | 5 | issue="70_agent-services" 6 | printf '\\e{yellow}Preparing to start installation\\e{reset}' | set_issue "${issue}" 7 | 8 | # Set assisted-service.env/images.env files 9 | /usr/local/bin/set-env-files.sh 10 | -------------------------------------------------------------------------------- /data/scripts/bin/pre-install.sh.template: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Create a virtual device for coreos-installer 4 | /usr/local/bin/create-virtual-device.sh 5 | -------------------------------------------------------------------------------- /data/scripts/bin/release-image-download.sh.template: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Overrides https://github.com/openshift/installer/blob/master/data/data/bootstrap/files/usr/local/bin/release-image-download.sh.template 4 | # No need to download the release image as already included in appliance -------------------------------------------------------------------------------- /data/scripts/bin/release-image.sh.template: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | image_for() { 4 | podman run --quiet --rm --net=none "{{.ReleaseImage}}" image "${1}" 5 | } 6 | -------------------------------------------------------------------------------- /data/scripts/bin/set-env-files.sh.template: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | assistedServiceEnvFile=/usr/local/share/assisted-service/assisted-service.env 4 | imagesEnvFile=/usr/local/share/assisted-service/images.env 5 | 6 | # Add registry domain to assisted-service.env 7 | sed -i 's/PUBLIC_CONTAINER_REGISTRIES=.*/&,{{.RegistryDomain}}:5000/g' $assistedServiceEnvFile 8 | 9 | # Set RELEASE_IMAGES in assisted-service.env 10 | sed -i '/^RELEASE_IMAGES/s|=.*$|={{.ReleaseImages}}|' $assistedServiceEnvFile 11 | 12 | # Set OPENSHIFT_INSTALL_RELEASE_IMAGE_MIRROR in assisted-service.env 13 | sed -i '/^OPENSHIFT_INSTALL_RELEASE_IMAGE_MIRROR/s|=.*$|={{.ReleaseImage}}|' $assistedServiceEnvFile 14 | 15 | # Set OS_IMAGES in images.env 16 | sed -i '/^OS_IMAGES/s|=.*$|={{.OsImages}}|' $imagesEnvFile 17 | 18 | # Replace cluster-image-set file (generated in bootstrap_ignition) 19 | mv -f /etc/assisted/cluster-image-set.yaml /etc/assisted/manifests 20 | -------------------------------------------------------------------------------- /data/scripts/bin/set-node-zero.sh.template: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rendezvous_host_env="/etc/assisted/rendezvous-host.env" 4 | source "${rendezvous_host_env}" 5 | echo "NODE_ZERO_IP: $NODE_ZERO_IP" 6 | 7 | is_node_zero() { 8 | local is_rendezvous_host 9 | is_rendezvous_host=$(ip -j address | jq "[.[].addr_info] | flatten | map(.local==\"$NODE_ZERO_IP\") | any") 10 | if [[ "${is_rendezvous_host}" == "true" ]]; then 11 | echo 1 12 | else 13 | echo 0 14 | fi 15 | } 16 | 17 | if [[ $(is_node_zero) -eq 1 ]]; then 18 | echo "Node 0 IP ${NODE_ZERO_IP} found on this host" 1>&2 19 | 20 | NODE0_PATH=/etc/assisted/node0 21 | mkdir -p "$(dirname "${NODE0_PATH}")" 22 | 23 | NODE_ZERO_MAC=$(ip -j address | jq -r ".[] | select(.addr_info | map(select(.local == \"$NODE_ZERO_IP\")) | any).address") 24 | echo "MAC Address for Node 0: ${NODE_ZERO_MAC}" 25 | 26 | cat >"${NODE0_PATH}" <> /etc/hosts 27 | -------------------------------------------------------------------------------- /data/scripts/bin/start-cluster-upgrade.sh.template: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export KUBECONFIG="/etc/kubernetes/static-pod-resources/kube-apiserver-certs/secrets/node-kubeconfigs/localhost.kubeconfig" 4 | 5 | release_version=$1 6 | release_image_file="/media/upgrade/release.env" 7 | upgrade_release_file="/etc/assisted/upgrade_release_$release_version.env" 8 | 9 | # Check if the upgrade has already been performed 10 | if [ -f ${upgrade_release_file} ]; then 11 | echo "Upgrade to $release_version has already been performed" 12 | exit 0 13 | fi 14 | 15 | # Wait for cluster to be ready 16 | until [ "$(oc get clusterversion -o jsonpath='{.items[*].status.conditions[?(@.type=="Available")].status}')" == "True" ]; 17 | do 18 | echo "Waiting for the cluster to be ready..." 19 | sleep 30 20 | done 21 | 22 | # Wait for the upgrade ISO to be mounted 23 | until [ -f ${release_image_file} ]; 24 | do 25 | echo "Waiting for the upgrade ISO to be mounted..." 26 | sleep 10 27 | done 28 | 29 | # Get all MachineHealthCheck resources in the specified namespace 30 | mhc_list=$(oc get machinehealthchecks -n openshift-machine-api -o custom-columns=":metadata.name") 31 | 32 | # Loop through each MachineHealthCheck and pause it 33 | for mhc in $mhc_list; do 34 | oc annotate machinehealthcheck "$mhc" -n openshift-machine-api "cluster.x-k8s.io/paused=true" --overwrite 35 | done 36 | 37 | # Upgrade the cluster 38 | source $release_image_file 39 | oc adm upgrade --allow-explicit-upgrade --allow-upgrade-with-warnings --to-image $RELEASE_IMAGE 40 | 41 | # Force upgrade (override any blocking conditions to proceed if there are non-critical issues) 42 | oc patch clusterversion version --type=merge -p '{"spec": {"desiredUpdate": {"force": true, "image": "'$RELEASE_IMAGE'"}}}' 43 | 44 | # Wait for the cluster to get upgraded 45 | until \ 46 | [[ "$(oc get clusterversion version -o=jsonpath='{.status.history[0].version}')" == $release_version ]] && \ 47 | [[ "$(oc get clusterversion version -o=jsonpath='{.status.history[0].state}')" == "Completed" ]]; \ 48 | do 49 | echo "Waiting for the cluster to get upgraded..." 50 | sleep 120 51 | done 52 | 53 | # Unpause all MachineHealthChecks 54 | # Loop through each MachineHealthCheck and unpause it 55 | for mhc in $mhc_list; do 56 | oc annotate machinehealthcheck "$mhc" -n openshift-machine-api "cluster.x-k8s.io/paused-" --overwrite 57 | done 58 | 59 | # Copy release image file (as an indication that the upgrade has been performed) 60 | cp $release_image_file /etc/assisted/$upgrade_release_file 61 | -------------------------------------------------------------------------------- /data/scripts/bin/stop-local-registry.sh.template: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export KUBECONFIG="/etc/kubernetes/static-pod-resources/kube-apiserver-certs/secrets/node-kubeconfigs/localhost.kubeconfig" 4 | 5 | # Wait for cluster to be ready 6 | until [ "$(sudo -E oc get clusterversion -o jsonpath='{.items[*].status.conditions[?(@.type=="Available")].status}')" == "True" ]; 7 | do 8 | echo "Waiting for the cluster to be ready..." 9 | sleep 60 10 | done 11 | 12 | # Stop local registry 13 | echo "Stopping the local registry..." 14 | sudo systemctl stop start-local-registry.service 15 | 16 | # Remove selector label set in 99-(master|worker)-generated-registries 17 | # This will delete mirror configuration in /etc/containers/registries.conf 18 | for role in master worker 19 | do 20 | sudo -E oc label mc "99-${role}-generated-registries" machineconfiguration.openshift.io/role- 21 | # Pause and resume the machineconfigpool to force a reconciliation 22 | sudo -E oc patch --type=merge --patch='{"spec":{"paused":true}}' "machineconfigpool/${role}" 23 | sudo -E oc patch --type=merge --patch='{"spec":{"paused":false}}' "machineconfigpool/${role}" 24 | done 25 | -------------------------------------------------------------------------------- /data/scripts/bin/update-hosts.sh.template: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | curl_assisted_service() { 5 | local endpoint=$1 6 | local method=${2:-GET} 7 | local additional_options=("${@:3}") # Capture all arguments starting from the third one 8 | local baseURL="${SERVICE_BASE_URL}api/assisted-install/v2" 9 | 10 | if [[ -n ${USER_AUTH_TOKEN} ]];then 11 | local token=${USER_AUTH_TOKEN} 12 | else 13 | local token=${AGENT_AUTH_TOKEN} 14 | fi 15 | 16 | headers=( 17 | -s -S 18 | -H "Authorization: ${token}" 19 | -H "accept: application/json" 20 | ) 21 | 22 | [[ "$method" == "POST" || "$method" == "PATCH" ]] && headers+=(-H "Content-Type: application/json") 23 | 24 | curl "${headers[@]}" -X "${method}" "${additional_options[@]}" "${baseURL}${endpoint}" 25 | } 26 | 27 | declare -A hosts 28 | declare cluster_id 29 | declare cluster_status 30 | 31 | # Set install ignition config 32 | ignition=$(echo '{{.InstallIgnitionConfig}}' | jq -c --raw-input) 33 | 34 | # Set rendezvous-host.env file in the ignition 35 | host_env_base64=$(base64 -w 0 /etc/assisted/rendezvous-host.env) 36 | placeholder_base64=$(echo -n '{{.RendezvousHostEnvPlaceholder}}' | base64) 37 | ignition="${ignition//$placeholder_base64/$host_env_base64}" 38 | 39 | # Waiting for the cluster-id to be available 40 | until [[ -n ${cluster_id} ]]; do 41 | echo "Querying assisted-service for cluster-id..." 42 | cluster_id=$(curl_assisted_service "/clusters" GET | jq -r '.[].id') 43 | sleep 1 44 | done 45 | echo "Fetched cluster-id: $cluster_id" 46 | 47 | # Register extra manifests, if present. Required only for the interactive workflow. 48 | if [ "{{.EnableInteractiveFlow}}" = "true" ]; then 49 | extraManifestsDir="/etc/assisted/extra-manifests" 50 | for file in "$extraManifestsDir"/*.yaml "$extraManifestsDir"/*.yml; do 51 | [ -e "$file" ] || continue 52 | 53 | filename=$(basename "${file}") 54 | encoded_content=$(base64 -w 0 "${file}") 55 | echo "Registering extra manifest ${file}" 56 | curl_assisted_service "/clusters/${cluster_id}/manifests" \ 57 | POST -d "{\"folder\": \"openshift\", \"file_name\": \"${filename}\", \"content\": \"${encoded_content}\"}" 58 | done 59 | fi 60 | 61 | # Updating hosts can be done before starting the installation 62 | until [[ $cluster_status == "preparing-for-installation" ]]; do 63 | cluster_status=$(curl_assisted_service "/clusters/${cluster_id}" | jq -r .status) 64 | 65 | # Update ignition for each host 66 | host_ids=$(curl_assisted_service "/infra-envs/${INFRA_ENV_ID}/hosts" | jq -r .[].id) 67 | if [[ -z ${host_ids} ]]; then 68 | sleep 2 69 | continue 70 | fi 71 | 72 | for id in ${host_ids}; do 73 | if [[ ${hosts[$id]} == "true" ]]; then 74 | # Host is already updated 75 | continue 76 | fi 77 | 78 | # Update host's ignition (used when booting from the installation disk after bootstrap) 79 | curl_assisted_service "/infra-envs/${INFRA_ENV_ID}/hosts/${id}/ignition" \ 80 | PATCH -d '{"config": '"${ignition}"'}' 81 | hosts[$id]=true 82 | echo "Updated ignition of host: ${id}" 83 | done 84 | 85 | sleep 1 86 | done 87 | 88 | echo "Done updating hosts" 89 | -------------------------------------------------------------------------------- /data/services/bootstrap/ironic-agent.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Stub service for debug to avoid reboot after coreos installation 3 | Before=pre-install.service 4 | 5 | [Service] 6 | ExecStart=echo 7 | KillMode=none 8 | Type=oneshot 9 | RemainAfterExit=true 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /data/services/bootstrap/pre-install-node-zero.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Agent-based installer preparation service 3 | Wants=network.target node-zero.service 4 | After=network-online.target node-zero.service 5 | Before=assisted-service.service 6 | ConditionPathExists=/etc/assisted/node0 7 | 8 | [Service] 9 | ExecStart=/usr/local/bin/pre-install-node-zero.sh 10 | Type=oneshot 11 | RemainAfterExit=no 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /data/services/bootstrap/pre-install.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Agent-based installer preparation service 3 | Wants=network.target 4 | After=network-online.target 5 | 6 | [Service] 7 | ExecStart=/usr/local/bin/pre-install.sh 8 | Type=oneshot 9 | RemainAfterExit=no 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /data/services/bootstrap/update-hosts.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Service that updates ignition on all hosts 3 | Wants=network-online.target 4 | Requires=apply-host-config.service 5 | After=network-online.target apply-host-config.service 6 | ConditionPathExists=/etc/assisted/node0 7 | 8 | [Service] 9 | EnvironmentFile=/usr/local/share/assisted-service/assisted-service.env 10 | EnvironmentFile=/usr/local/share/start-cluster/start-cluster.env 11 | EnvironmentFile=/etc/assisted/rendezvous-host.env 12 | ExecStart=/usr/local/bin/update-hosts.sh 13 | 14 | KillMode=none 15 | Type=oneshot 16 | RemainAfterExit=true 17 | 18 | [Install] 19 | WantedBy=multi-user.target 20 | -------------------------------------------------------------------------------- /data/services/common/start-local-registry.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Local Registry 3 | Wants=network.target 4 | 5 | [Service] 6 | Environment=PODMAN_SYSTEMD_UNIT=%n 7 | EnvironmentFile=/etc/assisted/registry.env 8 | ExecStartPre=/bin/rm -f %t/%n.ctr-id 9 | ExecStartPre=/usr/local/bin/setup-local-registry.sh 10 | ExecStart=podman run --net host --cidfile=%t/%n.ctr-id --privileged --replace --log-driver=journald --name=registry -p 5000:5000 -p 5443:5000 -v ${REGISTRY_DATA}:/var/lib/registry -v /tmp/certs:/certs -e REGISTRY_HTTP_ADDR=0.0.0.0:5000 -e REGISTRY_HTTP_TLS_CERTIFICATE=certs/domain.crt -e REGISTRY_HTTP_TLS_KEY=certs/domain.key $REGISTRY_IMAGE 11 | ExecStop=/usr/bin/podman stop --ignore --cidfile=%t/%n.ctr-id 12 | ExecStopPost=/usr/bin/podman rm -f --ignore --cidfile=%t/%n.ctr-id 13 | 14 | Restart=on-failure 15 | RestartSec=10 16 | TimeoutStartSec=9000 17 | TimeoutStopSec=300 18 | 19 | [Install] 20 | WantedBy=multi-user.target 21 | -------------------------------------------------------------------------------- /data/services/deploy/deploy.service.template: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Appliance disk image deployment service 3 | Wants=network.target 4 | After=network-online.target 5 | 6 | [Service] 7 | ExecStart={{ if not .DryRun }}/usr/local/bin/deploy.sh{{ end }} 8 | Type=oneshot 9 | RemainAfterExit=no 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /data/services/install/add-grub-menuitem.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Add recovery GRUB menu item 3 | After=coreos-boot-edit.service coreos-ignition-firstboot-complete.service 4 | ConditionPathExists={{.UserCfgFilePath}} 5 | 6 | [Service] 7 | ExecStart=/usr/local/bin/add-grub-menuitem.sh 8 | Type=oneshot 9 | RemainAfterExit=no 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /data/services/install/apply-operator-crs.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Apply operator CRs service 3 | Wants=network.target 4 | After=network-online.target 5 | 6 | [Service] 7 | ExecStart=/usr/local/bin/apply-operator-crs.sh 8 | Type=oneshot 9 | RemainAfterExit=no 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /data/services/install/create-pinned-image-sets.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Create PinnedImageSets service 3 | Wants=network.target 4 | After=network-online.target 5 | Before=stop-local-registry.service 6 | ConditionPathExists=!/etc/crio/crio.conf.d/50-pinned-images 7 | 8 | [Service] 9 | ExecStart=/usr/local/bin/create-pinned-image-sets.sh 10 | Type=oneshot 11 | RemainAfterExit=no 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /data/services/install/mount-live-iso@.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Mount CoreOS ISO 3 | Wants=network.target 4 | 5 | [Service] 6 | ExecStartPre=mkdir /media/iso 7 | ExecStart=systemd-mount --automount=yes --collect %I /media/iso 8 | Restart=on-failure 9 | RestartSec=10 10 | TimeoutStartSec=500 11 | TimeoutStopSec=300 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /data/services/install/set-node-zero.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Identify node zero 3 | Wants=network.target 4 | After=network-online.target 5 | 6 | [Service] 7 | ExecStart=/usr/local/bin/set-node-zero.sh 8 | Type=oneshot 9 | RemainAfterExit=no 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /data/services/install/start-cluster-upgrade@.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Start Cluster Upgrade 3 | ConditionPathExists=/etc/assisted/node0 4 | 5 | [Service] 6 | ExecStart=/usr/local/bin/start-cluster-upgrade.sh %I 7 | Type=oneshot 8 | RemainAfterExit=no 9 | 10 | [Install] 11 | WantedBy=multi-user.target 12 | -------------------------------------------------------------------------------- /data/services/install/start-local-registry-upgrade@.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Local Registry for Upgrade 3 | Wants=network.target 4 | 5 | [Service] 6 | Environment=PODMAN_SYSTEMD_UNIT=%n 7 | EnvironmentFile=/etc/assisted/registry.env 8 | ExecStartPre=/bin/rm -f %t/%n.ctr-id 9 | ExecStartPre=/usr/local/bin/setup-local-registry-upgrade.sh %I 10 | ExecStart=podman run --net host --cidfile=%t/%n.ctr-id --privileged --replace --log-driver=journald --name=registry_upgrade -p 5001:5000 -p 5444:5000 -v ${REGISTRY_UPGRADE}:/var/lib/registry -v /tmp/certs:/certs -e REGISTRY_HTTP_ADDR=0.0.0.0:5001 -e REGISTRY_HTTP_TLS_CERTIFICATE=certs/domain.crt -e REGISTRY_HTTP_TLS_KEY=certs/domain.key $REGISTRY_IMAGE 11 | ExecStop=/usr/bin/podman stop --ignore --cidfile=%t/%n.ctr-id 12 | ExecStopPost=/usr/bin/podman rm -f --ignore --cidfile=%t/%n.ctr-id 13 | 14 | Restart=on-failure 15 | RestartSec=10 16 | TimeoutStartSec=500 17 | TimeoutStopSec=300 18 | 19 | [Install] 20 | WantedBy=multi-user.target 21 | -------------------------------------------------------------------------------- /data/services/install/stop-local-registry.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Stop local registry service 3 | Wants=network.target 4 | After=network-online.target 5 | 6 | [Service] 7 | ExecStart=/usr/local/bin/stop-local-registry.sh 8 | Type=oneshot 9 | RemainAfterExit=no 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /data/udev/rules.d/99-live-iso.rules: -------------------------------------------------------------------------------- 1 | ACTION=="add|change", SUBSYSTEM=="block", ENV{ID_FS_LABEL}=="rhcos-*", ENV{SYSTEMD_WANTS}+="mount-live-iso@$devnode.service" 2 | ACTION=="remove", SUBSYSTEM=="block", ENV{ID_FS_LABEL}=="rhcos-*", RUN+="/bin/umount /media/iso" 3 | -------------------------------------------------------------------------------- /data/udev/rules.d/99-upgrade-iso.rules: -------------------------------------------------------------------------------- 1 | ACTION=="add|change", SUBSYSTEM=="block", ENV{ID_FS_LABEL}=="upgrade_*", ENV{SYSTEMD_WANTS}+="start-local-registry-upgrade@$devnode.service" 2 | ACTION=="remove", SUBSYSTEM=="block", ENV{ID_FS_LABEL}=="upgrade_*", RUN+="/usr/bin/systemctl stop start-local-registry-upgrade@$devnode.service" 3 | -------------------------------------------------------------------------------- /docs/images/deploy-iso.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openshift/appliance/64c330fab20159506afc3dad5ce9bad0ba726a9c/docs/images/deploy-iso.gif -------------------------------------------------------------------------------- /docs/images/grub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openshift/appliance/64c330fab20159506afc3dad5ce9bad0ba726a9c/docs/images/grub.png -------------------------------------------------------------------------------- /docs/images/hl-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openshift/appliance/64c330fab20159506afc3dad5ce9bad0ba726a9c/docs/images/hl-overview.png -------------------------------------------------------------------------------- /docs/images/upgrade-iso.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openshift/appliance/64c330fab20159506afc3dad5ce9bad0ba726a9c/docs/images/upgrade-iso.gif -------------------------------------------------------------------------------- /hack/diskimage/convert_to_qcow2.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Converts appliance diskimage to qcow2 for allowing snapshot creation 4 | 5 | source=${1:-assets/appliance.raw} 6 | target=${2:-assets/appliance.qcow2} 7 | 8 | qemu-img convert -f raw -O qcow2 "$source" "$target" 9 | -------------------------------------------------------------------------------- /hack/diskimage/embed_install_ignition.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Embeds install ignition config in boot partition of the diskimage 4 | 5 | appliance=${1:-appliance.qcow2} 6 | snapshot=${2:-assets/snapshot.qcow2} 7 | ignition=${3:-assets/ignition/install/config.ign} 8 | 9 | qemu-img create -f qcow2 -b "$appliance" -F qcow2 "$snapshot" 10 | guestfish add "$snapshot" : \ 11 | run : \ 12 | mount /dev/sda3 / : \ 13 | copy-in "$ignition" /ignition/ : \ 14 | unmount-all : \ 15 | exit 16 | -------------------------------------------------------------------------------- /hack/diskimage/extract_install_ignition.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Extracts ignition config from boot partition 4 | 5 | appliance=${1:-assets/appliance.qcow2} 6 | target=${2:-assets/ignition/base} 7 | 8 | mkdir -p "$target" 9 | guestfish add "$appliance" : \ 10 | run : \ 11 | mount /dev/sda3 / : \ 12 | copy-out /ignition/config.ign "$target" : \ 13 | unmount-all : \ 14 | exit 15 | -------------------------------------------------------------------------------- /hack/diskimage/test_install_ignition.md: -------------------------------------------------------------------------------- 1 | # Test changes in InstallIgnition asset 2 | 3 | ## Preparation steps: 4 | 1. Build appliance with --debug-bootstrap flag 5 | 2. Boot appliance and wait for "Ironic will reboot the node shortly" in assisted-service logs. 6 | - Leaves the appliance in pre-installed state (bootstrap completed) 7 | 3. Shutdown appliance 8 | 4. Run hack/diskimage/extract_install_ignition.sh 9 | - Extracts base ignition config to assets/ignition/base/config.ign 10 | 5. Run hack/diskimage/convert_to_qcow2.sh 11 | 12 | ## To test changes: 13 | 1. Run 'generate-install-ignition' command 14 | - Generates merged ignition (base ignition from step 4 + InstallIgnition asset) 15 | - Outputs to assets/ignition/install/config.ign 16 | 2. Run hack/diskimage/embed_install_ignition.sh 17 | - Creates a snapshot and embeds the merged ignition 18 | 3. Run the appliance 19 | -------------------------------------------------------------------------------- /hack/publish-codecov.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | CI_SERVER_URL=https://prow.ci.openshift.org/view/gcs/origin-ci-test 8 | 9 | # Configure the git refs and job link based on how the job was triggered via prow 10 | if [[ "${JOB_TYPE}" == "presubmit" ]]; then 11 | echo "detected PR code coverage job for #${PULL_NUMBER}" 12 | REF_FLAGS="-P ${PULL_NUMBER} -C ${PULL_PULL_SHA}" 13 | JOB_LINK="${CI_SERVER_URL}/pr-logs/pull/${REPO_OWNER}_${REPO_NAME}/${PULL_NUMBER}/${JOB_NAME}/${BUILD_ID}" 14 | elif [[ "${JOB_TYPE}" == "postsubmit" ]]; then 15 | echo "detected branch code coverage job for ${PULL_BASE_REF}" 16 | REF_FLAGS="-B ${PULL_BASE_REF} -C ${PULL_BASE_SHA}" 17 | JOB_LINK="${CI_SERVER_URL}/logs/${JOB_NAME}/${BUILD_ID}" 18 | else 19 | echo "${JOB_TYPE} job not supported" 20 | exit 0 21 | fi 22 | 23 | # Configure certain internal codecov variables with values from prow. 24 | export CI_BUILD_URL="${JOB_LINK}" 25 | export CI_BUILD_ID="${JOB_NAME}" 26 | export CI_JOB_ID="${BUILD_ID}" 27 | 28 | if [[ -z "${ARTIFACT_DIR:-}" ]] || [[ ! -d "${ARTIFACT_DIR}" ]] || [[ ! -w "${ARTIFACT_DIR}" ]]; then 29 | echo "${ARTIFACT_DIR} must be set for non-local jobs, and must point to a writable directory" >&2 30 | exit 1 31 | fi 32 | curl -sS https://codecov.io/bash -o "${ARTIFACT_DIR}/codecov.sh" 33 | bash <(cat "${ARTIFACT_DIR}/codecov.sh") -Z -K -f "${COVER_PROFILE}" -r "${REPO_OWNER}/${REPO_NAME}" "${REF_FLAGS}" 34 | -------------------------------------------------------------------------------- /pkg/asset/appliance/appliance_diskimage.go: -------------------------------------------------------------------------------- 1 | package appliance 2 | 3 | import ( 4 | "context" 5 | "path/filepath" 6 | 7 | "github.com/go-openapi/swag" 8 | 9 | "github.com/openshift/appliance/pkg/asset/config" 10 | "github.com/openshift/appliance/pkg/asset/data" 11 | "github.com/openshift/appliance/pkg/asset/recovery" 12 | "github.com/openshift/appliance/pkg/consts" 13 | "github.com/openshift/appliance/pkg/conversions" 14 | "github.com/openshift/appliance/pkg/executer" 15 | "github.com/openshift/appliance/pkg/installer" 16 | "github.com/openshift/appliance/pkg/log" 17 | "github.com/openshift/appliance/pkg/templates" 18 | "github.com/openshift/installer/pkg/asset" 19 | "github.com/pkg/errors" 20 | "github.com/sirupsen/logrus" 21 | ) 22 | 23 | // ApplianceDiskImage is an asset that generates the OpenShift-based appliance. 24 | type ApplianceDiskImage struct { 25 | File *asset.File 26 | InstallerBinaryName string 27 | } 28 | 29 | var _ asset.Asset = (*ApplianceDiskImage)(nil) 30 | 31 | // Dependencies returns the assets on which the Bootstrap asset depends. 32 | func (a *ApplianceDiskImage) Dependencies() []asset.Asset { 33 | return []asset.Asset{ 34 | &config.EnvConfig{}, 35 | &config.ApplianceConfig{}, 36 | &BaseDiskImage{}, 37 | &data.DataISO{}, 38 | &recovery.RecoveryISO{}, 39 | } 40 | } 41 | 42 | // Generate the appliance disk. 43 | func (a *ApplianceDiskImage) Generate(_ context.Context, dependencies asset.Parents) error { 44 | envConfig := &config.EnvConfig{} 45 | applianceConfig := &config.ApplianceConfig{} 46 | recoveryISO := &recovery.RecoveryISO{} 47 | dataISO := &data.DataISO{} 48 | baseDiskImage := &BaseDiskImage{} 49 | dependencies.Get(envConfig, applianceConfig, recoveryISO, dataISO, baseDiskImage) 50 | 51 | spinner := log.NewSpinner( 52 | "Generating appliance disk image...", 53 | "Successfully generated appliance disk image", 54 | "Failed to generate appliance disk image", 55 | envConfig, 56 | ) 57 | spinner.FileToMonitor = consts.ApplianceFileName 58 | 59 | // Render user.cfg 60 | if err := templates.RenderTemplateFile( 61 | consts.UserCfgTemplateFile, 62 | templates.GetUserCfgTemplateData( 63 | consts.GrubMenuEntryName, 64 | swag.BoolValue(applianceConfig.Config.EnableFips)), 65 | envConfig.TempDir); err != nil { 66 | return log.StopSpinner(spinner, err) 67 | } 68 | 69 | // Render guestfish.sh 70 | recoveryIsoSize := recoveryISO.Size 71 | dataIsoSize := dataISO.Size 72 | baseImageFile := baseDiskImage.File.Filename 73 | baseIsoSize := templates.NewPartitions().GetBootPartitionsSize(baseImageFile) 74 | diskSize := a.getDiskSize(applianceConfig.Config.DiskSizeGB, baseIsoSize, recoveryIsoSize, dataIsoSize) 75 | 76 | applianceImageFile := filepath.Join(envConfig.AssetsDir, consts.ApplianceFileName) 77 | recoveryIsoFile := filepath.Join(envConfig.CacheDir, consts.RecoveryIsoFileName) 78 | dataIsoFile := filepath.Join(envConfig.CacheDir, consts.DataIsoFileName) 79 | userCfgFile := templates.GetFilePathByTemplate(consts.UserCfgTemplateFile, envConfig.TempDir) 80 | isCompact := applianceConfig.Config.DiskSizeGB == nil 81 | gfTemplateData := templates.GetGuestfishScriptTemplateData( 82 | isCompact, diskSize, baseIsoSize, recoveryIsoSize, dataIsoSize, baseImageFile, 83 | applianceImageFile, recoveryIsoFile, dataIsoFile, userCfgFile, consts.GrubCfgFilePath, envConfig.TempDir) 84 | if err := templates.RenderTemplateFile( 85 | consts.GuestfishScriptTemplateFile, 86 | gfTemplateData, 87 | envConfig.TempDir); err != nil { 88 | return log.StopSpinner(spinner, err) 89 | } 90 | 91 | // Invoke guestfish.sh script 92 | logrus.Debug("Running guestfish script") 93 | guestfishFileName := templates.GetFilePathByTemplate( 94 | consts.GuestfishScriptTemplateFile, envConfig.TempDir) 95 | if _, err := executer.NewExecuter().Execute(guestfishFileName); err != nil { 96 | return log.StopSpinner(spinner, errors.Wrapf(err, "guestfish script failure")) 97 | } 98 | 99 | a.File = &asset.File{Filename: applianceImageFile} 100 | 101 | installerConfig := installer.InstallerConfig{ 102 | EnvConfig: envConfig, 103 | ApplianceConfig: applianceConfig, 104 | } 105 | a.InstallerBinaryName = installer.NewInstaller(installerConfig).GetInstallerBinaryName() 106 | 107 | return log.StopSpinner(spinner, nil) 108 | } 109 | 110 | // Name returns the human-friendly name of the asset. 111 | func (a *ApplianceDiskImage) Name() string { 112 | return "Appliance disk image" 113 | } 114 | 115 | func (a *ApplianceDiskImage) getDiskSize(diskSizeGB *int, baseIsoSize, recoveryIsoSize, dataIsoSize int64) int64 { 116 | if diskSizeGB != nil { 117 | return int64(*diskSizeGB) 118 | } 119 | 120 | // Calc appliance disk image size in bytes 121 | diskSize := baseIsoSize + recoveryIsoSize + dataIsoSize 122 | 123 | // Convert size to GiB (rounded up) 124 | return conversions.BytesToGib(diskSize) + 1 125 | } 126 | -------------------------------------------------------------------------------- /pkg/asset/appliance/appliance_liveiso.go: -------------------------------------------------------------------------------- 1 | package appliance 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/openshift/appliance/pkg/asset/config" 11 | "github.com/openshift/appliance/pkg/asset/data" 12 | "github.com/openshift/appliance/pkg/asset/recovery" 13 | "github.com/openshift/appliance/pkg/consts" 14 | "github.com/openshift/appliance/pkg/coreos" 15 | "github.com/openshift/appliance/pkg/fileutil" 16 | "github.com/openshift/appliance/pkg/installer" 17 | "github.com/openshift/appliance/pkg/log" 18 | "github.com/openshift/appliance/pkg/syslinux" 19 | "github.com/openshift/assisted-image-service/pkg/isoeditor" 20 | "github.com/openshift/installer/pkg/asset" 21 | "github.com/sirupsen/logrus" 22 | ) 23 | 24 | const ( 25 | LiveIsoWorkDir = "live-iso" 26 | LiveIsoDataDir = "data" 27 | ) 28 | 29 | // ApplianceLiveISO is an asset that generates the OpenShift-based appliance. 30 | type ApplianceLiveISO struct { 31 | File *asset.File 32 | InstallerBinaryName string 33 | } 34 | 35 | var _ asset.Asset = (*ApplianceLiveISO)(nil) 36 | 37 | // Dependencies returns the assets on which the Bootstrap asset depends. 38 | func (a *ApplianceLiveISO) Dependencies() []asset.Asset { 39 | return []asset.Asset{ 40 | &config.EnvConfig{}, 41 | &config.ApplianceConfig{}, 42 | &data.DataISO{}, 43 | &recovery.RecoveryISO{}, 44 | } 45 | } 46 | 47 | // Generate the appliance disk. 48 | func (a *ApplianceLiveISO) Generate(_ context.Context, dependencies asset.Parents) error { 49 | envConfig := &config.EnvConfig{} 50 | applianceConfig := &config.ApplianceConfig{} 51 | dataISO := &data.DataISO{} 52 | recoveryISO := &recovery.RecoveryISO{} 53 | dependencies.Get(envConfig, applianceConfig, dataISO, recoveryISO) 54 | 55 | // Build the live ISO 56 | if err := a.buildLiveISO(envConfig, applianceConfig); err != nil { 57 | return err 58 | } 59 | 60 | // Embed ignition in ISO 61 | coreOSConfig := coreos.CoreOSConfig{ 62 | ApplianceConfig: applianceConfig, 63 | EnvConfig: envConfig, 64 | } 65 | c := coreos.NewCoreOS(coreOSConfig) 66 | ignitionBytes, err := json.Marshal(recoveryISO.Ignition.Config) 67 | if err != nil { 68 | logrus.Errorf("Failed to marshal recovery ignition to json: %s", err.Error()) 69 | return err 70 | } 71 | applianceLiveIsoFile := filepath.Join(envConfig.AssetsDir, consts.ApplianceLiveIsoFileName) 72 | if err = c.EmbedIgnition(ignitionBytes, applianceLiveIsoFile); err != nil { 73 | logrus.Errorf("Failed to embed ignition in recovery ISO: %s", err.Error()) 74 | return err 75 | } 76 | 77 | // Get installer binary 78 | installerConfig := installer.InstallerConfig{ 79 | EnvConfig: envConfig, 80 | ApplianceConfig: applianceConfig, 81 | } 82 | a.InstallerBinaryName = installer.NewInstaller(installerConfig).GetInstallerBinaryName() 83 | 84 | a.File = &asset.File{Filename: applianceLiveIsoFile} 85 | 86 | return nil 87 | } 88 | 89 | // Name returns the human-friendly name of the asset. 90 | func (a *ApplianceLiveISO) Name() string { 91 | return "Appliance live ISO" 92 | } 93 | 94 | func (a *ApplianceLiveISO) buildLiveISO(envConfig *config.EnvConfig, applianceConfig *config.ApplianceConfig) error { 95 | // Create work dir 96 | workDir, err := os.MkdirTemp(envConfig.TempDir, LiveIsoWorkDir) 97 | if err != nil { 98 | return err 99 | } 100 | 101 | // Create data dir 102 | dataDir := filepath.Join(workDir, LiveIsoDataDir) 103 | if err = os.MkdirAll(dataDir, os.ModePerm); err != nil { 104 | return err 105 | } 106 | 107 | spinner := log.NewSpinner( 108 | "Extracting CoreOS ISO...", 109 | "Successfully extracted CoreOS ISO", 110 | "Failed to extract CoreOS ISO", 111 | envConfig, 112 | ) 113 | spinner.DirToMonitor = workDir 114 | 115 | // Extract base ISO 116 | coreosIsoFileName := fmt.Sprintf(consts.CoreosIsoName, applianceConfig.GetCpuArchitecture()) 117 | coreosIsoPath := filepath.Join(envConfig.CacheDir, coreosIsoFileName) 118 | if err = isoeditor.Extract(coreosIsoPath, workDir); err != nil { 119 | logrus.Errorf("Failed to extract ISO: %s", err.Error()) 120 | return err 121 | } 122 | 123 | if err = log.StopSpinner(spinner, nil); err != nil { 124 | return err 125 | } 126 | 127 | spinner = log.NewSpinner( 128 | "Copying data ISO...", 129 | "Successfully copied data ISO", 130 | "Failed to copy data ISO", 131 | envConfig, 132 | ) 133 | spinner.DirToMonitor = dataDir 134 | 135 | // Split data.iso file and output to work dir 136 | // (to bypass ISO9660 limitation for large files) 137 | dataIsoFile := filepath.Join(envConfig.CacheDir, consts.DataIsoFileName) 138 | dataIsoSplitFile := filepath.Join(dataDir, consts.DataIsoFileName) 139 | if err = fileutil.SplitFile(dataIsoFile, dataIsoSplitFile, "3G"); err != nil { 140 | logrus.Error(err) 141 | return err 142 | } 143 | 144 | if err = log.StopSpinner(spinner, nil); err != nil { 145 | return err 146 | } 147 | 148 | spinner = log.NewSpinner( 149 | "Generating appliance live ISO...", 150 | "Successfully generated appliance live ISO", 151 | "Failed to generate appliance live ISO", 152 | envConfig, 153 | ) 154 | spinner.FileToMonitor = consts.DeployIsoName 155 | 156 | // Generate live ISO 157 | volumeID, err := isoeditor.VolumeIdentifier(coreosIsoPath) 158 | if err != nil { 159 | return err 160 | } 161 | liveIsoFileName := filepath.Join(envConfig.AssetsDir, consts.ApplianceLiveIsoFileName) 162 | if err = isoeditor.Create(liveIsoFileName, workDir, volumeID); err != nil { 163 | logrus.Errorf("Failed to create ISO: %s", err.Error()) 164 | return err 165 | } 166 | 167 | hybrid := syslinux.NewIsoHybrid(nil) 168 | if err = hybrid.Convert(liveIsoFileName); err != nil { 169 | logrus.Errorf("Error creating isohybrid: %s", err) 170 | } 171 | 172 | return log.StopSpinner(spinner, nil) 173 | } 174 | -------------------------------------------------------------------------------- /pkg/asset/appliance/base_diskimage.go: -------------------------------------------------------------------------------- 1 | package appliance 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/openshift/appliance/pkg/asset/config" 8 | "github.com/openshift/appliance/pkg/consts" 9 | "github.com/openshift/appliance/pkg/coreos" 10 | "github.com/openshift/appliance/pkg/fileutil" 11 | "github.com/openshift/appliance/pkg/log" 12 | "github.com/openshift/installer/pkg/asset" 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | // BaseDiskImage is an asset that generates the base disk image (CoreOS) of OpenShift-based appliance. 17 | type BaseDiskImage struct { 18 | File *asset.File 19 | } 20 | 21 | var _ asset.Asset = (*BaseDiskImage)(nil) 22 | 23 | // Dependencies returns the assets on which the Bootstrap asset depends. 24 | func (a *BaseDiskImage) Dependencies() []asset.Asset { 25 | return []asset.Asset{ 26 | &config.EnvConfig{}, 27 | &config.ApplianceConfig{}, 28 | } 29 | } 30 | 31 | // Generate the base disk image. 32 | func (a *BaseDiskImage) Generate(_ context.Context, dependencies asset.Parents) error { 33 | envConfig := &config.EnvConfig{} 34 | applianceConfig := &config.ApplianceConfig{} 35 | dependencies.Get(envConfig, applianceConfig) 36 | 37 | // Search for disk image in cache dir 38 | filePattern := fmt.Sprintf(consts.CoreosImagePattern, applianceConfig.GetCpuArchitecture()) 39 | if fileName := envConfig.FindInCache(filePattern); fileName != "" { 40 | logrus.Info("Reusing appliance base disk image from cache") 41 | a.File = &asset.File{Filename: fileName} 42 | return nil 43 | } 44 | 45 | // Download using coreos-installer 46 | spinner := log.NewSpinner( 47 | "Downloading appliance base disk image...", 48 | "Successfully downloaded appliance base disk image", 49 | "Failed to download appliance base disk image", 50 | envConfig, 51 | ) 52 | spinner.FileToMonitor = coreos.CoreOsDiskImageGz 53 | coreOSConfig := coreos.CoreOSConfig{ 54 | ApplianceConfig: applianceConfig, 55 | EnvConfig: envConfig, 56 | } 57 | 58 | c := coreos.NewCoreOS(coreOSConfig) 59 | compressed, err := c.DownloadDiskImage() 60 | if err != nil { 61 | return log.StopSpinner(spinner, err) 62 | } 63 | if err = log.StopSpinner(spinner, nil); err != nil { 64 | return err 65 | } 66 | 67 | // Extracting gz file 68 | spinner = log.NewSpinner( 69 | "Extracting appliance base disk image...", 70 | "Successfully extracted appliance base disk image", 71 | "Failed to extract appliance base disk image", 72 | envConfig, 73 | ) 74 | spinner.FileToMonitor = filePattern 75 | fileName, err := fileutil.ExtractCompressedFile(compressed, envConfig.CacheDir) 76 | if err != nil { 77 | return log.StopSpinner(spinner, err) 78 | } 79 | 80 | a.File = &asset.File{Filename: fileName} 81 | 82 | return log.StopSpinner(spinner, nil) 83 | } 84 | 85 | // Name returns the human-friendly name of the asset. 86 | func (a *BaseDiskImage) Name() string { 87 | return "Base disk image (CoreOS)" 88 | } 89 | -------------------------------------------------------------------------------- /pkg/asset/config/deploy_config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/openshift/installer/pkg/asset" 7 | ) 8 | 9 | type DeployConfig struct { 10 | TargetDevice string 11 | PostScript string 12 | SparseClone bool 13 | DryRun bool 14 | } 15 | 16 | var _ asset.Asset = (*DeployConfig)(nil) 17 | 18 | // Dependencies returns no dependencies. 19 | func (e *DeployConfig) Dependencies() []asset.Asset { 20 | return []asset.Asset{} 21 | } 22 | 23 | // Generate EnvConfig asset 24 | func (e *DeployConfig) Generate(_ context.Context, dependencies asset.Parents) error { 25 | return nil 26 | } 27 | 28 | // Name returns the human-friendly name of the asset. 29 | func (e *DeployConfig) Name() string { 30 | return "Deploy Config" 31 | } 32 | -------------------------------------------------------------------------------- /pkg/asset/config/env_config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/hashicorp/go-version" 10 | "github.com/openshift/appliance/pkg/consts" 11 | "github.com/openshift/installer/pkg/asset" 12 | "github.com/pkg/errors" 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | const ( 17 | CacheDir = "cache" 18 | TempDir = "temp" 19 | ) 20 | 21 | type EnvConfig struct { 22 | AssetsDir string 23 | CacheDir string 24 | TempDir string 25 | 26 | IsLiveISO bool 27 | 28 | DebugBootstrap bool 29 | DebugBaseIgnition bool 30 | } 31 | 32 | var _ asset.Asset = (*EnvConfig)(nil) 33 | 34 | // Dependencies returns no dependencies. 35 | func (e *EnvConfig) Dependencies() []asset.Asset { 36 | return []asset.Asset{ 37 | &ApplianceConfig{}, 38 | } 39 | } 40 | 41 | // Generate EnvConfig asset 42 | func (e *EnvConfig) Generate(_ context.Context, dependencies asset.Parents) error { 43 | applianceConfig := &ApplianceConfig{} 44 | dependencies.Get(applianceConfig) 45 | 46 | if applianceConfig.File == nil { 47 | return errors.Errorf("Missing config file in assets directory: %s/%s", e.AssetsDir, ApplianceConfigFilename) 48 | } 49 | 50 | // Check whether the specified version is supported 51 | if err := e.validateOcpReleaseVersion(applianceConfig.Config.OcpRelease.Version); err != nil { 52 | return err 53 | } 54 | 55 | // Cache dir in 'version-arch' format 56 | cacheDirPattern := fmt.Sprintf("%s-%s", 57 | applianceConfig.Config.OcpRelease.Version, applianceConfig.GetCpuArchitecture()) 58 | 59 | e.CacheDir = filepath.Join(e.AssetsDir, CacheDir, cacheDirPattern) 60 | e.TempDir = filepath.Join(e.AssetsDir, TempDir) 61 | 62 | if err := os.MkdirAll(e.CacheDir, os.ModePerm); err != nil { 63 | logrus.Errorf("Failed to create dir: %s", e.CacheDir) 64 | } 65 | 66 | if err := os.MkdirAll(e.TempDir, os.ModePerm); err != nil { 67 | logrus.Errorf("Failed to create dir: %s", e.TempDir) 68 | } 69 | 70 | return nil 71 | } 72 | 73 | // Name returns the human-friendly name of the asset. 74 | func (e *EnvConfig) Name() string { 75 | return "Env Config" 76 | } 77 | 78 | func (e *EnvConfig) findInDir(dir, filePattern string) string { 79 | files, err := filepath.Glob(filepath.Join(dir, filePattern)) 80 | if err != nil { 81 | logrus.Errorf("Failed searching for file '%s' in dir '%s'", filePattern, e.CacheDir) 82 | return "" 83 | } 84 | if len(files) > 0 { 85 | file := files[0] 86 | if !e.isValidFileSize(file) { 87 | return "" 88 | } 89 | return file 90 | } 91 | return "" 92 | } 93 | 94 | func (e *EnvConfig) isValidFileSize(file string) bool { 95 | f, err := os.Stat(file) 96 | if err != nil { 97 | return false 98 | } 99 | return f.Size() != 0 100 | } 101 | 102 | func (e *EnvConfig) validateOcpReleaseVersion(releaseVersion string) error { 103 | maxOcpVer, err := version.NewVersion(consts.MaxOcpVersion) 104 | if err != nil { 105 | return err 106 | } 107 | ocpVer, err := version.NewVersion(releaseVersion) 108 | if err != nil { 109 | return err 110 | } 111 | majorMinor, err := version.NewVersion(fmt.Sprintf("%d.%d", ocpVer.Segments()[0], ocpVer.Segments()[1])) 112 | if err != nil { 113 | return err 114 | } 115 | 116 | if majorMinor.GreaterThan(maxOcpVer) { 117 | logrus.Warn(fmt.Sprintf("OCP release version %s is not supported. Latest supported version: %s.", 118 | releaseVersion, consts.MaxOcpVersion)) 119 | } 120 | return nil 121 | } 122 | 123 | func (e *EnvConfig) FindInCache(filePattern string) string { 124 | return e.findInDir(e.CacheDir, filePattern) 125 | } 126 | 127 | func (e *EnvConfig) FindInTemp(filePattern string) string { 128 | return e.findInDir(e.TempDir, filePattern) 129 | } 130 | 131 | func (e *EnvConfig) FindInAssets(filePattern string) string { 132 | if file := e.FindInCache(filePattern); file != "" { 133 | return file 134 | } 135 | if file := e.FindInTemp(filePattern); file != "" { 136 | return file 137 | } 138 | return e.findInDir(e.AssetsDir, filePattern) 139 | } 140 | 141 | // FindFilesInCache returns the files from cache whose name match the given regexp. 142 | func (e *EnvConfig) FindFilesInCache(pattern string) (files []*asset.File, err error) { 143 | matches, err := filepath.Glob(filepath.Join(e.CacheDir, pattern)) 144 | if err != nil { 145 | return nil, err 146 | } 147 | 148 | files = make([]*asset.File, 0, len(matches)) 149 | for _, path := range matches { 150 | data, err := os.ReadFile(path) 151 | if err != nil { 152 | return nil, err 153 | } 154 | 155 | filename, err := filepath.Rel(e.CacheDir, path) 156 | if err != nil { 157 | return nil, err 158 | } 159 | 160 | files = append(files, &asset.File{ 161 | Filename: filename, 162 | Data: data, 163 | }) 164 | } 165 | 166 | return files, nil 167 | } 168 | -------------------------------------------------------------------------------- /pkg/asset/data/data_iso.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/go-openapi/swag" 10 | "github.com/openshift/appliance/pkg/asset/config" 11 | "github.com/openshift/appliance/pkg/consts" 12 | "github.com/openshift/appliance/pkg/genisoimage" 13 | "github.com/openshift/appliance/pkg/log" 14 | "github.com/openshift/appliance/pkg/registry" 15 | "github.com/openshift/appliance/pkg/release" 16 | "github.com/openshift/installer/pkg/asset" 17 | "github.com/sirupsen/logrus" 18 | ) 19 | 20 | const ( 21 | bootstrapMirrorDir = "data/oc-mirror/bootstrap" 22 | installMirrorDir = "data/oc-mirror/install" 23 | dataDir = "data" 24 | dataIsoName = "data.iso" 25 | dataVolumeName = "agentdata" 26 | ) 27 | 28 | // DataISO is an asset that contains registry images 29 | // to a recovery partition in the OpenShift-based appliance. 30 | type DataISO struct { 31 | File *asset.File 32 | Size int64 33 | } 34 | 35 | var _ asset.Asset = (*DataISO)(nil) 36 | 37 | // Dependencies returns the assets on which the Bootstrap asset depends. 38 | func (a *DataISO) Dependencies() []asset.Asset { 39 | return []asset.Asset{ 40 | &config.EnvConfig{}, 41 | &config.ApplianceConfig{}, 42 | } 43 | } 44 | 45 | // Generate the recovery ISO. 46 | func (a *DataISO) Generate(_ context.Context, dependencies asset.Parents) error { 47 | envConfig := &config.EnvConfig{} 48 | applianceConfig := &config.ApplianceConfig{} 49 | dependencies.Get(envConfig, applianceConfig) 50 | 51 | // Search for ISO in cache dir 52 | if fileName := envConfig.FindInCache(consts.DataIsoFileName); fileName != "" { 53 | logrus.Info("Reusing data ISO from cache") 54 | return a.updateAsset(envConfig) 55 | } 56 | 57 | releaseConfig := release.ReleaseConfig{ 58 | ApplianceConfig: applianceConfig, 59 | EnvConfig: envConfig, 60 | } 61 | r := release.NewRelease(releaseConfig) 62 | 63 | dataDirPath := filepath.Join(envConfig.TempDir, dataDir) 64 | if err := os.MkdirAll(dataDirPath, os.ModePerm); err != nil { 65 | logrus.Errorf("Failed to create dir: %s", dataDirPath) 66 | return err 67 | } 68 | 69 | spinner := log.NewSpinner( 70 | "Generating container registry image...", 71 | "Successfully generated container registry image", 72 | "Failed to generate container registry image", 73 | envConfig, 74 | ) 75 | registryUri, err := registry.CopyRegistryImageIfNeeded(envConfig, applianceConfig) 76 | if err != nil { 77 | return log.StopSpinner(spinner, err) 78 | } 79 | if err = log.StopSpinner(spinner, nil); err != nil { 80 | return err 81 | } 82 | 83 | // Copying release images 84 | spinner = log.NewSpinner( 85 | fmt.Sprintf("Pulling OpenShift %s release images required for installation...", 86 | applianceConfig.Config.OcpRelease.Version), 87 | fmt.Sprintf("Successfully pulled OpenShift %s release images required for installation", 88 | applianceConfig.Config.OcpRelease.Version), 89 | fmt.Sprintf("Failed to pull OpenShift %s release images required for installation", 90 | applianceConfig.Config.OcpRelease.Version), 91 | envConfig, 92 | ) 93 | registryDir, err := registry.GetRegistryDataPath(envConfig.TempDir, installMirrorDir) 94 | if err != nil { 95 | return log.StopSpinner(spinner, err) 96 | } 97 | spinner.DirToMonitor = registryDir 98 | releaseImageRegistry := registry.NewRegistry( 99 | registry.RegistryConfig{ 100 | DataDirPath: registryDir, 101 | URI: registryUri, 102 | Port: swag.IntValue(applianceConfig.Config.ImageRegistry.Port), 103 | }) 104 | 105 | if err = releaseImageRegistry.StartRegistry(); err != nil { 106 | return log.StopSpinner(spinner, err) 107 | } 108 | if err = r.MirrorInstallImages(); err != nil { 109 | return log.StopSpinner(spinner, err) 110 | } 111 | if err = releaseImageRegistry.StopRegistry(); err != nil { 112 | return log.StopSpinner(spinner, err) 113 | } 114 | if err = log.StopSpinner(spinner, nil); err != nil { 115 | return err 116 | } 117 | 118 | spinner = log.NewSpinner( 119 | "Generating data ISO...", 120 | "Successfully generated data ISO", 121 | "Failed to generate data ISO", 122 | envConfig, 123 | ) 124 | spinner.FileToMonitor = dataIsoName 125 | imageGen := genisoimage.NewGenIsoImage(nil) 126 | if err = imageGen.GenerateImage(envConfig.CacheDir, dataIsoName, filepath.Join(envConfig.TempDir, dataDir), dataVolumeName); err != nil { 127 | return log.StopSpinner(spinner, err) 128 | } 129 | return log.StopSpinner(spinner, a.updateAsset(envConfig)) 130 | } 131 | 132 | // Name returns the human-friendly name of the asset. 133 | func (a *DataISO) Name() string { 134 | return "Data ISO" 135 | } 136 | 137 | func (a *DataISO) updateAsset(envConfig *config.EnvConfig) error { 138 | dataIsoPath := filepath.Join(envConfig.CacheDir, consts.DataIsoFileName) 139 | a.File = &asset.File{Filename: dataIsoPath} 140 | f, err := os.Stat(dataIsoPath) 141 | if err != nil { 142 | return err 143 | } 144 | a.Size = f.Size() 145 | 146 | return nil 147 | } 148 | -------------------------------------------------------------------------------- /pkg/asset/deploy/deploy_iso.go: -------------------------------------------------------------------------------- 1 | package deploy 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/openshift/appliance/pkg/asset/config" 11 | "github.com/openshift/appliance/pkg/asset/ignition" 12 | "github.com/openshift/appliance/pkg/asset/recovery" 13 | "github.com/openshift/appliance/pkg/consts" 14 | "github.com/openshift/appliance/pkg/coreos" 15 | "github.com/openshift/appliance/pkg/fileutil" 16 | "github.com/openshift/appliance/pkg/log" 17 | "github.com/openshift/appliance/pkg/skopeo" 18 | "github.com/openshift/appliance/pkg/syslinux" 19 | "github.com/openshift/assisted-image-service/pkg/isoeditor" 20 | "github.com/openshift/installer/pkg/asset" 21 | "github.com/sirupsen/logrus" 22 | ) 23 | 24 | type DeployISO struct { 25 | File *asset.File 26 | } 27 | 28 | var _ asset.Asset = (*DeployISO)(nil) 29 | 30 | // Name returns the human-friendly name of the asset. 31 | func (i *DeployISO) Name() string { 32 | return "Deployment ISO" 33 | } 34 | 35 | // Dependencies returns dependencies used by the asset. 36 | func (i *DeployISO) Dependencies() []asset.Asset { 37 | return []asset.Asset{ 38 | &config.EnvConfig{}, 39 | &config.ApplianceConfig{}, 40 | &ignition.DeployIgnition{}, 41 | &recovery.BaseISO{}, 42 | } 43 | } 44 | 45 | // Generate the base ISO. 46 | func (i *DeployISO) Generate(_ context.Context, dependencies asset.Parents) error { 47 | envConfig := &config.EnvConfig{} 48 | applianceConfig := &config.ApplianceConfig{} 49 | baseISO := &recovery.BaseISO{} 50 | deployIgnition := &ignition.DeployIgnition{} 51 | 52 | dependencies.Get(envConfig, applianceConfig, baseISO, deployIgnition) 53 | 54 | // Search for deployment ISO in cache dir 55 | if fileName := envConfig.FindInAssets(consts.DeployIsoName); fileName != "" { 56 | logrus.Info("Configuring appliance deployment ISO") 57 | i.File = &asset.File{Filename: fileName} 58 | } else if err := i.buildDeploymentIso(envConfig, applianceConfig); err != nil { 59 | return err 60 | } 61 | 62 | // Embed ignition in ISO 63 | coreOSConfig := coreos.CoreOSConfig{ 64 | ApplianceConfig: applianceConfig, 65 | EnvConfig: envConfig, 66 | } 67 | c := coreos.NewCoreOS(coreOSConfig) 68 | ignitionBytes, err := json.Marshal(deployIgnition.Config) 69 | if err != nil { 70 | logrus.Errorf("Failed to marshal deploy ignition to json: %s", err.Error()) 71 | return err 72 | } 73 | deployIsoFileName := filepath.Join(envConfig.AssetsDir, consts.DeployIsoName) 74 | if err = c.EmbedIgnition(ignitionBytes, deployIsoFileName); err != nil { 75 | logrus.Errorf("Failed to embed ignition in deploy ISO: %s", err.Error()) 76 | return err 77 | } 78 | 79 | return nil 80 | } 81 | 82 | func (i *DeployISO) buildDeploymentIso(envConfig *config.EnvConfig, applianceConfig *config.ApplianceConfig) error { 83 | if fileName := envConfig.FindInAssets(consts.ApplianceFileName); fileName == "" { 84 | logrus.Infof("The appliance.raw disk image file is missing.") 85 | logrus.Infof("Run 'build' command for building the appliance disk image.") 86 | logrus.Exit(1) 87 | return nil 88 | } 89 | 90 | spinner := log.NewSpinner( 91 | "Copying appliance disk image...", 92 | "Successfully copied appliance disk image", 93 | "Failed to copy appliance disk image", 94 | envConfig, 95 | ) 96 | deployIsoTempDir, err := os.MkdirTemp(envConfig.TempDir, consts.DeployDir) 97 | if err != nil { 98 | return err 99 | } 100 | spinner.DirToMonitor = deployIsoTempDir 101 | 102 | // Create deploy dir 103 | deployDir := filepath.Join(deployIsoTempDir, consts.DeployDir) 104 | if err = os.MkdirAll(deployDir, os.ModePerm); err != nil { 105 | logrus.Errorf("Failed to create dir: %s", deployDir) 106 | return err 107 | } 108 | 109 | // Split appliance.raw file and output to temp dir 110 | // (to bypass ISO9660 limitation for large files) 111 | applianceImageFile := filepath.Join(envConfig.AssetsDir, consts.ApplianceFileName) 112 | applianceSplitFile := filepath.Join(deployDir, consts.ApplianceFileName) 113 | if err = fileutil.SplitFile(applianceImageFile, applianceSplitFile, "3G"); err != nil { 114 | logrus.Error(err) 115 | return err 116 | } 117 | 118 | if err = log.StopSpinner(spinner, nil); err != nil { 119 | return err 120 | } 121 | 122 | // Pull appliance image into temp dir 123 | spinner = log.NewSpinner( 124 | "Pulling appliance container image...", 125 | "Successfully pulled appliance container image", 126 | "Failed to pull appliance container image", 127 | envConfig, 128 | ) 129 | applianceTarFile := filepath.Join(deployDir, consts.ApplianceImageTar) 130 | if err = skopeo.NewSkopeo(nil).CopyToFile( 131 | consts.ApplianceImage, consts.ApplianceImageName, applianceTarFile); err != nil { 132 | return err 133 | } 134 | 135 | // Extract base ISO 136 | coreosIsoFileName := fmt.Sprintf(consts.CoreosIsoName, applianceConfig.GetCpuArchitecture()) 137 | coreosIsoPath := filepath.Join(envConfig.CacheDir, coreosIsoFileName) 138 | if err = isoeditor.Extract(coreosIsoPath, deployIsoTempDir); err != nil { 139 | logrus.Errorf("Failed to extract ISO: %s", err.Error()) 140 | return err 141 | } 142 | 143 | if err = log.StopSpinner(spinner, nil); err != nil { 144 | return err 145 | } 146 | 147 | spinner = log.NewSpinner( 148 | "Generating appliance deployment ISO...", 149 | "Successfully generated appliance deployment ISO", 150 | "Failed to generate appliance deployment ISO", 151 | envConfig, 152 | ) 153 | spinner.FileToMonitor = consts.DeployIsoName 154 | 155 | // Generate deployment ISO 156 | volumeID, err := isoeditor.VolumeIdentifier(coreosIsoPath) 157 | if err != nil { 158 | return err 159 | } 160 | deployIsoFileName := filepath.Join(envConfig.AssetsDir, consts.DeployIsoName) 161 | if err = isoeditor.Create(deployIsoFileName, deployIsoTempDir, volumeID); err != nil { 162 | logrus.Errorf("Failed to create ISO: %s", err.Error()) 163 | return err 164 | } 165 | 166 | hybrid := syslinux.NewIsoHybrid(nil) 167 | if err = hybrid.Convert(deployIsoFileName); err != nil { 168 | logrus.Errorf("Error creating isohybrid: %s", err) 169 | } 170 | 171 | return log.StopSpinner(spinner, nil) 172 | } 173 | -------------------------------------------------------------------------------- /pkg/asset/ignition/deploy_ignition.go: -------------------------------------------------------------------------------- 1 | package ignition 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "path/filepath" 7 | 8 | igntypes "github.com/coreos/ignition/v2/config/v3_2/types" 9 | "github.com/openshift/installer/pkg/asset/ignition" 10 | "github.com/openshift/installer/pkg/asset/ignition/bootstrap" 11 | "golang.org/x/crypto/bcrypt" 12 | 13 | "github.com/openshift/appliance/pkg/asset/config" 14 | "github.com/openshift/appliance/pkg/templates" 15 | "github.com/openshift/installer/pkg/asset" 16 | ) 17 | 18 | var ( 19 | deployServices = []string{ 20 | "deploy.service", 21 | } 22 | 23 | deployScripts = []string{ 24 | "deploy.sh", 25 | } 26 | ) 27 | 28 | type DeployIgnition struct { 29 | Config igntypes.Config 30 | } 31 | 32 | var _ asset.Asset = (*BootstrapIgnition)(nil) 33 | 34 | // Name returns the human-friendly name of the asset. 35 | func (i *DeployIgnition) Name() string { 36 | return "Deploy ignition" 37 | } 38 | 39 | // Dependencies returns dependencies used by the asset. 40 | func (i *DeployIgnition) Dependencies() []asset.Asset { 41 | return []asset.Asset{ 42 | &config.EnvConfig{}, 43 | &config.ApplianceConfig{}, 44 | &config.DeployConfig{}, 45 | } 46 | } 47 | 48 | // Generate the base ISO. 49 | func (i *DeployIgnition) Generate(_ context.Context, dependencies asset.Parents) error { 50 | envConfig := &config.EnvConfig{} 51 | applianceConfig := &config.ApplianceConfig{} 52 | deployConfig := &config.DeployConfig{} 53 | dependencies.Get(envConfig, applianceConfig, deployConfig) 54 | 55 | i.Config = igntypes.Config{ 56 | Ignition: igntypes.Ignition{ 57 | Version: igntypes.MaxVersion.String(), 58 | }, 59 | } 60 | 61 | passwdUser := igntypes.PasswdUser{ 62 | Name: "core", 63 | } 64 | 65 | if applianceConfig.Config.UserCorePass != nil { 66 | // Add user 'core' password 67 | passBytes, err := bcrypt.GenerateFromPassword([]byte(*applianceConfig.Config.UserCorePass), bcrypt.DefaultCost) 68 | if err != nil { 69 | return err 70 | } 71 | pwdHash := string(passBytes) 72 | passwdUser.PasswordHash = &pwdHash 73 | } 74 | 75 | // Add public ssh key 76 | if applianceConfig.Config.SshKey != nil { 77 | passwdUser.SSHAuthorizedKeys = []igntypes.SSHAuthorizedKey{ 78 | igntypes.SSHAuthorizedKey(*applianceConfig.Config.SshKey), 79 | } 80 | } 81 | i.Config.Passwd.Users = append(i.Config.Passwd.Users, passwdUser) 82 | 83 | // Create template data 84 | templateData := templates.GetDeployIgnitionTemplateData( 85 | deployConfig.TargetDevice, deployConfig.PostScript, deployConfig.SparseClone, deployConfig.DryRun) 86 | 87 | // Add deploy services 88 | if err := bootstrap.AddSystemdUnits(&i.Config, "services/deploy", templateData, deployServices); err != nil { 89 | return err 90 | } 91 | 92 | // Add deploy scripts to ignition 93 | for _, script := range deployScripts { 94 | if err := bootstrap.AddStorageFiles(&i.Config, 95 | filepath.Join("/usr/local/bin/", script), 96 | "scripts/bin/"+script+".template", 97 | templateData); err != nil { 98 | return err 99 | } 100 | } 101 | 102 | // Add post script if specified 103 | if deployConfig.PostScript != "" { 104 | data, err := os.ReadFile(filepath.Join(envConfig.AssetsDir, deployConfig.PostScript)) 105 | if err != nil { 106 | return err 107 | } 108 | postScript := filepath.Join("/usr/local/bin/", deployConfig.PostScript) 109 | file := ignition.FileFromBytes(postScript, "root", 0755, data) 110 | i.Config.Storage.Files = append(i.Config.Storage.Files, file) 111 | } 112 | 113 | return nil 114 | } 115 | -------------------------------------------------------------------------------- /pkg/asset/ignition/recovery_ignition.go: -------------------------------------------------------------------------------- 1 | package ignition 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | configv32 "github.com/coreos/ignition/v2/config/v3_2" 8 | igntypes "github.com/coreos/ignition/v2/config/v3_2/types" 9 | "github.com/openshift/appliance/pkg/asset/config" 10 | "github.com/openshift/appliance/pkg/asset/manifests" 11 | "github.com/openshift/appliance/pkg/installer" 12 | "github.com/openshift/installer/pkg/asset" 13 | "github.com/pkg/errors" 14 | "github.com/sirupsen/logrus" 15 | ) 16 | 17 | // RecoveryIgnition generates the custom ignition file for the recovery ISO 18 | type RecoveryIgnition struct { 19 | Config igntypes.Config 20 | } 21 | 22 | var _ asset.Asset = (*RecoveryIgnition)(nil) 23 | 24 | // Name returns the human-friendly name of the asset. 25 | func (i *RecoveryIgnition) Name() string { 26 | return "Recovery Ignition" 27 | } 28 | 29 | // Dependencies returns dependencies used by the asset. 30 | func (i *RecoveryIgnition) Dependencies() []asset.Asset { 31 | return []asset.Asset{ 32 | &BootstrapIgnition{}, 33 | &config.EnvConfig{}, 34 | &config.ApplianceConfig{}, 35 | &manifests.UnconfiguredManifests{}, 36 | } 37 | } 38 | 39 | // Generate the ignition embedded in the recovery ISO. 40 | func (i *RecoveryIgnition) Generate(_ context.Context, dependencies asset.Parents) error { 41 | applianceConfig := &config.ApplianceConfig{} 42 | envConfig := &config.EnvConfig{} 43 | bootstrapIgnition := &BootstrapIgnition{} 44 | unconfiguredManifests := &manifests.UnconfiguredManifests{} 45 | dependencies.Get(envConfig, applianceConfig, bootstrapIgnition, unconfiguredManifests) 46 | 47 | // Persists cluster-manifests required for unconfigured ignition 48 | if err := asset.PersistToFile(unconfiguredManifests, envConfig.TempDir); err != nil { 49 | return err 50 | } 51 | 52 | installerConfig := installer.InstallerConfig{ 53 | EnvConfig: envConfig, 54 | ApplianceConfig: applianceConfig, 55 | } 56 | inst := installer.NewInstaller(installerConfig) 57 | unconfiguredIgnitionFileName, err := inst.CreateUnconfiguredIgnition() 58 | if err != nil { 59 | return errors.Wrapf(err, "failed to create un-configured ignition") 60 | } 61 | 62 | configBytes, err := os.ReadFile(unconfiguredIgnitionFileName) 63 | if err != nil { 64 | return errors.Wrapf(err, "failed to fetch un-configured ignition") 65 | } 66 | 67 | unconfiguredIgnitionConfig, _, err := configv32.Parse(configBytes) 68 | if err != nil { 69 | return errors.Wrapf(err, "failed to parse un-configured ignition") 70 | } 71 | 72 | i.Config = configv32.Merge(unconfiguredIgnitionConfig, bootstrapIgnition.Config) 73 | 74 | logrus.Debug("Successfully generated recovery ignition") 75 | 76 | return nil 77 | } 78 | -------------------------------------------------------------------------------- /pkg/asset/installer/installer_binary.go: -------------------------------------------------------------------------------- 1 | package installer 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/openshift/appliance/pkg/asset/config" 7 | "github.com/openshift/appliance/pkg/installer" 8 | "github.com/openshift/installer/pkg/asset" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | type InstallerBinary struct { 13 | URL string 14 | } 15 | 16 | var _ asset.Asset = (*InstallerBinary)(nil) 17 | 18 | func (a *InstallerBinary) Dependencies() []asset.Asset { 19 | return []asset.Asset{ 20 | &config.EnvConfig{}, 21 | &config.ApplianceConfig{}, 22 | } 23 | } 24 | 25 | func (a *InstallerBinary) Generate(_ context.Context, dependencies asset.Parents) error { 26 | envConfig := &config.EnvConfig{} 27 | applianceConfig := &config.ApplianceConfig{} 28 | dependencies.Get(envConfig, applianceConfig) 29 | 30 | installerConfig := installer.InstallerConfig{ 31 | EnvConfig: envConfig, 32 | ApplianceConfig: applianceConfig, 33 | } 34 | inst := installer.NewInstaller(installerConfig) 35 | installerDownloadURL, err := inst.GetInstallerDownloadURL() 36 | if err != nil { 37 | return errors.Wrapf(err, "Failed to generate installer download URL") 38 | } 39 | a.URL = installerDownloadURL 40 | 41 | return nil 42 | } 43 | 44 | // Name returns the human-friendly name of the asset. 45 | func (a *InstallerBinary) Name() string { 46 | return "Installer Binary" 47 | } 48 | -------------------------------------------------------------------------------- /pkg/asset/manifests/agentpullsecret.go: -------------------------------------------------------------------------------- 1 | package manifests 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/pkg/errors" 10 | corev1 "k8s.io/api/core/v1" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "sigs.k8s.io/yaml" 13 | 14 | "github.com/openshift/appliance/pkg/asset/config" 15 | "github.com/openshift/installer/pkg/asset" 16 | ) 17 | 18 | const ( 19 | PullSecretName = "appliance-pull-secret" //nolint:gosec // not a secret despite the word 20 | 21 | pullSecretKey = ".dockerconfigjson" //nolint:gosec 22 | dummyPullSecret = `{"auths":{"":{"auth":"dXNlcjpwYXNz"}}}` //nolint:gosec 23 | ) 24 | 25 | var pullSecretFilename = filepath.Join(clusterManifestDir, "pull-secret.yaml") 26 | 27 | // AgentPullSecret generates the pull-secret file used by the agent installer. 28 | type AgentPullSecret struct { 29 | File *asset.File 30 | Config *corev1.Secret 31 | } 32 | 33 | var _ asset.WritableAsset = (*AgentPullSecret)(nil) 34 | 35 | // Name returns a human friendly name for the asset. 36 | func (*AgentPullSecret) Name() string { 37 | return "Agent PullSecret" 38 | } 39 | 40 | // Dependencies returns all of the dependencies directly needed to generate 41 | // the asset. 42 | func (*AgentPullSecret) Dependencies() []asset.Asset { 43 | return []asset.Asset{ 44 | &config.EnvConfig{}, 45 | } 46 | } 47 | 48 | // Generate generates the AgentPullSecret manifest. 49 | func (a *AgentPullSecret) Generate(_ context.Context, dependencies asset.Parents) error { 50 | secret := &corev1.Secret{ 51 | TypeMeta: metav1.TypeMeta{ 52 | APIVersion: "v1", 53 | Kind: "Secret", 54 | }, 55 | ObjectMeta: metav1.ObjectMeta{ 56 | Name: PullSecretName, 57 | Namespace: "", 58 | }, 59 | StringData: map[string]string{ 60 | pullSecretKey: dummyPullSecret, 61 | }, 62 | } 63 | a.Config = secret 64 | 65 | configData, err := yaml.Marshal(secret) 66 | if err != nil { 67 | return errors.Wrap(err, "failed to marshal secret") 68 | } 69 | 70 | a.File = &asset.File{ 71 | Filename: pullSecretFilename, 72 | Data: configData, 73 | } 74 | 75 | return nil 76 | } 77 | 78 | // Files returns the files generated by the asset. 79 | func (a *AgentPullSecret) Files() []*asset.File { 80 | if a.File != nil { 81 | return []*asset.File{a.File} 82 | } 83 | return []*asset.File{} 84 | } 85 | 86 | // Load returns the asset from disk. 87 | func (a *AgentPullSecret) Load(f asset.FileFetcher) (bool, error) { 88 | file, err := f.FetchByName(pullSecretFilename) 89 | if err != nil { 90 | if os.IsNotExist(err) { 91 | return false, nil 92 | } 93 | return false, errors.Wrap(err, fmt.Sprintf("failed to load %s file", pullSecretFilename)) 94 | } 95 | 96 | config := &corev1.Secret{} 97 | if err := yaml.UnmarshalStrict(file.Data, config); err != nil { 98 | return false, errors.Wrapf(err, "failed to unmarshal %s", pullSecretFilename) 99 | } 100 | 101 | a.Config = config 102 | 103 | return true, nil 104 | } 105 | -------------------------------------------------------------------------------- /pkg/asset/manifests/clusterimageset.go: -------------------------------------------------------------------------------- 1 | package manifests 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/openshift/appliance/pkg/asset/config" 8 | "github.com/pkg/errors" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "sigs.k8s.io/yaml" 11 | 12 | hivev1 "github.com/openshift/hive/apis/hive/v1" 13 | "github.com/openshift/installer/pkg/asset" 14 | ) 15 | 16 | var ( 17 | clusterImageSetFilename = "cluster-image-set.yaml" 18 | ) 19 | 20 | // ClusterImageSet generates the cluster-image-set.yaml file. 21 | type ClusterImageSet struct { 22 | File *asset.File 23 | Config *hivev1.ClusterImageSet 24 | } 25 | 26 | var _ asset.Asset = (*ClusterImageSet)(nil) 27 | 28 | // Name returns a human friendly name for the asset. 29 | func (*ClusterImageSet) Name() string { 30 | return "ClusterImageSet Config" 31 | } 32 | 33 | // Dependencies returns all the dependencies directly needed to generate 34 | // the asset. 35 | func (*ClusterImageSet) Dependencies() []asset.Asset { 36 | return []asset.Asset{ 37 | &config.ApplianceConfig{}, 38 | } 39 | } 40 | 41 | // Generate generates the ClusterImageSet manifest. 42 | func (a *ClusterImageSet) Generate(_ context.Context, dependencies asset.Parents) error { 43 | applianceConfig := &config.ApplianceConfig{} 44 | dependencies.Get(applianceConfig) 45 | 46 | clusterImageSet := &hivev1.ClusterImageSet{ 47 | ObjectMeta: metav1.ObjectMeta{ 48 | Name: fmt.Sprintf("openshift-%s", applianceConfig.Config.OcpRelease.Version), 49 | }, 50 | Spec: hivev1.ClusterImageSetSpec{ 51 | ReleaseImage: *applianceConfig.Config.OcpRelease.URL, 52 | }, 53 | } 54 | a.Config = clusterImageSet 55 | 56 | configData, err := yaml.Marshal(clusterImageSet) 57 | if err != nil { 58 | return errors.Wrap(err, "failed to marshal agent cluster image set") 59 | } 60 | 61 | a.File = &asset.File{ 62 | Filename: clusterImageSetFilename, 63 | Data: configData, 64 | } 65 | 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /pkg/asset/manifests/infraenv.go: -------------------------------------------------------------------------------- 1 | package manifests 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/coreos/stream-metadata-go/arch" 10 | "github.com/pkg/errors" 11 | corev1 "k8s.io/api/core/v1" 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | "sigs.k8s.io/yaml" 14 | 15 | "github.com/openshift/appliance/pkg/asset/config" 16 | aiv1beta1 "github.com/openshift/assisted-service/api/v1beta1" 17 | "github.com/openshift/installer/pkg/asset" 18 | ) 19 | 20 | const ( 21 | infraEnvName = "appliance" 22 | ) 23 | 24 | var ( 25 | infraEnvFilename = filepath.Join(clusterManifestDir, "infraenv.yaml") 26 | ) 27 | 28 | // InfraEnv generates the infraenv.yaml file. 29 | type InfraEnv struct { 30 | File *asset.File 31 | Config *aiv1beta1.InfraEnv 32 | } 33 | 34 | var _ asset.WritableAsset = (*InfraEnv)(nil) 35 | 36 | // Name returns a human friendly name for the asset. 37 | func (*InfraEnv) Name() string { 38 | return "InfraEnv Config" 39 | } 40 | 41 | // Dependencies returns all of the dependencies directly needed to generate 42 | // the asset. 43 | func (*InfraEnv) Dependencies() []asset.Asset { 44 | return []asset.Asset{ 45 | &config.ApplianceConfig{}, 46 | } 47 | } 48 | 49 | // Generate generates the InfraEnv manifest. 50 | func (i *InfraEnv) Generate(_ context.Context, dependencies asset.Parents) error { 51 | applianceConfig := &config.ApplianceConfig{} 52 | dependencies.Get(applianceConfig) 53 | 54 | infraEnv := &aiv1beta1.InfraEnv{ 55 | ObjectMeta: metav1.ObjectMeta{ 56 | Name: infraEnvName, 57 | Namespace: "", 58 | }, 59 | Spec: aiv1beta1.InfraEnvSpec{ 60 | PullSecretRef: &corev1.LocalObjectReference{ 61 | Name: PullSecretName, 62 | }, 63 | CpuArchitecture: applianceConfig.GetCpuArchitecture(), 64 | }, 65 | } 66 | 67 | i.Config = infraEnv 68 | 69 | configData, err := yaml.Marshal(infraEnv) 70 | if err != nil { 71 | return errors.Wrap(err, "failed to marshal infraenv") 72 | } 73 | 74 | i.File = &asset.File{ 75 | Filename: infraEnvFilename, 76 | Data: configData, 77 | } 78 | 79 | return nil 80 | } 81 | 82 | // Files returns the files generated by the asset. 83 | func (i *InfraEnv) Files() []*asset.File { 84 | if i.File != nil { 85 | return []*asset.File{i.File} 86 | } 87 | return []*asset.File{} 88 | } 89 | 90 | // Load returns infraenv asset from the disk. 91 | func (i *InfraEnv) Load(f asset.FileFetcher) (bool, error) { 92 | 93 | file, err := f.FetchByName(infraEnvFilename) 94 | if err != nil { 95 | if os.IsNotExist(err) { 96 | return false, nil 97 | } 98 | return false, errors.Wrap(err, fmt.Sprintf("failed to load %s file", infraEnvFilename)) 99 | } 100 | 101 | config := &aiv1beta1.InfraEnv{} 102 | if err := yaml.UnmarshalStrict(file.Data, config); err != nil { 103 | return false, errors.Wrapf(err, "failed to unmarshal %s", infraEnvFilename) 104 | } 105 | // If defined, convert to RpmArch amd64 -> x86_64 or arm64 -> aarch64 106 | if config.Spec.CpuArchitecture != "" { 107 | config.Spec.CpuArchitecture = arch.RpmArch(config.Spec.CpuArchitecture) 108 | } 109 | i.File, i.Config = file, config 110 | return true, nil 111 | } 112 | -------------------------------------------------------------------------------- /pkg/asset/manifests/operator_crs.go: -------------------------------------------------------------------------------- 1 | package manifests 2 | 3 | import ( 4 | "context" 5 | "path/filepath" 6 | 7 | "github.com/pkg/errors" 8 | 9 | "github.com/openshift/installer/pkg/asset" 10 | ) 11 | 12 | var ( 13 | crsDir = filepath.Join("openshift", "crs") 14 | ) 15 | 16 | // OperatorCRs manifests required for activating deployed operators 17 | type OperatorCRs struct { 18 | FileList []*asset.File 19 | } 20 | 21 | var ( 22 | _ asset.WritableAsset = (*OperatorCRs)(nil) 23 | ) 24 | 25 | // Name returns a human friendly name for the operator 26 | func (em *OperatorCRs) Name() string { 27 | return "Operator CRs" 28 | } 29 | 30 | // Dependencies returns all of the dependencies directly needed by the 31 | // Master asset 32 | func (em *OperatorCRs) Dependencies() []asset.Asset { 33 | return []asset.Asset{} 34 | } 35 | 36 | // Generate is not required for OperatorCRs. 37 | func (em *OperatorCRs) Generate(_ context.Context, dependencies asset.Parents) error { 38 | return nil 39 | } 40 | 41 | // Files returns the files generated by the asset. 42 | func (em *OperatorCRs) Files() []*asset.File { 43 | return em.FileList 44 | } 45 | 46 | // Load reads the asset files from disk. 47 | func (em *OperatorCRs) Load(f asset.FileFetcher) (found bool, err error) { 48 | yamlFileList, err := f.FetchByPattern(filepath.Join(crsDir, "*.yaml")) 49 | if err != nil { 50 | return false, errors.Wrap(err, "failed to load *.yaml files") 51 | } 52 | ymlFileList, err := f.FetchByPattern(filepath.Join(crsDir, "*.yml")) 53 | if err != nil { 54 | return false, errors.Wrap(err, "failed to load *.yml files") 55 | } 56 | 57 | em.FileList = append(em.FileList, yamlFileList...) 58 | em.FileList = append(em.FileList, ymlFileList...) 59 | asset.SortFiles(em.FileList) 60 | 61 | return len(em.FileList) > 0, nil 62 | } 63 | -------------------------------------------------------------------------------- /pkg/asset/manifests/unconfigured.go: -------------------------------------------------------------------------------- 1 | package manifests 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/openshift/appliance/pkg/asset/registry" 7 | "github.com/openshift/installer/pkg/asset" 8 | ) 9 | 10 | const ( 11 | // This could be change to "cluster-manifests" once all the agent code will be migrated to using 12 | // assets (and will stop reading from the hard-code "manifests" relative path) 13 | clusterManifestDir = "cluster-manifests" 14 | ) 15 | 16 | var ( 17 | _ asset.WritableAsset = (*UnconfiguredManifests)(nil) 18 | ) 19 | 20 | // UnconfiguredManifests generates the required manifests for the unconfigured ignition. 21 | type UnconfiguredManifests struct { 22 | FileList []*asset.File 23 | } 24 | 25 | // Name returns a human friendly name. 26 | func (m *UnconfiguredManifests) Name() string { 27 | return "Unconfigured Manifests" 28 | } 29 | 30 | // Dependencies returns all of the dependencies directly needed the asset. 31 | func (m *UnconfiguredManifests) Dependencies() []asset.Asset { 32 | return []asset.Asset{ 33 | &InfraEnv{}, 34 | &AgentPullSecret{}, 35 | ®istry.RegistriesConf{}, 36 | } 37 | } 38 | 39 | // Generate generates the respective manifest files. 40 | func (m *UnconfiguredManifests) Generate(_ context.Context, dependencies asset.Parents) error { 41 | for _, a := range []asset.WritableAsset{ 42 | &InfraEnv{}, 43 | &AgentPullSecret{}, 44 | ®istry.RegistriesConf{}, 45 | } { 46 | dependencies.Get(a) 47 | 48 | m.FileList = append(m.FileList, a.Files()...) 49 | } 50 | 51 | return nil 52 | } 53 | 54 | // Files returns the files generated by the asset. 55 | func (m *UnconfiguredManifests) Files() []*asset.File { 56 | return m.FileList 57 | } 58 | 59 | // Load currently does nothing 60 | func (m *UnconfiguredManifests) Load(f asset.FileFetcher) (bool, error) { 61 | return false, nil 62 | } 63 | -------------------------------------------------------------------------------- /pkg/asset/recovery/base_iso.go: -------------------------------------------------------------------------------- 1 | package recovery 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/openshift/appliance/pkg/asset/config" 8 | "github.com/openshift/appliance/pkg/coreos" 9 | "github.com/openshift/appliance/pkg/log" 10 | "github.com/openshift/installer/pkg/asset" 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // BaseISO generates the base ISO file for the image (CoreOS LiveCD) 15 | type BaseISO struct { 16 | File *asset.File 17 | } 18 | 19 | const ( 20 | coreosIsoName = "coreos-%s.iso" 21 | ) 22 | 23 | var _ asset.Asset = (*BaseISO)(nil) 24 | 25 | // Name returns the human-friendly name of the asset. 26 | func (i *BaseISO) Name() string { 27 | return "Base ISO (CoreOS)" 28 | } 29 | 30 | // Dependencies returns dependencies used by the asset. 31 | func (i *BaseISO) Dependencies() []asset.Asset { 32 | return []asset.Asset{ 33 | &config.EnvConfig{}, 34 | &config.ApplianceConfig{}, 35 | } 36 | } 37 | 38 | // Generate the base ISO. 39 | func (i *BaseISO) Generate(_ context.Context, dependencies asset.Parents) error { 40 | envConfig := &config.EnvConfig{} 41 | applianceConfig := &config.ApplianceConfig{} 42 | dependencies.Get(envConfig, applianceConfig) 43 | 44 | // Search for disk image in cache dir 45 | filePattern := fmt.Sprintf(coreosIsoName, applianceConfig.GetCpuArchitecture()) 46 | if fileName := envConfig.FindInCache(filePattern); fileName != "" { 47 | logrus.Info("Reusing base CoreOS ISO from cache") 48 | i.File = &asset.File{Filename: fileName} 49 | return nil 50 | } 51 | 52 | // Download base CoreOS ISO according to specified release image 53 | spinner := log.NewSpinner( 54 | "Downloading CoreOS ISO...", 55 | "Successfully downloaded CoreOS ISO", 56 | "Failed to download CoreOS ISO", 57 | envConfig, 58 | ) 59 | spinner.FileToMonitor = filePattern 60 | 61 | coreOSConfig := coreos.CoreOSConfig{ 62 | ApplianceConfig: applianceConfig, 63 | EnvConfig: envConfig, 64 | } 65 | c := coreos.NewCoreOS(coreOSConfig) 66 | fileName, err := c.DownloadISO() 67 | if err != nil { 68 | return log.StopSpinner(spinner, err) 69 | } 70 | 71 | i.File = &asset.File{Filename: fileName} 72 | 73 | return log.StopSpinner(spinner, nil) 74 | } 75 | -------------------------------------------------------------------------------- /pkg/asset/recovery/recovery_iso.go: -------------------------------------------------------------------------------- 1 | package recovery 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/openshift/appliance/pkg/asset/config" 11 | "github.com/openshift/appliance/pkg/asset/ignition" 12 | "github.com/openshift/appliance/pkg/consts" 13 | "github.com/openshift/appliance/pkg/coreos" 14 | "github.com/openshift/appliance/pkg/log" 15 | "github.com/openshift/assisted-image-service/pkg/isoeditor" 16 | "github.com/openshift/installer/pkg/asset" 17 | "github.com/sirupsen/logrus" 18 | ) 19 | 20 | const ( 21 | recoveryIsoDirName = "recovery_iso" 22 | ) 23 | 24 | // RecoveryISO is an asset that generates the bootable ISO copied 25 | // to a recovery partition in the OpenShift-based appliance. 26 | type RecoveryISO struct { 27 | File *asset.File 28 | Size int64 29 | Ignition *ignition.RecoveryIgnition 30 | } 31 | 32 | var _ asset.Asset = (*RecoveryISO)(nil) 33 | 34 | // Dependencies returns the assets on which the Bootstrap asset depends. 35 | func (a *RecoveryISO) Dependencies() []asset.Asset { 36 | return []asset.Asset{ 37 | &config.EnvConfig{}, 38 | &config.ApplianceConfig{}, 39 | &ignition.RecoveryIgnition{}, 40 | &BaseISO{}, 41 | } 42 | } 43 | 44 | // Generate the recovery ISO. 45 | func (a *RecoveryISO) Generate(_ context.Context, dependencies asset.Parents) error { 46 | envConfig := &config.EnvConfig{} 47 | baseISO := &BaseISO{} 48 | applianceConfig := &config.ApplianceConfig{} 49 | recoveryIgnition := &ignition.RecoveryIgnition{} 50 | dependencies.Get(envConfig, baseISO, applianceConfig, recoveryIgnition) 51 | 52 | coreosIsoFileName := fmt.Sprintf(coreosIsoName, applianceConfig.GetCpuArchitecture()) 53 | coreosIsoPath := filepath.Join(envConfig.CacheDir, coreosIsoFileName) 54 | recoveryIsoPath := filepath.Join(envConfig.CacheDir, consts.RecoveryIsoFileName) 55 | recoveryIsoDirPath := filepath.Join(envConfig.TempDir, recoveryIsoDirName) 56 | 57 | var spinner *log.Spinner 58 | 59 | // Search for ISO in cache dir 60 | if fileName := envConfig.FindInCache(consts.RecoveryIsoFileName); fileName != "" { 61 | logrus.Info("Reusing recovery CoreOS ISO from cache") 62 | a.File = &asset.File{Filename: fileName} 63 | } else { 64 | spinner = log.NewSpinner( 65 | "Generating recovery CoreOS ISO...", 66 | "Successfully generated recovery CoreOS ISO", 67 | "Failed to generate recovery CoreOS ISO", 68 | envConfig, 69 | ) 70 | spinner.FileToMonitor = consts.RecoveryIsoFileName 71 | 72 | // Extracting the base ISO and generating the recovery ISO with a different volume label ('agentboot'). 73 | if err := os.MkdirAll(recoveryIsoDirPath, os.ModePerm); err != nil { 74 | logrus.Errorf("Failed to create dir: %s", recoveryIsoDirPath) 75 | return err 76 | } 77 | if err := isoeditor.Extract(coreosIsoPath, recoveryIsoDirPath); err != nil { 78 | logrus.Errorf("Failed to extract ISO: %s", err.Error()) 79 | return err 80 | } 81 | if err := isoeditor.Create(recoveryIsoPath, recoveryIsoDirPath, consts.RecoveryPartitionName); err != nil { 82 | logrus.Errorf("Failed to create ISO: %s", err.Error()) 83 | return err 84 | } 85 | } 86 | 87 | // Embed ignition in ISO 88 | coreOSConfig := coreos.CoreOSConfig{ 89 | ApplianceConfig: applianceConfig, 90 | EnvConfig: envConfig, 91 | } 92 | c := coreos.NewCoreOS(coreOSConfig) 93 | ignitionBytes, err := json.Marshal(recoveryIgnition.Config) 94 | if err != nil { 95 | logrus.Errorf("Failed to marshal recovery ignition to json: %s", err.Error()) 96 | return log.StopSpinner(spinner, err) 97 | } 98 | if err = c.EmbedIgnition(ignitionBytes, recoveryIsoPath); err != nil { 99 | logrus.Errorf("Failed to embed ignition in recovery ISO: %s", err.Error()) 100 | return log.StopSpinner(spinner, err) 101 | } 102 | 103 | return log.StopSpinner(spinner, a.updateAsset(recoveryIsoPath, recoveryIgnition)) 104 | } 105 | 106 | // Name returns the human-friendly name of the asset. 107 | func (a *RecoveryISO) Name() string { 108 | return "Appliance Recovery ISO" 109 | } 110 | 111 | func (a *RecoveryISO) updateAsset(recoveryIsoPath string, ignition *ignition.RecoveryIgnition) error { 112 | a.File = &asset.File{Filename: recoveryIsoPath} 113 | f, err := os.Stat(recoveryIsoPath) 114 | if err != nil { 115 | return err 116 | } 117 | a.Size = f.Size() 118 | a.Ignition = ignition 119 | 120 | return nil 121 | } 122 | -------------------------------------------------------------------------------- /pkg/asset/registry/registriesconf.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/containers/image/pkg/sysregistriesv2" 11 | "github.com/openshift/appliance/pkg/asset/config" 12 | "github.com/openshift/appliance/pkg/consts" 13 | "github.com/openshift/installer/pkg/asset" 14 | "github.com/pelletier/go-toml" 15 | "github.com/pkg/errors" 16 | "github.com/sirupsen/logrus" 17 | "gopkg.in/yaml.v2" 18 | ) 19 | 20 | const ( 21 | RegistryDomain = "registry.appliance.com" 22 | RegistryPort = 5000 23 | RegistryPortUpgrade = 5001 24 | ) 25 | 26 | var ( 27 | registriesConfFilename = filepath.Join("mirror", "registries.conf") 28 | idmsFileName = filepath.Join(consts.OcMirrorResourcesDir, "idms-oc-mirror.yaml") 29 | ) 30 | 31 | type ImageDigestMirrorSet struct { 32 | APIVersion string `yaml:"apiVersion"` 33 | Kind string `yaml:"kind"` 34 | Metadata struct { 35 | Name string `yaml:"name"` 36 | } `yaml:"metadata"` 37 | Spec struct { 38 | ImageDigestMirrors []ImageDigestMirror `yaml:"imageDigestMirrors"` 39 | } `yaml:"spec"` 40 | Status struct { 41 | // You can define specific fields for Status if needed. Here it is an empty struct for now. 42 | } `yaml:"status"` 43 | } 44 | type ImageDigestMirror struct { 45 | Mirrors []string `yaml:"mirrors"` 46 | Source string `yaml:"source"` 47 | } 48 | 49 | // RegistriesConf generates the registries.conf file. 50 | type RegistriesConf struct { 51 | File *asset.File 52 | Config *sysregistriesv2.V2RegistriesConf 53 | } 54 | 55 | var _ asset.Asset = (*RegistriesConf)(nil) 56 | 57 | // Name returns a human friendly name for the asset. 58 | func (*RegistriesConf) Name() string { 59 | return "Mirror Registries Config" 60 | } 61 | 62 | // Dependencies returns all the dependencies directly needed to generate 63 | // the asset. 64 | func (*RegistriesConf) Dependencies() []asset.Asset { 65 | return []asset.Asset{ 66 | &config.EnvConfig{}, 67 | &config.ApplianceConfig{}, 68 | } 69 | } 70 | 71 | // Generate generates the registries.conf file from install-config. 72 | func (i *RegistriesConf) Generate(_ context.Context, dependencies asset.Parents) error { 73 | envConfig := &config.EnvConfig{} 74 | applianceConfig := &config.ApplianceConfig{} 75 | dependencies.Get(envConfig, applianceConfig) 76 | 77 | releaseImagesLocation, releaseLocation := i.getEndpointLocations(envConfig.CacheDir) 78 | registries := &sysregistriesv2.V2RegistriesConf{ 79 | Registries: []sysregistriesv2.Registry{ 80 | { 81 | Endpoint: sysregistriesv2.Endpoint{ 82 | Location: releaseImagesLocation, 83 | }, 84 | Mirrors: []sysregistriesv2.Endpoint{ 85 | { 86 | Location: fmt.Sprintf("%s:%d/openshift/release-images", RegistryDomain, RegistryPort), 87 | }, 88 | // Mirror for the upgrade registry 89 | { 90 | Location: fmt.Sprintf("%s:%d/openshift/release-images", RegistryDomain, RegistryPortUpgrade), 91 | }, 92 | }, 93 | }, 94 | { 95 | Endpoint: sysregistriesv2.Endpoint{ 96 | Location: releaseLocation, 97 | }, 98 | Mirrors: []sysregistriesv2.Endpoint{ 99 | { 100 | Location: fmt.Sprintf("%s:%d/openshift/release", RegistryDomain, RegistryPort), 101 | }, 102 | // Mirror for the upgrade registry 103 | { 104 | Location: fmt.Sprintf("%s:%d/openshift/release", RegistryDomain, RegistryPortUpgrade), 105 | }, 106 | }, 107 | }, 108 | }, 109 | } 110 | 111 | registriesData, err := toml.Marshal(registries) 112 | if err != nil { 113 | return err 114 | } 115 | 116 | i.File = &asset.File{ 117 | Filename: registriesConfFilename, 118 | Data: registriesData, 119 | } 120 | 121 | return nil 122 | } 123 | 124 | func (i *RegistriesConf) getEndpointLocations(cacheDir string) (string, string) { 125 | releaseImagesLocation := "quay.io/openshift-release-dev/ocp-release" 126 | releaseLocation := "quay.io/openshift-release-dev/ocp-v4.0-art-dev" 127 | 128 | idmsFile, err := os.ReadFile(filepath.Join(cacheDir, idmsFileName)) 129 | if err != nil { 130 | logrus.Debugf("missing IDMS yaml (%v), fallback to defaults.", err) 131 | return releaseImagesLocation, releaseLocation 132 | } 133 | var idms *ImageDigestMirrorSet 134 | if err := yaml.UnmarshalStrict(idmsFile, &idms); err != nil { 135 | logrus.Debugf("failed to unmarshal IDMS yaml (%v), fallback to defaults.", err) 136 | return releaseImagesLocation, releaseLocation 137 | } 138 | 139 | for _, digestMirrors := range idms.Spec.ImageDigestMirrors { 140 | if len(digestMirrors.Mirrors) == 0 { 141 | continue 142 | } 143 | location := digestMirrors.Mirrors[0] 144 | if strings.HasSuffix(location, "release-images") { 145 | releaseImagesLocation = digestMirrors.Source 146 | } else if strings.HasSuffix(location, "release") { 147 | releaseLocation = digestMirrors.Source 148 | } 149 | } 150 | 151 | return releaseImagesLocation, releaseLocation 152 | } 153 | 154 | func (i *RegistriesConf) Load(f asset.FileFetcher) (bool, error) { 155 | file, err := f.FetchByName(registriesConfFilename) 156 | if err != nil { 157 | if os.IsNotExist(err) { 158 | return false, nil 159 | } 160 | return false, errors.Wrap(err, fmt.Sprintf("failed to load %s file", registriesConfFilename)) 161 | } 162 | 163 | registriesConf := &sysregistriesv2.V2RegistriesConf{} 164 | if err := toml.Unmarshal(file.Data, registriesConf); err != nil { 165 | return false, errors.Wrapf(err, "failed to unmarshal %s", registriesConfFilename) 166 | } 167 | 168 | i.File, i.Config = file, registriesConf 169 | 170 | return true, nil 171 | } 172 | 173 | // Files returns the files generated by the asset. 174 | func (i *RegistriesConf) Files() []*asset.File { 175 | if i.File != nil { 176 | return []*asset.File{i.File} 177 | } 178 | return []*asset.File{} 179 | } 180 | -------------------------------------------------------------------------------- /pkg/consts/consts.go: -------------------------------------------------------------------------------- 1 | package consts 2 | 3 | const ( 4 | MaxOcpVersion = "4.19" // Latest supported version (update on each release) 5 | MinOcpVersion = "4.14" 6 | 7 | // user.cfg template 8 | UserCfgTemplateFile = "scripts/grub/user.cfg.template" 9 | GrubTimeout = 10 10 | GrubMenuEntryName = "Agent-Based Installer" 11 | // For installation ignition 12 | GrubMenuEntryNameRecovery = "Recovery: Agent-Based Installer (Reinstall Cluster)" 13 | GrubCfgFilePath = "/boot/grub2/grub.cfg" 14 | UserCfgFilePath = "/etc/assisted/user.cfg" 15 | 16 | // guestfish.sh template 17 | GuestfishScriptTemplateFile = "scripts/guestfish/guestfish.sh.template" 18 | ApplianceFileName = "appliance.raw" 19 | RecoveryIsoFileName = "recovery.iso" 20 | DataIsoFileName = "data.iso" 21 | CoreosImagePattern = "rhcos-*%s.raw" 22 | 23 | // Appliance Live ISO 24 | ApplianceLiveIsoFileName = "appliance.iso" 25 | 26 | // ImageSetTemplateFile imageset.yaml.template 27 | ImageSetTemplateFile = "scripts/mirror/imageset.yaml.template" 28 | 29 | // PinnedImageSetTemplateFile template 30 | PinnedImageSetTemplateFile = "scripts/mirror/pinned-image-set.yaml.template" 31 | // PinnedImageSetPattern - for installation ignition 32 | PinnedImageSetPattern = "/etc/assisted/%s-pinned-image-set.yaml" 33 | // OcMirrorMappingFileName - name of the mapping file created by oc mirror 34 | OcMirrorMappingFileName = "mapping.txt" 35 | // OcMirrorResourcesDir - cluster resources directory created by oc mirror 36 | OcMirrorResourcesDir = "cluster-resources" 37 | // MinOcpVersionForPinnedImageSet - minimum version that supports PinnedImageSet 38 | MinOcpVersionForPinnedImageSet = "4.16" 39 | 40 | // Recovery/Data partitions 41 | RecoveryPartitionName = "agentboot" 42 | DataPartitionName = "agentdata" 43 | 44 | // ReservedPartitionGUID Set partition as Linux reserved partition: https://en.wikipedia.org/wiki/GUID_Partition_Table 45 | ReservedPartitionGUID = "8DA63339-0007-60C0-C436-083AC8230908" 46 | 47 | // Local registry 48 | RegistryImage = "localhost/registry:latest" 49 | RegistryFilePath = "registry/registry.tar" 50 | RegistryPort = 5005 51 | 52 | // Local registry env file 53 | RegistryEnvPath = "/etc/assisted/registry.env" 54 | RegistryDataBootstrap = "/tmp/registry" 55 | RegistryDataInstall = "/mnt/agentdata/oc-mirror/install" 56 | RegistryDataUpgrade = "/media/upgrade/oc-mirror/install" 57 | 58 | // Deployment ISO 59 | CoreosIsoName = "coreos-%s.iso" 60 | DeployIsoName = "appliance.iso" 61 | DeployDir = "deploy" 62 | ApplianceImageName = "appliance" 63 | ApplianceImageTar = "appliance.tar" 64 | ApplianceImage = "quay.io/edge-infrastructure/openshift-appliance:latest" 65 | 66 | // Upgrade ISO 67 | UpgradeISONamePattern = "upgrade-%s.iso" 68 | 69 | // Appliance config flags (default values) 70 | EnableDefaultSources = false 71 | StopLocalRegistry = false 72 | CreatePinnedImageSets = false 73 | EnableFips = false 74 | EnableInteractiveFlow = false 75 | UseDefaultSourceNames = false 76 | ) 77 | -------------------------------------------------------------------------------- /pkg/conversions/conversions.go: -------------------------------------------------------------------------------- 1 | package conversions 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/alecthomas/units" 7 | ) 8 | 9 | func GbToBytes(gb int64) int64 { 10 | return gb * int64(units.GB) 11 | } 12 | 13 | func GibToBytes(gib int64) int64 { 14 | return gib * int64(units.GiB) 15 | } 16 | 17 | func GibToMib(gib int64) int64 { 18 | return gib * int64(units.KiB) 19 | } 20 | 21 | func MibToGiB(mib int64) int64 { 22 | return mib / int64(units.KiB) 23 | } 24 | 25 | func BytesToGb(bytes int64) int64 { 26 | return bytes / int64(units.GB) 27 | } 28 | 29 | func BytesToGib(bytes int64) int64 { 30 | return bytes / int64(units.GiB) 31 | } 32 | 33 | func MibToBytes(mib int64) int64 { 34 | return mib * int64(units.MiB) 35 | } 36 | 37 | func BytesToMib(bytes int64) int64 { 38 | return bytes / int64(units.MiB) 39 | } 40 | 41 | func GbToMib(gb int64) int64 { 42 | return BytesToMib(GbToBytes(gb)) 43 | } 44 | 45 | const ( 46 | _ = iota 47 | // KiB 1024 bytes 48 | KiB = 1 << (10 * iota) 49 | // MiB 1024 KiB 50 | MiB 51 | // GiB 1024 MiB 52 | GiB 53 | // TiB 1024 GiB 54 | TiB 55 | // PiB 1024 TiB 56 | PiB 57 | ) 58 | 59 | const ( 60 | KB = 1000 61 | MB = 1000 * KB 62 | GB = 1000 * MB 63 | TB = 1000 * GB 64 | PB = 1000 * TB 65 | ) 66 | 67 | func BytesToString(b int64) string { 68 | if b >= PiB { 69 | return fmt.Sprintf("%.2f PiB", float64(b)/float64(PiB)) 70 | } 71 | if b >= TiB { 72 | return fmt.Sprintf("%.2f TiB", float64(b)/float64(TiB)) 73 | } 74 | if b >= GiB { 75 | return fmt.Sprintf("%.2f GiB", float64(b)/float64(GiB)) 76 | } 77 | if b >= MiB { 78 | return fmt.Sprintf("%v MiB", b/MiB) 79 | } 80 | if b >= KiB { 81 | return fmt.Sprintf("%v KiB", b/KiB) 82 | } 83 | return fmt.Sprintf("%v bytes", b) 84 | } 85 | -------------------------------------------------------------------------------- /pkg/coreos/coreos.go: -------------------------------------------------------------------------------- 1 | package coreos 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/cavaliergopher/grab/v3" 10 | "github.com/itchyny/gojq" 11 | "github.com/openshift/appliance/pkg/asset/config" 12 | "github.com/openshift/appliance/pkg/executer" 13 | "github.com/openshift/appliance/pkg/release" 14 | "github.com/pkg/errors" 15 | "github.com/sirupsen/logrus" 16 | ) 17 | 18 | const ( 19 | templateEmbedIgnition = "coreos-installer iso ignition embed -f --ignition-file %s %s" 20 | machineOsImageName = "machine-os-images" 21 | coreOsFileName = "coreos/coreos-%s.iso" 22 | coreOsStream = "coreos/coreos-stream.json" 23 | coreOsDiskImageUrlQuery = ".architectures.x86_64.artifacts.metal.formats[\"raw.gz\"].disk.location" 24 | 25 | CoreOsDiskImageGz = "coreos.tar.gz" 26 | ) 27 | 28 | type CoreOS interface { 29 | DownloadDiskImage() (string, error) 30 | DownloadISO() (string, error) 31 | EmbedIgnition(ignition []byte, isoPath string) error 32 | FetchCoreOSStream() (map[string]any, error) 33 | } 34 | 35 | type CoreOSConfig struct { 36 | Executer executer.Executer 37 | EnvConfig *config.EnvConfig 38 | Release release.Release 39 | ApplianceConfig *config.ApplianceConfig 40 | } 41 | 42 | type coreos struct { 43 | CoreOSConfig 44 | } 45 | 46 | func NewCoreOS(config CoreOSConfig) CoreOS { 47 | if config.Executer == nil { 48 | config.Executer = executer.NewExecuter() 49 | } 50 | if config.Release == nil { 51 | releaseConfig := release.ReleaseConfig{ 52 | ApplianceConfig: config.ApplianceConfig, 53 | EnvConfig: config.EnvConfig, 54 | } 55 | config.Release = release.NewRelease(releaseConfig) 56 | } 57 | 58 | return &coreos{ 59 | CoreOSConfig: config, 60 | } 61 | } 62 | 63 | func (c *coreos) DownloadDiskImage() (string, error) { 64 | coreosStream, err := c.FetchCoreOSStream() 65 | if err != nil { 66 | return "", err 67 | } 68 | query, err := gojq.Parse(coreOsDiskImageUrlQuery) 69 | if err != nil { 70 | return "", err 71 | } 72 | iter := query.Run(coreosStream) 73 | v, ok := iter.Next() 74 | if !ok { 75 | return "", err 76 | } 77 | 78 | rawGzUrl := v.(string) 79 | compressed := filepath.Join(c.EnvConfig.TempDir, CoreOsDiskImageGz) 80 | _, err = grab.Get(compressed, rawGzUrl) 81 | if err != nil { 82 | return "", err 83 | } 84 | 85 | return compressed, nil 86 | } 87 | 88 | func (c *coreos) DownloadISO() (string, error) { 89 | fileName := fmt.Sprintf(coreOsFileName, c.ApplianceConfig.GetCpuArchitecture()) 90 | return c.Release.ExtractFile(machineOsImageName, fileName) 91 | } 92 | 93 | func (c *coreos) EmbedIgnition(ignition []byte, isoPath string) error { 94 | // Write ignition to a temporary file 95 | ignitionFile, err := os.CreateTemp(c.EnvConfig.TempDir, "config.ign") 96 | if err != nil { 97 | return err 98 | } 99 | defer func() { 100 | ignitionFile.Close() 101 | os.Remove(ignitionFile.Name()) 102 | }() 103 | _, err = ignitionFile.Write(ignition) 104 | if err != nil { 105 | logrus.Errorf("Failed to write ignition data into %s: %s", ignitionFile.Name(), err.Error()) 106 | return err 107 | } 108 | ignitionFile.Close() 109 | 110 | // Invoke embed ignition command 111 | embedCmd := fmt.Sprintf(templateEmbedIgnition, ignitionFile.Name(), isoPath) 112 | _, err = c.Executer.Execute(embedCmd) 113 | return err 114 | } 115 | 116 | func (c *coreos) FetchCoreOSStream() (map[string]any, error) { 117 | path, err := c.Release.ExtractFile(machineOsImageName, coreOsStream) 118 | if err != nil { 119 | return nil, err 120 | } 121 | 122 | file, err := os.ReadFile(path) 123 | if err != nil { 124 | return nil, err 125 | } 126 | 127 | var m map[string]any 128 | if err = json.Unmarshal(file, &m); err != nil { 129 | return nil, errors.Wrap(err, "failed to parse CoreOS stream metadata") 130 | } 131 | 132 | return m, nil 133 | } 134 | -------------------------------------------------------------------------------- /pkg/coreos/coreos_test.go: -------------------------------------------------------------------------------- 1 | package coreos 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/go-openapi/swag" 9 | "github.com/openshift/appliance/pkg/types" 10 | 11 | "github.com/golang/mock/gomock" 12 | . "github.com/onsi/ginkgo/v2/dsl/core" 13 | . "github.com/onsi/gomega" 14 | "github.com/openshift/appliance/pkg/asset/config" 15 | "github.com/openshift/appliance/pkg/executer" 16 | "github.com/openshift/appliance/pkg/release" 17 | ) 18 | 19 | var _ = Describe("Test CoreOS", func() { 20 | var ( 21 | ctrl *gomock.Controller 22 | mockExecuter *executer.MockExecuter 23 | mockRelease *release.MockRelease 24 | testCoreOs CoreOS 25 | ) 26 | 27 | BeforeEach(func() { 28 | ctrl = gomock.NewController(GinkgoT()) 29 | mockExecuter = executer.NewMockExecuter(ctrl) 30 | mockRelease = release.NewMockRelease(ctrl) 31 | coreOSConfig := CoreOSConfig{ 32 | ApplianceConfig: &config.ApplianceConfig{ 33 | Config: &types.ApplianceConfig{ 34 | PullSecret: "'{\"auths\":{\"\":{\"auth\":\"dXNlcjpwYXNz\"}}}'", 35 | OcpRelease: types.ReleaseImage{ 36 | CpuArchitecture: swag.String(config.CpuArchitectureX86), 37 | }, 38 | }, 39 | }, 40 | Release: mockRelease, 41 | Executer: mockExecuter, 42 | EnvConfig: &config.EnvConfig{}, 43 | } 44 | testCoreOs = NewCoreOS(coreOSConfig) 45 | }) 46 | 47 | It("DownloadISO - success", func() { 48 | mockRelease.EXPECT().ExtractFile(machineOsImageName, fmt.Sprintf(coreOsFileName, config.CpuArchitectureX86)).Return("/path/to/file", nil).Times(1) 49 | _, err := testCoreOs.DownloadISO() 50 | Expect(err).ToNot(HaveOccurred()) 51 | }) 52 | 53 | It("DownloadISO - fail", func() { 54 | mockRelease.EXPECT().ExtractFile(machineOsImageName, fmt.Sprintf(coreOsFileName, config.CpuArchitectureX86)).Return("", errors.New("some error")).Times(1) 55 | _, err := testCoreOs.DownloadISO() 56 | Expect(err).To(HaveOccurred()) 57 | }) 58 | 59 | It("FetchCoreOSStream - fail", func() { 60 | mockRelease.EXPECT().ExtractFile(machineOsImageName, coreOsStream).Return("", errors.New("some error")).Times(1) 61 | _, err := testCoreOs.FetchCoreOSStream() 62 | Expect(err).To(HaveOccurred()) 63 | }) 64 | 65 | }) 66 | 67 | func TestCoreOS(t *testing.T) { 68 | RegisterFailHandler(Fail) 69 | RunSpecs(t, "coreos_test") 70 | } 71 | -------------------------------------------------------------------------------- /pkg/executer/executer.go: -------------------------------------------------------------------------------- 1 | package executer 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "os/exec" 7 | "strings" 8 | 9 | "github.com/pkg/errors" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | //go:generate mockgen -source=executer.go -package=executer -destination=mock_executer.go 14 | type Executer interface { 15 | Execute(command string) (string, error) 16 | TempFile(dir, pattern string) (f *os.File, err error) 17 | } 18 | 19 | type executer struct { 20 | } 21 | 22 | func NewExecuter() Executer { 23 | return &executer{} 24 | } 25 | 26 | func (e *executer) Execute(command string) (string, error) { 27 | var stdoutBytes, stderrBytes bytes.Buffer 28 | 29 | formattedCmd, args := e.formatCommand(command) 30 | logrus.Debugf("Running cmd: %s %s", formattedCmd, strings.Join(args[:], " ")) 31 | cmd := exec.Command(formattedCmd, args...) 32 | cmd.Stdout = &stdoutBytes 33 | cmd.Stderr = &stderrBytes 34 | err := cmd.Run() 35 | if err != nil { 36 | return "", errors.Wrapf(err, "Failed to execute cmd (%s): %s", cmd, stderrBytes.String()) 37 | } 38 | 39 | return strings.TrimSuffix(stdoutBytes.String(), "\n"), nil 40 | } 41 | 42 | func (e *executer) TempFile(dir, pattern string) (f *os.File, err error) { 43 | return os.CreateTemp(dir, pattern) 44 | } 45 | 46 | func (e *executer) formatCommand(command string) (string, []string) { 47 | formattedCmd := strings.Split(command, " ") 48 | return formattedCmd[0], formattedCmd[1:] 49 | } 50 | -------------------------------------------------------------------------------- /pkg/executer/mock_executer.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: executer.go 3 | 4 | // Package executer is a generated GoMock package. 5 | package executer 6 | 7 | import ( 8 | os "os" 9 | reflect "reflect" 10 | 11 | gomock "github.com/golang/mock/gomock" 12 | ) 13 | 14 | // MockExecuter is a mock of Executer interface. 15 | type MockExecuter struct { 16 | ctrl *gomock.Controller 17 | recorder *MockExecuterMockRecorder 18 | } 19 | 20 | // MockExecuterMockRecorder is the mock recorder for MockExecuter. 21 | type MockExecuterMockRecorder struct { 22 | mock *MockExecuter 23 | } 24 | 25 | // NewMockExecuter creates a new mock instance. 26 | func NewMockExecuter(ctrl *gomock.Controller) *MockExecuter { 27 | mock := &MockExecuter{ctrl: ctrl} 28 | mock.recorder = &MockExecuterMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use. 33 | func (m *MockExecuter) EXPECT() *MockExecuterMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // Execute mocks base method. 38 | func (m *MockExecuter) Execute(command string) (string, error) { 39 | m.ctrl.T.Helper() 40 | ret := m.ctrl.Call(m, "Execute", command) 41 | ret0, _ := ret[0].(string) 42 | ret1, _ := ret[1].(error) 43 | return ret0, ret1 44 | } 45 | 46 | // Execute indicates an expected call of Execute. 47 | func (mr *MockExecuterMockRecorder) Execute(command interface{}) *gomock.Call { 48 | mr.mock.ctrl.T.Helper() 49 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Execute", reflect.TypeOf((*MockExecuter)(nil).Execute), command) 50 | } 51 | 52 | // TempFile mocks base method. 53 | func (m *MockExecuter) TempFile(dir, pattern string) (*os.File, error) { 54 | m.ctrl.T.Helper() 55 | ret := m.ctrl.Call(m, "TempFile", dir, pattern) 56 | ret0, _ := ret[0].(*os.File) 57 | ret1, _ := ret[1].(error) 58 | return ret0, ret1 59 | } 60 | 61 | // TempFile indicates an expected call of TempFile. 62 | func (mr *MockExecuterMockRecorder) TempFile(dir, pattern interface{}) *gomock.Call { 63 | mr.mock.ctrl.T.Helper() 64 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TempFile", reflect.TypeOf((*MockExecuter)(nil).TempFile), dir, pattern) 65 | } 66 | -------------------------------------------------------------------------------- /pkg/fileutil/fileutil.go: -------------------------------------------------------------------------------- 1 | package fileutil 2 | 3 | import ( 4 | "compress/gzip" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/openshift/appliance/pkg/executer" 11 | ) 12 | 13 | const ( 14 | splitCmd = "split %s %s -b %s" 15 | ) 16 | 17 | type OSInterface interface { 18 | MkdirTemp(dir, prefix string) (string, error) 19 | Stat(name string) (os.FileInfo, error) 20 | Remove(name string) error 21 | UserHomeDir() (string, error) 22 | MkdirAll(path string, perm os.FileMode) error 23 | WriteFile(name string, data []byte, perm os.FileMode) error 24 | ReadFile(name string) ([]byte, error) 25 | RemoveAll(path string) error 26 | } 27 | 28 | type OSFS struct{} 29 | 30 | func (OSFS) MkdirTemp(dir, prefix string) (string, error) { 31 | return os.MkdirTemp(dir, prefix) 32 | } 33 | 34 | func (OSFS) Stat(name string) (os.FileInfo, error) { 35 | return os.Stat(name) 36 | } 37 | 38 | func (OSFS) Remove(name string) error { 39 | return os.Remove(name) 40 | } 41 | 42 | func (OSFS) UserHomeDir() (string, error) { 43 | return os.UserHomeDir() 44 | } 45 | 46 | func (OSFS) MkdirAll(path string, perm os.FileMode) error { 47 | return os.MkdirAll(path, perm) 48 | } 49 | 50 | func (OSFS) WriteFile(name string, data []byte, perm os.FileMode) error { 51 | return os.WriteFile(name, data, perm) 52 | } 53 | 54 | func (OSFS) ReadFile(name string) ([]byte, error) { 55 | return os.ReadFile(name) 56 | } 57 | 58 | func (OSFS) RemoveAll(path string) error { 59 | return os.RemoveAll(path) 60 | } 61 | 62 | func CopyFile(source, dest string) error { 63 | // Read source file 64 | bytesRead, err := os.ReadFile(source) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | // Get source file info 70 | fileinfo, err := os.Stat(source) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | // Create dest dir 76 | if err = os.MkdirAll(filepath.Dir(dest), os.ModePerm); err != nil { 77 | return err 78 | } 79 | 80 | // Copy file to dest 81 | if err = os.WriteFile(dest, bytesRead, fileinfo.Mode().Perm()); err != nil { 82 | return err 83 | } 84 | 85 | return nil 86 | } 87 | 88 | func ExtractCompressedFile(source, target string) (string, error) { 89 | reader, err := os.Open(source) 90 | if err != nil { 91 | return "", err 92 | } 93 | defer reader.Close() 94 | 95 | archive, err := gzip.NewReader(reader) 96 | if err != nil { 97 | return "", err 98 | } 99 | defer archive.Close() 100 | 101 | target = filepath.Join(target, archive.Name) 102 | writer, err := os.Create(target) 103 | if err != nil { 104 | return "", err 105 | } 106 | defer writer.Close() 107 | 108 | _, err = io.Copy(writer, archive) // #nosec G110 109 | return target, err 110 | } 111 | 112 | func SplitFile(filePath, destPath, partSize string) error { 113 | exec := executer.NewExecuter() 114 | _, err := exec.Execute(fmt.Sprintf(splitCmd, filePath, destPath, partSize)) 115 | return err 116 | } 117 | -------------------------------------------------------------------------------- /pkg/genisoimage/genisoimage.go: -------------------------------------------------------------------------------- 1 | package genisoimage 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/openshift/appliance/pkg/executer" 7 | ) 8 | 9 | const ( 10 | genDataImageCmd = "genisoimage --iso-level 3 -R -D -V %s -o %s/%s %s" 11 | ) 12 | 13 | type GenIsoImage interface { 14 | GenerateImage(imagePath, imageName, dirPath, volumeName string) error 15 | } 16 | 17 | type genisoimage struct { 18 | executer executer.Executer 19 | } 20 | 21 | func NewGenIsoImage(exec executer.Executer) GenIsoImage { 22 | if exec == nil { 23 | exec = executer.NewExecuter() 24 | } 25 | 26 | return &genisoimage{ 27 | executer: exec, 28 | } 29 | } 30 | 31 | func (s *genisoimage) GenerateImage(imagePath, imageName, dirPath, volumeName string) error { 32 | _, err := s.executer.Execute(fmt.Sprintf(genDataImageCmd, volumeName, imagePath, imageName, dirPath)) 33 | return err 34 | } 35 | -------------------------------------------------------------------------------- /pkg/genisoimage/genisoimage_test.go: -------------------------------------------------------------------------------- 1 | package genisoimage 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/golang/mock/gomock" 9 | . "github.com/onsi/ginkgo/v2/dsl/core" 10 | . "github.com/onsi/gomega" 11 | "github.com/openshift/appliance/pkg/executer" 12 | ) 13 | 14 | var _ = Describe("Test GenIsoImage", func() { 15 | var ( 16 | ctrl *gomock.Controller 17 | mockExecuter *executer.MockExecuter 18 | testGenIsoImage GenIsoImage 19 | fakeCachePath = "/path/to/cache" 20 | fakeDataPath = "/path/to/data" 21 | fakeImageName = "testdata.iso" 22 | fakeVolumeName = "testvolume" 23 | ) 24 | 25 | BeforeEach(func() { 26 | ctrl = gomock.NewController(GinkgoT()) 27 | mockExecuter = executer.NewMockExecuter(ctrl) 28 | testGenIsoImage = NewGenIsoImage(mockExecuter) 29 | }) 30 | 31 | It("genisoimage GenerateImage - success", func() { 32 | 33 | fakeCachePath = "/path/to/cache" 34 | fakeDataPath = "/path/to/data" 35 | fakeImageName = "testdata.iso" 36 | 37 | cmd := fmt.Sprintf(genDataImageCmd, fakeVolumeName, fakeCachePath, fakeImageName, fakeDataPath) 38 | mockExecuter.EXPECT().Execute(cmd).Return("", nil).Times(1) 39 | 40 | err := testGenIsoImage.GenerateImage(fakeCachePath, fakeImageName, fakeDataPath, fakeVolumeName) 41 | Expect(err).ToNot(HaveOccurred()) 42 | }) 43 | 44 | It("genisoimage GenerateImage - failure", func() { 45 | mockExecuter.EXPECT().Execute(gomock.Any()).Return("", errors.New("some error")).Times(1) 46 | 47 | err := testGenIsoImage.GenerateImage(fakeCachePath, fakeImageName, fakeDataPath, fakeVolumeName) 48 | Expect(err).To(HaveOccurred()) 49 | }) 50 | }) 51 | 52 | func TestGenIsoImage(t *testing.T) { 53 | RegisterFailHandler(Fail) 54 | RunSpecs(t, "geoisoimage_test") 55 | } 56 | -------------------------------------------------------------------------------- /pkg/graph/graph.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "regexp" 10 | "sort" 11 | "strings" 12 | "time" 13 | 14 | "github.com/blang/semver" 15 | "github.com/go-openapi/swag" 16 | "github.com/openshift/appliance/pkg/consts" 17 | "github.com/sirupsen/logrus" 18 | ) 19 | 20 | // Response is what Cincinnati sends us when querying for releases in a channel 21 | type Response struct { 22 | Nodes []Release `json:"nodes"` 23 | } 24 | 25 | // Release describes a release payload 26 | type Release struct { 27 | Version string `json:"version"` 28 | Payload string `json:"payload"` 29 | } 30 | 31 | // OcpRelease describes a generally available release payload 32 | type OcpRelease struct { 33 | // Version is the minor version to search for 34 | Version string `json:"version"` 35 | // Channel is the release channel to search in 36 | Channel ReleaseChannel `json:"channel"` 37 | // Architecture is the architecture for the release. 38 | // Defaults to amd64. 39 | Architecture string `json:"architecture,omitempty"` 40 | } 41 | 42 | type ReleaseChannel string 43 | 44 | const ( 45 | ReleaseChannelStable ReleaseChannel = "stable" 46 | ReleaseChannelFast ReleaseChannel = "fast" 47 | ReleaseChannelCandidate ReleaseChannel = "candidate" 48 | ReleaseChannelEUS ReleaseChannel = "eus" 49 | ) 50 | 51 | const ( 52 | cincinnatiAddress = "https://api.openshift.com/api/upgrades_info/graph" 53 | ) 54 | 55 | var ( 56 | majorMinorRegExp = regexp.MustCompile(`^(?P(?P0|[1-9]\d*)\.(?P0|[1-9]\d*))\.?.*`) 57 | ) 58 | 59 | // Graph is the interface for fetching info from api.openshift.com/api/upgrades_info/graph 60 | type Graph interface { 61 | GetReleaseImage() (string, string, error) 62 | } 63 | 64 | type HTTPClient interface { 65 | Do(req *http.Request) (*http.Response, error) 66 | } 67 | 68 | type GraphConfig struct { 69 | HTTPClient HTTPClient 70 | Arch string 71 | Version string 72 | CincinnatiAddress *string 73 | Channel *ReleaseChannel 74 | } 75 | 76 | type graph struct { 77 | GraphConfig 78 | } 79 | 80 | func NewGraph(config GraphConfig) Graph { 81 | if config.HTTPClient == nil { 82 | config.HTTPClient = &http.Client{ 83 | Timeout: 5 * time.Second, 84 | } 85 | } 86 | 87 | if config.CincinnatiAddress == nil { 88 | config.CincinnatiAddress = swag.String(cincinnatiAddress) 89 | } 90 | 91 | if config.Channel == nil { 92 | channel := ReleaseChannelStable 93 | config.Channel = &channel 94 | } 95 | return &graph{ 96 | GraphConfig: config, 97 | } 98 | } 99 | 100 | func (g *graph) GetReleaseImage() (string, string, error) { 101 | release := OcpRelease{ 102 | Version: g.Version, 103 | Channel: *g.Channel, 104 | Architecture: g.Arch, 105 | } 106 | 107 | payload, version, err := g.resolvePullSpec(*g.CincinnatiAddress, release) 108 | if err != nil { 109 | if g.Version != consts.MaxOcpVersion { 110 | // Trying to fallback to latest supported version 111 | logrus.Warnf("OCP %s is not available, fallback to latest supported version: %s", release.Version, consts.MaxOcpVersion) 112 | g.Version = consts.MaxOcpVersion 113 | payload, version, err = g.GetReleaseImage() 114 | if err != nil { 115 | return "", "", err 116 | } 117 | return payload, version, nil 118 | } 119 | return "", "", err 120 | } 121 | 122 | return payload, version, nil 123 | } 124 | 125 | // Copied from ci-tools (https://github.com/openshift/ci-tools/blob/master/pkg/release/official/client.go) 126 | 127 | func (g *graph) resolvePullSpec(endpoint string, release OcpRelease) (string, string, error) { 128 | req, err := http.NewRequest("GET", endpoint, nil) 129 | if err != nil { 130 | return "", "", err 131 | } 132 | req.Header.Set("Accept", "application/json") 133 | query := req.URL.Query() 134 | explicitVersion, channel, err := g.processVersionChannel(release.Version, release.Channel) 135 | if err != nil { 136 | return "", "", err 137 | } 138 | targetName := "latest release" 139 | if !explicitVersion { 140 | targetName = release.Version 141 | } 142 | query.Add("channel", channel) 143 | query.Add("arch", release.Architecture) 144 | req.URL.RawQuery = query.Encode() 145 | resp, err := g.HTTPClient.Do(req) 146 | if err != nil { 147 | return "", "", fmt.Errorf("failed to request %s: %w", targetName, err) 148 | } 149 | if resp == nil { 150 | return "", "", fmt.Errorf("failed to request %s: got a nil response", targetName) 151 | } 152 | defer resp.Body.Close() 153 | 154 | var buf bytes.Buffer 155 | _, readErr := io.Copy(&buf, resp.Body) 156 | if resp.StatusCode != http.StatusOK { 157 | return "", "", fmt.Errorf("failed to request %s: server responded with %d: %s", targetName, resp.StatusCode, buf.String()) 158 | } 159 | if readErr != nil { 160 | return "", "", fmt.Errorf("failed to read response body: %w", readErr) 161 | } 162 | response := Response{} 163 | err = json.Unmarshal(buf.Bytes(), &response) 164 | if err != nil { 165 | return "", "", fmt.Errorf("failed to unmarshal response: %w", err) 166 | } 167 | if len(response.Nodes) == 0 { 168 | return "", "", fmt.Errorf("failed to request %s from %s: server returned empty list of releases (despite status code 200)", targetName, req.URL.String()) 169 | } 170 | 171 | if explicitVersion { 172 | for _, node := range response.Nodes { 173 | if node.Version == release.Version { 174 | return node.Payload, node.Version, nil 175 | } 176 | } 177 | return "", "", fmt.Errorf("failed to request %s from %s: version not found in list of releases", release.Version, req.URL.String()) 178 | } 179 | 180 | pullspec, version := g.latestPullSpecAndVersion(response.Nodes) 181 | return pullspec, version, nil 182 | } 183 | 184 | // processVersionChannel takes the configured version and channel and 185 | // returns: 186 | // 187 | // - Whether the version is explicit (e.g. 4.7.0) or just a 188 | // major.minor (e.g. 4.7). 189 | // - The appropriate channel for a Cincinnati request, e.g. stable-4.7. 190 | // - Any errors that turn up while processing. 191 | func (g *graph) processVersionChannel(version string, channel ReleaseChannel) (explicitVersion bool, cincinnatiChannel string, err error) { 192 | explicitVersion, majorMinor, err := g.extractMajorMinor(version) 193 | if err != nil { 194 | return false, "", err 195 | } 196 | if strings.HasSuffix(string(channel), fmt.Sprintf("-%s", majorMinor)) { 197 | return explicitVersion, string(channel), nil 198 | } 199 | 200 | return explicitVersion, fmt.Sprintf("%s-%s", channel, majorMinor), nil 201 | } 202 | 203 | // latestPullSpecAndVersion returns the pullSpec of the latest release in the list as a payload and version 204 | func (g *graph) latestPullSpecAndVersion(options []Release) (string, string) { 205 | sort.Slice(options, func(i, j int) bool { 206 | vi := semver.MustParse(options[i].Version) 207 | vj := semver.MustParse(options[j].Version) 208 | return vi.GTE(vj) // greater, not less, so we get descending order 209 | }) 210 | return options[0].Payload, options[0].Version 211 | } 212 | 213 | func (g *graph) extractMajorMinor(version string) (explicitVersion bool, majorMinor string, err error) { 214 | majorMinorMatch := majorMinorRegExp.FindStringSubmatch(version) 215 | if majorMinorMatch == nil { 216 | return false, "", fmt.Errorf("version %q does not begin with a major.minor version", version) 217 | } 218 | 219 | majorMinorIndex := majorMinorRegExp.SubexpIndex("majorMinor") 220 | majorMinor = majorMinorMatch[majorMinorIndex] 221 | explicitVersion = version != majorMinor 222 | 223 | return explicitVersion, majorMinor, nil 224 | } 225 | -------------------------------------------------------------------------------- /pkg/graph/graph_test.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "strings" 11 | "testing" 12 | 13 | "github.com/go-openapi/swag" 14 | . "github.com/onsi/ginkgo/v2/dsl/core" 15 | . "github.com/onsi/gomega" 16 | "github.com/openshift/appliance/pkg/consts" 17 | ) 18 | 19 | const ( 20 | cincinnatiPartialResponse = "quay.io/openshift-release-dev/ocp-release@sha256" 21 | fakeCincinnatiAddress = "https://api.example.com/api/upgrades_info/graph" 22 | ) 23 | 24 | type ClientMock struct{} 25 | 26 | func (c *ClientMock) Do(req *http.Request) (*http.Response, error) { 27 | if strings.Contains(req.URL.String(), cincinnatiAddress) { 28 | cincinnatiFakeResponse := Response{ 29 | Nodes: []Release{ 30 | { 31 | Version: "4.13.1", 32 | Payload: fmt.Sprintf("%s:foobar1", cincinnatiPartialResponse), 33 | }, 34 | { 35 | Version: "4.13.2", 36 | Payload: fmt.Sprintf("%s:foobar2", cincinnatiPartialResponse), 37 | }, 38 | { 39 | Version: "4.13.3", 40 | Payload: fmt.Sprintf("%s:foobar3", cincinnatiPartialResponse), 41 | }, 42 | { 43 | Version: fmt.Sprintf("%s.0", consts.MaxOcpVersion), 44 | Payload: fmt.Sprintf("%s:foobar4", cincinnatiPartialResponse), 45 | }, 46 | }, 47 | } 48 | 49 | responseJSON, err := json.Marshal(cincinnatiFakeResponse) 50 | Expect(err).ToNot(HaveOccurred()) 51 | 52 | response := &http.Response{ 53 | StatusCode: http.StatusOK, 54 | Header: make(http.Header), 55 | ContentLength: int64(len(responseJSON)), 56 | Body: io.NopCloser(bytes.NewReader(responseJSON)), 57 | } 58 | 59 | return response, nil 60 | } else if strings.Contains(req.URL.String(), fakeCincinnatiAddress) { 61 | response := &http.Response{ 62 | StatusCode: http.StatusNotFound, 63 | Header: make(http.Header), 64 | Body: io.NopCloser(bytes.NewReader(nil)), 65 | } 66 | 67 | return response, nil 68 | } 69 | 70 | return nil, errors.New("test client error, unexpected URL") 71 | } 72 | 73 | var _ = Describe("Test Graph", func() { 74 | var ( 75 | testGraph Graph 76 | graphConfig GraphConfig 77 | ) 78 | 79 | BeforeEach(func() { 80 | channel := ReleaseChannelStable 81 | graphConfig = GraphConfig{ 82 | HTTPClient: &ClientMock{}, 83 | Arch: "amd64", 84 | Channel: &channel, 85 | } 86 | }) 87 | 88 | It("GetReleaseImage - Valid version", func() { 89 | version := "4.13.1" 90 | graphConfig.Version = version 91 | 92 | testGraph = NewGraph(graphConfig) 93 | cincinnatiResponse, verResponse, err := testGraph.GetReleaseImage() 94 | Expect(err).ToNot(HaveOccurred()) 95 | Expect(cincinnatiResponse).To(ContainSubstring(cincinnatiPartialResponse)) 96 | Expect(verResponse).To(Equal(version)) 97 | }) 98 | 99 | It("GetReleaseImage - missing version", func() { 100 | graphConfig.Version = "4.99" 101 | 102 | testGraph = NewGraph(graphConfig) 103 | cincinnatiResponse, verResponse, err := testGraph.GetReleaseImage() 104 | Expect(err).ToNot(HaveOccurred()) 105 | Expect(cincinnatiResponse).To(ContainSubstring(cincinnatiPartialResponse)) 106 | Expect(verResponse).To(Equal(fmt.Sprintf("%s.0", consts.MaxOcpVersion))) 107 | }) 108 | 109 | It("GetReleaseImage - Cincinnati returns 404", func() { 110 | graphConfig.Version = "4.13.1" 111 | graphConfig.CincinnatiAddress = swag.String(fakeCincinnatiAddress) 112 | 113 | testGraph = NewGraph(graphConfig) 114 | _, _, err := testGraph.GetReleaseImage() 115 | Expect(err).To(HaveOccurred()) 116 | }) 117 | }) 118 | 119 | func TestGraph(t *testing.T) { 120 | RegisterFailHandler(Fail) 121 | RunSpecs(t, "graph_test") 122 | } 123 | -------------------------------------------------------------------------------- /pkg/ignition/ignition.go: -------------------------------------------------------------------------------- 1 | package ignitionutil 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | ignitionConfig "github.com/coreos/ignition/v2/config/v3_2" 7 | "github.com/coreos/ignition/v2/config/v3_2/types" 8 | "github.com/coreos/ignition/v2/config/validate" 9 | "github.com/openshift/appliance/pkg/fileutil" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | //go:generate mockgen -source=ignition.go -package=ignitionutil -destination=mock_ignition.go 14 | type Ignition interface { 15 | ParseIgnitionFile(path string) (*types.Config, error) 16 | WriteIgnitionFile(path string, config *types.Config) error 17 | MergeIgnitionConfig(base *types.Config, overrides *types.Config) (*types.Config, error) 18 | } 19 | 20 | type IgnitionConfig struct { 21 | OSInterface fileutil.OSInterface 22 | } 23 | 24 | type ignition struct { 25 | IgnitionConfig 26 | } 27 | 28 | func NewIgnition(config IgnitionConfig) Ignition { 29 | if config.OSInterface == nil { 30 | config.OSInterface = &fileutil.OSFS{} 31 | } 32 | return &ignition{ 33 | IgnitionConfig: config, 34 | } 35 | } 36 | 37 | // ParseIgnitionFile reads an ignition config from a given path on disk 38 | func (i *ignition) ParseIgnitionFile(path string) (*types.Config, error) { 39 | configBytes, err := i.OSInterface.ReadFile(path) 40 | if err != nil { 41 | return nil, errors.Wrapf(err, "error reading file %s", path) 42 | } 43 | configLatest, _, err := ignitionConfig.Parse(configBytes) 44 | return &configLatest, err 45 | } 46 | 47 | // WriteIgnitionFile writes an ignition config to a given path on disk 48 | func (i *ignition) WriteIgnitionFile(path string, config *types.Config) error { 49 | updatedBytes, err := json.Marshal(config) 50 | if err != nil { 51 | return err 52 | } 53 | err = i.OSInterface.WriteFile(path, updatedBytes, 0600) 54 | if err != nil { 55 | return errors.Wrapf(err, "error writing file %s", path) 56 | } 57 | return nil 58 | } 59 | 60 | // MergeIgnitionConfig merges the specified configs and check the result is a valid ignition config 61 | func (i *ignition) MergeIgnitionConfig(base *types.Config, overrides *types.Config) (*types.Config, error) { 62 | config := ignitionConfig.Merge(*base, *overrides) 63 | report := validate.ValidateWithContext(config, nil) 64 | if report.IsFatal() { 65 | return &config, errors.Errorf("merged ignition config is invalid: %s", report.String()) 66 | } 67 | return &config, nil 68 | } 69 | -------------------------------------------------------------------------------- /pkg/ignition/ignition_test.go: -------------------------------------------------------------------------------- 1 | package ignitionutil 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "testing" 7 | 8 | igntypes "github.com/coreos/ignition/v2/config/v3_2/types" 9 | "github.com/go-openapi/swag" 10 | . "github.com/onsi/ginkgo/v2/dsl/core" 11 | . "github.com/onsi/gomega" 12 | assetignition "github.com/openshift/installer/pkg/asset/ignition" 13 | ) 14 | 15 | const ( 16 | fakeIgnition32 = `{ 17 | "ignition": { 18 | "config": {}, 19 | "version": "3.2.0" 20 | }, 21 | "storage": { 22 | "files": [] 23 | } 24 | }` 25 | 26 | fakeIgnition99 = `{ 27 | "ignition": { 28 | "config": {}, 29 | "version": "9.9.0" 30 | }, 31 | "storage": { 32 | "files": [] 33 | } 34 | }` 35 | ) 36 | 37 | type FakeOS struct{} 38 | 39 | func (FakeOS) MkdirTemp(dir, prefix string) (string, error) { 40 | return os.MkdirTemp(dir, prefix) 41 | } 42 | 43 | func (FakeOS) Stat(name string) (os.FileInfo, error) { 44 | return os.Stat(name) 45 | } 46 | 47 | func (FakeOS) Remove(name string) error { 48 | return os.Remove(name) 49 | } 50 | 51 | func (FakeOS) UserHomeDir() (string, error) { 52 | return os.UserHomeDir() 53 | } 54 | 55 | func (FakeOS) MkdirAll(path string, perm os.FileMode) error { 56 | return nil 57 | } 58 | 59 | func (FakeOS) WriteFile(name string, data []byte, perm os.FileMode) error { 60 | return os.WriteFile(name, data, perm) 61 | } 62 | 63 | func (FakeOS) ReadFile(name string) ([]byte, error) { 64 | if name == "/path/to/ignition32-file" { 65 | return []byte(fakeIgnition32), nil 66 | } 67 | if name == "/path/to/ignition99-file" { 68 | return []byte(fakeIgnition99), nil 69 | } 70 | return nil, errors.New("file not found") 71 | } 72 | 73 | func (FakeOS) RemoveAll(path string) error { 74 | return nil 75 | } 76 | 77 | var _ = Describe("Test Ignition", func() { 78 | var ( 79 | testIgnition Ignition 80 | ) 81 | 82 | BeforeEach(func() { 83 | coreOSConfig := IgnitionConfig{ 84 | OSInterface: &FakeOS{}, 85 | } 86 | testIgnition = NewIgnition(coreOSConfig) 87 | }) 88 | // 89 | It("ParseIgnitionFile - file not found", func() { 90 | _, err := testIgnition.ParseIgnitionFile("/bad/path/filename") 91 | Expect(err).To(HaveOccurred()) 92 | }) 93 | 94 | It("ParseIgnitionFile - bad ignition version", func() { 95 | _, err := testIgnition.ParseIgnitionFile("/path/to/ignition99-file") 96 | Expect(err).To(HaveOccurred()) 97 | Expect(err.Error()).To(Equal("unsupported config version")) 98 | }) 99 | 100 | It("ParseIgnitionFile - ignition v3.2.0 success", func() { 101 | config32, err := testIgnition.ParseIgnitionFile("/path/to/ignition32-file") 102 | Expect(err).NotTo(HaveOccurred()) 103 | Expect(config32.Ignition.Version).To(Equal("3.2.0")) 104 | }) 105 | 106 | It("MergeIgnitionConfig - success", func() { 107 | fakeUser := "core" 108 | fakePass := swag.String("fakePwdHash") 109 | fakeDevice := "/boot" 110 | fakeFormat := swag.String("ext4") 111 | fakePath := swag.String("/dev/disk/by-partlabel/boot") 112 | fakeInstallConfig := igntypes.Config{ 113 | Passwd: igntypes.Passwd{ 114 | Users: []igntypes.PasswdUser{ 115 | { 116 | Name: fakeUser, 117 | PasswordHash: fakePass, 118 | }, 119 | }, 120 | }, 121 | Ignition: igntypes.Ignition{ 122 | Version: igntypes.MaxVersion.String(), 123 | }, 124 | Storage: igntypes.Storage{ 125 | Files: []igntypes.File{assetignition.FileFromBytes("/path/to/file", 126 | "root", 0644, []byte("foobar")), 127 | }, 128 | Filesystems: []igntypes.Filesystem{ 129 | { 130 | Device: fakeDevice, 131 | Format: fakeFormat, 132 | Path: fakePath, 133 | }, 134 | }, 135 | }, 136 | } 137 | 138 | config32, err := testIgnition.ParseIgnitionFile("/path/to/ignition32-file") 139 | Expect(err).NotTo(HaveOccurred()) 140 | mergedConfig, err := testIgnition.MergeIgnitionConfig(config32, &fakeInstallConfig) 141 | Expect(err).NotTo(HaveOccurred()) 142 | Expect(mergedConfig.Ignition.Version).To(Equal(igntypes.MaxVersion.String())) 143 | Expect(mergedConfig.Passwd.Users[0].Name).To(Equal(fakeUser)) 144 | Expect(mergedConfig.Passwd.Users[0].PasswordHash).To(Equal(fakePass)) 145 | Expect(mergedConfig.Storage.Files[0].Mode).To(Equal(swag.Int(0644))) 146 | Expect(mergedConfig.Storage.Filesystems[0].Device).To(Equal(fakeDevice)) 147 | Expect(mergedConfig.Storage.Filesystems[0].Format).To(Equal(fakeFormat)) 148 | Expect(mergedConfig.Storage.Filesystems[0].Path).To(Equal(fakePath)) 149 | }) 150 | }) 151 | 152 | func TestIgnition(t *testing.T) { 153 | RegisterFailHandler(Fail) 154 | RunSpecs(t, "ignition_test") 155 | } 156 | -------------------------------------------------------------------------------- /pkg/ignition/mock_ignition.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: ignition.go 3 | 4 | // Package ignitionutil is a generated GoMock package. 5 | package ignitionutil 6 | 7 | import ( 8 | reflect "reflect" 9 | 10 | types "github.com/coreos/ignition/v2/config/v3_2/types" 11 | gomock "github.com/golang/mock/gomock" 12 | ) 13 | 14 | // MockIgnition is a mock of Ignition interface. 15 | type MockIgnition struct { 16 | ctrl *gomock.Controller 17 | recorder *MockIgnitionMockRecorder 18 | } 19 | 20 | // MockIgnitionMockRecorder is the mock recorder for MockIgnition. 21 | type MockIgnitionMockRecorder struct { 22 | mock *MockIgnition 23 | } 24 | 25 | // NewMockIgnition creates a new mock instance. 26 | func NewMockIgnition(ctrl *gomock.Controller) *MockIgnition { 27 | mock := &MockIgnition{ctrl: ctrl} 28 | mock.recorder = &MockIgnitionMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use. 33 | func (m *MockIgnition) EXPECT() *MockIgnitionMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // MergeIgnitionConfig mocks base method. 38 | func (m *MockIgnition) MergeIgnitionConfig(base, overrides *types.Config) (*types.Config, error) { 39 | m.ctrl.T.Helper() 40 | ret := m.ctrl.Call(m, "MergeIgnitionConfig", base, overrides) 41 | ret0, _ := ret[0].(*types.Config) 42 | ret1, _ := ret[1].(error) 43 | return ret0, ret1 44 | } 45 | 46 | // MergeIgnitionConfig indicates an expected call of MergeIgnitionConfig. 47 | func (mr *MockIgnitionMockRecorder) MergeIgnitionConfig(base, overrides interface{}) *gomock.Call { 48 | mr.mock.ctrl.T.Helper() 49 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MergeIgnitionConfig", reflect.TypeOf((*MockIgnition)(nil).MergeIgnitionConfig), base, overrides) 50 | } 51 | 52 | // ParseIgnitionFile mocks base method. 53 | func (m *MockIgnition) ParseIgnitionFile(path string) (*types.Config, error) { 54 | m.ctrl.T.Helper() 55 | ret := m.ctrl.Call(m, "ParseIgnitionFile", path) 56 | ret0, _ := ret[0].(*types.Config) 57 | ret1, _ := ret[1].(error) 58 | return ret0, ret1 59 | } 60 | 61 | // ParseIgnitionFile indicates an expected call of ParseIgnitionFile. 62 | func (mr *MockIgnitionMockRecorder) ParseIgnitionFile(path interface{}) *gomock.Call { 63 | mr.mock.ctrl.T.Helper() 64 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ParseIgnitionFile", reflect.TypeOf((*MockIgnition)(nil).ParseIgnitionFile), path) 65 | } 66 | 67 | // WriteIgnitionFile mocks base method. 68 | func (m *MockIgnition) WriteIgnitionFile(path string, config *types.Config) error { 69 | m.ctrl.T.Helper() 70 | ret := m.ctrl.Call(m, "WriteIgnitionFile", path, config) 71 | ret0, _ := ret[0].(error) 72 | return ret0 73 | } 74 | 75 | // WriteIgnitionFile indicates an expected call of WriteIgnitionFile. 76 | func (mr *MockIgnitionMockRecorder) WriteIgnitionFile(path, config interface{}) *gomock.Call { 77 | mr.mock.ctrl.T.Helper() 78 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WriteIgnitionFile", reflect.TypeOf((*MockIgnition)(nil).WriteIgnitionFile), path, config) 79 | } 80 | -------------------------------------------------------------------------------- /pkg/installer/installer.go: -------------------------------------------------------------------------------- 1 | package installer 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/go-openapi/swag" 10 | 11 | "github.com/hashicorp/go-version" 12 | "github.com/openshift/appliance/pkg/asset/config" 13 | "github.com/openshift/appliance/pkg/executer" 14 | "github.com/openshift/appliance/pkg/log" 15 | "github.com/openshift/appliance/pkg/release" 16 | "github.com/sirupsen/logrus" 17 | ) 18 | 19 | const ( 20 | installerBinaryName = "openshift-install" 21 | installerFipsBinaryName = "openshift-install-fips" 22 | installerBinaryGZ = "openshift-install-linux.tar.gz" 23 | templateUnconfiguredIgnitionBinary = "%s agent create unconfigured-ignition --dir %s" 24 | templateInstallerDownloadURL = "https://mirror.openshift.com/pub/openshift-v%s/%s/clients/%s/%s/openshift-install-linux.tar.gz" 25 | unconfiguredIgnitionFileName = "unconfigured-agent.ign" 26 | ) 27 | 28 | type Installer interface { 29 | CreateUnconfiguredIgnition() (string, error) 30 | GetInstallerDownloadURL() (string, error) 31 | GetInstallerBinaryName() string 32 | } 33 | 34 | type InstallerConfig struct { 35 | Executer executer.Executer 36 | EnvConfig *config.EnvConfig 37 | Release release.Release 38 | ApplianceConfig *config.ApplianceConfig 39 | InstallerBinaryName string 40 | } 41 | 42 | type installer struct { 43 | InstallerConfig 44 | } 45 | 46 | func NewInstaller(config InstallerConfig) Installer { 47 | if config.Executer == nil { 48 | config.Executer = executer.NewExecuter() 49 | } 50 | 51 | if config.Release == nil { 52 | releaseConfig := release.ReleaseConfig{ 53 | ApplianceConfig: config.ApplianceConfig, 54 | EnvConfig: config.EnvConfig, 55 | } 56 | config.Release = release.NewRelease(releaseConfig) 57 | } 58 | 59 | inst := &installer{ 60 | InstallerConfig: config, 61 | } 62 | inst.InstallerBinaryName = inst.GetInstallerBinaryName() 63 | 64 | return inst 65 | } 66 | 67 | func (i *installer) CreateUnconfiguredIgnition() (string, error) { 68 | var openshiftInstallFilePath string 69 | var err error 70 | 71 | if !i.EnvConfig.DebugBaseIgnition { 72 | if fileName := i.EnvConfig.FindInCache(i.InstallerBinaryName); fileName != "" { 73 | logrus.Infof("Reusing %s binary from cache", i.InstallerBinaryName) 74 | openshiftInstallFilePath = fileName 75 | } else { 76 | openshiftInstallFilePath, err = i.downloadInstallerBinary() 77 | if err != nil { 78 | return "", err 79 | } 80 | } 81 | } else { 82 | logrus.Debugf("Using openshift-install binary from assets dir to fetch unconfigured-ignition") 83 | openshiftInstallFilePath = filepath.Join(i.EnvConfig.AssetsDir, i.InstallerBinaryName) 84 | } 85 | 86 | createCmd := fmt.Sprintf(templateUnconfiguredIgnitionBinary, openshiftInstallFilePath, i.EnvConfig.TempDir) 87 | if swag.BoolValue(i.ApplianceConfig.Config.EnableInteractiveFlow) { 88 | createCmd = fmt.Sprintf("%s --interactive", createCmd) 89 | } 90 | _, err = i.Executer.Execute(createCmd) 91 | return filepath.Join(i.EnvConfig.TempDir, unconfiguredIgnitionFileName), err 92 | } 93 | 94 | func (i *installer) GetInstallerDownloadURL() (string, error) { 95 | releaseVersion, err := version.NewVersion(i.ApplianceConfig.Config.OcpRelease.Version) 96 | if err != nil { 97 | return "", err 98 | } 99 | majorVersion := fmt.Sprint(releaseVersion.Segments()[0]) 100 | cpuArch := i.ApplianceConfig.GetCpuArchitecture() 101 | 102 | ocpClient := "ocp" 103 | if strings.Contains(releaseVersion.String(), "-ec") { 104 | ocpClient = "ocp-dev-preview" 105 | } 106 | 107 | return fmt.Sprintf(templateInstallerDownloadURL, majorVersion, cpuArch, ocpClient, releaseVersion), nil 108 | } 109 | 110 | func (i *installer) downloadInstallerBinary() (string, error) { 111 | spinner := log.NewSpinner( 112 | fmt.Sprintf("Fetching %s binary...", i.InstallerBinaryName), 113 | fmt.Sprintf("Successfully fetched %s binary", i.InstallerBinaryName), 114 | fmt.Sprintf("Failed to fetch %s binary", i.InstallerBinaryName), 115 | i.EnvConfig, 116 | ) 117 | 118 | logrus.Debugf("Fetch %s binary from release payload", i.InstallerBinaryName) 119 | stdout, err := i.Release.ExtractCommand(i.InstallerBinaryName, i.EnvConfig.CacheDir) 120 | if err != nil { 121 | logrus.Errorf("%s", stdout) 122 | return "", log.StopSpinner(spinner, err) 123 | } 124 | 125 | err = log.StopSpinner(spinner, nil) 126 | if err != nil { 127 | return "", err 128 | } 129 | 130 | installerBinaryPath := filepath.Join(i.EnvConfig.CacheDir, i.InstallerBinaryName) 131 | err = os.Chmod(installerBinaryPath, 0755) 132 | if err != nil { 133 | // return "", err 134 | logrus.Warnf("%s", err) 135 | } 136 | return installerBinaryPath, nil 137 | } 138 | 139 | func (i *installer) GetInstallerBinaryName() string { 140 | if swag.BoolValue(i.ApplianceConfig.Config.EnableFips) { 141 | return installerFipsBinaryName 142 | } 143 | return installerBinaryName 144 | } 145 | -------------------------------------------------------------------------------- /pkg/installer/installer_test.go: -------------------------------------------------------------------------------- 1 | package installer 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/go-openapi/swag" 9 | "github.com/openshift/appliance/pkg/graph" 10 | "github.com/openshift/appliance/pkg/release" 11 | "github.com/openshift/appliance/pkg/types" 12 | 13 | "github.com/golang/mock/gomock" 14 | . "github.com/onsi/ginkgo/v2/dsl/core" 15 | . "github.com/onsi/gomega" 16 | "github.com/openshift/appliance/pkg/asset/config" 17 | "github.com/openshift/appliance/pkg/executer" 18 | ) 19 | 20 | var _ = Describe("Test Installer", func() { 21 | var ( 22 | ctrl *gomock.Controller 23 | mockExecuter *executer.MockExecuter 24 | mockRelease *release.MockRelease 25 | testInstaller Installer 26 | ) 27 | 28 | BeforeEach(func() { 29 | ctrl = gomock.NewController(GinkgoT()) 30 | mockExecuter = executer.NewMockExecuter(ctrl) 31 | mockRelease = release.NewMockRelease(ctrl) 32 | }) 33 | 34 | It("GetInstallerDownloadURL - x86_64 stable", func() { 35 | version := "4.13.1" 36 | channel := graph.ReleaseChannelStable 37 | cpuArc := swag.String(config.CpuArchitectureX86) 38 | installerConfig := InstallerConfig{ 39 | Executer: mockExecuter, 40 | Release: mockRelease, 41 | EnvConfig: &config.EnvConfig{}, 42 | ApplianceConfig: &config.ApplianceConfig{ 43 | Config: &types.ApplianceConfig{ 44 | OcpRelease: types.ReleaseImage{ 45 | Version: version, 46 | Channel: &channel, 47 | CpuArchitecture: cpuArc, 48 | }, 49 | }, 50 | }, 51 | } 52 | testInstaller = NewInstaller(installerConfig) 53 | 54 | res, err := testInstaller.GetInstallerDownloadURL() 55 | Expect(err).ToNot(HaveOccurred()) 56 | Expect(res).To(Equal(fmt.Sprintf(templateInstallerDownloadURL, "4", swag.StringValue(cpuArc), "ocp", version))) 57 | }) 58 | 59 | It("GetInstallerDownloadURL - aarch64 candidate", func() { 60 | version := "4.13.2" 61 | channel := graph.ReleaseChannelCandidate 62 | cpuArc := swag.String(config.CpuArchitectureAARCH64) 63 | installerConfig := InstallerConfig{ 64 | Executer: mockExecuter, 65 | Release: mockRelease, 66 | EnvConfig: &config.EnvConfig{}, 67 | ApplianceConfig: &config.ApplianceConfig{ 68 | Config: &types.ApplianceConfig{ 69 | OcpRelease: types.ReleaseImage{ 70 | Version: version, 71 | Channel: &channel, 72 | CpuArchitecture: cpuArc, 73 | }, 74 | }, 75 | }, 76 | } 77 | testInstaller = NewInstaller(installerConfig) 78 | 79 | res, err := testInstaller.GetInstallerDownloadURL() 80 | Expect(err).ToNot(HaveOccurred()) 81 | Expect(res).To(Equal(fmt.Sprintf(templateInstallerDownloadURL, "4", swag.StringValue(cpuArc), "ocp", version))) 82 | }) 83 | 84 | It("GetInstallerDownloadURL - x86_64 preview", func() { 85 | version := "4.16.0-ec.0" 86 | channel := graph.ReleaseChannelCandidate 87 | cpuArc := swag.String(config.CpuArchitectureX86) 88 | installerConfig := InstallerConfig{ 89 | Executer: mockExecuter, 90 | Release: mockRelease, 91 | EnvConfig: &config.EnvConfig{}, 92 | ApplianceConfig: &config.ApplianceConfig{ 93 | Config: &types.ApplianceConfig{ 94 | OcpRelease: types.ReleaseImage{ 95 | Version: version, 96 | Channel: &channel, 97 | CpuArchitecture: cpuArc, 98 | }, 99 | }, 100 | }, 101 | } 102 | testInstaller = NewInstaller(installerConfig) 103 | 104 | res, err := testInstaller.GetInstallerDownloadURL() 105 | Expect(err).ToNot(HaveOccurred()) 106 | Expect(res).To(Equal(fmt.Sprintf(templateInstallerDownloadURL, "4", swag.StringValue(cpuArc), "ocp-dev-preview", version))) 107 | }) 108 | 109 | It("CreateUnconfiguredIgnition - DebugBaseIgnition: false", func() { 110 | version := "4.13.1" 111 | channel := graph.ReleaseChannelStable 112 | cpuArc := swag.String(config.CpuArchitectureX86) 113 | 114 | tmpDir, err := filepath.Abs("") 115 | Expect(err).ToNot(HaveOccurred()) 116 | cmd := fmt.Sprintf(templateUnconfiguredIgnitionBinary, installerBinaryName, tmpDir) 117 | mockExecuter.EXPECT().Execute(cmd).Return("", nil).Times(1) 118 | 119 | installerConfig := InstallerConfig{ 120 | Executer: mockExecuter, 121 | Release: mockRelease, 122 | EnvConfig: &config.EnvConfig{ 123 | DebugBaseIgnition: false, 124 | TempDir: tmpDir, 125 | }, 126 | ApplianceConfig: &config.ApplianceConfig{ 127 | Config: &types.ApplianceConfig{ 128 | OcpRelease: types.ReleaseImage{ 129 | Version: version, 130 | Channel: &channel, 131 | CpuArchitecture: cpuArc, 132 | }, 133 | }, 134 | }, 135 | } 136 | testInstaller = NewInstaller(installerConfig) 137 | mockRelease.EXPECT().ExtractCommand(installerBinaryName, installerConfig.EnvConfig.CacheDir).Return("", nil).Times(1) 138 | 139 | res, err := testInstaller.CreateUnconfiguredIgnition() 140 | Expect(err).ToNot(HaveOccurred()) 141 | Expect(res).To(Equal(fmt.Sprintf("%s/unconfigured-agent.ign", tmpDir))) 142 | }) 143 | 144 | It("CreateUnconfiguredIgnition - DebugBaseIgnition: true", func() { 145 | version := "4.13.1" 146 | channel := graph.ReleaseChannelStable 147 | cpuArc := swag.String(config.CpuArchitectureX86) 148 | tmpDir := "/path/to/tempdir" 149 | 150 | cmd := fmt.Sprintf(templateUnconfiguredIgnitionBinary, installerBinaryName, tmpDir) 151 | mockExecuter.EXPECT().Execute(cmd).Return("", nil).Times(1) 152 | 153 | installerConfig := InstallerConfig{ 154 | Executer: mockExecuter, 155 | Release: mockRelease, 156 | EnvConfig: &config.EnvConfig{ 157 | DebugBaseIgnition: true, 158 | TempDir: tmpDir, 159 | }, 160 | ApplianceConfig: &config.ApplianceConfig{ 161 | Config: &types.ApplianceConfig{ 162 | OcpRelease: types.ReleaseImage{ 163 | Version: version, 164 | Channel: &channel, 165 | CpuArchitecture: cpuArc, 166 | }, 167 | }, 168 | }, 169 | } 170 | testInstaller = NewInstaller(installerConfig) 171 | 172 | res, err := testInstaller.CreateUnconfiguredIgnition() 173 | Expect(err).ToNot(HaveOccurred()) 174 | Expect(res).To(Equal(filepath.Join(tmpDir, unconfiguredIgnitionFileName))) 175 | }) 176 | 177 | It("CreateUnconfiguredIgnition - interactive flow enabled", func() { 178 | tmpDir, err := filepath.Abs("") 179 | Expect(err).ToNot(HaveOccurred()) 180 | cmd := fmt.Sprintf(templateUnconfiguredIgnitionBinary, installerBinaryName, tmpDir) 181 | cmd = fmt.Sprintf("%s --interactive", cmd) 182 | mockExecuter.EXPECT().Execute(cmd).Return("", nil).Times(1) 183 | 184 | installerConfig := InstallerConfig{ 185 | Executer: mockExecuter, 186 | Release: mockRelease, 187 | EnvConfig: &config.EnvConfig{ 188 | DebugBaseIgnition: false, 189 | TempDir: tmpDir, 190 | }, 191 | ApplianceConfig: &config.ApplianceConfig{ 192 | Config: &types.ApplianceConfig{ 193 | EnableInteractiveFlow: swag.Bool(true), 194 | }, 195 | }, 196 | } 197 | testInstaller = NewInstaller(installerConfig) 198 | mockRelease.EXPECT().ExtractCommand(installerBinaryName, installerConfig.EnvConfig.CacheDir).Return("", nil).Times(1) 199 | 200 | res, err := testInstaller.CreateUnconfiguredIgnition() 201 | Expect(err).ToNot(HaveOccurred()) 202 | Expect(res).To(Equal(filepath.Join(tmpDir, unconfiguredIgnitionFileName))) 203 | }) 204 | }) 205 | 206 | func TestInstaller(t *testing.T) { 207 | RegisterFailHandler(Fail) 208 | RunSpecs(t, "installer_test") 209 | } 210 | -------------------------------------------------------------------------------- /pkg/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/pkg/errors" 10 | "github.com/sirupsen/logrus" 11 | "golang.org/x/term" 12 | ) 13 | 14 | type Filehook struct { 15 | file io.Writer 16 | formatter logrus.Formatter 17 | level logrus.Level 18 | 19 | truncateAtNewLine bool 20 | } 21 | 22 | func NewFileHook(file io.Writer, level logrus.Level, formatter logrus.Formatter) *Filehook { 23 | return &Filehook{ 24 | file: file, 25 | formatter: formatter, 26 | level: level, 27 | } 28 | } 29 | 30 | func NewFileHookWithNewlineTruncate(file io.Writer, level logrus.Level, formatter logrus.Formatter) *Filehook { 31 | f := NewFileHook(file, level, formatter) 32 | f.truncateAtNewLine = true 33 | return f 34 | } 35 | 36 | func (h Filehook) Levels() []logrus.Level { 37 | var levels []logrus.Level 38 | for _, level := range logrus.AllLevels { 39 | if level <= h.level { 40 | levels = append(levels, level) 41 | } 42 | } 43 | 44 | return levels 45 | } 46 | 47 | func (h Filehook) Fire(entry *logrus.Entry) error { 48 | // logrus reuses the same entry for each invocation of hooks. 49 | // so we need to make sure we leave them message field as we received. 50 | orig := entry.Message 51 | defer func() { entry.Message = orig }() 52 | 53 | msgs := []string{orig} 54 | if h.truncateAtNewLine { 55 | msgs = strings.Split(orig, "\n") 56 | } 57 | 58 | for _, msg := range msgs { 59 | // this makes it easier to call format on entry 60 | // easy without creating a new one for each split message. 61 | entry.Message = msg 62 | line, err := h.formatter.Format(entry) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | if _, err := h.file.Write(line); err != nil { 68 | return err 69 | } 70 | } 71 | 72 | return nil 73 | } 74 | 75 | func SetupFileHook(baseDir string) func() { 76 | if err := os.MkdirAll(baseDir, 0755); err != nil { 77 | logrus.Fatal(errors.Wrap(err, "failed to create base directory for logs")) 78 | } 79 | 80 | logfile, err := os.OpenFile(filepath.Join(baseDir, ".openshift_appliance.log"), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666) 81 | if err != nil { 82 | logrus.Fatal(errors.Wrap(err, "failed to open log file")) 83 | } 84 | 85 | originalHooks := logrus.LevelHooks{} 86 | for k, v := range logrus.StandardLogger().Hooks { 87 | originalHooks[k] = v 88 | } 89 | logrus.AddHook(NewFileHook(logfile, logrus.TraceLevel, &logrus.TextFormatter{ 90 | DisableColors: true, 91 | DisableTimestamp: false, 92 | FullTimestamp: true, 93 | DisableLevelTruncation: false, 94 | })) 95 | 96 | return func() { 97 | logfile.Close() 98 | logrus.StandardLogger().ReplaceHooks(originalHooks) 99 | } 100 | } 101 | 102 | func SetupOutputHook(logLevel string) { 103 | logrus.SetOutput(io.Discard) 104 | logrus.SetLevel(logrus.TraceLevel) 105 | 106 | level, err := logrus.ParseLevel(logLevel) 107 | if err != nil { 108 | level = logrus.InfoLevel 109 | } 110 | 111 | logrus.AddHook(NewFileHookWithNewlineTruncate(os.Stderr, level, &logrus.TextFormatter{ 112 | // Setting ForceColors is necessary because logrus.TextFormatter determines 113 | // whether to enable colors by looking at the output of the logger. 114 | // In this case, the output is io.Discard, which is not a terminal. 115 | // Overriding it here allows the same check to be done, but against the 116 | // hook's output instead of the logger's output. 117 | ForceColors: term.IsTerminal(int(os.Stderr.Fd())), 118 | DisableTimestamp: true, 119 | DisableLevelTruncation: true, 120 | DisableQuote: true, 121 | })) 122 | } 123 | -------------------------------------------------------------------------------- /pkg/log/spinner.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | "syscall" 10 | "time" 11 | 12 | "github.com/alecthomas/units" 13 | "github.com/briandowns/spinner" 14 | "github.com/dustin/go-humanize" 15 | "github.com/openshift/appliance/pkg/asset/config" 16 | "github.com/sirupsen/logrus" 17 | ) 18 | 19 | const ( 20 | blockSize = 512 21 | ) 22 | 23 | type Spinner struct { 24 | Spinner *spinner.Spinner 25 | Ticker *time.Ticker 26 | ProgressMessage, SuccessMessage, FailureMessage string 27 | FileToMonitor, DirToMonitor string 28 | } 29 | 30 | func NewSpinner(progressMessage, successMessage, failureMessage string, envConfig *config.EnvConfig) *Spinner { 31 | // Create and start spinner with message 32 | s := spinner.New(spinner.CharSets[9], 100*time.Millisecond, spinner.WithWriter(os.Stderr)) 33 | s.Suffix = fmt.Sprintf(" %s", progressMessage) 34 | if err := s.Color("blue"); err != nil { 35 | logrus.Fatalln(err) 36 | } 37 | s.Start() 38 | 39 | wrapper := &Spinner{ 40 | Spinner: s, 41 | ProgressMessage: progressMessage, 42 | SuccessMessage: successMessage, 43 | FailureMessage: failureMessage, 44 | } 45 | 46 | wrapper.Ticker = time.NewTicker(1 * time.Second) 47 | go func() { 48 | for range wrapper.Ticker.C { 49 | size, err := getProgressSize(wrapper, envConfig) 50 | if err != nil || size < uint64(units.MiB) { 51 | continue 52 | } 53 | 54 | s.Suffix = fmt.Sprintf(" %s (%s)", progressMessage, humanize.Bytes(size)) 55 | } 56 | }() 57 | 58 | return wrapper 59 | } 60 | 61 | func getProgressSize(spinner *Spinner, envConfig *config.EnvConfig) (uint64, error) { 62 | var size uint64 63 | 64 | if spinner.FileToMonitor != "" { 65 | filename := envConfig.FindInAssets(spinner.FileToMonitor) 66 | if filename == "" { 67 | return 0, errors.New("file to monitor is missing") 68 | } 69 | var stat syscall.Stat_t 70 | err := syscall.Stat(filename, &stat) 71 | if err != nil { 72 | return 0, err 73 | } 74 | if strings.Contains(filename, ".raw") { 75 | // Get actual size of raw sparse file 76 | size = uint64(stat.Blocks * blockSize) 77 | } else { 78 | size = uint64(stat.Size) 79 | } 80 | } 81 | 82 | if spinner.DirToMonitor != "" { 83 | if _, err := os.Stat(spinner.DirToMonitor); os.IsNotExist(err) { 84 | return 0, err 85 | } 86 | if err := filepath.Walk(spinner.DirToMonitor, func(path string, info os.FileInfo, err error) error { 87 | if info != nil && !info.IsDir() { 88 | size += uint64(info.Size()) 89 | } 90 | return nil 91 | }); err != nil { 92 | return 0, err 93 | } 94 | } 95 | 96 | return size, nil 97 | } 98 | 99 | func StopSpinner(spinner *Spinner, err error) error { 100 | if spinner == nil { 101 | return err 102 | } 103 | spinner.Spinner.Stop() 104 | spinner.Ticker.Stop() 105 | if err != nil { 106 | logrus.Error(spinner.FailureMessage) 107 | } else { 108 | logrus.Info(spinner.SuccessMessage) 109 | } 110 | return err 111 | } 112 | -------------------------------------------------------------------------------- /pkg/registry/registry.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | "time" 10 | 11 | "github.com/go-openapi/swag" 12 | "github.com/openshift/appliance/pkg/asset/config" 13 | "github.com/openshift/appliance/pkg/consts" 14 | "github.com/openshift/appliance/pkg/executer" 15 | "github.com/openshift/appliance/pkg/fileutil" 16 | "github.com/openshift/appliance/pkg/skopeo" 17 | "github.com/pkg/errors" 18 | "github.com/sirupsen/logrus" 19 | ) 20 | 21 | const ( 22 | registryStartCmd = "podman run --net=host --privileged -d --name registry -v %s:/var/lib/registry --restart=always -e REGISTRY_HTTP_ADDR=0.0.0.0:%d %s" 23 | registryStopCmd = "podman rm registry -f" 24 | registryBuildCmd = "podman build -f Dockerfile.registry -t registry ." 25 | registrySaveCmd = "podman save -o %s/registry.tar %s" 26 | registryLoadCmd = "podman load -q -i %s/registry.tar" 27 | 28 | registryAttempts = 3 29 | registrySleepBetweenAttempts = 5 30 | 31 | dataDir = "data" 32 | imagesDir = "images" 33 | ) 34 | 35 | type Registry interface { 36 | StartRegistry() error 37 | StopRegistry() error 38 | } 39 | 40 | type HTTPClient interface { 41 | Do(req *http.Request) (*http.Response, error) 42 | } 43 | 44 | type RegistryConfig struct { 45 | Executer executer.Executer 46 | HTTPClient HTTPClient 47 | Port int 48 | URI string 49 | DataDirPath string 50 | } 51 | 52 | type registry struct { 53 | RegistryConfig 54 | registryURL string 55 | } 56 | 57 | func NewRegistry(config RegistryConfig) Registry { 58 | if config.Executer == nil { 59 | config.Executer = executer.NewExecuter() 60 | } 61 | 62 | if config.HTTPClient == nil { 63 | config.HTTPClient = &http.Client{ 64 | Timeout: 5 * time.Second, 65 | } 66 | } 67 | return ®istry{ 68 | RegistryConfig: config, 69 | registryURL: fmt.Sprintf("http://127.0.0.1:%d", config.Port), 70 | } 71 | } 72 | 73 | func (r *registry) verifyRegistryAvailability(registryURL string) error { 74 | for i := 0; i < registryAttempts; i++ { 75 | logrus.Debugf("image registry availability check attempts %d/%d", i+1, registryAttempts) 76 | req, _ := http.NewRequest("GET", registryURL, nil) 77 | resp, err := r.HTTPClient.Do(req) 78 | if err != nil || resp.StatusCode != http.StatusOK { 79 | time.Sleep(registrySleepBetweenAttempts * time.Second) 80 | continue 81 | } 82 | if resp.StatusCode == http.StatusOK { 83 | return nil 84 | } 85 | } 86 | return errors.Errorf("image registry at %s was not available after %d attempts", registryURL, registryAttempts) 87 | } 88 | 89 | func (r *registry) StartRegistry() error { 90 | var err error 91 | _ = r.StopRegistry() 92 | 93 | if err = os.RemoveAll(r.DataDirPath); err != nil { 94 | return err 95 | } 96 | if err = os.MkdirAll(r.DataDirPath, os.ModePerm); err != nil { 97 | return err 98 | } 99 | 100 | _, err = r.Executer.Execute(fmt.Sprintf(registryStartCmd, r.DataDirPath, r.Port, r.URI)) 101 | if err != nil { 102 | return errors.Wrapf(err, "registry start failure") 103 | } 104 | 105 | if err = r.verifyRegistryAvailability(r.registryURL); err != nil { 106 | return err 107 | } 108 | return nil 109 | } 110 | 111 | func (r *registry) StopRegistry() error { 112 | logrus.Debug("Stopping registry container") 113 | _, err := r.Executer.Execute(registryStopCmd) 114 | if err != nil { 115 | return errors.Wrapf(err, "registry stop failure") 116 | 117 | } 118 | return nil 119 | } 120 | 121 | func GetRegistryDataPath(directory, subDirectory string) (string, error) { 122 | pwd, err := os.Getwd() 123 | if err != nil { 124 | return "", err 125 | } 126 | if strings.HasPrefix(directory, pwd) { 127 | // Removes the working directory if already exists 128 | // (happens when using the openshift-appliance binary flow) 129 | directory = strings.ReplaceAll(directory, pwd, "") 130 | } 131 | return filepath.Join(pwd, directory, subDirectory), nil 132 | } 133 | 134 | func BuildRegistryImage(destDir string) error { 135 | exec := executer.NewExecuter() 136 | // Build image 137 | _, err := exec.Execute(registryBuildCmd) 138 | if err != nil { 139 | return err 140 | } 141 | // Store image 142 | _, err = exec.Execute(fmt.Sprintf(registrySaveCmd, destDir, consts.RegistryImage)) 143 | return err 144 | } 145 | 146 | func LoadRegistryImage(cacheDir string) error { 147 | exec := executer.NewExecuter() 148 | // Load image 149 | _, err := exec.Execute(fmt.Sprintf(registryLoadCmd, cacheDir)) 150 | return err 151 | } 152 | 153 | func CopyRegistryImageIfNeeded(envConfig *config.EnvConfig, applianceConfig *config.ApplianceConfig) (string, error) { 154 | registryFilename := filepath.Base(consts.RegistryFilePath) 155 | fileInCachePath := filepath.Join(envConfig.CacheDir, registryFilename) 156 | registryUri := swag.StringValue(applianceConfig.Config.ImageRegistry.URI) 157 | 158 | if registryUri == "" { 159 | // Use an internally built registry image 160 | registryUri = consts.RegistryImage 161 | } 162 | 163 | // Search for registry image in cache dir 164 | if fileName := envConfig.FindInCache(registryFilename); fileName != "" { 165 | logrus.Debug("Reusing registry.tar from cache") 166 | if err := LoadRegistryImage(envConfig.CacheDir); err != nil { 167 | return "", err 168 | } 169 | } else if registryUri == consts.RegistryImage { 170 | // Build the registry image internally 171 | if err := BuildRegistryImage(envConfig.CacheDir); err != nil { 172 | return "", err 173 | } 174 | } else { 175 | // Pulling the registry image and copying to cache 176 | if err := skopeo.NewSkopeo(nil).CopyToFile( 177 | registryUri, 178 | consts.RegistryImage, 179 | fileInCachePath); err != nil { 180 | return registryUri, err 181 | } 182 | } 183 | 184 | // Copy to data dir in temp 185 | fileInDataDir := filepath.Join(envConfig.TempDir, dataDir, imagesDir, consts.RegistryFilePath) 186 | if err := fileutil.CopyFile(fileInCachePath, fileInDataDir); err != nil { 187 | logrus.Error(err) 188 | return "", err 189 | } 190 | 191 | return registryUri, nil 192 | } 193 | -------------------------------------------------------------------------------- /pkg/registry/registry_test.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/golang/mock/gomock" 11 | . "github.com/onsi/ginkgo/v2/dsl/core" 12 | . "github.com/onsi/gomega" 13 | "github.com/openshift/appliance/pkg/executer" 14 | ) 15 | 16 | type ClientMock struct{} 17 | 18 | func (c *ClientMock) Do(req *http.Request) (*http.Response, error) { 19 | if strings.Contains(req.URL.String(), "127.0.0.1") { 20 | return &http.Response{StatusCode: 200}, nil 21 | } 22 | return nil, errors.New("test client error, unexpected URL") 23 | } 24 | 25 | var _ = Describe("Test Image Registry", func() { 26 | var ( 27 | ctrl *gomock.Controller 28 | mockExecuter *executer.MockExecuter 29 | port = 2345 30 | uri = "example.io/foobar/registry:1234" 31 | ) 32 | 33 | BeforeEach(func() { 34 | ctrl = gomock.NewController(GinkgoT()) 35 | mockExecuter = executer.NewMockExecuter(ctrl) 36 | }) 37 | 38 | It("Start Registry - Valid Config", func() { 39 | dataDirPath, err := GetRegistryDataPath("/fake/path", "/data") 40 | Expect(err).NotTo(HaveOccurred()) 41 | 42 | mockExecuter.EXPECT().Execute(fmt.Sprintf(registryStartCmd, dataDirPath, port, uri)).Return("", nil).Times(1) 43 | mockExecuter.EXPECT().Execute(registryStopCmd).Return("", nil).Times(1) 44 | 45 | imageRegistry := NewRegistry( 46 | RegistryConfig{ 47 | URI: uri, 48 | Port: port, 49 | Executer: mockExecuter, 50 | HTTPClient: &ClientMock{}, 51 | DataDirPath: dataDirPath, 52 | }) 53 | 54 | err = imageRegistry.StartRegistry() 55 | Expect(err).ToNot(HaveOccurred()) 56 | }) 57 | 58 | It("Start Registry - fail to start", func() { 59 | dataDirPath, err := GetRegistryDataPath("/fake/path", "/data") 60 | Expect(err).NotTo(HaveOccurred()) 61 | 62 | startCmd := fmt.Sprintf(registryStartCmd, dataDirPath, port, uri) 63 | 64 | mockExecuter.EXPECT().Execute(registryStopCmd).Return("", nil).Times(1) 65 | mockExecuter.EXPECT().Execute(startCmd).Return("", errors.New("some error")).Times(1) 66 | 67 | imageRegistry := NewRegistry( 68 | RegistryConfig{ 69 | URI: uri, 70 | Port: port, 71 | Executer: mockExecuter, 72 | HTTPClient: &ClientMock{}, 73 | DataDirPath: dataDirPath, 74 | }) 75 | 76 | err = imageRegistry.StartRegistry() 77 | Expect(err).To(HaveOccurred()) 78 | }) 79 | 80 | It("Stop Registry - Success", func() { 81 | mockExecuter.EXPECT().Execute(registryStopCmd).Return("", nil).Times(1) 82 | 83 | imageRegistry := NewRegistry( 84 | RegistryConfig{ 85 | URI: uri, 86 | Port: port, 87 | Executer: mockExecuter, 88 | HTTPClient: &ClientMock{}, 89 | }) 90 | 91 | err := imageRegistry.StopRegistry() 92 | Expect(err).NotTo(HaveOccurred()) 93 | }) 94 | 95 | It("Stop Registry - Fail", func() { 96 | mockExecuter.EXPECT().Execute(registryStopCmd).Return("", errors.New("some error")).Times(1) 97 | 98 | imageRegistry := NewRegistry( 99 | RegistryConfig{ 100 | URI: uri, 101 | Port: port, 102 | Executer: mockExecuter, 103 | HTTPClient: &ClientMock{}, 104 | }) 105 | 106 | err := imageRegistry.StopRegistry() 107 | Expect(err).To(HaveOccurred()) 108 | }) 109 | }) 110 | 111 | func TestRegistry(t *testing.T) { 112 | RegisterFailHandler(Fail) 113 | RunSpecs(t, "registry_test") 114 | } 115 | -------------------------------------------------------------------------------- /pkg/release/mock_release.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: release.go 3 | 4 | // Package release is a generated GoMock package. 5 | package release 6 | 7 | import ( 8 | reflect "reflect" 9 | 10 | gomock "github.com/golang/mock/gomock" 11 | ) 12 | 13 | // MockRelease is a mock of Release interface. 14 | type MockRelease struct { 15 | ctrl *gomock.Controller 16 | recorder *MockReleaseMockRecorder 17 | } 18 | 19 | // MockReleaseMockRecorder is the mock recorder for MockRelease. 20 | type MockReleaseMockRecorder struct { 21 | mock *MockRelease 22 | } 23 | 24 | // NewMockRelease creates a new mock instance. 25 | func NewMockRelease(ctrl *gomock.Controller) *MockRelease { 26 | mock := &MockRelease{ctrl: ctrl} 27 | mock.recorder = &MockReleaseMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use. 32 | func (m *MockRelease) EXPECT() *MockReleaseMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // ExtractFile mocks base method. 37 | func (m *MockRelease) ExtractFile(image, filename string) (string, error) { 38 | m.ctrl.T.Helper() 39 | ret := m.ctrl.Call(m, "ExtractFile", image, filename) 40 | ret0, _ := ret[0].(string) 41 | ret1, _ := ret[1].(error) 42 | return ret0, ret1 43 | } 44 | 45 | // ExtractFile indicates an expected call of ExtractFile. 46 | func (mr *MockReleaseMockRecorder) ExtractFile(image, filename interface{}) *gomock.Call { 47 | mr.mock.ctrl.T.Helper() 48 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExtractFile", reflect.TypeOf((*MockRelease)(nil).ExtractFile), image, filename) 49 | } 50 | 51 | func (m *MockRelease) ExtractCommand(command string, dest string) (string, error) { 52 | m.ctrl.T.Helper() 53 | ret := m.ctrl.Call(m, "ExtractCommand", command, dest) 54 | ret0, _ := ret[0].(string) 55 | ret1, _ := ret[1].(error) 56 | return ret0, ret1 57 | } 58 | 59 | func (mr *MockReleaseMockRecorder) ExtractCommand(command string, dest string) *gomock.Call { 60 | mr.mock.ctrl.T.Helper() 61 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExtractCommand", reflect.TypeOf((*MockRelease)(nil).ExtractCommand), command, dest) 62 | } 63 | 64 | // GetImageFromRelease mocks base method. 65 | func (m *MockRelease) GetImageFromRelease(imageName string) (string, error) { 66 | m.ctrl.T.Helper() 67 | ret := m.ctrl.Call(m, "GetImageFromRelease", imageName) 68 | ret0, _ := ret[0].(string) 69 | ret1, _ := ret[1].(error) 70 | return ret0, ret1 71 | } 72 | 73 | // GetImageFromRelease indicates an expected call of GetImageFromRelease. 74 | func (mr *MockReleaseMockRecorder) GetImageFromRelease(imageName interface{}) *gomock.Call { 75 | mr.mock.ctrl.T.Helper() 76 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetImageFromRelease", reflect.TypeOf((*MockRelease)(nil).GetImageFromRelease), imageName) 77 | } 78 | 79 | // MirrorInstallImages mocks base method. 80 | func (m *MockRelease) MirrorInstallImages() error { 81 | m.ctrl.T.Helper() 82 | ret := m.ctrl.Call(m, "MirrorReleaseImages") 83 | ret0, _ := ret[0].(error) 84 | return ret0 85 | } 86 | 87 | // MirrorReleaseImages indicates an expected call of MirrorReleaseImages. 88 | func (mr *MockReleaseMockRecorder) MirrorReleaseImages() *gomock.Call { 89 | mr.mock.ctrl.T.Helper() 90 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MirrorReleaseImages", reflect.TypeOf((*MockRelease)(nil).MirrorInstallImages)) 91 | } 92 | -------------------------------------------------------------------------------- /pkg/release/release_test.go: -------------------------------------------------------------------------------- 1 | package release 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "testing" 9 | 10 | "github.com/go-openapi/swag" 11 | "github.com/golang/mock/gomock" 12 | . "github.com/onsi/ginkgo/v2/dsl/core" 13 | . "github.com/onsi/gomega" 14 | "github.com/openshift/appliance/pkg/asset/config" 15 | "github.com/openshift/appliance/pkg/executer" 16 | "github.com/openshift/appliance/pkg/graph" 17 | "github.com/openshift/appliance/pkg/types" 18 | ) 19 | 20 | type FakeOS struct{} 21 | 22 | func (FakeOS) MkdirTemp(dir, prefix string) (string, error) { 23 | return os.MkdirTemp(dir, prefix) 24 | } 25 | 26 | func (FakeOS) Stat(name string) (os.FileInfo, error) { 27 | return os.Stat(name) 28 | } 29 | 30 | func (FakeOS) Remove(name string) error { 31 | return os.Remove(name) 32 | } 33 | 34 | func (FakeOS) UserHomeDir() (string, error) { 35 | return os.UserHomeDir() 36 | } 37 | 38 | func (FakeOS) MkdirAll(path string, perm os.FileMode) error { 39 | return nil 40 | } 41 | 42 | func (FakeOS) WriteFile(name string, data []byte, perm os.FileMode) error { 43 | return nil 44 | } 45 | 46 | func (FakeOS) ReadFile(name string) ([]byte, error) { 47 | return nil, nil 48 | } 49 | 50 | func (FakeOS) RemoveAll(path string) error { 51 | return nil 52 | } 53 | 54 | var _ = Describe("Test Release", func() { 55 | var ( 56 | ctrl *gomock.Controller 57 | mockExecuter *executer.MockExecuter 58 | applianceConfig *config.ApplianceConfig 59 | testRelease Release 60 | tempDir string 61 | err error 62 | ) 63 | 64 | BeforeEach(func() { 65 | ctrl = gomock.NewController(GinkgoT()) 66 | mockExecuter = executer.NewMockExecuter(ctrl) 67 | tempDir, err = filepath.Abs("") 68 | Expect(err).ToNot(HaveOccurred()) 69 | 70 | channel := graph.ReleaseChannelStable 71 | applianceConfig = &config.ApplianceConfig{ 72 | Config: &types.ApplianceConfig{ 73 | OcpRelease: types.ReleaseImage{ 74 | CpuArchitecture: swag.String(config.CpuArchitectureX86), 75 | Version: "4.13.1", 76 | Channel: &channel, 77 | }, 78 | ImageRegistry: &types.ImageRegistry{ 79 | Port: swag.Int(5123), 80 | }, 81 | }, 82 | } 83 | 84 | coreOSConfig := ReleaseConfig{ 85 | OSInterface: &FakeOS{}, 86 | ApplianceConfig: applianceConfig, 87 | Executer: mockExecuter, 88 | EnvConfig: &config.EnvConfig{ 89 | TempDir: tempDir, 90 | }, 91 | } 92 | testRelease = NewRelease(coreOSConfig) 93 | }) 94 | 95 | It("MirrorInstallImages - success", func() { 96 | mockExecuter.EXPECT().Execute(gomock.Any()).Return("", nil).Times(1) 97 | 98 | err = testRelease.MirrorInstallImages() 99 | Expect(err).ToNot(HaveOccurred()) 100 | }) 101 | 102 | It("MirrorInstallImages - fail oc mirror", func() { 103 | mockExecuter.EXPECT().Execute(gomock.Any()).Return("", errors.New("some error")).Times(1) 104 | 105 | err = testRelease.MirrorInstallImages() 106 | Expect(err).To(HaveOccurred()) 107 | }) 108 | 109 | It("GetImageFromRelease - success", func() { 110 | imageName := "machine-os-images" 111 | cmd := fmt.Sprintf(templateGetImage, imageName, true, swag.StringValue(applianceConfig.Config.OcpRelease.URL)) 112 | mockExecuter.EXPECT().Execute(cmd).Return("", nil).Times(1) 113 | 114 | _, err := testRelease.GetImageFromRelease(imageName) 115 | Expect(err).NotTo(HaveOccurred()) 116 | }) 117 | 118 | It("GetImageFromRelease - fail oc adm release info", func() { 119 | imageName := "machine-os-images" 120 | mockExecuter.EXPECT().Execute(gomock.Any()).Return("", errors.New("some error")).Times(1) 121 | 122 | _, err := testRelease.GetImageFromRelease(imageName) 123 | Expect(err).To(HaveOccurred()) 124 | }) 125 | }) 126 | 127 | func TestRelease(t *testing.T) { 128 | RegisterFailHandler(Fail) 129 | RunSpecs(t, "release_test") 130 | } 131 | -------------------------------------------------------------------------------- /pkg/skopeo/skopeo.go: -------------------------------------------------------------------------------- 1 | package skopeo 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/openshift/appliance/pkg/executer" 9 | ) 10 | 11 | const ( 12 | templateCopyToFile = "skopeo copy docker://%s docker-archive:%s:%s" 13 | ) 14 | 15 | type Skopeo interface { 16 | CopyToFile(imageUrl, imageName, filePath string) error 17 | } 18 | 19 | type skopeo struct { 20 | executer executer.Executer 21 | } 22 | 23 | func NewSkopeo(exec executer.Executer) Skopeo { 24 | if exec == nil { 25 | exec = executer.NewExecuter() 26 | } 27 | 28 | return &skopeo{ 29 | executer: exec, 30 | } 31 | } 32 | 33 | func (s *skopeo) CopyToFile(imageUrl, imageName, filePath string) error { 34 | if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil { 35 | return err 36 | } 37 | 38 | _, err := s.executer.Execute(fmt.Sprintf(templateCopyToFile, imageUrl, filePath, imageName)) 39 | return err 40 | } 41 | -------------------------------------------------------------------------------- /pkg/skopeo/skopeo_test.go: -------------------------------------------------------------------------------- 1 | package skopeo 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/openshift/appliance/pkg/consts" 9 | 10 | "github.com/golang/mock/gomock" 11 | . "github.com/onsi/ginkgo/v2/dsl/core" 12 | . "github.com/onsi/gomega" 13 | "github.com/openshift/appliance/pkg/executer" 14 | ) 15 | 16 | var _ = Describe("Test Skopeo", func() { 17 | var ( 18 | ctrl *gomock.Controller 19 | mockExecuter *executer.MockExecuter 20 | testSkopeo Skopeo 21 | ) 22 | 23 | BeforeEach(func() { 24 | ctrl = gomock.NewController(GinkgoT()) 25 | mockExecuter = executer.NewMockExecuter(ctrl) 26 | testSkopeo = NewSkopeo(mockExecuter) 27 | }) 28 | 29 | It("skopeo CopyToFile - success", func() { 30 | 31 | fakePath := "path/to/registry.tar" 32 | cmd := fmt.Sprintf(templateCopyToFile, consts.RegistryImage, fakePath, consts.RegistryImage) 33 | mockExecuter.EXPECT().Execute(cmd).Return("", nil).Times(1) 34 | 35 | err := testSkopeo.CopyToFile(consts.RegistryImage, consts.RegistryImage, fakePath) 36 | Expect(err).ToNot(HaveOccurred()) 37 | }) 38 | 39 | It("skopeo CopyToFile - failure", func() { 40 | fakePath := "path/to/registry.tar" 41 | mockExecuter.EXPECT().Execute(gomock.Any()).Return("", errors.New("some error")).Times(1) 42 | 43 | err := testSkopeo.CopyToFile(consts.RegistryImage, consts.RegistryImage, fakePath) 44 | Expect(err).To(HaveOccurred()) 45 | }) 46 | }) 47 | 48 | func TestSkopeo(t *testing.T) { 49 | RegisterFailHandler(Fail) 50 | RunSpecs(t, "skopeo_test") 51 | } 52 | -------------------------------------------------------------------------------- /pkg/syslinux/syslinux.go: -------------------------------------------------------------------------------- 1 | package syslinux 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/openshift/appliance/pkg/executer" 7 | ) 8 | 9 | const ( 10 | isoHybridCmd = "isohybrid -u %s" 11 | ) 12 | 13 | type IsoHybrid interface { 14 | Convert(imagePath string) error 15 | } 16 | 17 | type isohybrid struct { 18 | executer executer.Executer 19 | } 20 | 21 | func NewIsoHybrid(exec executer.Executer) IsoHybrid { 22 | if exec == nil { 23 | exec = executer.NewExecuter() 24 | } 25 | 26 | return &isohybrid{ 27 | executer: exec, 28 | } 29 | } 30 | 31 | func (s *isohybrid) Convert(imagePath string) error { 32 | _, err := s.executer.Execute(fmt.Sprintf(isoHybridCmd, imagePath)) 33 | return err 34 | } 35 | -------------------------------------------------------------------------------- /pkg/syslinux/syslinux_test.go: -------------------------------------------------------------------------------- 1 | package syslinux 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/golang/mock/gomock" 9 | . "github.com/onsi/ginkgo/v2/dsl/core" 10 | . "github.com/onsi/gomega" 11 | "github.com/openshift/appliance/pkg/executer" 12 | ) 13 | 14 | var _ = Describe("Test IsoHybrid", func() { 15 | var ( 16 | ctrl *gomock.Controller 17 | mockExecuter *executer.MockExecuter 18 | testIsoHybrid IsoHybrid 19 | fakeImagePath = "/path/to/testdata.iso" 20 | ) 21 | 22 | BeforeEach(func() { 23 | ctrl = gomock.NewController(GinkgoT()) 24 | mockExecuter = executer.NewMockExecuter(ctrl) 25 | testIsoHybrid = NewIsoHybrid(mockExecuter) 26 | }) 27 | 28 | It("isohybrid Convert - success", func() { 29 | 30 | fakeImagePath = "/path/to/testdata.iso" 31 | 32 | cmd := fmt.Sprintf(isoHybridCmd, fakeImagePath) 33 | mockExecuter.EXPECT().Execute(cmd).Return("", nil).Times(1) 34 | 35 | err := testIsoHybrid.Convert(fakeImagePath) 36 | Expect(err).ToNot(HaveOccurred()) 37 | }) 38 | 39 | It("isohybrid Convert - failure", func() { 40 | mockExecuter.EXPECT().Execute(gomock.Any()).Return("", errors.New("some error")).Times(1) 41 | 42 | err := testIsoHybrid.Convert(fakeImagePath) 43 | Expect(err).To(HaveOccurred()) 44 | }) 45 | }) 46 | 47 | func TestGenIsoImage(t *testing.T) { 48 | RegisterFailHandler(Fail) 49 | RunSpecs(t, "isohybrid_test") 50 | } 51 | -------------------------------------------------------------------------------- /pkg/templates/data.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "path/filepath" 7 | 8 | "github.com/go-openapi/swag" 9 | "github.com/openshift/appliance/pkg/asset/config" 10 | "github.com/openshift/appliance/pkg/asset/registry" 11 | "github.com/openshift/appliance/pkg/consts" 12 | "github.com/openshift/appliance/pkg/types" 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | func GetUserCfgTemplateData(grubMenuEntryName string, enableFips bool) interface{} { 17 | var fipsArg string 18 | if enableFips { 19 | fipsArg = "fips=1" 20 | } 21 | 22 | return struct { 23 | GrubTimeout int 24 | GrubMenuEntryName, RecoveryPartitionName string 25 | FipsArg string 26 | }{ 27 | GrubTimeout: consts.GrubTimeout, 28 | RecoveryPartitionName: consts.RecoveryPartitionName, 29 | GrubMenuEntryName: grubMenuEntryName, 30 | FipsArg: fipsArg, 31 | } 32 | } 33 | 34 | func GetGuestfishScriptTemplateData(isCompact bool, diskSize, baseIsoSize, recoveryIsoSize, dataIsoSize int64, 35 | baseImageFile, applianceImageFile, recoveryIsoFile, dataIsoFile, userCfgFile, grubCfgFile, tempDir string) interface{} { 36 | 37 | partitionsInfo := NewPartitions().GetAgentPartitions(diskSize, baseIsoSize, recoveryIsoSize, dataIsoSize, isCompact) 38 | 39 | return struct { 40 | ApplianceFile, RecoveryIsoFile, DataIsoFile, CoreOSImage, RecoveryPartitionName, DataPartitionName, ReservedPartitionGUID string 41 | UserCfgFile, GrubCfgFile, GrubTempDir string 42 | DiskSize, RecoveryStartSector, RecoveryEndSector, DataStartSector, DataEndSector, RootStartSector, RootEndSector int64 43 | }{ 44 | ApplianceFile: applianceImageFile, 45 | RecoveryIsoFile: recoveryIsoFile, 46 | DataIsoFile: dataIsoFile, 47 | DiskSize: diskSize, 48 | CoreOSImage: baseImageFile, 49 | RecoveryStartSector: partitionsInfo.RecoveryPartition.StartSector, 50 | RecoveryEndSector: partitionsInfo.RecoveryPartition.EndSector, 51 | DataStartSector: partitionsInfo.DataPartition.StartSector, 52 | DataEndSector: partitionsInfo.DataPartition.EndSector, 53 | RootStartSector: partitionsInfo.RootPartition.StartSector, 54 | RootEndSector: partitionsInfo.RootPartition.EndSector, 55 | RecoveryPartitionName: consts.RecoveryPartitionName, 56 | DataPartitionName: consts.DataPartitionName, 57 | ReservedPartitionGUID: consts.ReservedPartitionGUID, 58 | UserCfgFile: userCfgFile, 59 | GrubCfgFile: grubCfgFile, 60 | GrubTempDir: filepath.Join(tempDir, "scripts/grub"), 61 | } 62 | } 63 | 64 | func GetImageSetTemplateData(applianceConfig *config.ApplianceConfig, blockedImages, additionalImages, operators string) interface{} { 65 | return struct { 66 | ReleaseImage string 67 | BlockedImages string 68 | AdditionalImages string 69 | Operators string 70 | }{ 71 | ReleaseImage: swag.StringValue(applianceConfig.Config.OcpRelease.URL), 72 | BlockedImages: blockedImages, 73 | AdditionalImages: additionalImages, 74 | Operators: operators, 75 | } 76 | } 77 | 78 | func GetPinnedImageSetTemplateData(images, role string) interface{} { 79 | return struct { 80 | Role string 81 | Images string 82 | }{ 83 | Role: role, 84 | Images: images, 85 | } 86 | } 87 | 88 | func GetBootstrapIgnitionTemplateData(isLiveISO, enableInteractiveFlow bool, ocpReleaseImage types.ReleaseImage, registryDataPath, installIgnitionConfig, coreosImagePath, rendezvousHostEnvPlaceholder string) interface{} { 89 | releaseImageArr := []map[string]any{ 90 | { 91 | "openshift_version": ocpReleaseImage.Version, 92 | "version": ocpReleaseImage.Version, 93 | "cpu_architecture": swag.StringValue(ocpReleaseImage.CpuArchitecture), 94 | "url": ocpReleaseImage.URL, 95 | }, 96 | } 97 | releaseImages, _ := json.Marshal(releaseImageArr) 98 | 99 | osImageArr := []map[string]any{ 100 | { 101 | "openshift_version": ocpReleaseImage.Version, 102 | "cpu_architecture": swag.StringValue(ocpReleaseImage.CpuArchitecture), 103 | "version": "n/a", 104 | "url": "n/a", 105 | }, 106 | } 107 | osImages, _ := json.Marshal(osImageArr) 108 | 109 | data := struct { 110 | IsBootstrapStep bool 111 | IsLiveISO bool 112 | EnableInteractiveFlow bool 113 | InstallIgnitionConfig string 114 | RendezvousHostEnvPlaceholder string 115 | 116 | ReleaseImages, ReleaseImage, OsImages string 117 | RegistryDataPath, RegistryDomain, RegistryFilePath, RegistryImage string 118 | 119 | Partition0, Partition1, Partition2, Partition3 Partition 120 | }{ 121 | IsBootstrapStep: true, 122 | IsLiveISO: isLiveISO, 123 | EnableInteractiveFlow: enableInteractiveFlow, 124 | InstallIgnitionConfig: installIgnitionConfig, 125 | RendezvousHostEnvPlaceholder: rendezvousHostEnvPlaceholder, 126 | 127 | // Images 128 | ReleaseImages: string(releaseImages), 129 | ReleaseImage: swag.StringValue(ocpReleaseImage.URL), 130 | OsImages: string(osImages), 131 | 132 | // Registry 133 | RegistryDataPath: registryDataPath, 134 | RegistryDomain: registry.RegistryDomain, 135 | RegistryFilePath: consts.RegistryFilePath, 136 | RegistryImage: consts.RegistryImage, 137 | } 138 | 139 | // Fetch base image partitions (Disk image mode) 140 | if coreosImagePath != "" { 141 | partitions, err := NewPartitions().GetCoreOSPartitions(coreosImagePath) 142 | if err != nil { 143 | logrus.Fatal(err) 144 | } 145 | 146 | // CoreOS Partitions 147 | data.Partition0 = partitions[0] 148 | data.Partition1 = partitions[1] 149 | data.Partition2 = partitions[2] 150 | data.Partition3 = partitions[3] 151 | } 152 | 153 | return data 154 | } 155 | 156 | func GetInstallIgnitionTemplateData(isLiveISO bool, registryDataPath, corePassHash string) interface{} { 157 | return struct { 158 | IsBootstrapStep bool 159 | IsLiveISO bool 160 | 161 | RegistryDataPath, RegistryDomain, RegistryFilePath, RegistryImage string 162 | CorePassHash, GrubCfgFilePath, UserCfgFilePath string 163 | }{ 164 | IsBootstrapStep: false, 165 | IsLiveISO: isLiveISO, 166 | 167 | // Registry 168 | RegistryDataPath: registryDataPath, 169 | RegistryDomain: registry.RegistryDomain, 170 | RegistryFilePath: consts.RegistryFilePath, 171 | RegistryImage: consts.RegistryImage, 172 | GrubCfgFilePath: consts.GrubCfgFilePath, 173 | UserCfgFilePath: consts.UserCfgFilePath, 174 | CorePassHash: corePassHash, 175 | } 176 | } 177 | 178 | func GetDeployIgnitionTemplateData(targetDevice, postScript string, sparseClone, dryRun bool) interface{} { 179 | return struct { 180 | ApplianceFileName, ApplianceImageName, ApplianceImageTar string 181 | TargetDevice, PostScript string 182 | SparseClone, DryRun bool 183 | }{ 184 | ApplianceFileName: consts.ApplianceFileName, 185 | ApplianceImageName: consts.ApplianceImageName, 186 | ApplianceImageTar: consts.ApplianceImageTar, 187 | TargetDevice: targetDevice, 188 | PostScript: postScript, 189 | SparseClone: sparseClone, 190 | DryRun: dryRun, 191 | } 192 | } 193 | 194 | func GetRegistryEnv(registryData, registryUpgrade string) string { 195 | return fmt.Sprintf(`REGISTRY_IMAGE=%s 196 | REGISTRY_DATA=%s 197 | REGISTRY_UPGRADE=%s 198 | `, consts.RegistryImage, registryData, registryUpgrade) 199 | } 200 | 201 | func GetUpgradeISOEnv(releaseImage, releaseVersion string) string { 202 | return fmt.Sprintf(`RELEASE_IMAGE=%s 203 | RELEASE_VERSION=%s 204 | `, releaseImage, releaseVersion) 205 | } 206 | -------------------------------------------------------------------------------- /pkg/templates/partitions.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/diskfs/go-diskfs" 7 | "github.com/openshift/appliance/pkg/conversions" 8 | "github.com/sirupsen/logrus" 9 | ) 10 | 11 | const ( 12 | sectorSize = int64(512) 13 | sectorSize64K = int64(64 * 1024) 14 | 15 | // We align the partitions to block size of 64K, as suggested for best performance: 16 | // https://libguestfs.org/virt-alignment-scan.1.html 17 | sectorAlignmentFactor = sectorSize64K / sectorSize 18 | ) 19 | 20 | type Partitions interface { 21 | GetAgentPartitions(diskSize, baseIsoSize, recoveryIsoSize, dataIsoSize int64, isCompact bool) *AgentPartitions 22 | GetCoreOSPartitions(coreosImagePath string) ([]Partition, error) 23 | GetBootPartitionsSize(baseImageFile string) int64 24 | } 25 | 26 | type Partition struct { 27 | StartSector, EndSector, Size int64 28 | } 29 | 30 | type AgentPartitions struct { 31 | RecoveryPartition, DataPartition, RootPartition *Partition 32 | } 33 | 34 | type partitions struct { 35 | } 36 | 37 | func NewPartitions() Partitions { 38 | return &partitions{} 39 | } 40 | 41 | func (p *partitions) GetAgentPartitions(diskSize, baseIsoSize, recoveryIsoSize, dataIsoSize int64, isCompact bool) *AgentPartitions { 42 | // Calc data partition start/end sectors 43 | dataEndSector := (conversions.GibToBytes(diskSize) - conversions.MibToBytes(1)) / sectorSize 44 | dataStartSector := dataEndSector - (dataIsoSize / sectorSize) 45 | dataStartSector = roundToNearestSector(dataStartSector, sectorAlignmentFactor) 46 | 47 | // Calc recovery partition start/end sectors 48 | recoveryEndSector := dataStartSector - sectorAlignmentFactor 49 | recoveryStartSector := recoveryEndSector - (recoveryIsoSize / sectorSize) 50 | recoveryStartSector = roundToNearestSector(recoveryStartSector, sectorAlignmentFactor) 51 | 52 | // Calc root partition start/end sectors 53 | rootPartitionSize := p.getRootPartitionSize(diskSize, baseIsoSize, recoveryIsoSize, dataIsoSize, isCompact) 54 | rootEndSector := recoveryStartSector - sectorAlignmentFactor 55 | rootStartSector := rootEndSector - (rootPartitionSize / sectorSize) 56 | rootStartSector = roundToNearestSector(rootStartSector, sectorAlignmentFactor) 57 | 58 | return &AgentPartitions{ 59 | RecoveryPartition: &Partition{StartSector: recoveryStartSector, EndSector: recoveryEndSector}, 60 | DataPartition: &Partition{StartSector: dataStartSector, EndSector: dataEndSector}, 61 | RootPartition: &Partition{StartSector: rootStartSector, EndSector: rootEndSector}, 62 | } 63 | } 64 | 65 | func (p *partitions) GetCoreOSPartitions(coreosImagePath string) ([]Partition, error) { 66 | partitionsInfo := []Partition{} 67 | 68 | disk, err := diskfs.Open(coreosImagePath) 69 | if err != nil { 70 | return nil, err 71 | } 72 | partitionTable, err := disk.GetPartitionTable() 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | partitions := partitionTable.GetPartitions() 78 | for _, partition := range partitions { 79 | partitionsInfo = append(partitionsInfo, Partition{ 80 | StartSector: partition.GetStart() / sectorSize, 81 | Size: partition.GetSize() / sectorSize, 82 | }) 83 | } 84 | 85 | // Root partition should be at least 8GiB 86 | // (https://docs.fedoraproject.org/en-US/fedora-coreos/storage/) 87 | partitionsInfo[3].Size = conversions.GibToBytes(8) / sectorSize 88 | 89 | return partitionsInfo, nil 90 | } 91 | 92 | func (p *partitions) GetBootPartitionsSize(baseImageFile string) int64 { 93 | partitions, err := p.GetCoreOSPartitions(baseImageFile) 94 | if err != nil { 95 | logrus.Fatal(err) 96 | } 97 | 98 | // Calc base disk image size in bytes (including an additional overhead for alignment) 99 | return sectorSize*(partitions[0].Size+partitions[1].Size+partitions[2].Size) + conversions.MibToBytes(1) 100 | } 101 | 102 | func (p *partitions) getRootPartitionSize(diskSize, baseIsoSize, recoveryIsoSize, dataIsoSize int64, isCompact bool) int64 { 103 | if isCompact { 104 | // When using a compact disk image, the root partition is resized during cloning 105 | return sectorSize64K 106 | } 107 | 108 | // Calc root partition size 109 | return conversions.GbToBytes(diskSize) - (baseIsoSize + recoveryIsoSize + dataIsoSize) 110 | } 111 | 112 | // Returns the nearest (and lowest) sector according to a specified alignment factor 113 | // E.g. for 'sector: 19' and 'alignmentFactor: 8' -> returns 16 114 | func roundToNearestSector(sector int64, alignmentFactor int64) int64 { 115 | sectorFloat := float64(sector) 116 | alignmentFactorFloat := float64(alignmentFactor) 117 | return int64(math.Floor(sectorFloat/alignmentFactorFloat) * alignmentFactorFloat) 118 | } 119 | -------------------------------------------------------------------------------- /pkg/templates/partitions_test.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo/v2/dsl/core" 5 | . "github.com/onsi/gomega" 6 | "github.com/openshift/appliance/pkg/conversions" 7 | ) 8 | 9 | var _ = Describe("Test Partitions", func() { 10 | var ( 11 | testPartitions *AgentPartitions 12 | diskSize, baseIsoSize, recoveryIsoSize, dataIsoSize int64 13 | ) 14 | 15 | BeforeEach(func() { 16 | diskSize = 200 17 | baseIsoSize = conversions.GibToBytes(2) 18 | recoveryIsoSize = conversions.GibToBytes(5) 19 | dataIsoSize = conversions.GibToBytes(30) 20 | testPartitions = NewPartitions().GetAgentPartitions(diskSize, baseIsoSize, recoveryIsoSize, dataIsoSize, false) 21 | }) 22 | 23 | It("partitions are aligned to 4K", func() { 24 | Expect(testPartitions.RecoveryPartition.StartSector % sectorAlignmentFactor).To(Equal(int64(0))) 25 | Expect(testPartitions.RecoveryPartition.StartSector % sectorAlignmentFactor).To(Equal(int64(0))) 26 | }) 27 | 28 | It("partitions are not overlapping", func() { 29 | Expect(testPartitions.RecoveryPartition.EndSector < testPartitions.DataPartition.StartSector).To(BeTrue()) 30 | }) 31 | 32 | It("recovery partition is large enough", func() { 33 | partitionSize := (testPartitions.RecoveryPartition.EndSector - testPartitions.RecoveryPartition.StartSector) * sectorSize 34 | Expect(partitionSize >= recoveryIsoSize).To(BeTrue()) 35 | }) 36 | 37 | It("data partition is large enough", func() { 38 | partitionSize := (testPartitions.DataPartition.EndSector - testPartitions.RecoveryPartition.StartSector) * sectorSize 39 | Expect(partitionSize >= dataIsoSize).To(BeTrue()) 40 | }) 41 | 42 | It("end of disk image has an empty 1MiB", func() { 43 | diskSizeInSectors := conversions.GibToBytes(diskSize) / sectorSize 44 | emptyBytes := (diskSizeInSectors - testPartitions.DataPartition.EndSector) * sectorSize 45 | Expect(emptyBytes).To(Equal(conversions.MibToBytes(1))) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /pkg/templates/scripts/grub/user.cfg.template: -------------------------------------------------------------------------------- 1 | set timeout={{.GrubTimeout}} 2 | menuentry '{{.GrubMenuEntryName}}' --class gnu-linux --class gnu --class os { 3 | search --set=root --label {{.RecoveryPartitionName}} 4 | linux /images/pxeboot/vmlinuz coreos.liveiso={{.RecoveryPartitionName}} random.trust_cpu=on console=tty0 console=ttyS0,115200n8 ignition.firstboot ignition.platform.id=metal {{.FipsArg}} 5 | initrd /images/pxeboot/initrd.img /images/ignition.img 6 | } 7 | -------------------------------------------------------------------------------- /pkg/templates/scripts/guestfish/guestfish.sh.template: -------------------------------------------------------------------------------- 1 | #!/usr/bin/guestfish -f 2 | 3 | sparse {{.ApplianceFile}} {{.DiskSize}}G 4 | add-ro {{.CoreOSImage}} 5 | add-ro {{.RecoveryIsoFile}} 6 | add-ro {{.DataIsoFile}} 7 | run 8 | 9 | # Copy CoreOS to appliance diskimage 10 | copy-device-to-device /dev/sdb /dev/sda 11 | 12 | # Move backup GPT data structures to the end of the disk 13 | part-expand-gpt /dev/sda 14 | 15 | # Remove the root partition 16 | # Note: we don't need CoreOS root partition as we boot from recovery partition. 17 | part-del /dev/sda 4 18 | 19 | # Create an empty root partition (for resizing when cloning the disk image) 20 | part-add /dev/sda p {{.RootStartSector}} {{.RootEndSector}} 21 | 22 | # Set root partition name 23 | part-set-name /dev/sda 4 root 24 | 25 | # Create recovery partition 26 | part-add /dev/sda p {{.RecoveryStartSector}} {{.RecoveryEndSector}} 27 | 28 | # Create data partition 29 | part-add /dev/sda p {{.DataStartSector}} {{.DataEndSector}} 30 | 31 | # Copy recovery ISO to data partition 32 | copy-device-to-device /dev/sdc /dev/sda5 33 | 34 | # Copy data ISO to data partition 35 | copy-device-to-device /dev/sdd /dev/sda6 36 | 37 | # Set partition/filesystem labels 38 | part-set-name /dev/sda 5 {{.RecoveryPartitionName}} 39 | part-set-name /dev/sda 6 {{.DataPartitionName}} 40 | 41 | # Set partition as Linux reserved partition 42 | part-set-gpt-type /dev/sda 5 {{.ReservedPartitionGUID}} 43 | part-set-gpt-type /dev/sda 6 {{.ReservedPartitionGUID}} 44 | 45 | # Handle GRUB 46 | mount /dev/sda3 / 47 | copy-out {{.GrubCfgFile}} {{.GrubTempDir}} 48 | ! cat {{.UserCfgFile}} >> {{.GrubTempDir}}/grub.cfg 49 | copy-in {{.GrubTempDir}}/grub.cfg /grub2 50 | rm-f /boot/loader/entries/ostree-1-rhcos.conf 51 | rm-f /boot/loader/entries/ostree-1.conf 52 | umount /dev/sda3 53 | -------------------------------------------------------------------------------- /pkg/templates/scripts/mirror/imageset.yaml.template: -------------------------------------------------------------------------------- 1 | kind: ImageSetConfiguration 2 | apiVersion: mirror.openshift.io/v2alpha1 3 | archiveSize: 8 4 | mirror: 5 | platform: 6 | release: {{.ReleaseImage}} 7 | {{if .AdditionalImages}} additionalImages: 8 | {{.AdditionalImages}}{{end}} 9 | {{if .BlockedImages}} blockedImages: 10 | {{.BlockedImages}}{{end}} 11 | {{if .Operators}} operators: 12 | {{.Operators}}{{end}} 13 | -------------------------------------------------------------------------------- /pkg/templates/scripts/mirror/pinned-image-set.yaml.template: -------------------------------------------------------------------------------- 1 | apiVersion: machineconfiguration.openshift.io/v1alpha1 2 | kind: PinnedImageSet 3 | metadata: 4 | name: {{.Role}}-pinned-image-set 5 | labels: 6 | machineconfiguration.openshift.io/role: {{.Role}} 7 | spec: 8 | pinnedImages: 9 | {{.Images}} 10 | -------------------------------------------------------------------------------- /pkg/templates/templates.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | import ( 4 | "bytes" 5 | "embed" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | "text/template" 10 | 11 | "github.com/pkg/errors" 12 | "github.com/sirupsen/logrus" 13 | ) 14 | 15 | //go:embed scripts 16 | var Scripts embed.FS 17 | 18 | func RenderTemplateFile(fileName string, templateData interface{}, outputDir string) error { 19 | logrus.Debugf("Rendering %s", fileName) 20 | 21 | // Read the template file 22 | content, err := Scripts.ReadFile(fileName) 23 | if err != nil { 24 | return errors.Wrapf(err, "Failed reading file: %s", fileName) 25 | } 26 | 27 | // Apply data on template 28 | renderedFileName := strings.TrimSuffix(fileName, ".template") 29 | data, err := applyTemplateData(renderedFileName, content, templateData) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | // Write the rendered file 35 | if err := writeFile(renderedFileName, data, outputDir); err != nil { 36 | return err 37 | } 38 | return nil 39 | } 40 | 41 | func GetFilePathByTemplate(templateFile, location string) string { 42 | fileName := strings.TrimSuffix(templateFile, ".template") 43 | return filepath.Join(location, fileName) 44 | } 45 | 46 | func writeFile(name string, data []byte, directory string) error { 47 | path := filepath.Join(directory, name) 48 | if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil { 49 | return err 50 | } 51 | if err := os.WriteFile(path, data, os.ModePerm); err != nil { // #nosec G306 52 | return errors.Wrap(err, "failed to write file") 53 | } 54 | 55 | return nil 56 | } 57 | 58 | func applyTemplateData(fileName string, templateFileContent []byte, templateData interface{}) ([]byte, error) { 59 | tmpl := template.New(fileName) 60 | tmpl, err := tmpl.Parse(string(templateFileContent)) 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | buf := &bytes.Buffer{} 66 | if err := tmpl.Execute(buf, templateData); err != nil { 67 | return nil, err 68 | } 69 | return buf.Bytes(), nil 70 | } 71 | -------------------------------------------------------------------------------- /pkg/templates/templates_suite_test.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2/dsl/core" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestTemplates(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "templates_test") 13 | } 14 | -------------------------------------------------------------------------------- /pkg/types/appliance_config_type.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "github.com/openshift/appliance/pkg/graph" 5 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 6 | ) 7 | 8 | // ApplianceConfigApiVersion is the version supported by this package. 9 | const ApplianceConfigApiVersion = "v1beta1" 10 | 11 | type ApplianceConfig struct { 12 | metav1.TypeMeta `json:",inline"` 13 | metav1.ObjectMeta `json:"metadata,omitempty"` 14 | 15 | OcpRelease ReleaseImage `json:"ocpRelease"` 16 | DiskSizeGB *int `json:"diskSizeGb"` 17 | PullSecret string `json:"pullSecret"` 18 | SshKey *string `json:"sshKey"` 19 | UserCorePass *string `json:"userCorePass"` 20 | ImageRegistry *ImageRegistry `json:"imageRegistry"` 21 | EnableDefaultSources *bool `json:"enableDefaultSources"` 22 | EnableFips *bool `json:"enableFips"` 23 | StopLocalRegistry *bool `json:"stopLocalRegistry"` 24 | CreatePinnedImageSets *bool `json:"createPinnedImageSets"` 25 | EnableInteractiveFlow *bool `json:"enableInteractiveFlow"` 26 | UseDefaultSourceNames *bool `json:"useDefaultSourceNames"` 27 | AdditionalImages *[]Image `json:"additionalImages,omitempty"` 28 | BlockedImages *[]Image `json:"blockedImages,omitempty"` 29 | Operators *[]Operator `json:"operators,omitempty"` 30 | } 31 | 32 | type ReleaseImage struct { 33 | Version string `json:"version"` 34 | Channel *graph.ReleaseChannel `json:"channel"` 35 | CpuArchitecture *string `json:"cpuArchitecture"` 36 | URL *string `json:"url"` 37 | } 38 | 39 | type ImageRegistry struct { 40 | URI *string `json:"uri"` 41 | Port *int `json:"port"` 42 | } 43 | 44 | // Structs copied from oc-mirror: https://github.com/openshift/oc-mirror/blob/main/v2/pkg/api/v1alpha2/types_config.go 45 | 46 | // Image contains image pull information. 47 | type Image struct { 48 | // Name of the image. This should be an exact image pin (registry/namespace/name@sha256:) 49 | // but is not required to be. 50 | Name string `json:"name"` 51 | } 52 | 53 | // Operator defines the configuration for operator catalog mirroring. 54 | type Operator struct { 55 | // Mirror specific operator packages, channels, and versions, and their dependencies. 56 | // If HeadsOnly is true, these objects are mirrored on top of heads of all channels. 57 | // Otherwise, only these specific objects are mirrored. 58 | IncludeConfig `json:",inline"` 59 | 60 | // Catalog image to mirror. This image must be pullable and available for subsequent 61 | // pulls on later mirrors. 62 | // This image should be an exact image pin (registry/namespace/name@sha256:) 63 | // but is not required to be. 64 | Catalog string `json:"catalog"` 65 | } 66 | 67 | // IncludeConfig defines a list of packages for 68 | // operator version selection. 69 | type IncludeConfig struct { 70 | // Packages to include. 71 | Packages []IncludePackage `json:"packages" yaml:"packages"` 72 | } 73 | 74 | // IncludePackage contains a name (required) and channels and/or versions 75 | // (optional) to include in the diff. The full package is only included if no channels 76 | // or versions are specified. 77 | type IncludePackage struct { 78 | // Name of package. 79 | Name string `json:"name" yaml:"name"` 80 | // Channels to include. 81 | Channels []IncludeChannel `json:"channels,omitempty" yaml:"channels,omitempty"` 82 | 83 | // All channels containing these bundles are parsed for an upgrade graph. 84 | IncludeBundle `json:",inline"` 85 | } 86 | 87 | // IncludeChannel contains a name (required) and versions (optional) 88 | // to include in the diff. The full channel is only included if no versions are specified. 89 | type IncludeChannel struct { 90 | // Name of channel. 91 | Name string `json:"name" yaml:"name"` 92 | 93 | IncludeBundle `json:",inline"` 94 | } 95 | 96 | // IncludeBundle contains a name (required) and versions (optional) to 97 | // include in the diff. The full package or channel is only included if no 98 | // versions are specified. 99 | type IncludeBundle struct { 100 | // MinVersion to include, plus all versions in the upgrade graph to the MaxVersion. 101 | MinVersion string `json:"minVersion,omitempty" yaml:"minVersion,omitempty"` 102 | // MaxVersion to include as the channel head version. 103 | MaxVersion string `json:"maxVersion,omitempty" yaml:"maxVersion,omitempty"` 104 | // MinBundle to include, plus all bundles in the upgrade graph to the channel head. 105 | // Set this field only if the named bundle has no semantic version metadata. 106 | MinBundle string `json:"minBundle,omitempty" yaml:"minBundle,omitempty"` 107 | } 108 | -------------------------------------------------------------------------------- /registry/Dockerfile.registry: -------------------------------------------------------------------------------- 1 | FROM registry.access.redhat.com/ubi9/ubi:9.6-1747219013 2 | 3 | COPY config.yml config.yml 4 | COPY registry registry 5 | 6 | VOLUME ["/var/lib/registry"] 7 | EXPOSE 5000 8 | 9 | # Disable export traces (see: https://github.com/distribution/distribution/issues/4270) 10 | ENV OTEL_TRACES_EXPORTER none 11 | 12 | ENTRYPOINT ["/registry"] 13 | CMD ["serve", "config.yml"] 14 | 15 | -------------------------------------------------------------------------------- /registry/config.yml: -------------------------------------------------------------------------------- 1 | version: 0.1 2 | log: 3 | fields: 4 | service: registry 5 | storage: 6 | cache: 7 | blobdescriptor: inmemory 8 | filesystem: 9 | rootdirectory: /var/lib/registry 10 | http: 11 | addr: :5000 12 | headers: 13 | X-Content-Type-Options: [nosniff] 14 | health: 15 | storagedriver: 16 | enabled: true 17 | interval: 10s 18 | threshold: 3 19 | -------------------------------------------------------------------------------- /registry/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // #nosec G108 4 | import ( 5 | _ "net/http/pprof" 6 | 7 | "github.com/distribution/distribution/v3/registry" 8 | _ "github.com/distribution/distribution/v3/registry/auth/htpasswd" 9 | _ "github.com/distribution/distribution/v3/registry/auth/silly" 10 | _ "github.com/distribution/distribution/v3/registry/auth/token" 11 | _ "github.com/distribution/distribution/v3/registry/proxy" 12 | _ "github.com/distribution/distribution/v3/registry/storage/driver/filesystem" 13 | ) 14 | 15 | func main() { 16 | // nolint:errcheck 17 | registry.RootCmd.Execute() 18 | } 19 | -------------------------------------------------------------------------------- /rpm-prefetching/README.md: -------------------------------------------------------------------------------- 1 | In order for our builds to be hermetic (without network access) we configure rpm-prefetching. 2 | 3 | If you want to prefetch more RPMs in order to install them during the build, you need to update the `rpms.in.yaml` file and follow [this doc](https://konflux.pages.redhat.com/docs/users/building/prefetching-dependencies.html#enabling-prefetch-builds-for-rpm) to generate the `rpms.lock.yaml` file. 4 | 5 | If you want to better understand the `rpms.in.yaml` file you can look at the project's README [here](https://github.com/konflux-ci/rpm-lockfile-prototype/blob/main/README.md). 6 | -------------------------------------------------------------------------------- /rpm-prefetching/rpms.in.yaml: -------------------------------------------------------------------------------- 1 | contentOrigin: 2 | repofiles: ["./redhat.repo"] 3 | packages: 4 | - yum-utils 5 | - guestfs-tools 6 | - genisoimage 7 | - coreos-installer 8 | - syslinux 9 | - skopeo 10 | - podman 11 | arches: 12 | - x86_64 13 | -------------------------------------------------------------------------------- /skipper-mac.yaml: -------------------------------------------------------------------------------- 1 | registry: quay.io 2 | build-container-image: openshift-appliance-build 3 | 4 | containers: 5 | openshift-appliance-build: Dockerfile.openshift-appliance-build 6 | volumes: 7 | - $HOME/.cache/go-build:/go/pkg/mod 8 | - $HOME/.cache/golangci-lint:$HOME/.cache/golangci-lint 9 | env: 10 | IMAGE: $IMAGE 11 | GOCACHE: "/go/pkg/mod" 12 | -------------------------------------------------------------------------------- /skipper.yaml: -------------------------------------------------------------------------------- 1 | registry: quay.io 2 | build-container-image: openshift-appliance-build 3 | 4 | containers: 5 | openshift-appliance-build: Dockerfile.openshift-appliance-build 6 | volumes: 7 | - $HOME/.cache/go-build:/go/pkg/mod 8 | - $HOME/.cache/golangci-lint:$HOME/.cache/golangci-lint 9 | env: 10 | IMAGE: $IMAGE 11 | GOCACHE: "/go/pkg/mod" 12 | --------------------------------------------------------------------------------