Skip to content

Commit

Permalink
feat(cmd/rofl): Add support for multiple deployments
Browse files Browse the repository at this point in the history
  • Loading branch information
kostko committed Jan 14, 2025
1 parent e7a0f48 commit 1c065ee
Show file tree
Hide file tree
Showing 8 changed files with 228 additions and 120 deletions.
72 changes: 52 additions & 20 deletions build/rofl/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,31 +34,21 @@ const (

// 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"`
// Admin is the identifier of the admin account.
Admin string `yaml:"admin,omitempty" json:"admin,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"`
// Deployments are the ROFL app deployments.
Deployments map[string]*Deployment `yaml:"deployments" json:"deployments"`

// sourceFn is the filename from which the manifest has been loaded.
sourceFn string
Expand Down Expand Up @@ -111,14 +101,6 @@ func LoadManifest() (*Manifest, error) {

// 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")
}
Expand Down Expand Up @@ -150,6 +132,18 @@ func (m *Manifest) Validate() error {
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
}

Expand All @@ -159,6 +153,44 @@ func (m *Manifest) SourceFileName() string {
return m.sourceFn
}

// 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" json:"app_id"`
// 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"`
// 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"`
}

// Validate validates the manifest for correctness.
func (d *Deployment) Validate() error {
if len(d.AppID) == 0 {
return fmt.Errorf("app ID cannot be empty")
}
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")
}
return nil
}

// TrustRootConfig is the trust root configuration.
type TrustRootConfig struct {
// Height is the consensus layer block height where to take the trust root.
Expand Down
75 changes: 51 additions & 24 deletions build/rofl/manifest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,6 @@ func TestManifestValidation(t *testing.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.
Expand Down Expand Up @@ -61,37 +51,68 @@ func TestManifestValidation(t *testing.T) {
err = m.Validate()
require.ErrorContains(err, "bad resources config: vCPU count must be at least 1")

// Finally, everything is valid.
// No default deployment.
m.Resources.CPUCount = 1
err = m.Validate()
require.ErrorContains(err, "must define at least the 'default' deployment")

// Missing app ID in deployment.
m.Deployments = map[string]*Deployment{
"default": {},
}
err = m.Validate()
require.ErrorContains(err, "bad deployment 'default': app ID cannot be empty")

// Invalid app ID.
m.Deployments["default"].AppID = "foo"
err = m.Validate()
require.ErrorContains(err, "bad deployment 'default': malformed app ID")

// Missing network in deployment.
m.Deployments["default"].AppID = "rofl1qpa9ydy3qmka3yrqzx0pxuvyfexf9mlh75hker5j"
err = m.Validate()
require.ErrorContains(err, "bad deployment 'default': network cannot be empty")

// Missing paratime in deployment.
m.Deployments["default"].Network = "foo"
err = m.Validate()
require.ErrorContains(err, "bad deployment 'default': paratime cannot be empty")

// Finally, everything is valid.
m.Deployments["default"].ParaTime = "bar"
err = m.Validate()
require.NoError(err)

// Add ephemeral storage configuration.
m.Resources.EphemeralStorage = &EphemeralStorageConfig{}
m.Resources.Storage = &StorageConfig{}
err = m.Validate()
require.ErrorContains(err, "bad resources config: bad ephemeral storage config: unsupported ephemeral storage kind")
require.ErrorContains(err, "bad resources config: bad storage config: unsupported storage kind")

m.Resources.EphemeralStorage.Kind = "ram"
m.Resources.Storage.Kind = "ram"
err = m.Validate()
require.ErrorContains(err, "bad resources config: bad ephemeral storage config: ephemeral storage size must be at least 16M")
require.ErrorContains(err, "bad resources config: bad storage config: storage size must be at least 16M")

m.Resources.EphemeralStorage.Size = 16
m.Resources.Storage.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:
storage:
kind: ram
size: 16
deployments:
default:
app_id: rofl1qpa9ydy3qmka3yrqzx0pxuvyfexf9mlh75hker5j
network: foo
paratime: bar
`

func TestManifestSerialization(t *testing.T) {
Expand All @@ -102,16 +123,18 @@ func TestManifestSerialization(t *testing.T) {
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)
require.NotNil(m.Resources.Storage)
require.Equal("ram", m.Resources.Storage.Kind)
require.EqualValues(16, m.Resources.Storage.Size)
require.Len(m.Deployments, 1)
require.Contains(m.Deployments, "default")
require.Equal("rofl1qpa9ydy3qmka3yrqzx0pxuvyfexf9mlh75hker5j", m.Deployments["default"].AppID)

enc, err := yaml.Marshal(m)
require.NoError(err, "yaml.Marshal")
Expand Down Expand Up @@ -147,7 +170,9 @@ func TestLoadManifest(t *testing.T) {
require.NoError(err)
m, err := LoadManifest()
require.NoError(err)
require.Equal("rofl1qpa9ydy3qmka3yrqzx0pxuvyfexf9mlh75hker5j", m.AppID)
require.Len(m.Deployments, 1)
require.Contains(m.Deployments, "default")
require.Equal("rofl1qpa9ydy3qmka3yrqzx0pxuvyfexf9mlh75hker5j", m.Deployments["default"].AppID)

err = os.Remove(manifestFn)
require.NoError(err)
Expand All @@ -157,5 +182,7 @@ func TestLoadManifest(t *testing.T) {
require.NoError(err)
m, err = LoadManifest()
require.NoError(err)
require.Equal("rofl1qpa9ydy3qmka3yrqzx0pxuvyfexf9mlh75hker5j", m.AppID)
require.Len(m.Deployments, 1)
require.Contains(m.Deployments, "default")
require.Equal("rofl1qpa9ydy3qmka3yrqzx0pxuvyfexf9mlh75hker5j", m.Deployments["default"].AppID)
}
47 changes: 26 additions & 21 deletions cmd/rofl/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@ const (
)

var (
outputFn string
buildMode string
offline bool
doUpdate bool
outputFn string
buildMode string
offline bool
doUpdate bool
deploymentName string

Cmd = &cobra.Command{
Use: "build",
Expand All @@ -44,14 +45,17 @@ var (
Run: func(_ *cobra.Command, _ []string) {
cfg := cliConfig.Global()
npa := common.GetNPASelection(cfg)
manifest := roflCommon.LoadManifestAndSetNPA(cfg, npa)
manifest, deployment := roflCommon.LoadManifestAndSetNPA(cfg, npa, deploymentName)

fmt.Println("Building a ROFL application...")
fmt.Printf("App ID: %s\n", manifest.AppID)
fmt.Printf("Name: %s\n", manifest.Name)
fmt.Printf("Version: %s\n", manifest.Version)
fmt.Printf("TEE: %s\n", manifest.TEE)
fmt.Printf("Kind: %s\n", manifest.Kind)
fmt.Printf("Deployment: %s\n", deploymentName)
fmt.Printf("Network: %s\n", deployment.Network)
fmt.Printf("ParaTime: %s\n", deployment.ParaTime)
fmt.Printf("App ID: %s\n", deployment.AppID)
fmt.Printf("Name: %s\n", manifest.Name)
fmt.Printf("Version: %s\n", manifest.Version)
fmt.Printf("TEE: %s\n", manifest.TEE)
fmt.Printf("Kind: %s\n", manifest.Kind)

// Prepare temporary build directory.
tmpDir, err := os.MkdirTemp("", "oasis-build")
Expand All @@ -62,7 +66,7 @@ var (

bnd := &bundle.Bundle{
Manifest: &bundle.Manifest{
Name: manifest.AppID,
Name: deployment.AppID,
ID: npa.ParaTime.Namespace(),
},
}
Expand All @@ -80,14 +84,14 @@ var (
return
}

sgxBuild(npa, manifest, bnd)
sgxBuild(npa, manifest, deployment, bnd)
case buildRofl.TEETypeTDX:
// TDX.
switch manifest.Kind {
case buildRofl.AppKindRaw:
err = tdxBuildRaw(tmpDir, npa, manifest, bnd)
err = tdxBuildRaw(tmpDir, npa, manifest, deployment, bnd)
case buildRofl.AppKindContainer:
err = tdxBuildContainer(tmpDir, npa, manifest, bnd)
err = tdxBuildContainer(tmpDir, npa, manifest, deployment, bnd)
}
default:
fmt.Printf("unsupported TEE kind: %s\n", manifest.TEE)
Expand All @@ -99,7 +103,7 @@ var (
}

// Write the bundle out.
outFn := fmt.Sprintf("%s.orc", manifest.Name)
outFn := fmt.Sprintf("%s.%s.orc", manifest.Name, deploymentName)
if outputFn != "" {
outFn = outputFn
}
Expand All @@ -119,7 +123,7 @@ var (
}

// Override the update manifest flag in case the policy does not exist.
if manifest.Policy == nil {
if deployment.Policy == nil {
doUpdate = false
}

Expand All @@ -135,9 +139,9 @@ var (
fmt.Println()
case true:
// Update the manifest with the given enclave identities, overwriting existing ones.
manifest.Policy.Enclaves = make([]sgx.EnclaveIdentity, 0, len(eids))
deployment.Policy.Enclaves = make([]sgx.EnclaveIdentity, 0, len(eids))
for _, eid := range eids {
manifest.Policy.Enclaves = append(manifest.Policy.Enclaves, *eid)
deployment.Policy.Enclaves = append(deployment.Policy.Enclaves, *eid)
}

// Serialize manifest and write it to file.
Expand Down Expand Up @@ -173,12 +177,12 @@ func detectBuildMode(npa *common.NPASelection) {
}
}

func setupBuildEnv(manifest *buildRofl.Manifest, npa *common.NPASelection) {
func setupBuildEnv(deployment *buildRofl.Deployment, npa *common.NPASelection) {
// Configure app ID.
os.Setenv("ROFL_APP_ID", manifest.AppID)
os.Setenv("ROFL_APP_ID", deployment.AppID)

// Obtain and configure trust root.
trustRoot, err := fetchTrustRoot(npa, manifest.TrustRoot)
trustRoot, err := fetchTrustRoot(npa, deployment.TrustRoot)
cobra.CheckErr(err)
os.Setenv("ROFL_CONSENSUS_TRUST_ROOT", trustRoot)
}
Expand Down Expand Up @@ -250,6 +254,7 @@ func init() {
buildFlags.BoolVar(&offline, "offline", false, "do not perform any operations requiring network access")
buildFlags.StringVar(&outputFn, "output", "", "output bundle filename")
buildFlags.BoolVar(&doUpdate, "update-manifest", false, "automatically update the manifest")
buildFlags.StringVar(&deploymentName, "deployment", buildRofl.DefaultDeploymentName, "deployment name")

Cmd.Flags().AddFlagSet(buildFlags)
}
12 changes: 9 additions & 3 deletions cmd/rofl/build/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@ const (
)

// tdxBuildContainer builds a TDX-based container ROFL app.
func tdxBuildContainer(tmpDir string, npa *common.NPASelection, manifest *buildRofl.Manifest, bnd *bundle.Bundle) error {
func tdxBuildContainer(
tmpDir string,
npa *common.NPASelection,
manifest *buildRofl.Manifest,
deployment *buildRofl.Deployment,
bnd *bundle.Bundle,
) error {
fmt.Println("Building a container-based TDX ROFL application...")

tdxStage2TemplateURI = defaultContainerStage2TemplateURI
Expand Down Expand Up @@ -54,11 +60,11 @@ func tdxBuildContainer(tmpDir string, npa *common.NPASelection, manifest *buildR
// Configure app ID.
var extraKernelOpts []string
extraKernelOpts = append(extraKernelOpts,
fmt.Sprintf("ROFL_APP_ID=%s", manifest.AppID),
fmt.Sprintf("ROFL_APP_ID=%s", deployment.AppID),
)

// Obtain and configure trust root.
trustRoot, err := fetchTrustRoot(npa, manifest.TrustRoot)
trustRoot, err := fetchTrustRoot(npa, deployment.TrustRoot)
if err != nil {
return err
}
Expand Down
Loading

0 comments on commit 1c065ee

Please sign in to comment.