From ef35e3662befa0a1fb1147c830029fd98670025f Mon Sep 17 00:00:00 2001 From: Malte Poll <1780588+malt3@users.noreply.github.com> Date: Fri, 15 Mar 2024 16:01:16 +0100 Subject: [PATCH] node-installer: initialize --- go.work | 1 + node-installer/README.md | 44 +++ node-installer/cmd/node-installer.go | 356 ++++++++++++++++++ node-installer/go.mod | 5 + node-installer/go.sum | 2 + node-installer/internal/config/config.go | 64 ++++ node-installer/internal/config/containerd.go | 94 +++++ .../constants/configuration-clh-snp.toml | 42 +++ .../internal/constants/constants.go | 55 +++ .../internal/constants/containerd-config.toml | 46 +++ .../contrast-node-installer/package.nix | 53 +++ packages/by-name/contrast/package.nix | 2 +- 12 files changed, 763 insertions(+), 1 deletion(-) create mode 100644 node-installer/README.md create mode 100644 node-installer/cmd/node-installer.go create mode 100644 node-installer/go.mod create mode 100644 node-installer/go.sum create mode 100644 node-installer/internal/config/config.go create mode 100644 node-installer/internal/config/containerd.go create mode 100644 node-installer/internal/constants/configuration-clh-snp.toml create mode 100644 node-installer/internal/constants/constants.go create mode 100644 node-installer/internal/constants/containerd-config.toml create mode 100644 packages/by-name/contrast-node-installer/package.nix diff --git a/go.work b/go.work index 6dd3847131..e3971c83b7 100644 --- a/go.work +++ b/go.work @@ -3,4 +3,5 @@ go 1.21 use ( . ./service-mesh + ./node-installer ) diff --git a/node-installer/README.md b/node-installer/README.md new file mode 100644 index 0000000000..eb65186747 --- /dev/null +++ b/node-installer/README.md @@ -0,0 +1,44 @@ +# Contrast node installer + +This program runs as a daemonset on every node of a Kubernetes cluster. +It expects the host filesystem of the node to be mounted under `/host`. +On start, it will read a configuration file under `$CONFIG_DIR/contrast-node-install.json` and install binary artifacts on the host filesystem according to the configuration. +After installing binary artifacts, it installs and patches configuration files for the Contrast runtime class `contrast-cc-isolation` and restarts containerd. + +## Configuration + +By default, the installer ships with a config file under `/config/contrast-node-install.json`, which takes binary artifacts from the container image. +If desired, you can replace the configuration using a Kubernetes configmap by mounting it into the container. + +- `files`: List of files to be installed. +- `files[*].url`: Source of the file's content. Use `http://` or `https://` to download it or `file://` to copy a file from the container image. +- `files[*].path`: Target location of the file on the host filesystem. +- `files[*].integrity`: Expected Subresource Integrity (SRI) digest of the file. Only required if url starts with `http://` or `https://`. + +Consider the following example: + +```json +{ + "files": [ + { + "url": "https://cdn.confidential.cloud/contrast/node-components/2024-03-13/kata-containers.img", + "path": "/opt/edgeless/share/kata-containers.img", + "integrity": "sha256-EdFywKAU+xD0BXmmfbjV4cB6Gqbq9R9AnMWoZFCM3A0=" + }, + { + "url": "https://cdn.confidential.cloud/contrast/node-components/2024-03-13/kata-containers-igvm.img", + "path": "/opt/edgeless/share/kata-containers-igvm.img", + "integrity": "sha256-E9Ttx6f9QYwKlQonO/fl1bF2MNBoU4XG3/HHvt9Zv30=" + }, + { + "url": "https://cdn.confidential.cloud/contrast/node-components/2024-03-13/cloud-hypervisor-cvm", + "path": "/opt/edgeless/bin/cloud-hypervisor-snp", + "integrity": "sha256-coTHzd5/QLjlPQfrp9d2TJTIXKNuANTN7aNmpa8PRXo=" + }, + { + "url": "file:///opt/edgeless/bin/containerd-shim-contrast-cc-v2", + "path": "/opt/edgeless/bin/containerd-shim-contrast-cc-v2", + } + ] +} +``` diff --git a/node-installer/cmd/node-installer.go b/node-installer/cmd/node-installer.go new file mode 100644 index 0000000000..4da19693f8 --- /dev/null +++ b/node-installer/cmd/node-installer.go @@ -0,0 +1,356 @@ +package main + +import ( + "bytes" + "context" + "crypto/sha256" + "crypto/sha512" + "encoding/base64" + "encoding/json" + "fmt" + "hash" + "io" + "net/http" + "net/url" + "os" + "os/exec" + "path/filepath" + + "github.com/edgelesssys/contrast/node-installer/internal/config" + "github.com/edgelesssys/contrast/node-installer/internal/constants" + "github.com/pelletier/go-toml" +) + +// this file is created by the installer on the host to signal that containerd has been restarted. +const containerdRestartedPath = "/run/.contrast-node-installer-containerd-restarted" + +func main() { + if err := run(context.Background()); err != nil { + fmt.Println(err) + os.Exit(1) + } + fmt.Println("Installation completed successfully.") +} + +func run(ctx context.Context) error { + configDir := envWithDefault("CONFIG_DIR", "/config") + hostMount := envWithDefault("HOST_MOUNT", "/host") + + // Create directory structure, ignore if it already exists + _ = os.MkdirAll(filepath.Join(hostMount, "opt", "edgeless", "bin"), os.ModePerm) + _ = os.MkdirAll(filepath.Join(hostMount, "opt", "edgeless", "share"), os.ModePerm) + _ = os.MkdirAll(filepath.Join(hostMount, "opt", "edgeless", "etc"), os.ModePerm) + _ = os.MkdirAll(filepath.Join(hostMount, "etc", "containerd"), os.ModePerm) + + configPath := filepath.Join(configDir, "contrast-node-install.json") + configData, err := os.ReadFile(configPath) + if err != nil { + return fmt.Errorf("reading config %q: %w", configPath, err) + } + var config config.Config + if err := json.Unmarshal(configData, &config); err != nil { + return fmt.Errorf("parsing config %q: %w", configPath, err) + } + + for _, file := range config.Files { + if err := file.Validate(); err != nil { + return fmt.Errorf("validating file %q: %w", file.Path, err) + } + } + for _, file := range config.Files { + uri, err := url.Parse(file.URL) + if err != nil { + return fmt.Errorf("parsing URL %q: %w", file.URL, err) + } + switch uri.Scheme { + case "http", "https": + if err := download(ctx, file.URL, filepath.Join(hostMount, file.Path), file.Integrity); err != nil { + return fmt.Errorf("downloading file: %w", err) + } + case "file": + if err := copyFile(uri.Path, filepath.Join(hostMount, file.Path)); err != nil { + return fmt.Errorf("copying file: %w", err) + } + default: + return fmt.Errorf("unsupported URL scheme: %s", uri.Scheme) + } + } + items, err := os.ReadDir(filepath.Join(hostMount, "opt", "edgeless", "bin")) + if err != nil { + return fmt.Errorf("reading bin directory %q: %w", filepath.Join(hostMount, "opt", "edgeless", "bin"), err) + } + + for _, item := range items { + if !item.Type().IsRegular() { + continue + } + if err := os.Chmod(filepath.Join(hostMount, "opt", "edgeless", "bin", item.Name()), 0o755); err != nil { + return fmt.Errorf("chmod %q: %w", item.Name(), err) + } + } + err = containerdRuntimeConfig(filepath.Join(hostMount, "opt", "edgeless", "etc", "configuration-clh-snp.toml")) + if err != nil { + return fmt.Errorf("generating clh_config.toml: %w", err) + } + err = patchContainerdConfig(filepath.Join(hostMount, "etc", "containerd", "config.toml")) + if err != nil { + return fmt.Errorf("patching containerd config: %w", err) + } + + return restartHostContainerd(hostMount) +} + +// copyFile copies a file from src to file, verifying the integrity hash. +// It is intended to be used for files that ship with the installer. +func copyFile(src, file string) error { + fmt.Printf("Copying %s to %s\n", src, file) + hash := sha512.New() + + srcFile, err := os.Open(src) + if err != nil { + return fmt.Errorf("opening source file %s: %w", src, err) + } + defer srcFile.Close() + + if _, err := io.Copy(hash, srcFile); err != nil { + return fmt.Errorf("hashing source file %s: %w", src, err) + } + expectedSum := hash.Sum(nil) + hash.Reset() + + // check if the file already exists and has the correct hash + if existing, err := os.Open(file); err == nil { + defer existing.Close() + if _, err := io.Copy(hash, existing); err != nil { + return fmt.Errorf("hashing existing file %s: %w", file, err) + } + if sum := hash.Sum(nil); bytes.Equal(sum, expectedSum) { + // File already exists and has the correct hash + return nil + } + } + + dst, err := os.Create(file) + if err != nil { + return err + } + defer dst.Close() + _, err = srcFile.Seek(0, 0) + if err != nil { + return fmt.Errorf("seeking to start of file %s: %w", srcFile.Name(), err) + } + _, err = io.Copy(dst, srcFile) + if err != nil { + return fmt.Errorf("copying file %s to %s: %w", srcFile.Name(), file, err) + } + return nil +} + +// download downloads a file from url to file, verifying the integrity hash. +// It is intended to be used for files that are downloaded from the internet. +func download(ctx context.Context, url, file, integrity string) error { + fmt.Printf("Downloading %s to %s\n", url, file) + + hash, expectedSum, err := hashFromIntegrity(integrity) + if err != nil { + return err + } + + if existing, err := os.Open(file); err == nil { + defer existing.Close() + if _, err := io.Copy(hash, existing); err != nil { + return fmt.Errorf("hashing existing file %s: %w", file, err) + } + if sum := hash.Sum(nil); bytes.Equal(sum, expectedSum) { + // File already exists and has the correct hash + return nil + } + hash.Reset() + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return err + } + response, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer response.Body.Close() + + tmpfile, err := os.CreateTemp("", "download") + if err != nil { + return err + } + defer tmpfile.Close() + defer os.Remove(tmpfile.Name()) + + reader := io.TeeReader(response.Body, hash) + + _, err = io.Copy(tmpfile, reader) + if err != nil { + return fmt.Errorf("downloading file contents from %s: %w", url, err) + } + + sum := hash.Sum(nil) + if !bytes.Equal(sum, expectedSum) { + return fmt.Errorf("hash mismatch for %s: expected %x, got %x", url, expectedSum, sum) + } + + if err := tmpfile.Sync(); err != nil { + return fmt.Errorf("syncing file %s: %w", tmpfile.Name(), err) + } + + err = os.Rename(tmpfile.Name(), file) + if err == nil { + return nil + } + + // If rename fails, try to copy the file instead + dst, err := os.Create(file) + if err != nil { + return err + } + defer dst.Close() + _, err = tmpfile.Seek(0, 0) + if err != nil { + return fmt.Errorf("seeking to start of file %s: %w", tmpfile.Name(), err) + } + _, err = io.Copy(dst, tmpfile) + if err != nil { + return fmt.Errorf("copying file %s to %s: %w", tmpfile.Name(), file, err) + } + + return nil +} + +func hashFromIntegrity(integrity string) (hash.Hash, []byte, error) { + var hash hash.Hash + switch integrity[:7] { + case "sha256-": + hash = sha256.New() + case "sha384-": + hash = sha512.New384() + case "sha512-": + hash = sha512.New() + default: + return nil, nil, fmt.Errorf("unsupported hash algorithm: %s", integrity[:7]) + } + expectedSum, err := base64.StdEncoding.DecodeString(integrity[7:]) + if err != nil { + return nil, nil, fmt.Errorf("decoding integrity value: %w", err) + } + return hash, expectedSum, nil +} + +func envWithDefault(key, dflt string) string { + value, ok := os.LookupEnv(key) + if !ok { + return dflt + } + return value +} + +func containerdRuntimeConfig(path string) error { + return os.WriteFile(path, []byte(constants.ContainerdRuntimeConfig), os.ModePerm) +} + +func patchContainerdConfig(path string) error { + var needsUpdate bool + existing, err := parseExistingContainerdConfig(path) + if err != nil { + needsUpdate = true + existing = constants.ContainerdBaseConfig() + } + + // Add tardev snapshotter + if existing.ProxyPlugins == nil { + needsUpdate = true + existing.ProxyPlugins = make(map[string]config.ProxyPlugin) + } + if _, ok := existing.ProxyPlugins["tardev"]; !ok { + needsUpdate = true + existing.ProxyPlugins["tardev"] = constants.TardevSnapshotterConfigFragment() + } + + // Add contrast-cc runtime + needsUpdate = needsUpdate || ensureMapPath(&existing.Plugins, constants.CRIFQDN, "containerd", "runtimes") + runtimes := existing.Plugins[constants.CRIFQDN].(map[string]any)["containerd"].(map[string]any)["runtimes"].(map[string]any) + if runtimes[constants.RuntimeName] == nil { + needsUpdate = true + runtimes[constants.RuntimeName] = constants.ContainerdRuntimeConfigFragment() + } + + if !needsUpdate { + return nil + } + + rawConfig, err := toml.Marshal(existing) + if err != nil { + return err + } + err = os.WriteFile(path, rawConfig, os.ModePerm) + if err != nil { + return err + } + + return nil +} + +func parseExistingContainerdConfig(path string) (config.ContainerdConfig, error) { + configData, err := os.ReadFile(path) + if err != nil { + return config.ContainerdConfig{}, err + } + + var cfg config.ContainerdConfig + if err := toml.Unmarshal(configData, &cfg); err != nil { + return config.ContainerdConfig{}, err + } + + return cfg, nil +} + +func restartHostContainerd(hostMount string) error { + // Create a file to signal that containerd has been restarted + f, err := os.OpenFile(filepath.Join(hostMount, containerdRestartedPath), os.O_CREATE|os.O_EXCL, 0) + if os.IsExist(err) { + // File already exists, containerd has already been restarted + f.Close() + return nil + } + if err != nil { + return fmt.Errorf("creating containerd restarted file: %w", err) + } + defer f.Close() + + // This command will restart containerd on the host and will take down the installer with it + // The file created above will be used to signal that containerd has been restarted so we avoid running this command again + out, err := exec.Command("nsenter", "--target", "1", "--mount", "--", "systemctl", "restart", "containerd").CombinedOutput() + if err != nil { + _ = os.Remove(f.Name()) // remove the file if the restart failed + return fmt.Errorf("restarting containerd: %w: %s", err, out) + } + fmt.Printf("containerd restarted: %s\n", out) + return nil +} + +func ensureMapPath(in *map[string]any, path ...string) bool { + var changed bool + if len(path) == 0 { + return false + } + if *in == nil { + changed = true + *in = make(map[string]any) + } + current := *in + for _, p := range path { + if current[p] == nil { + changed = true + current[p] = make(map[string]any) + } + current = current[p].(map[string]any) + } + return changed +} diff --git a/node-installer/go.mod b/node-installer/go.mod new file mode 100644 index 0000000000..f8b76f4d64 --- /dev/null +++ b/node-installer/go.mod @@ -0,0 +1,5 @@ +module github.com/edgelesssys/contrast/node-installer + +go 1.21 + +require github.com/pelletier/go-toml v1.9.5 diff --git a/node-installer/go.sum b/node-installer/go.sum new file mode 100644 index 0000000000..02b71dfc0d --- /dev/null +++ b/node-installer/go.sum @@ -0,0 +1,2 @@ +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= diff --git a/node-installer/internal/config/config.go b/node-installer/internal/config/config.go new file mode 100644 index 0000000000..ffa22aef67 --- /dev/null +++ b/node-installer/internal/config/config.go @@ -0,0 +1,64 @@ +package config + +import ( + "encoding/base64" + "errors" + "net/url" + "path/filepath" +) + +// Config is the configuration for the node-installer. +type Config struct { + // Files is a list of files to download. + Files []File `json:"files"` +} + +// File is a file to download. +type File struct { + // URL is the URL to download the file from. + URL string `json:"url"` + // Path is the absolute path (on the host) to save the file to. + Path string `json:"path"` + // Integrity is the content sri (expected hash) of the file. Required if the file is downloaded. + Integrity string `json:"integrity"` +} + +// Validate validates the configuration. +func (f File) Validate() error { + if f.URL == "" { + return errors.New("url is required") + } + uri, err := url.Parse(f.URL) + if err != nil { + return errors.New("url is not valid") + } + var needsSRI bool + switch uri.Scheme { + case "http", "https": + needsSRI = true + case "file": + needsSRI = false + default: + return errors.New("url scheme must be http, https, or file") + + } + if f.Path == "" { + return errors.New("path is required") + } + if !filepath.IsAbs(f.Path) { + return errors.New("path must be absolute") + } + if f.Integrity == "" { + if needsSRI { + return errors.New("integrity is required for http/https URLs") + } + return nil + } + if f.Integrity[:7] != "sha256-" && f.Integrity[:7] != "sha384-" && f.Integrity[:7] != "sha512-" { + return errors.New("integrity must use a valid content sri algorithm (sha256, sha384, sha512)") + } + if _, err := base64.StdEncoding.DecodeString(f.Integrity[7:]); err != nil { + return errors.New("integrity value is not valid base64") + } + return nil +} diff --git a/node-installer/internal/config/containerd.go b/node-installer/internal/config/containerd.go new file mode 100644 index 0000000000..266de787bc --- /dev/null +++ b/node-installer/internal/config/containerd.go @@ -0,0 +1,94 @@ +package config + +// ContainerdConfig provides containerd configuration data. +// This is a simplified version of the actual struct. +type ContainerdConfig struct { + // Version of the config file + Version int `toml:"version"` + // Root is the path to a directory where containerd will store persistent data + Root string `toml:"root"` + // State is the path to a directory where containerd will store transient data + State string `toml:"state"` + // TempDir is the path to a directory where to place containerd temporary files + TempDir string `toml:"temp"` + // PluginDir is the directory for dynamic plugins to be stored + PluginDir string `toml:"plugin_dir"` + // GRPC configuration settings + GRPC any `toml:"grpc"` + // TTRPC configuration settings + TTRPC any `toml:"ttrpc"` + // Debug and profiling settings + Debug any `toml:"debug"` + // Metrics and monitoring settings + Metrics any `toml:"metrics"` + // DisabledPlugins are IDs of plugins to disable. Disabled plugins won't be + // initialized and started. + DisabledPlugins []string `toml:"disabled_plugins"` + // RequiredPlugins are IDs of required plugins. Containerd exits if any + // required plugin doesn't exist or fails to be initialized or started. + RequiredPlugins []string `toml:"required_plugins"` + // Plugins provides plugin specific configuration for the initialization of a plugin + Plugins map[string]any `toml:"plugins"` + // OOMScore adjust the containerd's oom score + OOMScore int `toml:"oom_score"` + // Cgroup specifies cgroup information for the containerd daemon process + Cgroup any `toml:"cgroup"` + // ProxyPlugins configures plugins which are communicated to over GRPC + ProxyPlugins map[string]ProxyPlugin `toml:"proxy_plugins"` + // Timeouts specified as a duration + Timeouts map[string]string `toml:"timeouts"` + // Imports are additional file path list to config files that can overwrite main config file fields + Imports []string `toml:"imports"` + // StreamProcessors configuration + StreamProcessors map[string]any `toml:"stream_processors"` +} + +// ProxyPlugin provides a proxy plugin configuration. +type ProxyPlugin struct { + Type string `toml:"type"` + Address string `toml:"address"` +} + +// Runtime defines a containerd runtime. +type Runtime struct { + // Type is the runtime type to use in containerd e.g. io.containerd.runtime.v1.linux + Type string `toml:"runtime_type" json:"runtimeType"` + // Path is an optional field that can be used to overwrite path to a shim runtime binary. + // When specified, containerd will ignore runtime name field when resolving shim location. + // Path must be abs. + Path string `toml:"runtime_path,omitempty" json:"runtimePath,omitempty"` + // PodAnnotations is a list of pod annotations passed to both pod sandbox as well as + // container OCI annotations. + PodAnnotations []string `toml:"pod_annotations" json:"PodAnnotations"` + // ContainerAnnotations is a list of container annotations passed through to the OCI config of the containers. + // Container annotations in CRI are usually generated by other Kubernetes node components (i.e., not users). + // Currently, only device plugins populate the annotations. + ContainerAnnotations []string `toml:"container_annotations,omitempty" json:"ContainerAnnotations,omitempty"` + // Options are config options for the runtime. + Options map[string]interface{} `toml:"options,omitempty" json:"options,omitempty"` + // PrivilegedWithoutHostDevices overloads the default behaviour for adding host devices to the + // runtime spec when the container is privileged. Defaults to false. + PrivilegedWithoutHostDevices bool `toml:"privileged_without_host_devices,omitempty" json:"privileged_without_host_devices,omitempty"` + // PrivilegedWithoutHostDevicesAllDevicesAllowed overloads the default behaviour device allowlisting when + // to the runtime spec when the container when PrivilegedWithoutHostDevices is already enabled. Requires + // PrivilegedWithoutHostDevices to be enabled. Defaults to false. + PrivilegedWithoutHostDevicesAllDevicesAllowed bool `toml:"privileged_without_host_devices_all_devices_allowed,omitempty" json:"privileged_without_host_devices_all_devices_allowed,omitempty"` + // BaseRuntimeSpec is a json file with OCI spec to use as base spec that all container's will be created from. + BaseRuntimeSpec string `toml:"base_runtime_spec,omitempty" json:"baseRuntimeSpec,omitempty"` + // NetworkPluginConfDir is a directory containing the CNI network information for the runtime class. + NetworkPluginConfDir string `toml:"cni_conf_dir,omitempty" json:"cniConfDir,omitempty"` + // NetworkPluginMaxConfNum is the max number of plugin config files that will + // be loaded from the cni config directory by go-cni. Set the value to 0 to + // load all config files (no arbitrary limit). The legacy default value is 1. + NetworkPluginMaxConfNum int `toml:"cni_max_conf_num,omitempty" json:"cniMaxConfNum,omitempty"` + // Snapshotter setting snapshotter at runtime level instead of making it as a global configuration. + // An example use case is to use devmapper or other snapshotters in Kata containers for performance and security + // while using default snapshotters for operational simplicity. + // See https://github.com/containerd/containerd/issues/6657 for details. + Snapshotter string `toml:"snapshotter,omitempty" json:"snapshotter,omitempty"` + // Sandboxer defines which sandbox runtime to use when scheduling pods + // This features requires the new CRI server implementation (enabled by default in 2.0) + // shim - means use whatever Controller implementation provided by shim (e.g. use RemoteController). + // podsandbox - means use Controller implementation from sbserver podsandbox package. + Sandboxer string `toml:"sandboxer,omitempty" json:"sandboxer,omitempty"` +} diff --git a/node-installer/internal/constants/configuration-clh-snp.toml b/node-installer/internal/constants/configuration-clh-snp.toml new file mode 100644 index 0000000000..e4e681b821 --- /dev/null +++ b/node-installer/internal/constants/configuration-clh-snp.toml @@ -0,0 +1,42 @@ +[hypervisor.clh] +path = "/opt/edgeless/bin/cloud-hypervisor-snp" +igvm = "/opt/edgeless/share/kata-containers-igvm.img" +image = "/opt/edgeless/share/kata-containers.img" +rootfs_type="ext4" +confidential_guest = true +sev_snp_guest = true +snp_guest_policy=0x30000 +disable_selinux=false +disable_guest_selinux=true +enable_annotations = ["enable_iommu", "virtio_fs_extra_args", "kernel_params"] +valid_hypervisor_paths = ["/opt/edgeless/bin/cloud-hypervisor-snp"] +kernel_params = "" +default_vcpus = 1 +default_maxvcpus = 0 +default_memory = 256 +default_maxmemory = 0 +shared_fs = "none" +virtio_fs_daemon = "/opt/confidential-containers/libexec/virtiofsd" +valid_virtio_fs_daemon_paths = ["/opt/confidential-containers/libexec/virtiofsd"] +virtio_fs_cache_size = 0 +virtio_fs_queue_size = 1024 +virtio_fs_extra_args = ["--thread-pool-size=1", "--announce-submounts"] +virtio_fs_cache = "auto" +block_device_driver = "virtio-blk" + +[agent.kata] +dial_timeout = 90 + +[runtime] +internetworking_model="tcfilter" +disable_guest_seccomp=true +sandbox_cgroup_only=false +static_sandbox_resource_mgmt=true +static_sandbox_default_workload_mem=1792 +sandbox_bind_mounts=[] +vfio_mode="guest-kernel" +disable_guest_empty_dir=false +experimental=[] + +[image] +service_offload = false diff --git a/node-installer/internal/constants/constants.go b/node-installer/internal/constants/constants.go new file mode 100644 index 0000000000..6e079b9981 --- /dev/null +++ b/node-installer/internal/constants/constants.go @@ -0,0 +1,55 @@ +package constants + +import ( + _ "embed" + + "github.com/edgelesssys/contrast/node-installer/internal/config" + "github.com/pelletier/go-toml" +) + +// ContainerdRuntimeConfig is the configuration file for the containerd runtime +// +//go:embed configuration-clh-snp.toml +var ContainerdRuntimeConfig string + +// RuntimeName is the name of the runtime. +const ( + RuntimeName = "contrast-cc" + CRIFQDN = "io.containerd.grpc.v1.cri" +) + +// ContainerdBaseConfig returns the base containerd configuration. +func ContainerdBaseConfig() config.ContainerdConfig { + var config config.ContainerdConfig + if err := toml.Unmarshal([]byte(containerdBaseConfig), &config); err != nil { + panic(err) // should never happen + } + return config +} + +// ContainerdRuntimeConfigFragment returns the containerd runtime configuration fragment. +func ContainerdRuntimeConfigFragment() config.Runtime { + return config.Runtime{ + Type: "io.containerd.contrast-cc.v2", + Path: "/opt/edgeless/bin/containerd-shim-contrast-cc-v2", + PodAnnotations: []string{"io.katacontainers.*"}, + Options: map[string]any{ + "ConfigPath": "/opt/edgeless/etc/configuration-clh-snp.toml", + }, + PrivilegedWithoutHostDevices: true, + Snapshotter: "tardev", + } +} + +// TardevSnapshotterConfigFragment returns the tardev snapshotter configuration fragment. +func TardevSnapshotterConfigFragment() config.ProxyPlugin { + return config.ProxyPlugin{ + Type: "snapshot", + Address: "/run/containerd/tardev-snapshotter.sock", + } +} + +// containerdBaseConfig is the base configuration file for containerd +// +//go:embed containerd-config.toml +var containerdBaseConfig string diff --git a/node-installer/internal/constants/containerd-config.toml b/node-installer/internal/constants/containerd-config.toml new file mode 100644 index 0000000000..e149d09af7 --- /dev/null +++ b/node-installer/internal/constants/containerd-config.toml @@ -0,0 +1,46 @@ +version = 2 +oom_score = 0 +[plugins."io.containerd.grpc.v1.cri"] + sandbox_image = "mcr.microsoft.com/oss/kubernetes/pause:3.6" + [plugins."io.containerd.grpc.v1.cri".containerd] + disable_snapshot_annotations = false + default_runtime_name = "runc" + [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc] + runtime_type = "io.containerd.runc.v2" + [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options] + BinaryName = "/usr/bin/runc" + [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.untrusted] + runtime_type = "io.containerd.runc.v2" + [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.untrusted.options] + BinaryName = "/usr/bin/runc" + [plugins."io.containerd.grpc.v1.cri".cni] + bin_dir = "/opt/cni/bin" + conf_dir = "/etc/cni/net.d" + conf_template = "/etc/containerd/kubenet_template.conf" + [plugins."io.containerd.grpc.v1.cri".registry] + config_path = "/etc/containerd/certs.d" + [plugins."io.containerd.grpc.v1.cri".registry.headers] + X-Meta-Source-Client = ["azure/aks"] +[metrics] + address = "0.0.0.0:10257" +[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.kata] + runtime_type = "io.containerd.kata.v2" +[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.katacli] + runtime_type = "io.containerd.runc.v1" +[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.katacli.options] + NoPivotRoot = false + NoNewKeyring = false + ShimCgroup = "" + IoUid = 0 + IoGid = 0 + BinaryName = "/usr/bin/kata-runtime" + Root = "" + CriuPath = "" + SystemdCgroup = false +[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.kata-cc] + snapshotter = "tardev" + runtime_type = "io.containerd.kata-cc.v2" + privileged_without_host_devices = true + pod_annotations = ["io.katacontainers.*"] + [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.kata-cc.options] + ConfigPath = "/opt/confidential-containers/share/defaults/kata-containers/configuration-clh-snp.toml" diff --git a/packages/by-name/contrast-node-installer/package.nix b/packages/by-name/contrast-node-installer/package.nix new file mode 100644 index 0000000000..f8df642e0d --- /dev/null +++ b/packages/by-name/contrast-node-installer/package.nix @@ -0,0 +1,53 @@ +{ lib +, buildGoModule +}: + +buildGoModule rec { + pname = "contrast-node-installer"; + version = builtins.readFile ../../../version.txt; + + # The source of the main module of this repo. We filter for Go files so that + # changes in the other parts of this repo don't trigger a rebuild. + src = + let + inherit (lib) fileset path hasSuffix; + root = ../../../node-installer; + in + fileset.toSource { + inherit root; + fileset = fileset.unions [ + (path.append root "go.mod") + (path.append root "go.sum") + (lib.fileset.fileFilter (file: lib.hasSuffix ".toml" file.name) (path.append root "internal/constants")) + (lib.fileset.fileFilter (file: lib.hasSuffix ".go" file.name) root) + ]; + }; + + proxyVendor = true; + vendorHash = "sha256-AmgmB8oVV815b69MroR7/IEKL+b42WY86vISd12/LBE="; + + subPackages = [ "./cmd" ]; + + CGO_ENABLED = 0; + ldflags = [ + "-s" + "-w" + "-X main.version=v${version}" + ]; + + preCheck = '' + export CGO_ENABLED=1 + ''; + + checkPhase = '' + runHook preCheck + go test -race ./... + runHook postCheck + ''; + + postInstall = '' + mv "$out/bin/cmd" "$out/bin/node-installer" + ''; + + meta.mainProgram = "node-installer"; +} diff --git a/packages/by-name/contrast/package.nix b/packages/by-name/contrast/package.nix index 017fd6e147..1fe40b2d3e 100644 --- a/packages/by-name/contrast/package.nix +++ b/packages/by-name/contrast/package.nix @@ -40,7 +40,7 @@ buildGoModule rec { (path.append root "go.sum") (lib.fileset.difference (lib.fileset.fileFilter (file: lib.hasSuffix ".go" file.name) root) - (path.append root "service-mesh")) + (fileset.unions [ (path.append root "service-mesh") (path.append root "node-installer") ])) ]; };