diff --git a/build/download-deps.sh b/build/download-deps.sh index 0819eedf..9b1f76a5 100755 --- a/build/download-deps.sh +++ b/build/download-deps.sh @@ -12,6 +12,7 @@ PORTAINER_AGENT_VERSION="2.39.0" COREDNS_VERSION="1.14.1" LOCAL_PATH_PROVISIONER_VERSION="v0.0.34" PAUSE_IMAGE_VERSION="3.10" +RUNWASI_VERSION="v0.6.0" # Process command line arguments while [[ "$#" -gt 0 ]]; do @@ -104,6 +105,38 @@ if ! curl -L -f --silent -o internal/core/embedded/bin/runc https://github.com/o fi chmod +x internal/core/embedded/bin/runc +# Download WASM shim (containerd-shim-wasmtime-v1) +if [ "${ARCH}" = "arm" ] || [ "${ARCH}" = "riscv64" ]; then + echo "Skipping WASM shim for ${ARCH} (not supported)" +else + # Map architecture to runwasi release asset naming + RUNWASI_ARCH="${ARCH}" + if [ "${ARCH}" = "amd64" ]; then + RUNWASI_ARCH="x86_64" + elif [ "${ARCH}" = "arm64" ]; then + RUNWASI_ARCH="aarch64" + fi + + echo "Downloading WASM shim ${RUNWASI_VERSION} for ${OS}-${ARCH}..." + if ! curl -L -f --silent -o internal/core/embedded/bin/wasm-shim.tar.gz \ + https://github.com/containerd/runwasi/releases/download/containerd-shim-wasmtime/${RUNWASI_VERSION}/containerd-shim-wasmtime-${RUNWASI_ARCH}-linux-musl.tar.gz; then + echo "Error downloading WASM shim. Please check the version and URL." + exit 1 + fi + + # Verify the download is a valid tar file + if ! tar -tf internal/core/embedded/bin/wasm-shim.tar.gz >/dev/null 2>&1; then + echo "Downloaded WASM shim archive is not valid. Check URL or try again." + rm -f internal/core/embedded/bin/wasm-shim.tar.gz + exit 1 + fi + + tar -xzf internal/core/embedded/bin/wasm-shim.tar.gz -C internal/core/embedded/bin/ ./containerd-shim-wasmtime-v1 + rm internal/core/embedded/bin/wasm-shim.tar.gz + chmod +x internal/core/embedded/bin/containerd-shim-wasmtime-v1 + echo "Successfully downloaded WASM shim." +fi + # Download CNI plugins - add error checking echo "Downloading CNI plugins ${CNI_VERSION} for ${OS}-${ARCH}..." if ! curl -L -f --silent -o internal/core/embedded/bin/cni/cni-plugins.tgz https://github.com/containernetworking/plugins/releases/download/${CNI_VERSION}/cni-plugins-${OS}-${ARCH}-${CNI_VERSION}.tgz; then diff --git a/cmd/kubesolo/main.go b/cmd/kubesolo/main.go index c1c9c5d6..813518ee 100644 --- a/cmd/kubesolo/main.go +++ b/cmd/kubesolo/main.go @@ -19,6 +19,7 @@ import ( "github.com/portainer/kubesolo/pkg/components/coredns" "github.com/portainer/kubesolo/pkg/components/localpath" "github.com/portainer/kubesolo/pkg/components/portainer" + "github.com/portainer/kubesolo/pkg/components/runtimeclass" "github.com/portainer/kubesolo/pkg/kine" "github.com/portainer/kubesolo/pkg/kubernetes/apiserver" "github.com/portainer/kubesolo/pkg/kubernetes/controller" @@ -206,6 +207,14 @@ func (s *kubesolo) run() { } } + if s.embedded.EnableWasm { + go func() { + if err := runtimeclass.Deploy(ctx, s.embedded.AdminKubeconfigFile); err != nil { + log.Error().Err(err).Str("component", "runtimeclass").Msg("failed to deploy RuntimeClass") + } + }() + } + log.Info().Str("component", "kubesolo").Msg("deploying coredns...") if err := coredns.Deploy(s.embedded.AdminKubeconfigFile); err != nil { log.Fatal().Err(err).Msg("failed to deploy coredns") @@ -351,12 +360,12 @@ func (s *kubesolo) bootstrap() { }, // Containerd paths - ContainerdDir: filepath.Join(basePath, types.DefaultContainerdDir), - ContainerdSocketFile: filepath.Join(basePath, types.DefaultContainerdDir, types.DefaultContainerdSocket), - ContainerdBinaryFile: filepath.Join(basePath, types.DefaultContainerdDir, "containerd"), - ContainerdImagesDir: filepath.Join(basePath, types.DefaultContainerdDir, "images"), - ContainerdShimBinaryFile: filepath.Join(basePath, types.DefaultContainerdDir, "containerd-shim-runc-v2"), - ContainerdConfigFile: filepath.Join(basePath, types.DefaultContainerdDir, "config.toml"), + ContainerdDir: filepath.Join(basePath, types.DefaultContainerdDir), + ContainerdSocketFile: filepath.Join(basePath, types.DefaultContainerdDir, types.DefaultContainerdSocket), + ContainerdBinaryFile: filepath.Join(basePath, types.DefaultContainerdDir, "containerd"), + ContainerdImagesDir: filepath.Join(basePath, types.DefaultContainerdDir, "images"), + ContainerdShimBinaryFile: filepath.Join(basePath, types.DefaultContainerdDir, "containerd-shim-runc-v2"), + ContainerdConfigFile: filepath.Join(basePath, types.DefaultContainerdDir, "config.toml"), ContainerdRootDir: filepath.Join(basePath, types.DefaultContainerdDir, "root"), ContainerdStateDir: filepath.Join(basePath, types.DefaultContainerdDir, "state"), ContainerdRegistryConfigDir: filepath.Join(basePath, types.DefaultContainerdDir, "registry"), @@ -370,6 +379,10 @@ func (s *kubesolo) bootstrap() { // Runc binary RuncBinaryFile: filepath.Join(basePath, types.DefaultContainerdDir, "runc"), + // Wasm shim + WasmShimBinaryFile: filepath.Join(basePath, types.DefaultContainerdDir, types.DefaultWasmShimBinaryName), + EnableWasm: *flags.Wasm, + // Kubelet paths KubeletDir: filepath.Join(basePath, types.DefaultKubeletDir), KubeletConfigDir: filepath.Join(basePath, types.DefaultKubeletDir, "config"), diff --git a/internal/config/flags/flags.go b/internal/config/flags/flags.go index 52f37c6b..7e54d738 100644 --- a/internal/config/flags/flags.go +++ b/internal/config/flags/flags.go @@ -23,6 +23,7 @@ var ( LoadBalancer = Application.Flag("load-balancer", "Enable load balancer. With this enabled, kubesolo will update a newly deployed service with the load balancer type so that the EXTERNAL-IP is set to the node IP. Defaults to true.").Envar("KUBESOLO_LOAD_BALANCER").Default("true").Bool() LocalStorage = Application.Flag("local-storage", "Enable local storage. Defaults to false.").Envar("KUBESOLO_LOCAL_STORAGE").Default("true").Bool() LocalStorageSharedPath = Application.Flag("local-storage-shared-path", "Path to the shared file system for the local storage. Defaults to empty string.").Envar("KUBESOLO_LOCAL_STORAGE_SHARED_PATH").Default("").String() + Wasm = Application.Flag("wasm", "Enable WebAssembly runtime support via wasmtime").Envar("KUBESOLO_WASM").Default("false").Bool() Debug = Application.Flag("debug", "Enable debug logging. Defaults to false.").Envar("KUBESOLO_DEBUG").Default("false").Bool() PprofServer = Application.Flag("pprof-server", "Enable pprof server. Defaults to false.").Envar("KUBESOLO_PPROF_SERVER").Default("false").Bool() ) diff --git a/internal/core/embedded/embedded.go b/internal/core/embedded/embedded.go index cc4d5f3b..f25053fb 100644 --- a/internal/core/embedded/embedded.go +++ b/internal/core/embedded/embedded.go @@ -1,4 +1,4 @@ -//go:build (linux && amd64) || (linux && arm64) || (linux && arm) +//go:build (linux && amd64) || (linux && arm64) package embedded @@ -13,6 +13,9 @@ import ( //go:embed bin/containerd/bin/containerd-shim-runc-v2 var containerdShimBinary []byte +//go:embed bin/containerd-shim-wasmtime-v1 +var wasmShimBinary []byte + //go:embed bin/runc var runcBinary []byte diff --git a/internal/core/embedded/embedded_arm.go b/internal/core/embedded/embedded_arm.go new file mode 100644 index 00000000..61323bcb --- /dev/null +++ b/internal/core/embedded/embedded_arm.go @@ -0,0 +1,71 @@ +//go:build linux && arm + +package embedded + +import ( + _ "embed" + "fmt" + + "github.com/portainer/kubesolo/types" + "github.com/rs/zerolog/log" +) + +//go:embed bin/containerd/bin/containerd-shim-runc-v2 +var containerdShimBinary []byte + +// containerd-shim-wasmtime-v1 is not available for arm32 +var wasmShimBinary []byte + +//go:embed bin/runc +var runcBinary []byte + +//go:embed bin/cni/bridge +var cniPluginBridge []byte + +//go:embed bin/cni/host-local +var cniPluginHostLocal []byte + +//go:embed bin/cni/portmap +var cniPluginPortmap []byte + +//go:embed bin/cni/loopback +var cniPluginLoopback []byte + +//go:embed bin/images/coredns.tar.gz +var corednsImageFile []byte + +//go:embed bin/images/portainer-agent.tar.gz +var portainerAgentImageFile []byte + +//go:embed bin/images/local-path-provisioner.tar.gz +var localPathProvisionerImageFile []byte + +//go:embed bin/images/pause.tar.gz +var sandboxImageFile []byte + +// EnsureEmbeddedDependencies ensures all required components are available +// it loads the containerd components, cni plugins, cni config, images, and kernel modules +// before the kubesolo application starts +func EnsureEmbeddedDependencies(embedded types.Embedded) error { + if err := loadContainerdComponents(embedded); err != nil { + return fmt.Errorf("failed to load containerd: %v", err) + } + + if err := loadCNIPlugins(embedded.ContainerdCNIDir, embedded.ContainerdCNIPluginsDir); err != nil { + return fmt.Errorf("failed to load cni plugins: %v", err) + } + + if err := loadCNIConfig(embedded.ContainerdCNIConfigDir, embedded.ContainerdCNIConfigFile); err != nil { + return fmt.Errorf("failed to load cni config: %v", err) + } + + if err := loadImages(embedded.ContainerdImagesDir); err != nil { + return fmt.Errorf("failed to load images: %v", err) + } + + if err := loadKernelModules(); err != nil { + log.Warn().Str("component", "embedded").Msgf("failed to load kernel modules: %v", err) + } + + return nil +} diff --git a/internal/core/embedded/embedded_riscv64.go b/internal/core/embedded/embedded_riscv64.go index f61284d8..1fc508b3 100644 --- a/internal/core/embedded/embedded_riscv64.go +++ b/internal/core/embedded/embedded_riscv64.go @@ -13,6 +13,9 @@ import ( //go:embed bin/containerd/bin/containerd-shim-runc-v2 var containerdShimBinary []byte +// containerd-shim-wasmtime-v1 is not available for riscv64 +var wasmShimBinary []byte + //go:embed bin/runc var runcBinary []byte diff --git a/internal/core/embedded/load.go b/internal/core/embedded/load.go index f2135d8d..4a8f59ce 100644 --- a/internal/core/embedded/load.go +++ b/internal/core/embedded/load.go @@ -32,6 +32,14 @@ func loadContainerdComponents(embedded types.Embedded) error { return fmt.Errorf("failed to extract %s binary: %v", binary.name, err) } } + + if embedded.EnableWasm && len(wasmShimBinary) > 0 { + if err := filesystem.ExtractBinary(wasmShimBinary, embedded.WasmShimBinaryFile); err != nil { + return fmt.Errorf("failed to extract containerd-shim-wasmtime-v1 binary: %v", err) + } + log.Debug().Str("component", "embedded").Msg("containerd-shim-wasmtime-v1 extracted") + } + return nil } diff --git a/pkg/components/runtimeclass/runtimeclass.go b/pkg/components/runtimeclass/runtimeclass.go new file mode 100644 index 00000000..08d74e34 --- /dev/null +++ b/pkg/components/runtimeclass/runtimeclass.go @@ -0,0 +1,46 @@ +package runtimeclass + +import ( + "context" + "fmt" + "time" + + kubesolokubernetes "github.com/portainer/kubesolo/internal/kubernetes" + "github.com/portainer/kubesolo/types" + "github.com/rs/zerolog/log" + nodeapi "k8s.io/api/node/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// Deploy creates the wasmtime RuntimeClass resource in the cluster +func Deploy(ctx context.Context, adminKubeconfig string) error { + time.Sleep(types.DefaultComponentSleep) + + ctx, cancel := context.WithTimeout(ctx, types.DefaultContextTimeout) + defer cancel() + + clientset, err := kubesolokubernetes.GetKubernetesClient(adminKubeconfig) + if err != nil { + return fmt.Errorf("failed to create kubernetes client: %v", err) + } + + log.Info().Str("component", "runtimeclass").Msg("deploying RuntimeClass wasmtime") + + rc := &nodeapi.RuntimeClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "wasmtime", + }, + Handler: "wasmtime", + } + + _, err = clientset.NodeV1().RuntimeClasses().Create(ctx, rc, metav1.CreateOptions{}) + if err != nil { + if k8serrors.IsAlreadyExists(err) { + return nil + } + return fmt.Errorf("failed to create RuntimeClass wasmtime: %v", err) + } + + return nil +} diff --git a/pkg/kubernetes/apiserver/flags.go b/pkg/kubernetes/apiserver/flags.go index 20a94b50..536ec29d 100644 --- a/pkg/kubernetes/apiserver/flags.go +++ b/pkg/kubernetes/apiserver/flags.go @@ -44,7 +44,7 @@ func (s *service) configureAPIServerFlags(command *cobra.Command) error { _ = flags.Set("allow-privileged", "true") _ = flags.Set("authorization-mode", "Node,RBAC") _ = flags.Set("enable-admission-plugins", "NodeRestriction,ServiceAccount,ValidatingAdmissionWebhook,MutatingAdmissionWebhook,DefaultStorageClass,CertificateApproval,CertificateSigning,CertificateSubjectRestriction,ValidatingAdmissionPolicy,MutatingAdmissionPolicy") - _ = flags.Set("disable-admission-plugins", "RuntimeClass,PodSecurity,ClusterTrustBundleAttest,DefaultIngressClass,TaintNodesByCondition,DefaultTolerationSeconds,StorageObjectInUseProtection,PersistentVolumeClaimResize,ResourceQuota,LimitRanger,Priority") + _ = flags.Set("disable-admission-plugins", "PodSecurity,ClusterTrustBundleAttest,DefaultIngressClass,TaintNodesByCondition,DefaultTolerationSeconds,StorageObjectInUseProtection,PersistentVolumeClaimResize,ResourceQuota,LimitRanger,Priority") _ = flags.Set("enable-bootstrap-token-auth", "false") // performance and resource limits diff --git a/pkg/runtime/containerd/config.go b/pkg/runtime/containerd/config.go index d48de803..071bc1fb 100644 --- a/pkg/runtime/containerd/config.go +++ b/pkg/runtime/containerd/config.go @@ -41,6 +41,41 @@ func (s *service) writeContainerdConfigFile() error { return nil } +// generateContainerdRuntimes builds the runtimes map for the CRI runtime plugin. +// The runc runtime is always present; wasmtime is added only when WASM support +// is enabled via s.enableWasm. +func (s *service) generateContainerdRuntimes() map[string]any { + runtimes := map[string]any{ + "runc": map[string]any{ + "runtime_type": "io.containerd.runc.v2", + "runtime_path": s.containerdShimBinaryFile, + "pod_annotations": []string{}, + "container_annotations": []string{}, + "privileged_without_host_devices": false, + "privileged_without_host_devices_all_devices_allowed": false, + "base_runtime_spec": "", + "cni_conf_dir": "", + "cni_max_conf_num": 0, + "snapshotter": "", + "sandboxer": "podsandbox", + "io_type": "", + "options": map[string]any{ + "BinaryName": s.runcBinaryFile, + "SystemdCgroup": isCgroupV2(), + }, + }, + } + + if s.enableWasm { + runtimes["wasmtime"] = map[string]any{ + "runtime_type": "io.containerd.wasmtime.v1", + "runtime_path": s.wasmShimBinaryFile, + } + } + + return runtimes +} + // generateConfig generates the containerd config func (s *service) generateContainerdConfig() map[string]any { return map[string]any{ @@ -101,26 +136,7 @@ func (s *service) generateContainerdConfig() map[string]any { "default_runtime_name": "runc", "ignore_blockio_not_enabled_errors": false, "ignore_rdt_not_enabled_errors": false, - "runtimes": map[string]any{ - "runc": map[string]any{ - "runtime_type": "io.containerd.runc.v2", - "runtime_path": s.containerdShimBinaryFile, - "pod_annotations": []string{}, - "container_annotations": []string{}, - "privileged_without_host_devices": false, - "privileged_without_host_devices_all_devices_allowed": false, - "base_runtime_spec": "", - "cni_conf_dir": "", - "cni_max_conf_num": 0, - "snapshotter": "", - "sandboxer": "podsandbox", - "io_type": "", - "options": map[string]any{ - "BinaryName": s.runcBinaryFile, - "SystemdCgroup": isCgroupV2(), - }, - }, - }, + "runtimes": s.generateContainerdRuntimes(), }, "cni": map[string]any{ "bin_dir": s.containerdCNIPluginsDir, diff --git a/pkg/runtime/containerd/service.go b/pkg/runtime/containerd/service.go index c6af318e..2b65bcb5 100644 --- a/pkg/runtime/containerd/service.go +++ b/pkg/runtime/containerd/service.go @@ -22,6 +22,8 @@ type service struct { containerdCNIPluginsDir string containerdRegistryConfigDir string runcBinaryFile string + wasmShimBinaryFile string + enableWasm bool containerdShimBinaryFile string portainerAgentImageFile string corednsImageFile string @@ -45,6 +47,8 @@ func NewService(ctx context.Context, cancel context.CancelFunc, containerdReady containerdCNIPluginsDir: embedded.ContainerdCNIPluginsDir, containerdRegistryConfigDir: embedded.ContainerdRegistryConfigDir, runcBinaryFile: embedded.RuncBinaryFile, + wasmShimBinaryFile: embedded.WasmShimBinaryFile, + enableWasm: embedded.EnableWasm, containerdShimBinaryFile: embedded.ContainerdShimBinaryFile, portainerAgentImageFile: embedded.PortainerAgentImageFile, corednsImageFile: embedded.CorednsImageFile, diff --git a/types/const.go b/types/const.go index 605948d3..2ded3467 100644 --- a/types/const.go +++ b/types/const.go @@ -10,6 +10,7 @@ const ( DefaultWebhookPort = 10443 DefaultPKIDir = "pki" DefaultContainerdDir = "containerd" + DefaultWasmShimBinaryName = "containerd-shim-wasmtime-v1" DefaultContainerdSocket = "containerd.sock" DefaultSystemContainerdSock = "/run/containerd/containerd.sock" DefaultStandardCNIConfDir = "/etc/cni/net.d" diff --git a/types/types.go b/types/types.go index 828b55d9..54c431f7 100644 --- a/types/types.go +++ b/types/types.go @@ -92,6 +92,10 @@ type Embedded struct { // Runc binary RuncBinaryFile string + // Wasm shim binary + WasmShimBinaryFile string + EnableWasm bool + // Kubelet directories KubeletDir string KubeletConfigDir string