Skip to content

Commit

Permalink
feat(cmd/rofl): Add TDX container build support
Browse files Browse the repository at this point in the history
  • Loading branch information
kostko committed Dec 17, 2024
1 parent 9d0a497 commit 840afff
Show file tree
Hide file tree
Showing 11 changed files with 1,113 additions and 220 deletions.
218 changes: 218 additions & 0 deletions build/rofl/manifest.go
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"`
}
161 changes: 161 additions & 0 deletions build/rofl/manifest_test.go
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

View workflow job for this annotation

GitHub Actions / tests-ubuntu-latest

cannot use "no ROFL app manifest found" (constant of type string) as error value in argument to require.ErrorIs: string does not implement error (missing method Error)

Check failure on line 138 in build/rofl/manifest_test.go

View workflow job for this annotation

GitHub Actions / lint

cannot use "no ROFL app manifest found" (constant of type string) as error value in argument to require.ErrorIs: string does not implement error (missing method Error) (typecheck)

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)
}
Loading

0 comments on commit 840afff

Please sign in to comment.