Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions build/download-deps.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
25 changes: 19 additions & 6 deletions cmd/kubesolo/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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"),
Expand All @@ -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"),
Expand Down
1 change: 1 addition & 0 deletions internal/config/flags/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
)
5 changes: 4 additions & 1 deletion internal/core/embedded/embedded.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build (linux && amd64) || (linux && arm64) || (linux && arm)
//go:build (linux && amd64) || (linux && arm64)

package embedded

Expand All @@ -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

Expand Down
71 changes: 71 additions & 0 deletions internal/core/embedded/embedded_arm.go
Original file line number Diff line number Diff line change
@@ -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
}
3 changes: 3 additions & 0 deletions internal/core/embedded/embedded_riscv64.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 8 additions & 0 deletions internal/core/embedded/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Comment on lines +36 to +41
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When embedded.EnableWasm is true but wasmShimBinary is empty (e.g. on riscv64 where it’s intentionally unavailable), this silently skips extraction and continues. Containerd will still be configured with the wasmtime runtime, leading to a missing-binary failure later. Consider returning an explicit error when EnableWasm is requested but the shim binary isn’t available, or force-disable WASM support in this case so the system boots predictably.

Copilot uses AI. Check for mistakes.

return nil
}

Expand Down
46 changes: 46 additions & 0 deletions pkg/components/runtimeclass/runtimeclass.go
Original file line number Diff line number Diff line change
@@ -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)
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deploy accepts a caller context but immediately does an unconditional time.Sleep(...), which can delay shutdown/cancellation even if ctx is already done. Consider replacing the sleep with a ctx-aware wait (select on ctx.Done() vs time.After) so this component respects cancellation consistently.

Suggested change
time.Sleep(types.DefaultComponentSleep)
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(types.DefaultComponentSleep):
}

Copilot uses AI. Check for mistakes.

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
}
2 changes: 1 addition & 1 deletion pkg/kubernetes/apiserver/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
56 changes: 36 additions & 20 deletions pkg/runtime/containerd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
Comment on lines +69 to +74
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generateContainerdRuntimes() adds the wasmtime runtime solely based on s.enableWasm, without verifying that s.wasmShimBinaryFile exists on disk. On platforms where the shim is not embedded (e.g. riscv64) or if extraction fails, this will produce a containerd config referencing a non-existent runtime binary. Consider guarding this with a file existence check (or failing fast during validation) so containerd doesn’t start with an invalid runtime configuration.

Copilot uses AI. Check for mistakes.

return runtimes
}

// generateConfig generates the containerd config
func (s *service) generateContainerdConfig() map[string]any {
return map[string]any{
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions pkg/runtime/containerd/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ type service struct {
containerdCNIPluginsDir string
containerdRegistryConfigDir string
runcBinaryFile string
wasmShimBinaryFile string
enableWasm bool
containerdShimBinaryFile string
portainerAgentImageFile string
corednsImageFile string
Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions types/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 4 additions & 0 deletions types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ type Embedded struct {
// Runc binary
RuncBinaryFile string

// Wasm shim binary
WasmShimBinaryFile string
EnableWasm bool

// Kubelet directories
KubeletDir string
KubeletConfigDir string
Expand Down