Skip to content
Open
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
51 changes: 49 additions & 2 deletions build/rofl/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,14 +180,21 @@ func (m *Manifest) Validate() error {
return fmt.Errorf("bad resources config: %w", err)
}

var defaultNames []string
for name, d := range m.Deployments {
if d == nil {
return fmt.Errorf("bad deployment: %s", name)
}
if d.Default {
defaultNames = append(defaultNames, name)
}
if err := d.Validate(); err != nil {
return fmt.Errorf("bad deployment '%s': %w", name, err)
}
}
if len(defaultNames) > 1 {
return fmt.Errorf("multiple deployments marked as default: %s", strings.Join(defaultNames, ", "))
}

return nil
}
Expand Down Expand Up @@ -250,16 +257,56 @@ func (m *Manifest) Save() error {
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.
// DefaultDeploymentName is the legacy name of the default deployment. It is used as a fallback
// when no deployment is explicitly marked as default.
const DefaultDeploymentName = "default"

// DefaultDeployment returns the name of the default deployment. Resolution order:
// 1. Deployment explicitly marked as default (default: true).
// 2. Legacy fallback: deployment named "default".
// 3. If exactly one deployment exists, use it.
//
// Returns an empty string if no default can be determined.
func (m *Manifest) DefaultDeployment() string {
for name, d := range m.Deployments {
if d != nil && d.Default {
return name
}
}
if _, ok := m.Deployments[DefaultDeploymentName]; ok {
return DefaultDeploymentName
}
if len(m.Deployments) == 1 {
for name := range m.Deployments {
return name
}
}
return ""
}

// SetDefaultDeployment sets the given deployment as the default, clearing the flag from all others.
func (m *Manifest) SetDefaultDeployment(name string) error {
d := m.Deployments[name]
if d == nil {
return fmt.Errorf("deployment '%s' does not exist", name)
}
for _, other := range m.Deployments {
if other != nil {
other.Default = false
}
}
d.Default = true
return nil
}

// DefaultMachineName is the name of the default machine into which the app is deployed when no
// specific machine is passed.
const DefaultMachineName = "default"

// Deployment describes a single ROFL app deployment.
type Deployment struct {
// Default indicates whether this is the default deployment.
Default bool `yaml:"default,omitempty" json:"default,omitempty"`
// 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.
Expand Down
74 changes: 74 additions & 0 deletions build/rofl/manifest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,24 @@ func TestManifestValidation(t *testing.T) {
err = m.Validate()
require.NoError(err)

// Multiple defaults should be rejected.
m.Deployments["default"].Default = true
m.Deployments["testnet"] = &Deployment{
Default: true,
Network: "testnet",
ParaTime: "sapphire",
}
err = m.Validate()
require.ErrorContains(err, "multiple deployments marked as default")

// Single default is fine.
m.Deployments["default"].Default = false
err = m.Validate()
require.NoError(err)

// Clean up for subsequent tests.
delete(m.Deployments, "testnet")

// Add ephemeral storage configuration.
m.Resources.Storage = &StorageConfig{}
err = m.Validate()
Expand Down Expand Up @@ -297,3 +315,59 @@ func TestUpgradePossible(t *testing.T) {
require.False((&ContainerArtifactsConfig{Runtime: "new"}).UpgradePossible(&containerLatest))
require.False((&ContainerArtifactsConfig{}).UpgradePossible(&containerLatest))
}

func TestDefaultDeployment(t *testing.T) {
require := require.New(t)

m := Manifest{}

// No deployments -> empty.
require.Empty(m.DefaultDeployment())

// Single deployment -> use it regardless of name.
m.Deployments = map[string]*Deployment{
"testnet": {Network: "testnet", ParaTime: "sapphire"},
}
require.Equal("testnet", m.DefaultDeployment())

// Legacy "default" key is preferred over arbitrary single deployment when multiple exist.
m.Deployments["default"] = &Deployment{Network: "mainnet", ParaTime: "sapphire"}
require.Equal("default", m.DefaultDeployment())

// Explicit default flag takes precedence.
m.Deployments["testnet"].Default = true
require.Equal("testnet", m.DefaultDeployment())

// With only the "default" key and no flag, fallback works.
delete(m.Deployments, "testnet")
require.Equal("default", m.DefaultDeployment())

// Multiple deployments, none marked default, no "default" key -> empty.
m.Deployments = map[string]*Deployment{
"testnet": {Network: "testnet", ParaTime: "sapphire"},
"mainnet": {Network: "mainnet", ParaTime: "sapphire"},
}
require.Empty(m.DefaultDeployment())
}

func TestSetDefaultDeployment(t *testing.T) {
require := require.New(t)

m := Manifest{
Deployments: map[string]*Deployment{
"testnet": {Default: true, Network: "testnet", ParaTime: "sapphire"},
"mainnet": {Network: "mainnet", ParaTime: "sapphire"},
},
}

// Setting non-existent deployment fails.
err := m.SetDefaultDeployment("staging")
require.ErrorContains(err, "deployment 'staging' does not exist")

// Setting mainnet as default clears testnet.
err = m.SetDefaultDeployment("mainnet")
require.NoError(err)
require.False(m.Deployments["testnet"].Default)
require.True(m.Deployments["mainnet"].Default)
require.Equal("mainnet", m.DefaultDeployment())
}
4 changes: 1 addition & 3 deletions cmd/rofl/common/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package common

import (
flag "github.com/spf13/pflag"

buildRofl "github.com/oasisprotocol/cli/build/rofl"
)

var (
Expand Down Expand Up @@ -46,7 +44,7 @@ func init() {
WipeFlags.BoolVar(&WipeStorage, "wipe-storage", false, "whether to wipe machine storage")

DeploymentFlags = flag.NewFlagSet("", flag.ContinueOnError)
DeploymentFlags.StringVar(&DeploymentName, "deployment", buildRofl.DefaultDeploymentName, "deployment name")
DeploymentFlags.StringVar(&DeploymentName, "deployment", "", "deployment name")

NoUpdateFlag = flag.NewFlagSet("", flag.ContinueOnError)
NoUpdateFlag.BoolVar(&NoUpdate, "no-update-manifest", false, "do not update the manifest")
Expand Down
49 changes: 35 additions & 14 deletions cmd/rofl/common/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ func LoadManifestAndSetNPA(opts *ManifestOptions) (*rofl.Manifest, *rofl.Deploym
cfg := cliConfig.Global()
npa := common.GetNPASelection(cfg)

manifest, d, err := MaybeLoadManifestAndSetNPA(cfg, npa, DeploymentName, opts)
manifest, resolvedName, d, err := MaybeLoadManifestAndSetNPA(cfg, npa, DeploymentName, opts)
cobra.CheckErr(err)
DeploymentName = resolvedName
if opts != nil && opts.NeedAppID && !d.HasAppID() {
cobra.CheckErr(fmt.Errorf("deployment '%s' does not have an app ID set, maybe you need to run `oasis rofl create`", DeploymentName))
}
Expand All @@ -51,34 +52,44 @@ func LoadManifestAndSetNPA(opts *ManifestOptions) (*rofl.Manifest, *rofl.Deploym
// MaybeLoadManifestAndSetNPA loads the ROFL app manifest and reconfigures the
// network/paratime/account selection.
//
// In case there is an error in loading the manifest, it is returned.
func MaybeLoadManifestAndSetNPA(cfg *cliConfig.Config, npa *common.NPASelection, deployment string, opts *ManifestOptions) (*rofl.Manifest, *rofl.Deployment, error) {
// In case there is an error in loading the manifest, it is returned. The resolved deployment name
// is always returned alongside the deployment.
func MaybeLoadManifestAndSetNPA(cfg *cliConfig.Config, npa *common.NPASelection, deployment string, opts *ManifestOptions) (*rofl.Manifest, string, *rofl.Deployment, error) {
manifest, err := rofl.LoadManifest()
if err != nil {
return nil, nil, err
return nil, "", nil, err
}

// Warn if manifest was created with an older CLI version.
checkToolingVersion(manifest)

// Resolve deployment name when not explicitly provided.
if deployment == "" {
deployment = manifest.DefaultDeployment()
}
if deployment == "" {
if len(manifest.Deployments) == 0 {
return nil, "", nil, fmt.Errorf("no deployments configured\nHint: use `oasis rofl create` to register a new ROFL app and create a deployment")
}
printAvailableDeployments(manifest)
return nil, "", nil, fmt.Errorf("no default deployment configured\nHint: use `oasis rofl set-default <name>` to set a default deployment")
}

d, ok := manifest.Deployments[deployment]
if !ok {
fmt.Println("The following deployments are configured in the app manifest:")
for name := range manifest.Deployments {
fmt.Printf(" - %s\n", name)
}
return nil, nil, fmt.Errorf("deployment '%s' does not exist", deployment)
printAvailableDeployments(manifest)
return nil, "", nil, fmt.Errorf("deployment '%s' does not exist", deployment)
}

switch d.Network {
case "":
if npa.Network == nil {
return nil, nil, fmt.Errorf("no network selected")
return nil, "", nil, fmt.Errorf("no network selected")
}
default:
npa.Network = cfg.Networks.All[d.Network]
if npa.Network == nil {
return nil, nil, fmt.Errorf("network '%s' does not exist", d.Network)
return nil, "", nil, fmt.Errorf("network '%s' does not exist", d.Network)
}
npa.NetworkName = d.Network
}
Expand All @@ -88,7 +99,7 @@ func MaybeLoadManifestAndSetNPA(cfg *cliConfig.Config, npa *common.NPASelection,
default:
npa.ParaTime = npa.Network.ParaTimes.All[d.ParaTime]
if npa.ParaTime == nil {
return nil, nil, fmt.Errorf("paratime '%s' does not exist", d.ParaTime)
return nil, "", nil, fmt.Errorf("paratime '%s' does not exist", d.ParaTime)
}
npa.ParaTimeName = d.ParaTime
}
Expand All @@ -104,12 +115,22 @@ func MaybeLoadManifestAndSetNPA(cfg *cliConfig.Config, npa *common.NPASelection,
npa.Account = accCfg
npa.AccountName = d.Admin
case opts != nil && opts.NeedAdmin:
return nil, nil, err
return nil, "", nil, err
default:
// Admin account is not valid, but it is also not required, so do not override.
}
}
return manifest, d, nil
return manifest, deployment, d, nil
}

func printAvailableDeployments(manifest *rofl.Manifest) {
if len(manifest.Deployments) == 0 {
return
}
fmt.Println("The following deployments are configured in the app manifest:")
for name := range manifest.Deployments {
fmt.Printf(" - %s\n", name)
}
}

// GetOrcFilename generates a filename based on the project name and deployment.
Expand Down
27 changes: 22 additions & 5 deletions cmd/rofl/mgmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,22 +211,34 @@ var (
manifest, err := buildRofl.LoadManifest()
cobra.CheckErr(err)

// Resolve deployment name: explicit flag > network name for new deployments.
deploymentName := roflCommon.DeploymentName
if deploymentName == "" {
deploymentName = npa.NetworkName
}

// Load or create a deployment.
deployment, ok := manifest.Deployments[roflCommon.DeploymentName]
deployment, ok := manifest.Deployments[deploymentName]
switch ok {
case true:
if deployment.AppID != "" {
cobra.CheckErr(fmt.Errorf("ROFL app identifier already defined (%s) for deployment '%s', refusing to overwrite", deployment.AppID, roflCommon.DeploymentName))
cobra.CheckErr(fmt.Errorf("ROFL app identifier already defined (%s) for deployment '%s', refusing to overwrite", deployment.AppID, deploymentName))
}

// An existing deployment is defined, but without an AppID. Load everything else for
// the deployment and proceed with creating a new app.
roflCommon.DeploymentName = deploymentName
manifest, deployment, npa = roflCommon.LoadManifestAndSetNPA(&roflCommon.ManifestOptions{
NeedAppID: false,
NeedAdmin: true,
})

// Mark as default if no default is currently set (must be after reload).
if manifest.DefaultDeployment() == "" {
deployment.Default = true
}
case false:
// No deployment defined, create a new default one.
// No deployment defined, create a new one named after the network.
npa.MustHaveAccount()
npa.MustHaveParaTime()
if txCfg.Offline {
Expand Down Expand Up @@ -283,10 +295,15 @@ var (
Hash: blk.Hash.Hex(),
},
}
// Mark as default if this is the first deployment.
if len(manifest.Deployments) == 0 {
deployment.Default = true
}
if manifest.Deployments == nil {
manifest.Deployments = make(map[string]*buildRofl.Deployment)
}
manifest.Deployments[roflCommon.DeploymentName] = deployment
manifest.Deployments[deploymentName] = deployment
roflCommon.DeploymentName = deploymentName
}

idScheme, ok := identifierSchemes[scheme]
Expand All @@ -298,7 +315,7 @@ var (
tx := rofl.NewCreateTx(nil, &rofl.Create{
Policy: *deployment.Policy.AsDescriptor(),
Scheme: idScheme,
Metadata: manifest.GetMetadata(roflCommon.DeploymentName),
Metadata: manifest.GetMetadata(deploymentName),
})

acc := common.LoadAccount(cfg, npa.AccountName)
Expand Down
1 change: 1 addition & 0 deletions cmd/rofl/rofl.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ func init() {
Cmd.AddCommand(removeCmd)
Cmd.AddCommand(showCmd)
Cmd.AddCommand(listCmd)
Cmd.AddCommand(setDefaultCmd)
Cmd.AddCommand(trustRootCmd)
Cmd.AddCommand(build.Cmd)
Cmd.AddCommand(identityCmd)
Expand Down
29 changes: 29 additions & 0 deletions cmd/rofl/set_default.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package rofl

import (
"fmt"

"github.com/spf13/cobra"

buildRofl "github.com/oasisprotocol/cli/build/rofl"
)

var setDefaultCmd = &cobra.Command{
Use: "set-default <deployment>",
Short: "Sets the given deployment as the default deployment",
Args: cobra.ExactArgs(1),
Run: func(_ *cobra.Command, args []string) {
name := args[0]

manifest, err := buildRofl.LoadManifest()
cobra.CheckErr(err)

err = manifest.SetDefaultDeployment(name)
cobra.CheckErr(err)

err = manifest.Save()
cobra.CheckErr(err)

fmt.Printf("Set '%s' as the default deployment.\n", name)
},
}
Loading