-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
18 changed files
with
1,140 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,4 +3,5 @@ go 1.21 | |
use ( | ||
. | ||
./service-mesh | ||
./node-installer | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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", | ||
} | ||
] | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,213 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"os" | ||
"os/exec" | ||
"path/filepath" | ||
"slices" | ||
"strings" | ||
"time" | ||
|
||
"github.com/edgelesssys/contrast/node-installer/internal/asset" | ||
"github.com/edgelesssys/contrast/node-installer/internal/config" | ||
"github.com/edgelesssys/contrast/node-installer/internal/constants" | ||
"github.com/pelletier/go-toml" | ||
) | ||
|
||
func main() { | ||
fetcher := asset.NewDefaultFetcher() | ||
if err := run(context.Background(), fetcher); err != nil { | ||
fmt.Println(err) | ||
os.Exit(1) | ||
} | ||
fmt.Println("Installation completed successfully.") | ||
} | ||
|
||
func run(ctx context.Context, fetcher assetFetcher) error { | ||
configDir := envWithDefault("CONFIG_DIR", "/config") | ||
hostMount := envWithDefault("HOST_MOUNT", "/host") | ||
binDir := filepath.Join(hostMount, "opt", "edgeless", "bin") | ||
|
||
// Create directory structure, ignore if it already exists | ||
_ = os.MkdirAll(binDir, 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) | ||
} | ||
if err := config.Validate(); err != nil { | ||
return fmt.Errorf("validating config: %w", err) | ||
} | ||
|
||
for _, file := range config.Files { | ||
fmt.Printf("Fetching %q to %q\n", file.URL, file.Path) | ||
var fetchErr error | ||
if file.Integrity == "" { | ||
fetchErr = fetcher.FetchUnchecked(ctx, file.URL, filepath.Join(hostMount, file.Path)) | ||
} else { | ||
_, fetchErr = fetcher.Fetch(ctx, file.URL, filepath.Join(hostMount, file.Path), file.Integrity) | ||
} | ||
if fetchErr != nil { | ||
return fmt.Errorf("fetching file from %q to %q: %w", file.URL, file.Path, fetchErr) | ||
} | ||
} | ||
items, err := os.ReadDir(binDir) | ||
if err != nil { | ||
return fmt.Errorf("reading bin directory %q: %w", binDir, err) | ||
} | ||
|
||
for _, item := range items { | ||
if !item.Type().IsRegular() { | ||
continue | ||
} | ||
if err := os.Chmod(filepath.Join(binDir, 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) | ||
} | ||
containerdConfigPath := filepath.Join(hostMount, "etc", "containerd", "config.toml") | ||
err = patchContainerdConfig(containerdConfigPath) | ||
if err != nil { | ||
return fmt.Errorf("patching containerd config: %w", err) | ||
} | ||
|
||
return restartHostContainerd(containerdConfigPath) | ||
} | ||
|
||
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 { | ||
existing, err := parseExistingContainerdConfig(path) | ||
if err != nil { | ||
existing = constants.ContainerdBaseConfig() | ||
} | ||
existingRaw, err := toml.Marshal(existing) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
// Add tardev snapshotter | ||
if existing.ProxyPlugins == nil { | ||
existing.ProxyPlugins = make(map[string]config.ProxyPlugin) | ||
} | ||
if _, ok := existing.ProxyPlugins["tardev"]; !ok { | ||
existing.ProxyPlugins["tardev"] = constants.TardevSnapshotterConfigFragment() | ||
} | ||
|
||
// Add contrast-cc runtime | ||
ensureMapPath(&existing.Plugins, constants.CRIFQDN, "containerd", "runtimes") | ||
runtimes := existing.Plugins[constants.CRIFQDN].(map[string]any)["containerd"].(map[string]any)["runtimes"].(map[string]any) | ||
runtimes[constants.RuntimeName] = constants.ContainerdRuntimeConfigFragment() | ||
|
||
rawConfig, err := toml.Marshal(existing) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if slices.Equal(existingRaw, rawConfig) { | ||
return nil | ||
} | ||
|
||
fmt.Println("Patching containerd config") | ||
return os.WriteFile(path, rawConfig, os.ModePerm) | ||
} | ||
|
||
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(containerdConfigPath string) error { | ||
// get mtime of the config file | ||
info, err := os.Stat(containerdConfigPath) | ||
if err != nil { | ||
return fmt.Errorf("stat %q: %w", containerdConfigPath, err) | ||
} | ||
configMtime := info.ModTime() | ||
|
||
// get containerd start time | ||
// Note that "--timestamp=unix" is not supported in the installed version of systemd (v250) at the time of writing. | ||
containerdStartTime, err := exec.Command("nsenter", "--target", "1", "--mount", "--", "systemctl", "show", "--timestamp=utc", "--property=ActiveEnterTimestamp", "containerd").CombinedOutput() | ||
if err != nil { | ||
return fmt.Errorf("getting containerd start time: %w %q", err, containerdStartTime) | ||
} | ||
|
||
// format: ActiveEnterTimestamp=Day YYYY-MM-DD HH:MM:SS UTC | ||
dayUTC := strings.TrimPrefix(strings.TrimSpace(string(containerdStartTime)), "ActiveEnterTimestamp=") | ||
startTime, err := time.Parse("Mon 2006-01-02 15:04:05 MST", dayUTC) | ||
if err != nil { | ||
return fmt.Errorf("parsing containerd start time: %w", err) | ||
} | ||
|
||
fmt.Printf("containerd start time: %s\n", startTime.Format(time.RFC3339)) | ||
if startTime.After(configMtime) { | ||
fmt.Println("containerd already running with the new config") | ||
return nil | ||
} | ||
|
||
// This command will restart containerd on the host and will take down the installer with it. | ||
out, err := exec.Command("nsenter", "--target", "1", "--mount", "--", "systemctl", "restart", "containerd").CombinedOutput() | ||
if err != nil { | ||
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 | ||
} | ||
|
||
type assetFetcher interface { | ||
Fetch(ctx context.Context, sourceURI, destination, integrity string) (changed bool, retErr error) | ||
FetchUnchecked(ctx context.Context, sourceURI, destination string) error | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
module github.com/edgelesssys/contrast/node-installer | ||
|
||
go 1.21 | ||
|
||
require github.com/pelletier/go-toml v1.9.5 | ||
|
||
require golang.org/x/sys v0.18.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
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= | ||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= | ||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
package asset | ||
|
||
import ( | ||
"context" | ||
"crypto/sha256" | ||
"crypto/sha512" | ||
"encoding/base64" | ||
"errors" | ||
"fmt" | ||
"hash" | ||
"net/url" | ||
"sync" | ||
) | ||
|
||
// Fetcher can retrieve assets from various sources. | ||
// It works by delegating to a handler for the scheme of the source URI. | ||
type Fetcher struct { | ||
handlers map[string]handler | ||
mux sync.RWMutex | ||
} | ||
|
||
// NewDefaultFetcher creates a new fetcher with default handlers. | ||
func NewDefaultFetcher() *Fetcher { | ||
fetcher := NewFetcher() | ||
fetcher.RegisterHandler("file", NewFileFetcher()) | ||
fetcher.RegisterHandler("http", NewHTTPFetcher()) | ||
return fetcher | ||
} | ||
|
||
// NewFetcher creates a new fetcher. | ||
func NewFetcher() *Fetcher { | ||
return &Fetcher{ | ||
handlers: make(map[string]handler), | ||
} | ||
} | ||
|
||
// Fetch retrieves a file from a source URI. | ||
func (f *Fetcher) Fetch(ctx context.Context, sourceURI, destination, integrity string) (changed bool, retErr error) { | ||
uri, err := url.Parse(sourceURI) | ||
if err != nil { | ||
return false, err | ||
} | ||
hasher, expectedSum, err := hashFromIntegrity(integrity) | ||
if err != nil { | ||
return false, err | ||
} | ||
schemeFetcher := f.getHandler(uri.Scheme) | ||
if schemeFetcher == nil { | ||
return false, errors.New("no handler for scheme " + uri.Scheme) | ||
} | ||
return schemeFetcher.Fetch(ctx, uri, destination, expectedSum, hasher) | ||
} | ||
|
||
// FetchUnchecked retrieves a file from a source URI without verifying its integrity. | ||
func (f *Fetcher) FetchUnchecked(ctx context.Context, sourceURI, destination string) error { | ||
uri, err := url.Parse(sourceURI) | ||
if err != nil { | ||
return err | ||
} | ||
schemeFetcher := f.getHandler(uri.Scheme) | ||
if schemeFetcher == nil { | ||
return errors.New("no handler for scheme " + uri.Scheme) | ||
} | ||
return schemeFetcher.FetchUnchecked(ctx, uri, destination) | ||
} | ||
|
||
// RegisterHandler registers a handler for a scheme. | ||
func (f *Fetcher) RegisterHandler(scheme string, h handler) { | ||
f.mux.Lock() | ||
defer f.mux.Unlock() | ||
f.handlers[scheme] = h | ||
} | ||
|
||
func (f *Fetcher) getHandler(scheme string) handler { | ||
f.mux.RLock() | ||
defer f.mux.RUnlock() | ||
return f.handlers[scheme] | ||
} | ||
|
||
type handler interface { | ||
Fetch(ctx context.Context, uri *url.URL, destination string, expectedSum []byte, hasher hash.Hash) (bool, error) | ||
FetchUnchecked(ctx context.Context, uri *url.URL, destination string) error | ||
} | ||
|
||
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 | ||
} |
Oops, something went wrong.