-
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
21 changed files
with
1,565 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 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", | ||
} | ||
] | ||
} | ||
``` |
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,17 @@ | ||
module github.com/edgelesssys/contrast/node-installer | ||
|
||
go 1.21 | ||
|
||
require ( | ||
github.com/pelletier/go-toml v1.9.5 | ||
github.com/stretchr/testify v1.9.0 | ||
go.uber.org/goleak v1.3.0 | ||
golang.org/x/sys v0.18.0 | ||
) | ||
|
||
require ( | ||
github.com/davecgh/go-spew v1.1.1 // indirect | ||
github.com/kr/text v0.2.0 // indirect | ||
github.com/pmezard/go-difflib v1.0.0 // indirect | ||
gopkg.in/yaml.v3 v3.0.1 // indirect | ||
) |
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,22 @@ | ||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= | ||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= | ||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= | ||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= | ||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= | ||
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= | ||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= | ||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= | ||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= | ||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= | ||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= | ||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= | ||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
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,89 @@ | ||
package asset | ||
|
||
import ( | ||
"context" | ||
"crypto/sha256" | ||
"crypto/sha512" | ||
"encoding/base64" | ||
"fmt" | ||
"hash" | ||
"net/url" | ||
) | ||
|
||
// 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 | ||
} | ||
|
||
// NewDefaultFetcher creates a new fetcher with default handlers. | ||
func NewDefaultFetcher() *Fetcher { | ||
fileFetcher := NewFileFetcher() | ||
httpFetcher := NewHTTPFetcher() | ||
return NewFetcher(map[string]handler{ | ||
"file": fileFetcher, | ||
"http": httpFetcher, | ||
"https": httpFetcher, | ||
}) | ||
} | ||
|
||
// NewFetcher creates a new fetcher. | ||
func NewFetcher(handlers map[string]handler) *Fetcher { | ||
return &Fetcher{ | ||
handlers: handlers, | ||
} | ||
} | ||
|
||
// 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.handlers[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.handlers[uri.Scheme] | ||
if schemeFetcher == nil { | ||
return fmt.Errorf("no handler for scheme %s", uri.Scheme) | ||
} | ||
return schemeFetcher.FetchUnchecked(ctx, uri, destination) | ||
} | ||
|
||
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 | ||
} |
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,139 @@ | ||
package asset | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"io" | ||
"net/http" | ||
"os" | ||
"path/filepath" | ||
"testing" | ||
|
||
"github.com/edgelesssys/contrast/node-installer/internal/fileop" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
"go.uber.org/goleak" | ||
) | ||
|
||
func TestMain(m *testing.M) { | ||
goleak.VerifyTestMain(m) | ||
} | ||
|
||
func TestFetch(t *testing.T) { | ||
sourceDir, err := os.MkdirTemp("", "fileop-test-empty") | ||
require.NoError(t, err) | ||
fooSource := filepath.Join(sourceDir, "foo") | ||
require.NoError(t, os.WriteFile(fooSource, []byte("foo"), 0o644)) | ||
defer os.RemoveAll(sourceDir) | ||
|
||
testCases := map[string]struct { | ||
dstContent []byte | ||
sourceURIs []string | ||
sri string | ||
wantModified bool | ||
wantErr bool | ||
}{ | ||
"identical": { | ||
dstContent: []byte("foo"), | ||
sourceURIs: []string{"file://" + fooSource, "http://example.com/foo"}, | ||
sri: "sha256-LCa0a2j/xo/5m0U8HTBBNBNCLXBkg7+g+YpeiGJm564=", | ||
}, | ||
"different": { | ||
dstContent: []byte("bar"), | ||
sourceURIs: []string{"file://" + fooSource, "http://example.com/foo"}, | ||
sri: "sha256-LCa0a2j/xo/5m0U8HTBBNBNCLXBkg7+g+YpeiGJm564=", | ||
wantModified: true, | ||
}, | ||
"sri mismatch": { | ||
dstContent: []byte("foo"), | ||
sourceURIs: []string{"file://" + fooSource, "http://example.com/foo"}, | ||
sri: "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", | ||
wantErr: true, | ||
}, | ||
"unchecked": { | ||
dstContent: []byte("bar"), | ||
sourceURIs: []string{"file://" + fooSource, "http://example.com/foo"}, | ||
wantModified: true, | ||
}, | ||
"src not found": { | ||
dstContent: []byte("foo"), | ||
sourceURIs: []string{"file://this/file/is/nonexistent", "http://example.com//this/file/is/nonexistent"}, | ||
wantErr: true, | ||
}, | ||
"dst not found": { | ||
sourceURIs: []string{"file://" + fooSource, "http://example.com/foo"}, | ||
wantModified: true, | ||
}, | ||
} | ||
for name, tc := range testCases { | ||
for _, sourceURI := range tc.sourceURIs { | ||
t.Run(name+"_"+sourceURI, func(t *testing.T) { | ||
assert := assert.New(t) | ||
require := require.New(t) | ||
|
||
emptyDir, err := os.MkdirTemp("", "fileop-test-empty") | ||
require.NoError(err) | ||
defer os.RemoveAll(emptyDir) | ||
dst := filepath.Join(emptyDir, "dst") | ||
if tc.dstContent != nil { | ||
require.NoError(os.WriteFile(dst, tc.dstContent, 0o644)) | ||
defer os.Remove(dst) | ||
} | ||
httpResponses := map[string][]byte{ | ||
"/foo": []byte("foo"), | ||
} | ||
fetcher := NewFetcher(map[string]handler{ | ||
"file": NewFileFetcher(), | ||
"http": newFakeHTTPFetcher(httpResponses), | ||
}) | ||
var modified bool | ||
var fetchErr error | ||
if tc.sri != "" { | ||
modified, fetchErr = fetcher.Fetch(context.Background(), sourceURI, dst, tc.sri) | ||
} else { | ||
fetchErr = fetcher.FetchUnchecked(context.Background(), sourceURI, dst) | ||
modified = true // FetchUnchecked always modifies the file | ||
} | ||
if tc.wantErr { | ||
require.Error(fetchErr) | ||
return | ||
} | ||
require.NoError(fetchErr) | ||
assert.Equal(tc.wantModified, modified) | ||
got, err := os.ReadFile(dst) | ||
require.NoError(err) | ||
assert.Equal([]byte("foo"), got) | ||
}) | ||
} | ||
} | ||
} | ||
|
||
func newFakeHTTPFetcher(responses map[string][]byte) *HTTPFetcher { | ||
hClient := http.Client{ | ||
Transport: &fakeRoundTripper{store: responses}, | ||
} | ||
|
||
return &HTTPFetcher{ | ||
client: &hClient, | ||
mover: fileop.NewDefault(), | ||
} | ||
} | ||
|
||
type fakeRoundTripper struct { | ||
store map[string][]byte | ||
} | ||
|
||
func (f *fakeRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { | ||
path := req.URL.Path | ||
body, ok := f.store[path] | ||
if !ok { | ||
return &http.Response{ | ||
StatusCode: http.StatusNotFound, | ||
Body: io.NopCloser(bytes.NewReader([]byte("not found"))), | ||
}, nil | ||
} | ||
return &http.Response{ | ||
StatusCode: http.StatusOK, | ||
Body: io.NopCloser(bytes.NewReader(body)), | ||
}, nil | ||
} |
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,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) | ||
} |
Oops, something went wrong.