Skip to content

Commit

Permalink
node-installer: initialize
Browse files Browse the repository at this point in the history
  • Loading branch information
malt3 committed Mar 18, 2024
1 parent 7cf0533 commit 52222be
Show file tree
Hide file tree
Showing 18 changed files with 1,150 additions and 1 deletion.
1 change: 1 addition & 0 deletions go.work
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ go 1.21
use (
.
./service-mesh
./node-installer
)
44 changes: 44 additions & 0 deletions node-installer/README.md
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 CC-enabled 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",
}
]
}
```
8 changes: 8 additions & 0 deletions node-installer/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module github.com/edgelesssys/contrast/node-installer

go 1.21

require (
github.com/pelletier/go-toml v1.9.5
golang.org/x/sys v0.18.0
)
4 changes: 4 additions & 0 deletions node-installer/go.sum
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=
103 changes: 103 additions & 0 deletions node-installer/internal/asset/fetcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
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())
fetcher.RegisterHandler("https", 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, fmt.Errorf("no handler for scheme %s", 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
}
65 changes: 65 additions & 0 deletions node-installer/internal/asset/file.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package asset

import (
"context"
"fmt"
"hash"
"io"
"net/url"
"os"
"slices"

"github.com/edgelesssys/contrast/node-installer/internal/fileop"
)

// FileFetcher is a Fetcher that retrieves assets from a file.
// It handles the "file" scheme.
type FileFetcher struct {
copier copier
}

// NewFileFetcher creates a new file fetcher.
func NewFileFetcher() *FileFetcher {
return &FileFetcher{copier: fileop.NewDefault()}
}

// Fetch retrieves a file from the local filesystem.
func (f *FileFetcher) Fetch(_ context.Context, uri *url.URL, destination string, expectedSum []byte, hasher hash.Hash) (bool, error) {
if uri.Scheme != "file" {
return false, fmt.Errorf("file fetcher does not support scheme %s", uri.Scheme)
}
sourceFile, err := os.Open(uri.Path)
if err != nil {
return false, fmt.Errorf("opening file: %w", err)
}
defer sourceFile.Close()

if _, err := io.Copy(hasher, sourceFile); err != nil {
return false, fmt.Errorf("hashing file: %w", err)
}
if err := sourceFile.Close(); err != nil {
return false, fmt.Errorf("closing file: %w", err)
}
actualSum := hasher.Sum(nil)
if !slices.Equal(actualSum, expectedSum) {
return false, fmt.Errorf("file hash mismatch: expected %x, got %x", expectedSum, actualSum)
}
changed, err := f.copier.CopyOnDiff(uri.Path, destination)
if err != nil {
return false, fmt.Errorf("copying file: %w", err)
}
return changed, nil
}

// FetchUnchecked retrieves a file from the local filesystem without verifying its integrity.
func (f *FileFetcher) FetchUnchecked(_ context.Context, uri *url.URL, destination string) error {
if uri.Scheme != "file" {
return fmt.Errorf("file fetcher does not support scheme %s", uri.Scheme)
}
_, err := f.copier.CopyOnDiff(uri.Path, destination)
return err
}

type copier interface {
CopyOnDiff(src, dst string) (bool, error)
}
114 changes: 114 additions & 0 deletions node-installer/internal/asset/http.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package asset

import (
"bytes"
"context"
"fmt"
"hash"
"io"
"net/http"
"net/url"
"os"

"github.com/edgelesssys/contrast/node-installer/internal/fileop"
)

// HTTPFetcher is a Fetcher that retrieves assets from http(s).
// It handles the "http" and "https" schemes.
type HTTPFetcher struct {
mover mover
client *http.Client
}

// NewHTTPFetcher creates a new HTTP fetcher.
func NewHTTPFetcher() *HTTPFetcher {
return &HTTPFetcher{mover: fileop.NewDefault(), client: http.DefaultClient}
}

// Fetch retrieves a file from an HTTP server.
func (f *HTTPFetcher) Fetch(ctx context.Context, uri *url.URL, destination string, expectedSum []byte, hasher hash.Hash) (bool, error) {
if uri.Scheme != "http" && uri.Scheme != "https" {
return false, fmt.Errorf("http fetcher does not support scheme %s", uri.Scheme)
}

if existing, err := os.Open(destination); err == nil {
defer existing.Close()
if _, err := io.Copy(hasher, existing); err != nil {
return false, fmt.Errorf("hashing existing file %s: %w", destination, err)
}
if sum := hasher.Sum(nil); bytes.Equal(sum, expectedSum) {
// File already exists and has the correct hash
return false, nil
}
hasher.Reset()
}

req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri.String(), nil)
if err != nil {
return false, fmt.Errorf("creating request: %w", err)
}
response, err := f.client.Do(req)
if err != nil {
return false, fmt.Errorf("fetching file: %w", err)
}
defer response.Body.Close()

tmpfile, err := os.CreateTemp("", "download")
if err != nil {
return false, err
}
defer tmpfile.Close()
defer os.Remove(tmpfile.Name())

reader := io.TeeReader(response.Body, hasher)

if _, err := io.Copy(tmpfile, reader); err != nil {
return false, fmt.Errorf("downloading file contents from %s: %w", uri.String(), err)
}

sum := hasher.Sum(nil)
if !bytes.Equal(sum, expectedSum) {
return false, fmt.Errorf("hash mismatch for %s: expected %x, got %x", uri.String(), expectedSum, sum)
}
if err := tmpfile.Sync(); err != nil {
return false, fmt.Errorf("syncing file %s: %w", tmpfile.Name(), err)
}
if err := f.mover.Move(tmpfile.Name(), destination); err != nil {
return false, fmt.Errorf("moving file: %w", err)
}

return true, nil
}

// FetchUnchecked retrieves a file from an HTTP server without verifying its integrity.
func (f *HTTPFetcher) FetchUnchecked(ctx context.Context, uri *url.URL, destination string) error {
if uri.Scheme != "http" && uri.Scheme != "https" {
return fmt.Errorf("http fetcher does not support scheme %s", uri.Scheme)
}

req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri.String(), nil)
if err != nil {
return fmt.Errorf("creating request: %w", err)
}
response, err := f.client.Do(req)
if err != nil {
return fmt.Errorf("fetching file: %w", err)
}
defer response.Body.Close()

dstFile, err := os.Create(destination)
if err != nil {
return err
}
defer dstFile.Close()

_, err = io.Copy(dstFile, response.Body)
if err != nil {
return fmt.Errorf("downloading file contents from %s: %w", uri.String(), err)
}
return nil
}

type mover interface {
Move(src, dst string) error
}
Loading

0 comments on commit 52222be

Please sign in to comment.