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

Merged
merged 20 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
98a2b65
feat(cmd/rofl): Add TDX container build support
kostko Dec 12, 2024
56bf57c
feat(cmd/rofl): Unify build command and always use manifest
kostko Jan 8, 2025
26199ac
feat(cmd/rofl): Update to oasis-boot v0.3.0
kostko Jan 8, 2025
11302d6
feat(cmd/rofl): Use cached artifacts when available
kostko Jan 8, 2025
aaaca2d
Bump Go Client SDK to v0.12.1
kostko Jan 11, 2025
053ba76
feat(cmd/rofl): Add rofl init subcommand for easier boostrap
kostko Jan 13, 2025
e336473
feat(cmd/rofl): Support --update-manifest flag for build
kostko Jan 13, 2025
fb5a404
Rename ephemeral storage to just storage
kostko Jan 14, 2025
02edf7f
feat(cmd/rofl): Add support for multiple deployments
kostko Jan 14, 2025
aaf57f0
feat(cmd/rofl): Add support for build scripts
kostko Jan 14, 2025
30992cb
feat(cmd/rofl): Add support for persistent storage
kostko Jan 16, 2025
819e8f1
feat(cmd/rofl): Separate init from create
kostko Jan 16, 2025
ae8920c
feat(cmd/rofl): Verify integrity of cached artifacts
kostko Jan 16, 2025
f8934ff
feat(cmd/rofl): Add bundle-post script
kostko Jan 16, 2025
8fc716b
feat(cmd/rofl): Pad rootfs partition to allow for growth during upgrades
kostko Jan 17, 2025
1fd79c1
feat(cmd/rofl): Add support for secret management
kostko Jan 21, 2025
4c8eb0a
feat(cmd/rofl): Move debug flag to manifest
kostko Jan 21, 2025
3cf278c
feat(cmd/rofl): Fix init to create app directory if needed
kostko Jan 21, 2025
753c930
feat(cmd/rofl): Reduce default manifest indentation
kostko Jan 22, 2025
64231c6
feat(cmd/rofl): Create an empty compose.yaml on init
kostko Jan 22, 2025
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
322 changes: 322 additions & 0 deletions build/rofl/manifest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,322 @@
package rofl

import (
"errors"
"fmt"
"os"
"path/filepath"
"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.yaml",
"rofl.yml",
}

// Supported ROFL app kinds.
const (
AppKindRaw = "raw"
AppKindContainer = "container"
)

// Supported TEE types.
const (
TEETypeSGX = "sgx"
TEETypeTDX = "tdx"
)

// Well-known scripts.
const (
ScriptBuildPre = "build-pre"
ScriptBuildPost = "build-post"
ScriptBundlePost = "bundle-post"
)

// Manifest is the ROFL app manifest that configures various aspects of the app in a single place.
type Manifest struct {
// 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"`
// 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"`
// 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"`

// Deployments are the ROFL app deployments.
Deployments map[string]*Deployment `yaml:"deployments" json:"deployments"`

// Scripts are custom scripts that are executed by the build system at specific stages.
Scripts map[string]string `yaml:"scripts,omitempty" json:"scripts,omitempty"`

// sourceFn is the filename from which the manifest has been loaded.
sourceFn string
}

// ManifestExists checks whether a manifest file exist. No attempt is made to load, parse or
// validate any of the found manifest files.
func ManifestExists() bool {
for _, fn := range ManifestFileNames {
_, err := os.Stat(fn)
switch {
case errors.Is(err, os.ErrNotExist):
continue
default:
return true
}
}
return false
}

// 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)
}
m.sourceFn, _ = filepath.Abs(f.Name()) // Record source filename.

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

for name, d := range m.Deployments {
if d == nil {
return fmt.Errorf("bad deployment: %s", name)
}
if err := d.Validate(); err != nil {
return fmt.Errorf("bad deployment '%s': %w", name, err)
}
}
if _, ok := m.Deployments[DefaultDeploymentName]; !ok {
return fmt.Errorf("must define at least the '%s' deployment", DefaultDeploymentName)
}

return nil
}

// SourceFileName returns the filename of the manifest file from which the manifest was loaded or
// an empty string in case the filename is not available.
func (m *Manifest) SourceFileName() string {
return m.sourceFn
}

// Save serializes the manifest and writes it to the file returned by `SourceFileName`, overwriting
// any previous manifest.
//
// If no previous source filename is available, a default one is set.
func (m *Manifest) Save() error {
if m.sourceFn == "" {
m.sourceFn = ManifestFileNames[0]
}

f, err := os.Create(m.sourceFn)
if err != nil {
return err
}
defer f.Close()

enc := yaml.NewEncoder(f)
enc.SetIndent(2)
return enc.Encode(m)
}

// DefaultDeploymentName is the name of the default deployment that must always be defined and is
// used in case no deployment is passed.
const DefaultDeploymentName = "default"

// Deployment describes a single ROFL app deployment.
type Deployment struct {
// AppID is the Bech32-encoded ROFL app ID.
AppID string `yaml:"app_id,omitempty" json:"app_id,omitempty"`
// Network is the identifier of the network to deploy to.
Network string `yaml:"network" json:"network"`
// ParaTime is the identifier of the paratime to deploy to.
ParaTime string `yaml:"paratime" json:"paratime"`
// Admin is the identifier of the admin account.
Admin string `yaml:"admin,omitempty" json:"admin,omitempty"`
// Debug is a flag denoting whether this is a debuggable deployment.
Debug bool `yaml:"debug,omitempty" json:"debug,omitempty"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If people will be copying this for the Localnet, the debug should be visible in rofl.yaml, so they don't forget to change it.

Suggested change
Debug bool `yaml:"debug,omitempty" json:"debug,omitempty"`
Debug bool `yaml:"debug" json:"debug,omitempty"`

// TrustRoot is the optional trust root configuration.
TrustRoot *TrustRootConfig `yaml:"trust_root,omitempty" json:"trust_root,omitempty"`
// Policy is the ROFL app policy.
Policy *rofl.AppAuthPolicy `yaml:"policy,omitempty" json:"policy,omitempty"`
// Metadata contains custom metadata.
Metadata map[string]string `yaml:"metadata,omitempty" json:"metadata,omitempty"`
// Secrets contains encrypted secrets.
Secrets []*SecretConfig `yaml:"secrets,omitempty" json:"secrets,omitempty"`
}

// Validate validates the manifest for correctness.
func (d *Deployment) Validate() error {
if len(d.AppID) > 0 {
var appID rofl.AppID
if err := appID.UnmarshalText([]byte(d.AppID)); err != nil {
return fmt.Errorf("malformed app ID: %w", err)
}
}
if d.Network == "" {
return fmt.Errorf("network cannot be empty")
}
if d.ParaTime == "" {
return fmt.Errorf("paratime cannot be empty")
}
for _, s := range d.Secrets {
if err := s.Validate(); err != nil {
return fmt.Errorf("bad secret: %w", err)
}
}
return nil
}

// HasAppID returns true iff the deployment has an application identifier set.
func (d *Deployment) HasAppID() bool {
return len(d.AppID) > 0
}

// 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"`
// Storage is the storage configuration.
Storage *StorageConfig `yaml:"storage,omitempty" json:"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.Storage != nil {
err := r.Storage.Validate()
if err != nil {
return fmt.Errorf("bad storage config: %w", err)
}
}
return nil
}

// Supported storage kinds.
const (
StorageKindNone = "none"
StorageKindDiskEphemeral = "disk-ephemeral"
StorageKindDiskPersistent = "disk-persistent"
StorageKindRAM = "ram"
)

// StorageConfig is the storage configuration.
type StorageConfig struct {
// Kind is the storage kind.
Kind string `yaml:"kind" json:"kind"`
// Size is the amount of storage in megabytes.
Size uint64 `yaml:"size" json:"size"`
}

// Validate validates the storage configuration for correctness.
func (e *StorageConfig) Validate() error {
switch e.Kind {
case StorageKindNone, StorageKindDiskEphemeral, StorageKindDiskPersistent, StorageKindRAM:
default:
return fmt.Errorf("unsupported storage kind: %s", e.Kind)
}

if e.Size < 16 {
return fmt.Errorf("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"`
}
Loading
Loading