-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cmd/rofl): Add TDX container build support
- Loading branch information
Showing
11 changed files
with
1,130 additions
and
220 deletions.
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 |
---|---|---|
@@ -0,0 +1,218 @@ | ||
package rofl | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"os" | ||
"strings" | ||
|
||
"gopkg.in/yaml.v3" | ||
|
||
"github.com/oasisprotocol/oasis-core/go/common/version" | ||
|
||
"github.com/oasisprotocol/oasis-sdk/client-sdk/go/modules/rofl" | ||
) | ||
|
||
// ManifestFileNames are the manifest file names that are tried when loading the manifest. | ||
var ManifestFileNames = []string{ | ||
"rofl.yml", | ||
"rofl.yaml", | ||
} | ||
|
||
// Supported ROFL app kinds. | ||
const ( | ||
AppKindRaw = "raw" | ||
AppKindContainer = "container" | ||
) | ||
|
||
// Supported TEE types. | ||
const ( | ||
TEETypeSGX = "sgx" | ||
TEETypeTDX = "tdx" | ||
) | ||
|
||
// Manifest is the ROFL app manifest that configures various aspects of the app in a single place. | ||
type Manifest struct { | ||
// AppID is the Bech32-encoded ROFL app ID. | ||
AppID string `yaml:"app_id" json:"app_id"` | ||
// Name is the human readable ROFL app name. | ||
Name string `yaml:"name" json:"name"` | ||
// Version is the ROFL app version. | ||
Version string `yaml:"version" json:"version"` | ||
// Network is the identifier of the network to deploy to by default. | ||
Network string `yaml:"network,omitempty" json:"network,omitempty"` | ||
// ParaTime is the identifier of the paratime to deploy to by default. | ||
ParaTime string `yaml:"paratime,omitempty" json:"paratime,omitempty"` | ||
// TEE is the type of TEE to build for. | ||
TEE string `yaml:"tee" json:"tee"` | ||
// Kind is the kind of ROFL app to build. | ||
Kind string `yaml:"kind" json:"kind"` | ||
// TrustRoot is the optional trust root configuration. | ||
TrustRoot *TrustRootConfig `yaml:"trust_root,omitempty" json:"trust_root,omitempty"` | ||
// Resources are the requested ROFL app resources. | ||
Resources ResourcesConfig `yaml:"resources" json:"resources"` | ||
// Artifacts are the optional artifact location overrides. | ||
Artifacts *ArtifactsConfig `yaml:"artifacts,omitempty" json:"artifacts,omitempty"` | ||
|
||
// Policy is the ROFL app policy to deploy by default. | ||
Policy *rofl.AppAuthPolicy `yaml:"policy,omitempty" json:"policy,omitempty"` | ||
// Admin is the identifier of the admin account. | ||
Admin string `yaml:"admin,omitempty" json:"admin,omitempty"` | ||
} | ||
|
||
// LoadManifest attempts to find and load the ROFL app manifest from a local file. | ||
func LoadManifest() (*Manifest, error) { | ||
for _, fn := range ManifestFileNames { | ||
f, err := os.Open(fn) | ||
switch { | ||
case err == nil: | ||
case errors.Is(err, os.ErrNotExist): | ||
continue | ||
default: | ||
return nil, fmt.Errorf("failed to load manifest from '%s': %w", fn, err) | ||
} | ||
|
||
var m Manifest | ||
dec := yaml.NewDecoder(f) | ||
if err = dec.Decode(&m); err != nil { | ||
f.Close() | ||
return nil, fmt.Errorf("malformed manifest '%s': %w", fn, err) | ||
} | ||
if err = m.Validate(); err != nil { | ||
f.Close() | ||
return nil, fmt.Errorf("invalid manifest '%s': %w", fn, err) | ||
} | ||
|
||
f.Close() | ||
return &m, nil | ||
} | ||
return nil, fmt.Errorf("no ROFL app manifest found (tried: %s)", strings.Join(ManifestFileNames, ", ")) | ||
} | ||
|
||
// Validate validates the manifest for correctness. | ||
func (m *Manifest) Validate() error { | ||
if len(m.AppID) == 0 { | ||
return fmt.Errorf("app ID cannot be empty") | ||
} | ||
var appID rofl.AppID | ||
if err := appID.UnmarshalText([]byte(m.AppID)); err != nil { | ||
return fmt.Errorf("malformed app ID: %w", err) | ||
} | ||
|
||
if len(m.Name) == 0 { | ||
return fmt.Errorf("name cannot be empty") | ||
} | ||
|
||
if len(m.Version) == 0 { | ||
return fmt.Errorf("version cannot be empty") | ||
} | ||
if _, err := version.FromString(m.Version); err != nil { | ||
return fmt.Errorf("malformed version: %w", err) | ||
} | ||
|
||
switch m.TEE { | ||
case TEETypeSGX, TEETypeTDX: | ||
default: | ||
return fmt.Errorf("unsupported TEE type: %s", m.TEE) | ||
} | ||
|
||
switch m.Kind { | ||
case AppKindRaw: | ||
case AppKindContainer: | ||
if m.TEE != TEETypeTDX { | ||
return fmt.Errorf("containers are only supported under TDX") | ||
} | ||
default: | ||
return fmt.Errorf("unsupported app kind: %s", m.Kind) | ||
} | ||
|
||
if err := m.Resources.Validate(); err != nil { | ||
return fmt.Errorf("bad resources config: %w", err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// TrustRootConfig is the trust root configuration. | ||
type TrustRootConfig struct { | ||
// Height is the consensus layer block height where to take the trust root. | ||
Height uint64 `yaml:"height,omitempty" json:"height,omitempty"` | ||
// Hash is the consensus layer block header hash corresponding to the passed height. | ||
Hash string `yaml:"hash,omitempty" json:"hash,omitempty"` | ||
} | ||
|
||
// ResourcesConfig is the resources configuration. | ||
type ResourcesConfig struct { | ||
// Memory is the amount of memory needed by the app in megabytes. | ||
Memory uint64 `yaml:"memory" json:"memory"` | ||
// CpuCount is the number of vCPUs needed by the app. | ||
CpuCount uint8 `yaml:"cpus" json:"cpus"` | ||
// EphemeralStorage is the ephemeral storage configuration. | ||
EphemeralStorage *EphemeralStorageConfig `yaml:"ephemeral_storage,omitempty" json:"ephemeral_storage,omitempty"` | ||
} | ||
|
||
// Validate validates the resources configuration for correctness. | ||
func (r *ResourcesConfig) Validate() error { | ||
if r.Memory < 16 { | ||
return fmt.Errorf("memory size must be at least 16M") | ||
} | ||
if r.CpuCount < 1 { | ||
return fmt.Errorf("vCPU count must be at least 1") | ||
} | ||
if r.EphemeralStorage != nil { | ||
err := r.EphemeralStorage.Validate() | ||
if err != nil { | ||
return fmt.Errorf("bad ephemeral storage config: %w", err) | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
// Supported ephemeral storage kinds. | ||
const ( | ||
EphemeralStorageKindNone = "none" | ||
EphemeralStorageKindDisk = "disk" | ||
EphemeralStorageKindRAM = "ram" | ||
) | ||
|
||
// EphemeralStorageConfig is the ephemeral storage configuration. | ||
type EphemeralStorageConfig struct { | ||
// Kind is the storage kind. | ||
Kind string `yaml:"kind" json:"kind"` | ||
// Size is the amount of ephemeral storage in megabytes. | ||
Size uint64 `yaml:"size" json:"size"` | ||
} | ||
|
||
// Validate validates the ephemeral storage configuration for correctness. | ||
func (e *EphemeralStorageConfig) Validate() error { | ||
switch e.Kind { | ||
case EphemeralStorageKindNone, EphemeralStorageKindDisk, EphemeralStorageKindRAM: | ||
default: | ||
return fmt.Errorf("unsupported ephemeral storage kind: %s", e.Kind) | ||
} | ||
|
||
if e.Size < 16 { | ||
return fmt.Errorf("ephemeral storage size must be at least 16M") | ||
} | ||
return nil | ||
} | ||
|
||
// ArtifactsConfig is the artifact location override configuration. | ||
type ArtifactsConfig struct { | ||
// Firmware is the URI/path to the firmware artifact (empty to use default). | ||
Firmware string `yaml:"firmware,omitempty" json:"firmware,omitempty"` | ||
// Kernel is the URI/path to the kernel artifact (empty to use default). | ||
Kernel string `yaml:"kernel,omitempty" json:"kernel,omitempty"` | ||
// Stage2 is the URI/path to the stage 2 disk artifact (empty to use default). | ||
Stage2 string `yaml:"stage2,omitempty" json:"stage2,omitempty"` | ||
// Container is the container artifacts configuration. | ||
Container ContainerArtifactsConfig `yaml:"container,omitempty" json:"container,omitempty"` | ||
} | ||
|
||
// ContainerArtifactsConfig is the container artifacts configuration. | ||
type ContainerArtifactsConfig struct { | ||
// Runtime is the URI/path to the container runtime artifact (empty to use default). | ||
Runtime string `yaml:"runtime,omitempty" json:"runtime,omitempty"` | ||
// Compose is the URI/path to the docker-compose.yaml artifact (empty to use default). | ||
Compose string `yaml:"compose,omitempty" json:"compose,omitempty"` | ||
} |
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,161 @@ | ||
package rofl | ||
|
||
import ( | ||
"os" | ||
"path/filepath" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
"gopkg.in/yaml.v3" | ||
) | ||
|
||
func TestManifestValidation(t *testing.T) { | ||
require := require.New(t) | ||
|
||
// Empty manifest is not valid. | ||
m := Manifest{} | ||
err := m.Validate() | ||
require.ErrorContains(err, "app ID cannot be empty") | ||
|
||
// Invalid app ID. | ||
m.AppID = "foo" | ||
err = m.Validate() | ||
require.ErrorContains(err, "malformed app ID") | ||
|
||
// Empty name. | ||
m.AppID = "rofl1qpa9ydy3qmka3yrqzx0pxuvyfexf9mlh75hker5j" | ||
err = m.Validate() | ||
require.ErrorContains(err, "name cannot be empty") | ||
|
||
// Empty version. | ||
m.Name = "my-simple-app" | ||
err = m.Validate() | ||
require.ErrorContains(err, "version cannot be empty") | ||
|
||
// Invalid version. | ||
m.Version = "foo" | ||
err = m.Validate() | ||
require.ErrorContains(err, "malformed version") | ||
|
||
// Unsupported TEE type. | ||
m.Version = "0.1.0" | ||
err = m.Validate() | ||
require.ErrorContains(err, "unsupported TEE type") | ||
|
||
// Unsupported app kind. | ||
m.TEE = "sgx" | ||
err = m.Validate() | ||
require.ErrorContains(err, "unsupported app kind") | ||
|
||
// Containers are only supported under TDX. | ||
m.Kind = "container" | ||
err = m.Validate() | ||
require.ErrorContains(err, "containers are only supported under TDX") | ||
|
||
// Bad resources configuration. | ||
m.TEE = "tdx" | ||
err = m.Validate() | ||
require.ErrorContains(err, "bad resources config: memory size must be at least 16M") | ||
|
||
m.Resources.Memory = 16 | ||
err = m.Validate() | ||
require.ErrorContains(err, "bad resources config: vCPU count must be at least 1") | ||
|
||
// Finally, everything is valid. | ||
m.Resources.CpuCount = 1 | ||
err = m.Validate() | ||
require.NoError(err) | ||
|
||
// Add ephemeral storage configuration. | ||
m.Resources.EphemeralStorage = &EphemeralStorageConfig{} | ||
err = m.Validate() | ||
require.ErrorContains(err, "bad resources config: bad ephemeral storage config: unsupported ephemeral storage kind") | ||
|
||
m.Resources.EphemeralStorage.Kind = "ram" | ||
err = m.Validate() | ||
require.ErrorContains(err, "bad resources config: bad ephemeral storage config: ephemeral storage size must be at least 16M") | ||
|
||
m.Resources.EphemeralStorage.Size = 16 | ||
err = m.Validate() | ||
require.NoError(err) | ||
} | ||
|
||
const serializedYamlManifest = ` | ||
app_id: rofl1qpa9ydy3qmka3yrqzx0pxuvyfexf9mlh75hker5j | ||
name: my-simple-app | ||
version: 0.1.0 | ||
tee: tdx | ||
kind: container | ||
resources: | ||
memory: 16 | ||
cpus: 1 | ||
ephemeral_storage: | ||
kind: ram | ||
size: 16 | ||
` | ||
|
||
func TestManifestSerialization(t *testing.T) { | ||
require := require.New(t) | ||
|
||
var m Manifest | ||
err := yaml.Unmarshal([]byte(serializedYamlManifest), &m) | ||
require.NoError(err, "yaml.Unmarshal") | ||
err = m.Validate() | ||
require.NoError(err, "m.Validate") | ||
require.Equal("rofl1qpa9ydy3qmka3yrqzx0pxuvyfexf9mlh75hker5j", m.AppID) | ||
require.Equal("my-simple-app", m.Name) | ||
require.Equal("0.1.0", m.Version) | ||
require.Equal("tdx", m.TEE) | ||
require.Equal("container", m.Kind) | ||
require.EqualValues(16, m.Resources.Memory) | ||
require.EqualValues(1, m.Resources.CpuCount) | ||
require.NotNil(m.Resources.EphemeralStorage) | ||
require.Equal("ram", m.Resources.EphemeralStorage.Kind) | ||
require.EqualValues(16, m.Resources.EphemeralStorage.Size) | ||
|
||
enc, err := yaml.Marshal(m) | ||
require.NoError(err, "yaml.Marshal") | ||
|
||
var dec Manifest | ||
err = yaml.Unmarshal(enc, &dec) | ||
require.NoError(err, "yaml.Unmarshal(round-trip)") | ||
require.EqualValues(m, dec, "serialization should round-trip") | ||
err = dec.Validate() | ||
require.NoError(err, "dec.Validate") | ||
} | ||
|
||
func TestLoadManifest(t *testing.T) { | ||
require := require.New(t) | ||
|
||
tmpDir, err := os.MkdirTemp("", "oasis-test-load-manifest") | ||
require.NoError(err) | ||
defer os.RemoveAll(tmpDir) | ||
|
||
err = os.Chdir(tmpDir) | ||
require.NoError(err) | ||
|
||
_, err = LoadManifest() | ||
require.ErrorIs(err, "no ROFL app manifest found") | ||
Check failure on line 138 in build/rofl/manifest_test.go GitHub Actions / lint
|
||
|
||
manifestFn := filepath.Join(tmpDir, "rofl.yml") | ||
err = os.WriteFile(manifestFn, []byte("foo"), 0o644) | ||
require.NoError(err) | ||
_, err = LoadManifest() | ||
require.ErrorContains(err, "malformed manifest 'rofl.yml'") | ||
|
||
err = os.WriteFile(manifestFn, []byte(serializedYamlManifest), 0o644) | ||
require.NoError(err) | ||
m, err := LoadManifest() | ||
require.NoError(err) | ||
require.Equal("rofl1qpa9ydy3qmka3yrqzx0pxuvyfexf9mlh75hker5j", m.AppID) | ||
|
||
err = os.Remove(manifestFn) | ||
require.NoError(err) | ||
|
||
manifestFn = "rofl.yaml" | ||
err = os.WriteFile(manifestFn, []byte(serializedYamlManifest), 0o644) | ||
require.NoError(err) | ||
m, err = LoadManifest() | ||
require.NoError(err) | ||
require.Equal("rofl1qpa9ydy3qmka3yrqzx0pxuvyfexf9mlh75hker5j", m.AppID) | ||
} |
Oops, something went wrong.