Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cmd/rofl): Add TDX container build support #335

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 / 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)

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)

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
Loading