diff --git a/Dockerfile.docs b/Dockerfile.docs new file mode 100644 index 00000000..afb16870 --- /dev/null +++ b/Dockerfile.docs @@ -0,0 +1,3 @@ +FROM squidfunk/mkdocs-material +RUN pip install mkdocs-markdownextradata-plugin +RUN apk add -U git openssh diff --git a/README.md b/README.md deleted file mode 100644 index dd8d42ce..00000000 --- a/README.md +++ /dev/null @@ -1,62 +0,0 @@ -# RancherOS v2 - -# WORK IN PROGRESS - -RancherOS v2 is an immutable Linux distribution built to run Rancher and -it's corresponding Kubernetes distributions [RKE2](https://rke2.io) -and [k3s](https://k3s.io). It is built using the [cOS-toolkit](https://rancher-sandbox.github.io/cos-toolkit-docs/docs/) -and based on openSUSE. Initial node configurations is done using only a -cloud-init style approach and all further maintenance is done using -Kubernetes operators. - -## Use Cases - -RancherOS is intended to be ran as the operating system beneath a Rancher Multi-Cluster -Management server or as a node in a Kubernetes cluster managed by Rancher. RancherOS -also allows you to build stand alone Kubernetes clusters that run an embedded -and smaller version of Rancher to manage the local cluster. A key attribute of RancherOS -is that it is managed by Rancher and thus Rancher will exist either locally in the cluster -or centrally with Rancher Multi-Cluster Manager. - -## Architecture - -### OCI Image based - -RancherOS v2 is an A/B style image based distribution. One first runs -on a read-only image A and to do an upgrade pulls a new read only image -B and then reboots the system to run on B. What is unique about -RancherOS v2 is that the runtime images come from OCI Images. Not an -OCI Image containing special artifacts, but an actual Docker runnable -image that is built using standard Docker build processes. RancherOS is -built using normal `docker build` and if you wish to customize the OS -image all you need to do is create a new `Dockerfile`. - -### rancherd - -RancherOS v2 includes no container runtime, Kubernetes distribution, -or Rancher itself. All of these assests are dynamically pulled at runtime. All that -is included in RancherOS is [rancherd](https://github.com/rancher/rancherd) which -is responsible for bootstrapping RKE2/k3s and Rancher from an OCI registry. This means -an update to containerd, k3s, RKE2, or Rancher does not require an OS upgrade -or node reboot. - -### cloud-init - -RancherOS v2 is initially configured using a simple version of `cloud-init`. -It is not expected that one will need to do a lot of customization to RancherOS -as the core OS's sole purpose is to run Rancher and Kubernetes and not serve as -a generic Linux distribution. - -### RancherOS Operator - -RancherOS v2 includes an operator that is responsible for managing OS upgrades -and assiting with secure device onboarding (SDO). - - -### openSUSE Leap - -RancherOS v2 is based off of openSUSE Leap. There is no specific tie in to -openSUSE beyond that RancherOS assumes the underlying distribution is -based on systemd. We choose openSUSE for obvious reasons, but beyond -that openSUSE Leap provides a stable layer to build upon that is well -tested and has paths to commercial support, if one chooses. \ No newline at end of file diff --git a/chart/templates/rbac.yaml b/chart/templates/rbac.yaml index b96bef98..840c71f4 100644 --- a/chart/templates/rbac.yaml +++ b/chart/templates/rbac.yaml @@ -3,6 +3,14 @@ kind: ClusterRole metadata: name: rancheros-operator rules: +- apiGroups: + - "" + resources: + - 'secrets' + verbs: + - 'get' + - 'watch' + - 'list' - apiGroups: - rancheros.cattle.io resources: @@ -15,10 +23,19 @@ rules: - 'bundles' verbs: - '*' +- apiGroups: + - provisioning.cattle.io + resources: + - 'clusters' + verbs: + - 'get' + - 'watch' + - 'list' - apiGroups: - management.cattle.io resources: - 'settings' + - 'clusterregistrationtokens' verbs: - 'get' - 'watch' diff --git a/cmd/ros-installer/main.go b/cmd/ros-installer/main.go index 7da5f268..5c2b059b 100644 --- a/cmd/ros-installer/main.go +++ b/cmd/ros-installer/main.go @@ -11,7 +11,7 @@ import ( ) var ( - output = flag.Bool("automatic", false, "Check for and run automatic installation") + automatic = flag.Bool("automatic", false, "Check for and run automatic installation") printConfig = flag.Bool("print-config", false, "Print effective configuration and exit") configFile = flag.String("config-file", "", "Config file to use, local file or http/tftp URL") ) @@ -31,7 +31,7 @@ func main() { return } - if err := install.Run(*output, *configFile); err != nil { + if err := install.Run(*automatic, *configFile); err != nil { logrus.Fatal(err) } } diff --git a/framework/files/etc/dracut.conf.d/51-livenet-initrd.conf b/framework/files/etc/dracut.conf.d/51-livenet-initrd.conf index 5edc7d8c..e305f453 100644 --- a/framework/files/etc/dracut.conf.d/51-livenet-initrd.conf +++ b/framework/files/etc/dracut.conf.d/51-livenet-initrd.conf @@ -1,4 +1,4 @@ # This is required for booting a squashfs from network add_dracutmodules+=" livenet " -install_items+=" /etc/ssl " -install_optional_items+=" /var/lib/ca-certificates " +install_items+=" /etc/ssl/* " +install_optional_items+=" /var/lib/ca-certificates/* /var/lib/ca-certificates/*/* " diff --git a/framework/files/system/oem/04_accounting.yaml b/framework/files/system/oem/04_accounting.yaml index 3b82b061..91a59b5c 100644 --- a/framework/files/system/oem/04_accounting.yaml +++ b/framework/files/system/oem/04_accounting.yaml @@ -13,4 +13,4 @@ stages: entity: | kind: "shadow" username: "root" - password: "cos" + password: "ros" diff --git a/framework/files/usr/sbin/ros-operator-install b/framework/files/usr/sbin/ros-operator-install index 51c2b58a..5cb9f764 100755 --- a/framework/files/usr/sbin/ros-operator-install +++ b/framework/files/usr/sbin/ros-operator-install @@ -12,8 +12,6 @@ metadata: namespace: fleet-local spec: osImage: "${IMAGE}" - clusterTargets: - - clusterName: local EOF } diff --git a/pkg/apis/rancheros.cattle.io/v1/os.go b/pkg/apis/rancheros.cattle.io/v1/os.go index ec3ca6aa..120ec9e9 100644 --- a/pkg/apis/rancheros.cattle.io/v1/os.go +++ b/pkg/apis/rancheros.cattle.io/v1/os.go @@ -18,7 +18,6 @@ type ManagedOSImage struct { } type ManagedOSImageSpec struct { - Paused bool `json:"paused,omitempty"` OSImage string `json:"osImage,omitempty"` NodeSelector *metav1.LabelSelector `json:"nodeSelector,omitempty"` Concurrency *int64 `json:"concurrency,omitempty"` diff --git a/pkg/config/config.go b/pkg/config/config.go index 18653d1d..84546b78 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -15,7 +15,10 @@ type Install struct { NoFormat bool `json:"noFormat,omitempty"` Debug bool `json:"debug,omitempty"` TTY string `json:"tty,omitempty"` - Password string `json:"password,omitempy"` + ServerURL string `json:"-"` + Token string `json:"-"` + Role string `json:"-"` + Password string `json:"password,omitempty"` } type Config struct { @@ -24,13 +27,20 @@ type Config struct { } type YipConfig struct { - Stages map[string][]Stage `json:"stages,omitempty"` + Stages map[string][]Stage `json:"stages,omitempty"` + Rancherd Rancherd `json:"rancherd,omitempty"` } type Stage struct { Users map[string]User `json:"users,omitempty"` } +type Rancherd struct { + Server string `json:"server,omitempty"` + Role string `json:"role,omitempty"` + Token string `json:"token,omitempty"` +} + type User struct { Name string `json:"name,omitempty"` PasswordHash string `json:"passwd,omitempty"` diff --git a/pkg/config/read.go b/pkg/config/read.go index 202d1c0a..4d8d84b5 100644 --- a/pkg/config/read.go +++ b/pkg/config/read.go @@ -54,6 +54,35 @@ func mapToEnv(prefix string, data map[string]interface{}) []string { return result } +func readFileFunc(path string) func() (map[string]interface{}, error) { + return func() (map[string]interface{}, error) { + return readFile(path) + } +} + +func readNested(data map[string]interface{}) (map[string]interface{}, error) { + var ( + nestedConfigFiles = convert.ToStringSlice(values.GetValueN(data, "rancheros", "install", "configUrl")) + funcs []reader + ) + + if len(nestedConfigFiles) == 0 { + return data, nil + } + + values.RemoveValue(data, "rancheros", "install", "configUrl") + + for _, nestedConfigFile := range nestedConfigFiles { + funcs = append(funcs, readFileFunc(nestedConfigFile)) + } + + funcs = append(funcs, func() (map[string]interface{}, error) { + return data, nil + }) + + return merge(funcs...) +} + func readFile(path string) (result map[string]interface{}, _ error) { result = map[string]interface{}{} defer func() { @@ -109,26 +138,17 @@ func merge(readers ...reader) (map[string]interface{}, error) { if err := schema.Mapper.ToInternal(newData); err != nil { return nil, err } + newData, err = readNested(newData) + if err != nil { + return nil, err + } d = values.MergeMapsConcatSlice(d, newData) } return d, nil } func readConfigMap(cfg string) (map[string]interface{}, error) { - var ( - err error - data map[string]interface{} - ) - return merge(func() (map[string]interface{}, error) { - data, err = merge(readCmdline, - func() (map[string]interface{}, error) { - return readFile(cfg) - }, - ) - return data, err - }, func() (map[string]interface{}, error) { - return readFile(convert.ToString(values.GetValueN(data, "rancheros", "install", "configUrl"))) - }) + return merge(readCmdline, readFileFunc(cfg)) } func ReadConfig(cfg string) (result Config, err error) { diff --git a/pkg/controllers/managedos/managedos.go b/pkg/controllers/managedos/managedos.go index 2ba9db19..cab5eb03 100644 --- a/pkg/controllers/managedos/managedos.go +++ b/pkg/controllers/managedos/managedos.go @@ -2,6 +2,7 @@ package managedos import ( "context" + "fmt" "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1" provv1 "github.com/rancher/os2/pkg/apis/rancheros.cattle.io/v1" @@ -71,6 +72,10 @@ func (h *handler) OnChange(mos *provv1.ManagedOSImage, status provv1.ManagedOSIm return nil, status, err } + if mos.Namespace == "fleet-local" && len(mos.Spec.Targets) > 0 { + return nil, status, fmt.Errorf("spec.targets should be empty if in the fleet-local namespace") + } + bundle := &v1alpha1.Bundle{ ObjectMeta: metav1.ObjectMeta{ Name: name.SafeConcatName("mos", mos.Name), @@ -79,12 +84,15 @@ func (h *handler) OnChange(mos *provv1.ManagedOSImage, status provv1.ManagedOSIm Spec: v1alpha1.BundleSpec{ Resources: resources, BundleDeploymentOptions: v1alpha1.BundleDeploymentOptions{}, - Paused: mos.Spec.Paused, RolloutStrategy: mos.Spec.ClusterRolloutStrategy, Targets: mos.Spec.Targets, }, } + if mos.Namespace == "fleet-local" { + bundle.Spec.Targets = []v1alpha1.BundleTarget{{ClusterName: "local"}} + } + status, err = h.updateStatus(status, bundle) return []runtime.Object{ bundle, diff --git a/pkg/install/ask.go b/pkg/install/ask.go index 66d4d303..65877f7d 100644 --- a/pkg/install/ask.go +++ b/pkg/install/ask.go @@ -26,6 +26,10 @@ func Ask(cfg *config.Config) error { if err := AskPassword(cfg); err != nil { return err } + + if err := AskServerAgent(cfg); err != nil { + return err + } } return nil @@ -50,9 +54,33 @@ func AskInstallDevice(cfg *config.Config) error { return nil } -func isServer(cfg *config.Config) (bool, bool, error) { +func AskToken(cfg *config.Config, server bool) error { + var ( + token string + err error + ) + + if cfg.RancherOS.Install.Token != "" { + return nil + } + + msg := "Token or cluster secret" + if server { + msg += " (optional)" + } + if server { + token, err = questions.PromptOptional(msg+": ", "") + } else { + token, err = questions.Prompt(msg+": ", "") + } + cfg.RancherOS.Install.Token = token + + return err +} + +func isServer() (bool, bool, error) { opts := []string{"server", "agent", "none"} - i, err := questions.PromptFormattedOptions("Run as server or agent?", 0, opts...) + i, err := questions.PromptFormattedOptions("Run as server or agent (Choose none if building an image)?", 0, opts...) if err != nil { return false, false, err } @@ -60,6 +88,35 @@ func isServer(cfg *config.Config) (bool, bool, error) { return i == 0, i == 1, nil } +func AskServerAgent(cfg *config.Config) error { + if cfg.RancherOS.Install.ServerURL != "" || cfg.RancherOS.Install.Silent { + return nil + } + + server, agent, err := isServer() + if err != nil { + return err + } + + if !server && !agent { + return nil + } + + if server { + cfg.RancherOS.Install.Role = "server" + return AskToken(cfg, true) + } + + cfg.RancherOS.Install.Role = "agent" + url, err := questions.Prompt("URL of server: ", "") + if err != nil { + return err + } + cfg.RancherOS.Install.ServerURL = url + + return AskToken(cfg, false) +} + func AskPassword(cfg *config.Config) error { if cfg.RancherOS.Install.Silent || cfg.RancherOS.Install.Password != "" { return nil diff --git a/pkg/install/install.go b/pkg/install/install.go index 7c42263d..b6cc8c92 100644 --- a/pkg/install/install.go +++ b/pkg/install/install.go @@ -55,7 +55,13 @@ func runInstall(cfg config.Config, output string) error { } if cfg.RancherOS.Install.ConfigURL == "" && !cfg.RancherOS.Install.Silent { - yip := config.YipConfig{} + yip := config.YipConfig{ + Rancherd: config.Rancherd{ + Server: cfg.RancherOS.Install.ServerURL, + Token: cfg.RancherOS.Install.Token, + Role: cfg.RancherOS.Install.Role, + }, + } if cfg.RancherOS.Install.Password != "" || len(cfg.SSHAuthorizedKeys) > 0 { yip.Stages = map[string][]config.Stage{ "network": {{ diff --git a/ros-image-build b/ros-image-build index cc174cc0..9fe47648 100755 --- a/ros-image-build +++ b/ros-image-build @@ -124,22 +124,31 @@ RUN cd /iso && \ luet-makeiso iso.yaml FROM iso-build AS qcow-build +ARG ACCEL=tcg RUN SUFFIX= && \ FIRMWARE= && \ if [ "$(uname -m)" = "aarch64" ]; then SUFFIX=-arm64; FIRMWARE=/usr/share/qemu/qemu-uefi-aarch64.bin; fi && \ - PACKER_LOG=1 packer build \ + echo '#!/bin/bash' > /usr/bin/image && \ + echo 'set -e -x' >> /usr/bin/image && \ + echo PACKER_LOG=1 packer build \ + -var "root_password=ros" \ -var "firmware=${FIRMWARE}" \ -var "memory=1024" \ -var "iso=/iso/output.iso" \ - -var "accelerator=tcg" \ - -only qemu.cos${SUFFIX} . -RUN mkdir /output && \ - mv *.box /output/output.box && \ - pigz -dc *.tar.gz | tar xvf - && \ - cat cOS | pigz -c > /output/output.qcow.gz + -var "accelerator=${ACCEL}" \ + -only qemu.cos${SUFFIX} . >> /usr/bin/image && \ + chmod +x /usr/bin/image +RUN echo 'mkdir /output &&' >> /usr/bin/image && \ + echo 'mv *.box /output/output.box' >> /usr/bin/image && \ + echo 'pigz -dc *.tar.gz | tar xvf -' >> /usr/bin/image && \ + echo 'cat cOS | pigz -c > /output/output.qcow.gz'>> /usr/bin/image +CMD ["bash", "-x", "/usr/bin/image"] + +FROM qcow-build AS qcow-build2 +RUN bash -x /usr/bin/image FROM scratch AS qcow -COPY --from=qcow-build /output/ / +COPY --from=qcow-build2 /output/ / FROM scratch AS iso COPY --from=iso-build /iso/output.iso / @@ -153,12 +162,17 @@ ARG NAME=RancherOS-Image-dev ARG VERSION=1 ARG GIT_COMMIT=HEAD RUN packer build \ + -var "root_password=ros" \ -var "cos_version=${VERSION}" \ -var "git_sha=${GIT_COMMIT}" \ -var 'aws_source_ami_filter_owners=["053594193760"]' \ -var "aws_cos_deploy_args=cos-deploy --no-verify --docker-image ${IMAGE}" \ -var "name=${NAME}" \ -only amazon-ebs.cos . + +FROM scratch AS default +COPY --from=iso / / +COPY --from=qcow / / EOF } @@ -170,7 +184,22 @@ iso() qcow() { - build --target qcow -o build/ + ID=qcow-${RANDOM} + if docker run -it --device /dev/kvm busybox /bin/true; then + build --target qcow-build --build-arg ACCEL=kvm -t $ID + docker run --net=host -it --device /dev/kvm --name $ID $ID + else + build --target qcow-build --build-arg ACCEL=tcg -t $ID + docker run --net=host -it --name $ID $ID + fi || { + docker rm -fv $ID + docker rmi $ID + exit 1 + } + mkdir -p build/ + docker export $ID | tar xvf - -C build/ output/ --strip-components=1 + docker rm -fv $ID + docker rmi $ID } ami() @@ -204,6 +233,11 @@ VERSION=${IMAGE##*:} NAME=${IMAGE%%:${VERSION}} NAME=${NAME//[^a-zA-Z0-9-@.\/_]/-} +if [ "$1" == dockerfile ]; then + dockerfile + exit 0 +fi + if [ -z "${OUTPUT}" ] || [ -z "${IMAGE}" ] || echo "$@" | grep -q -- -h; then usage exit 1 @@ -222,10 +256,6 @@ fi iso) iso ;; - dockerfile) - dockerfile - exit 0 - ;; *) echo Unknown format $i exit 1