Skip to content

Commit b1cd477

Browse files
feat: name deployments by network, add set-default
1 parent 983a52c commit b1cd477

File tree

7 files changed

+211
-24
lines changed

7 files changed

+211
-24
lines changed

build/rofl/manifest.go

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,14 +180,21 @@ func (m *Manifest) Validate() error {
180180
return fmt.Errorf("bad resources config: %w", err)
181181
}
182182

183+
var defaultNames []string
183184
for name, d := range m.Deployments {
184185
if d == nil {
185186
return fmt.Errorf("bad deployment: %s", name)
186187
}
188+
if d.Default {
189+
defaultNames = append(defaultNames, name)
190+
}
187191
if err := d.Validate(); err != nil {
188192
return fmt.Errorf("bad deployment '%s': %w", name, err)
189193
}
190194
}
195+
if len(defaultNames) > 1 {
196+
return fmt.Errorf("multiple deployments marked as default: %s", strings.Join(defaultNames, ", "))
197+
}
191198

192199
return nil
193200
}
@@ -250,16 +257,56 @@ func (m *Manifest) Save() error {
250257
return enc.Encode(m)
251258
}
252259

253-
// DefaultDeploymentName is the name of the default deployment that must always be defined and is
254-
// used in case no deployment is passed.
260+
// DefaultDeploymentName is the legacy name of the default deployment. It is used as a fallback
261+
// when no deployment is explicitly marked as default.
255262
const DefaultDeploymentName = "default"
256263

264+
// DefaultDeployment returns the name of the default deployment. Resolution order:
265+
// 1. Deployment explicitly marked as default (default: true).
266+
// 2. Legacy fallback: deployment named "default".
267+
// 3. If exactly one deployment exists, use it.
268+
//
269+
// Returns an empty string if no default can be determined.
270+
func (m *Manifest) DefaultDeployment() string {
271+
for name, d := range m.Deployments {
272+
if d != nil && d.Default {
273+
return name
274+
}
275+
}
276+
if _, ok := m.Deployments[DefaultDeploymentName]; ok {
277+
return DefaultDeploymentName
278+
}
279+
if len(m.Deployments) == 1 {
280+
for name := range m.Deployments {
281+
return name
282+
}
283+
}
284+
return ""
285+
}
286+
287+
// SetDefaultDeployment sets the given deployment as the default, clearing the flag from all others.
288+
func (m *Manifest) SetDefaultDeployment(name string) error {
289+
d := m.Deployments[name]
290+
if d == nil {
291+
return fmt.Errorf("deployment '%s' does not exist", name)
292+
}
293+
for _, other := range m.Deployments {
294+
if other != nil {
295+
other.Default = false
296+
}
297+
}
298+
d.Default = true
299+
return nil
300+
}
301+
257302
// DefaultMachineName is the name of the default machine into which the app is deployed when no
258303
// specific machine is passed.
259304
const DefaultMachineName = "default"
260305

261306
// Deployment describes a single ROFL app deployment.
262307
type Deployment struct {
308+
// Default indicates whether this is the default deployment.
309+
Default bool `yaml:"default,omitempty" json:"default,omitempty"`
263310
// AppID is the Bech32-encoded ROFL app ID.
264311
AppID string `yaml:"app_id,omitempty" json:"app_id,omitempty"`
265312
// Network is the identifier of the network to deploy to.

build/rofl/manifest_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,24 @@ func TestManifestValidation(t *testing.T) {
8383
err = m.Validate()
8484
require.NoError(err)
8585

86+
// Multiple defaults should be rejected.
87+
m.Deployments["default"].Default = true
88+
m.Deployments["testnet"] = &Deployment{
89+
Default: true,
90+
Network: "testnet",
91+
ParaTime: "sapphire",
92+
}
93+
err = m.Validate()
94+
require.ErrorContains(err, "multiple deployments marked as default")
95+
96+
// Single default is fine.
97+
m.Deployments["default"].Default = false
98+
err = m.Validate()
99+
require.NoError(err)
100+
101+
// Clean up for subsequent tests.
102+
delete(m.Deployments, "testnet")
103+
86104
// Add ephemeral storage configuration.
87105
m.Resources.Storage = &StorageConfig{}
88106
err = m.Validate()
@@ -297,3 +315,59 @@ func TestUpgradePossible(t *testing.T) {
297315
require.False((&ContainerArtifactsConfig{Runtime: "new"}).UpgradePossible(&containerLatest))
298316
require.False((&ContainerArtifactsConfig{}).UpgradePossible(&containerLatest))
299317
}
318+
319+
func TestDefaultDeployment(t *testing.T) {
320+
require := require.New(t)
321+
322+
m := Manifest{}
323+
324+
// No deployments -> empty.
325+
require.Empty(m.DefaultDeployment())
326+
327+
// Single deployment -> use it regardless of name.
328+
m.Deployments = map[string]*Deployment{
329+
"testnet": {Network: "testnet", ParaTime: "sapphire"},
330+
}
331+
require.Equal("testnet", m.DefaultDeployment())
332+
333+
// Legacy "default" key is preferred over arbitrary single deployment when multiple exist.
334+
m.Deployments["default"] = &Deployment{Network: "mainnet", ParaTime: "sapphire"}
335+
require.Equal("default", m.DefaultDeployment())
336+
337+
// Explicit default flag takes precedence.
338+
m.Deployments["testnet"].Default = true
339+
require.Equal("testnet", m.DefaultDeployment())
340+
341+
// With only the "default" key and no flag, fallback works.
342+
delete(m.Deployments, "testnet")
343+
require.Equal("default", m.DefaultDeployment())
344+
345+
// Multiple deployments, none marked default, no "default" key -> empty.
346+
m.Deployments = map[string]*Deployment{
347+
"testnet": {Network: "testnet", ParaTime: "sapphire"},
348+
"mainnet": {Network: "mainnet", ParaTime: "sapphire"},
349+
}
350+
require.Empty(m.DefaultDeployment())
351+
}
352+
353+
func TestSetDefaultDeployment(t *testing.T) {
354+
require := require.New(t)
355+
356+
m := Manifest{
357+
Deployments: map[string]*Deployment{
358+
"testnet": {Default: true, Network: "testnet", ParaTime: "sapphire"},
359+
"mainnet": {Network: "mainnet", ParaTime: "sapphire"},
360+
},
361+
}
362+
363+
// Setting non-existent deployment fails.
364+
err := m.SetDefaultDeployment("staging")
365+
require.ErrorContains(err, "deployment 'staging' does not exist")
366+
367+
// Setting mainnet as default clears testnet.
368+
err = m.SetDefaultDeployment("mainnet")
369+
require.NoError(err)
370+
require.False(m.Deployments["testnet"].Default)
371+
require.True(m.Deployments["mainnet"].Default)
372+
require.Equal("mainnet", m.DefaultDeployment())
373+
}

cmd/rofl/common/flags.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ package common
22

33
import (
44
flag "github.com/spf13/pflag"
5-
6-
buildRofl "github.com/oasisprotocol/cli/build/rofl"
75
)
86

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

4846
DeploymentFlags = flag.NewFlagSet("", flag.ContinueOnError)
49-
DeploymentFlags.StringVar(&DeploymentName, "deployment", buildRofl.DefaultDeploymentName, "deployment name")
47+
DeploymentFlags.StringVar(&DeploymentName, "deployment", "", "deployment name")
5048

5149
NoUpdateFlag = flag.NewFlagSet("", flag.ContinueOnError)
5250
NoUpdateFlag.BoolVar(&NoUpdate, "no-update-manifest", false, "do not update the manifest")

cmd/rofl/common/manifest.go

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,9 @@ func LoadManifestAndSetNPA(opts *ManifestOptions) (*rofl.Manifest, *rofl.Deploym
4040
cfg := cliConfig.Global()
4141
npa := common.GetNPASelection(cfg)
4242

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

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

66+
// Resolve deployment name when not explicitly provided.
67+
if deployment == "" {
68+
deployment = manifest.DefaultDeployment()
69+
}
70+
if deployment == "" {
71+
if len(manifest.Deployments) == 0 {
72+
return nil, "", nil, fmt.Errorf("no deployments configured\nHint: use `oasis rofl create` to register a new ROFL app and create a deployment")
73+
}
74+
printAvailableDeployments(manifest)
75+
return nil, "", nil, fmt.Errorf("no default deployment configured\nHint: use `oasis rofl set-default <name>` to set a default deployment")
76+
}
77+
6478
d, ok := manifest.Deployments[deployment]
6579
if !ok {
66-
fmt.Println("The following deployments are configured in the app manifest:")
67-
for name := range manifest.Deployments {
68-
fmt.Printf(" - %s\n", name)
69-
}
70-
return nil, nil, fmt.Errorf("deployment '%s' does not exist", deployment)
80+
printAvailableDeployments(manifest)
81+
return nil, "", nil, fmt.Errorf("deployment '%s' does not exist", deployment)
7182
}
7283

7384
switch d.Network {
7485
case "":
7586
if npa.Network == nil {
76-
return nil, nil, fmt.Errorf("no network selected")
87+
return nil, "", nil, fmt.Errorf("no network selected")
7788
}
7889
default:
7990
npa.Network = cfg.Networks.All[d.Network]
8091
if npa.Network == nil {
81-
return nil, nil, fmt.Errorf("network '%s' does not exist", d.Network)
92+
return nil, "", nil, fmt.Errorf("network '%s' does not exist", d.Network)
8293
}
8394
npa.NetworkName = d.Network
8495
}
@@ -88,7 +99,7 @@ func MaybeLoadManifestAndSetNPA(cfg *cliConfig.Config, npa *common.NPASelection,
8899
default:
89100
npa.ParaTime = npa.Network.ParaTimes.All[d.ParaTime]
90101
if npa.ParaTime == nil {
91-
return nil, nil, fmt.Errorf("paratime '%s' does not exist", d.ParaTime)
102+
return nil, "", nil, fmt.Errorf("paratime '%s' does not exist", d.ParaTime)
92103
}
93104
npa.ParaTimeName = d.ParaTime
94105
}
@@ -104,12 +115,22 @@ func MaybeLoadManifestAndSetNPA(cfg *cliConfig.Config, npa *common.NPASelection,
104115
npa.Account = accCfg
105116
npa.AccountName = d.Admin
106117
case opts != nil && opts.NeedAdmin:
107-
return nil, nil, err
118+
return nil, "", nil, err
108119
default:
109120
// Admin account is not valid, but it is also not required, so do not override.
110121
}
111122
}
112-
return manifest, d, nil
123+
return manifest, deployment, d, nil
124+
}
125+
126+
func printAvailableDeployments(manifest *rofl.Manifest) {
127+
if len(manifest.Deployments) == 0 {
128+
return
129+
}
130+
fmt.Println("The following deployments are configured in the app manifest:")
131+
for name := range manifest.Deployments {
132+
fmt.Printf(" - %s\n", name)
133+
}
113134
}
114135

115136
// GetOrcFilename generates a filename based on the project name and deployment.

cmd/rofl/mgmt.go

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -211,22 +211,34 @@ var (
211211
manifest, err := buildRofl.LoadManifest()
212212
cobra.CheckErr(err)
213213

214+
// Resolve deployment name: explicit flag > network name for new deployments.
215+
deploymentName := roflCommon.DeploymentName
216+
if deploymentName == "" {
217+
deploymentName = npa.NetworkName
218+
}
219+
214220
// Load or create a deployment.
215-
deployment, ok := manifest.Deployments[roflCommon.DeploymentName]
221+
deployment, ok := manifest.Deployments[deploymentName]
216222
switch ok {
217223
case true:
218224
if deployment.AppID != "" {
219-
cobra.CheckErr(fmt.Errorf("ROFL app identifier already defined (%s) for deployment '%s', refusing to overwrite", deployment.AppID, roflCommon.DeploymentName))
225+
cobra.CheckErr(fmt.Errorf("ROFL app identifier already defined (%s) for deployment '%s', refusing to overwrite", deployment.AppID, deploymentName))
220226
}
221227

222228
// An existing deployment is defined, but without an AppID. Load everything else for
223229
// the deployment and proceed with creating a new app.
230+
roflCommon.DeploymentName = deploymentName
224231
manifest, deployment, npa = roflCommon.LoadManifestAndSetNPA(&roflCommon.ManifestOptions{
225232
NeedAppID: false,
226233
NeedAdmin: true,
227234
})
235+
236+
// Mark as default if no default is currently set (must be after reload).
237+
if manifest.DefaultDeployment() == "" {
238+
deployment.Default = true
239+
}
228240
case false:
229-
// No deployment defined, create a new default one.
241+
// No deployment defined, create a new one named after the network.
230242
npa.MustHaveAccount()
231243
npa.MustHaveParaTime()
232244
if txCfg.Offline {
@@ -283,10 +295,15 @@ var (
283295
Hash: blk.Hash.Hex(),
284296
},
285297
}
298+
// Mark as default if this is the first deployment.
299+
if len(manifest.Deployments) == 0 {
300+
deployment.Default = true
301+
}
286302
if manifest.Deployments == nil {
287303
manifest.Deployments = make(map[string]*buildRofl.Deployment)
288304
}
289-
manifest.Deployments[roflCommon.DeploymentName] = deployment
305+
manifest.Deployments[deploymentName] = deployment
306+
roflCommon.DeploymentName = deploymentName
290307
}
291308

292309
idScheme, ok := identifierSchemes[scheme]
@@ -298,7 +315,7 @@ var (
298315
tx := rofl.NewCreateTx(nil, &rofl.Create{
299316
Policy: *deployment.Policy.AsDescriptor(),
300317
Scheme: idScheme,
301-
Metadata: manifest.GetMetadata(roflCommon.DeploymentName),
318+
Metadata: manifest.GetMetadata(deploymentName),
302319
})
303320

304321
acc := common.LoadAccount(cfg, npa.AccountName)

cmd/rofl/rofl.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ func init() {
2323
Cmd.AddCommand(removeCmd)
2424
Cmd.AddCommand(showCmd)
2525
Cmd.AddCommand(listCmd)
26+
Cmd.AddCommand(setDefaultCmd)
2627
Cmd.AddCommand(trustRootCmd)
2728
Cmd.AddCommand(build.Cmd)
2829
Cmd.AddCommand(identityCmd)

cmd/rofl/set_default.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package rofl
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/spf13/cobra"
7+
8+
buildRofl "github.com/oasisprotocol/cli/build/rofl"
9+
)
10+
11+
var setDefaultCmd = &cobra.Command{
12+
Use: "set-default <deployment>",
13+
Short: "Sets the given deployment as the default deployment",
14+
Args: cobra.ExactArgs(1),
15+
Run: func(_ *cobra.Command, args []string) {
16+
name := args[0]
17+
18+
manifest, err := buildRofl.LoadManifest()
19+
cobra.CheckErr(err)
20+
21+
err = manifest.SetDefaultDeployment(name)
22+
cobra.CheckErr(err)
23+
24+
err = manifest.Save()
25+
cobra.CheckErr(err)
26+
27+
fmt.Printf("Set '%s' as the default deployment.\n", name)
28+
},
29+
}

0 commit comments

Comments
 (0)