Skip to content

Commit 2d39520

Browse files
committed
feat(cmd/rofl): Add TDX container build support
1 parent d46f750 commit 2d39520

File tree

11 files changed

+1172
-241
lines changed

11 files changed

+1172
-241
lines changed

build/rofl/manifest.go

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
package rofl
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"os"
7+
"strings"
8+
9+
"gopkg.in/yaml.v3"
10+
11+
"github.com/oasisprotocol/oasis-core/go/common/version"
12+
13+
"github.com/oasisprotocol/oasis-sdk/client-sdk/go/modules/rofl"
14+
)
15+
16+
// ManifestFileNames are the manifest file names that are tried when loading the manifest.
17+
var ManifestFileNames = []string{
18+
"rofl.yml",
19+
"rofl.yaml",
20+
}
21+
22+
// Supported ROFL app kinds.
23+
const (
24+
AppKindRaw = "raw"
25+
AppKindContainer = "container"
26+
)
27+
28+
// Supported TEE types.
29+
const (
30+
TEETypeSGX = "sgx"
31+
TEETypeTDX = "tdx"
32+
)
33+
34+
// Manifest is the ROFL app manifest that configures various aspects of the app in a single place.
35+
type Manifest struct {
36+
// AppID is the Bech32-encoded ROFL app ID.
37+
AppID string `yaml:"app_id" json:"app_id"`
38+
// Name is the human readable ROFL app name.
39+
Name string `yaml:"name" json:"name"`
40+
// Version is the ROFL app version.
41+
Version string `yaml:"version" json:"version"`
42+
// Network is the identifier of the network to deploy to by default.
43+
Network string `yaml:"network,omitempty" json:"network,omitempty"`
44+
// ParaTime is the identifier of the paratime to deploy to by default.
45+
ParaTime string `yaml:"paratime,omitempty" json:"paratime,omitempty"`
46+
// TEE is the type of TEE to build for.
47+
TEE string `yaml:"tee" json:"tee"`
48+
// Kind is the kind of ROFL app to build.
49+
Kind string `yaml:"kind" json:"kind"`
50+
// TrustRoot is the optional trust root configuration.
51+
TrustRoot *TrustRootConfig `yaml:"trust_root,omitempty" json:"trust_root,omitempty"`
52+
// Resources are the requested ROFL app resources.
53+
Resources ResourcesConfig `yaml:"resources" json:"resources"`
54+
// Artifacts are the optional artifact location overrides.
55+
Artifacts *ArtifactsConfig `yaml:"artifacts,omitempty" json:"artifacts,omitempty"`
56+
57+
// Policy is the ROFL app policy to deploy by default.
58+
Policy *rofl.AppAuthPolicy `yaml:"policy,omitempty" json:"policy,omitempty"`
59+
// Admin is the identifier of the admin account.
60+
Admin string `yaml:"admin,omitempty" json:"admin,omitempty"`
61+
}
62+
63+
// LoadManifest attempts to find and load the ROFL app manifest from a local file.
64+
func LoadManifest() (*Manifest, error) {
65+
for _, fn := range ManifestFileNames {
66+
f, err := os.Open(fn)
67+
switch {
68+
case err == nil:
69+
case errors.Is(err, os.ErrNotExist):
70+
continue
71+
default:
72+
return nil, fmt.Errorf("failed to load manifest from '%s': %w", fn, err)
73+
}
74+
75+
var m Manifest
76+
dec := yaml.NewDecoder(f)
77+
if err = dec.Decode(&m); err != nil {
78+
f.Close()
79+
return nil, fmt.Errorf("malformed manifest '%s': %w", fn, err)
80+
}
81+
if err = m.Validate(); err != nil {
82+
f.Close()
83+
return nil, fmt.Errorf("invalid manifest '%s': %w", fn, err)
84+
}
85+
86+
f.Close()
87+
return &m, nil
88+
}
89+
return nil, fmt.Errorf("no ROFL app manifest found (tried: %s)", strings.Join(ManifestFileNames, ", "))
90+
}
91+
92+
// Validate validates the manifest for correctness.
93+
func (m *Manifest) Validate() error {
94+
if len(m.AppID) == 0 {
95+
return fmt.Errorf("app ID cannot be empty")
96+
}
97+
var appID rofl.AppID
98+
if err := appID.UnmarshalText([]byte(m.AppID)); err != nil {
99+
return fmt.Errorf("malformed app ID: %w", err)
100+
}
101+
102+
if len(m.Name) == 0 {
103+
return fmt.Errorf("name cannot be empty")
104+
}
105+
106+
if len(m.Version) == 0 {
107+
return fmt.Errorf("version cannot be empty")
108+
}
109+
if _, err := version.FromString(m.Version); err != nil {
110+
return fmt.Errorf("malformed version: %w", err)
111+
}
112+
113+
switch m.TEE {
114+
case TEETypeSGX, TEETypeTDX:
115+
default:
116+
return fmt.Errorf("unsupported TEE type: %s", m.TEE)
117+
}
118+
119+
switch m.Kind {
120+
case AppKindRaw:
121+
case AppKindContainer:
122+
if m.TEE != TEETypeTDX {
123+
return fmt.Errorf("containers are only supported under TDX")
124+
}
125+
default:
126+
return fmt.Errorf("unsupported app kind: %s", m.Kind)
127+
}
128+
129+
if err := m.Resources.Validate(); err != nil {
130+
return fmt.Errorf("bad resources config: %w", err)
131+
}
132+
133+
return nil
134+
}
135+
136+
// TrustRootConfig is the trust root configuration.
137+
type TrustRootConfig struct {
138+
// Height is the consensus layer block height where to take the trust root.
139+
Height uint64 `yaml:"height,omitempty" json:"height,omitempty"`
140+
// Hash is the consensus layer block header hash corresponding to the passed height.
141+
Hash string `yaml:"hash,omitempty" json:"hash,omitempty"`
142+
}
143+
144+
// ResourcesConfig is the resources configuration.
145+
type ResourcesConfig struct {
146+
// Memory is the amount of memory needed by the app in megabytes.
147+
Memory uint64 `yaml:"memory" json:"memory"`
148+
// CpuCount is the number of vCPUs needed by the app.
149+
CpuCount uint8 `yaml:"cpus" json:"cpus"`
150+
// EphemeralStorage is the ephemeral storage configuration.
151+
EphemeralStorage *EphemeralStorageConfig `yaml:"ephemeral_storage,omitempty" json:"ephemeral_storage,omitempty"`
152+
}
153+
154+
// Validate validates the resources configuration for correctness.
155+
func (r *ResourcesConfig) Validate() error {
156+
if r.Memory < 16 {
157+
return fmt.Errorf("memory size must be at least 16M")
158+
}
159+
if r.CpuCount < 1 {
160+
return fmt.Errorf("vCPU count must be at least 1")
161+
}
162+
if r.EphemeralStorage != nil {
163+
err := r.EphemeralStorage.Validate()
164+
if err != nil {
165+
return fmt.Errorf("bad ephemeral storage config: %w", err)
166+
}
167+
}
168+
return nil
169+
}
170+
171+
// Supported ephemeral storage kinds.
172+
const (
173+
EphemeralStorageKindNone = "none"
174+
EphemeralStorageKindDisk = "disk"
175+
EphemeralStorageKindRAM = "ram"
176+
)
177+
178+
// EphemeralStorageConfig is the ephemeral storage configuration.
179+
type EphemeralStorageConfig struct {
180+
// Kind is the storage kind.
181+
Kind string `yaml:"kind" json:"kind"`
182+
// Size is the amount of ephemeral storage in megabytes.
183+
Size uint64 `yaml:"size" json:"size"`
184+
}
185+
186+
// Validate validates the ephemeral storage configuration for correctness.
187+
func (e *EphemeralStorageConfig) Validate() error {
188+
switch e.Kind {
189+
case EphemeralStorageKindNone, EphemeralStorageKindDisk, EphemeralStorageKindRAM:
190+
default:
191+
return fmt.Errorf("unsupported ephemeral storage kind: %s", e.Kind)
192+
}
193+
194+
if e.Size < 16 {
195+
return fmt.Errorf("ephemeral storage size must be at least 16M")
196+
}
197+
return nil
198+
}
199+
200+
// ArtifactsConfig is the artifact location override configuration.
201+
type ArtifactsConfig struct {
202+
// Firmware is the URI/path to the firmware artifact (empty to use default).
203+
Firmware string `yaml:"firmware,omitempty" json:"firmware,omitempty"`
204+
// Kernel is the URI/path to the kernel artifact (empty to use default).
205+
Kernel string `yaml:"kernel,omitempty" json:"kernel,omitempty"`
206+
// Stage2 is the URI/path to the stage 2 disk artifact (empty to use default).
207+
Stage2 string `yaml:"stage2,omitempty" json:"stage2,omitempty"`
208+
// Container is the container artifacts configuration.
209+
Container ContainerArtifactsConfig `yaml:"container,omitempty" json:"container,omitempty"`
210+
}
211+
212+
// ContainerArtifactsConfig is the container artifacts configuration.
213+
type ContainerArtifactsConfig struct {
214+
// Runtime is the URI/path to the container runtime artifact (empty to use default).
215+
Runtime string `yaml:"runtime,omitempty" json:"runtime,omitempty"`
216+
// Compose is the URI/path to the docker-compose.yaml artifact (empty to use default).
217+
Compose string `yaml:"compose,omitempty" json:"compose,omitempty"`
218+
}

build/rofl/manifest_test.go

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package rofl
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
"gopkg.in/yaml.v3"
10+
)
11+
12+
func TestManifestValidation(t *testing.T) {
13+
require := require.New(t)
14+
15+
// Empty manifest is not valid.
16+
m := Manifest{}
17+
err := m.Validate()
18+
require.ErrorContains(err, "app ID cannot be empty")
19+
20+
// Invalid app ID.
21+
m.AppID = "foo"
22+
err = m.Validate()
23+
require.ErrorContains(err, "malformed app ID")
24+
25+
// Empty name.
26+
m.AppID = "rofl1qpa9ydy3qmka3yrqzx0pxuvyfexf9mlh75hker5j"
27+
err = m.Validate()
28+
require.ErrorContains(err, "name cannot be empty")
29+
30+
// Empty version.
31+
m.Name = "my-simple-app"
32+
err = m.Validate()
33+
require.ErrorContains(err, "version cannot be empty")
34+
35+
// Invalid version.
36+
m.Version = "foo"
37+
err = m.Validate()
38+
require.ErrorContains(err, "malformed version")
39+
40+
// Unsupported TEE type.
41+
m.Version = "0.1.0"
42+
err = m.Validate()
43+
require.ErrorContains(err, "unsupported TEE type")
44+
45+
// Unsupported app kind.
46+
m.TEE = "sgx"
47+
err = m.Validate()
48+
require.ErrorContains(err, "unsupported app kind")
49+
50+
// Containers are only supported under TDX.
51+
m.Kind = "container"
52+
err = m.Validate()
53+
require.ErrorContains(err, "containers are only supported under TDX")
54+
55+
// Bad resources configuration.
56+
m.TEE = "tdx"
57+
err = m.Validate()
58+
require.ErrorContains(err, "bad resources config: memory size must be at least 16M")
59+
60+
m.Resources.Memory = 16
61+
err = m.Validate()
62+
require.ErrorContains(err, "bad resources config: vCPU count must be at least 1")
63+
64+
// Finally, everything is valid.
65+
m.Resources.CpuCount = 1
66+
err = m.Validate()
67+
require.NoError(err)
68+
69+
// Add ephemeral storage configuration.
70+
m.Resources.EphemeralStorage = &EphemeralStorageConfig{}
71+
err = m.Validate()
72+
require.ErrorContains(err, "bad resources config: bad ephemeral storage config: unsupported ephemeral storage kind")
73+
74+
m.Resources.EphemeralStorage.Kind = "ram"
75+
err = m.Validate()
76+
require.ErrorContains(err, "bad resources config: bad ephemeral storage config: ephemeral storage size must be at least 16M")
77+
78+
m.Resources.EphemeralStorage.Size = 16
79+
err = m.Validate()
80+
require.NoError(err)
81+
}
82+
83+
const serializedYamlManifest = `
84+
app_id: rofl1qpa9ydy3qmka3yrqzx0pxuvyfexf9mlh75hker5j
85+
name: my-simple-app
86+
version: 0.1.0
87+
tee: tdx
88+
kind: container
89+
resources:
90+
memory: 16
91+
cpus: 1
92+
ephemeral_storage:
93+
kind: ram
94+
size: 16
95+
`
96+
97+
func TestManifestSerialization(t *testing.T) {
98+
require := require.New(t)
99+
100+
var m Manifest
101+
err := yaml.Unmarshal([]byte(serializedYamlManifest), &m)
102+
require.NoError(err, "yaml.Unmarshal")
103+
err = m.Validate()
104+
require.NoError(err, "m.Validate")
105+
require.Equal("rofl1qpa9ydy3qmka3yrqzx0pxuvyfexf9mlh75hker5j", m.AppID)
106+
require.Equal("my-simple-app", m.Name)
107+
require.Equal("0.1.0", m.Version)
108+
require.Equal("tdx", m.TEE)
109+
require.Equal("container", m.Kind)
110+
require.EqualValues(16, m.Resources.Memory)
111+
require.EqualValues(1, m.Resources.CpuCount)
112+
require.NotNil(m.Resources.EphemeralStorage)
113+
require.Equal("ram", m.Resources.EphemeralStorage.Kind)
114+
require.EqualValues(16, m.Resources.EphemeralStorage.Size)
115+
116+
enc, err := yaml.Marshal(m)
117+
require.NoError(err, "yaml.Marshal")
118+
119+
var dec Manifest
120+
err = yaml.Unmarshal(enc, &dec)
121+
require.NoError(err, "yaml.Unmarshal(round-trip)")
122+
require.EqualValues(m, dec, "serialization should round-trip")
123+
err = dec.Validate()
124+
require.NoError(err, "dec.Validate")
125+
}
126+
127+
func TestLoadManifest(t *testing.T) {
128+
require := require.New(t)
129+
130+
tmpDir, err := os.MkdirTemp("", "oasis-test-load-manifest")
131+
require.NoError(err)
132+
defer os.RemoveAll(tmpDir)
133+
134+
err = os.Chdir(tmpDir)
135+
require.NoError(err)
136+
137+
_, err = LoadManifest()
138+
require.ErrorContains(err, "no ROFL app manifest found")
139+
140+
manifestFn := filepath.Join(tmpDir, "rofl.yml")
141+
err = os.WriteFile(manifestFn, []byte("foo"), 0o644)
142+
require.NoError(err)
143+
_, err = LoadManifest()
144+
require.ErrorContains(err, "malformed manifest 'rofl.yml'")
145+
146+
err = os.WriteFile(manifestFn, []byte(serializedYamlManifest), 0o644)
147+
require.NoError(err)
148+
m, err := LoadManifest()
149+
require.NoError(err)
150+
require.Equal("rofl1qpa9ydy3qmka3yrqzx0pxuvyfexf9mlh75hker5j", m.AppID)
151+
152+
err = os.Remove(manifestFn)
153+
require.NoError(err)
154+
155+
manifestFn = "rofl.yaml"
156+
err = os.WriteFile(manifestFn, []byte(serializedYamlManifest), 0o644)
157+
require.NoError(err)
158+
m, err = LoadManifest()
159+
require.NoError(err)
160+
require.Equal("rofl1qpa9ydy3qmka3yrqzx0pxuvyfexf9mlh75hker5j", m.AppID)
161+
}

0 commit comments

Comments
 (0)