Skip to content

Commit 40a7fc0

Browse files
committed
Add oasis rofl public-var subcommand
1 parent 7e8b7e0 commit 40a7fc0

11 files changed

Lines changed: 485 additions & 0 deletions

build/rofl/artifacts.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ var LatestContainerArtifacts = ArtifactsConfig{
2121
Kernel: "https://github.com/oasisprotocol/oasis-boot/releases/download/v0.6.2/stage1.bin#e5d4d654ca1fa2c388bf64b23fc6e67815893fc7cb8b7cfee253d87963f54973",
2222
Stage2: "https://github.com/oasisprotocol/oasis-boot/releases/download/v0.6.2/stage2-podman.tar.bz2#b2ea2a0ca769b6b2d64e3f0c577ee9c08f0bb81a6e33ed5b15b2a7e50ef9a09f",
2323
Container: ContainerArtifactsConfig{
24+
// TODO release new rofl-container
2425
Runtime: "https://github.com/oasisprotocol/oasis-sdk/releases/download/rofl-containers%2Fv0.8.6/rofl-containers#5aa26a3c5a7e1d284e217959d89b7a620ea7b7fc5079909e25042124ffb384e8",
2526
},
2627
}

cmd/rofl/public_var.go

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
package rofl
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"os"
7+
"sort"
8+
"unicode/utf8"
9+
10+
"github.com/spf13/cobra"
11+
12+
buildDotenv "github.com/oasisprotocol/cli/build/dotenv"
13+
"github.com/oasisprotocol/cli/cmd/common"
14+
roflCommon "github.com/oasisprotocol/cli/cmd/rofl/common"
15+
)
16+
17+
const publicVarMetadataPrefix = "env."
18+
19+
var (
20+
publicVarCmd = &cobra.Command{
21+
Use: "public-var",
22+
Short: "Public variable management commands",
23+
Long: "Public variable management commands.\n\n" +
24+
"Values are stored unencrypted in ROFL app metadata and exposed to containers as\n" +
25+
"environment variables. Use `oasis rofl secret` for confidential values.",
26+
}
27+
28+
publicVarSetCmd = &cobra.Command{
29+
Use: "set <name> <file>|-",
30+
Short: "Set a public variable in the manifest, reading the value from file or stdin",
31+
Args: cobra.ExactArgs(2),
32+
Run: func(_ *cobra.Command, args []string) {
33+
publicVarName := args[0]
34+
publicVarFn := args[1]
35+
36+
manifest, deployment, _ := roflCommon.LoadManifestAndSetNPA(&roflCommon.ManifestOptions{
37+
NeedAppID: false,
38+
NeedAdmin: false,
39+
})
40+
41+
key, err := publicVarMetadataKey(publicVarName)
42+
if err != nil {
43+
cobra.CheckErr(err)
44+
}
45+
46+
// Read public variable.
47+
var publicVarValueRaw []byte
48+
if publicVarFn == "-" {
49+
publicVarValueRaw, err = io.ReadAll(os.Stdin)
50+
if err != nil {
51+
cobra.CheckErr(fmt.Errorf("failed to read public variable from standard input: %w", err))
52+
}
53+
} else {
54+
publicVarValueRaw, err = os.ReadFile(publicVarFn)
55+
if err != nil {
56+
cobra.CheckErr(fmt.Errorf("failed to read public variable from file: %w", err))
57+
}
58+
}
59+
publicVarValue, err := parsePublicVarValue(publicVarValueRaw)
60+
if err != nil {
61+
cobra.CheckErr(err)
62+
}
63+
64+
if existing, ok := deployment.Metadata[key]; ok {
65+
if existing == publicVarValue {
66+
fmt.Printf("Public variable '%s' did not change; no update needed.\n", publicVarName)
67+
return
68+
}
69+
common.CheckForceErr(fmt.Errorf("the public variable named '%s' for deployment '%s' already exists", publicVarName, roflCommon.DeploymentName))
70+
}
71+
if deployment.Metadata == nil {
72+
deployment.Metadata = make(map[string]string)
73+
}
74+
deployment.Metadata[key] = publicVarValue
75+
76+
// Update manifest.
77+
if err = manifest.Save(); err != nil {
78+
cobra.CheckErr(fmt.Errorf("failed to update manifest: %w", err))
79+
}
80+
81+
fmt.Printf("Run `oasis rofl update` to update your ROFL app's on-chain configuration.\n")
82+
},
83+
}
84+
85+
// publicVarImportCmd bulk-imports public variables from a .env file (key=value with # comments).
86+
// Supports '-' to read from stdin. Existing public variables are replaced only with --force.
87+
publicVarImportCmd = &cobra.Command{
88+
Use: "import <dot-env-file>|-",
89+
Short: "Import multiple public variables from a .env file",
90+
Args: cobra.ExactArgs(1),
91+
Run: func(_ *cobra.Command, args []string) {
92+
envFn := args[0]
93+
94+
manifest, deployment, _ := roflCommon.LoadManifestAndSetNPA(&roflCommon.ManifestOptions{
95+
NeedAppID: false,
96+
NeedAdmin: false,
97+
})
98+
99+
// Read .env data (supports stdin via "-").
100+
var (
101+
raw []byte
102+
err error
103+
)
104+
if envFn == "-" {
105+
raw, err = io.ReadAll(os.Stdin)
106+
if err != nil {
107+
cobra.CheckErr(fmt.Errorf("failed to read .env from standard input: %w", err))
108+
}
109+
} else {
110+
raw, err = os.ReadFile(envFn)
111+
if err != nil {
112+
cobra.CheckErr(fmt.Errorf("failed to read .env file: %w", err))
113+
}
114+
}
115+
116+
entries, err := buildDotenv.Parse(string(raw))
117+
if err != nil {
118+
cobra.CheckErr(fmt.Errorf("failed to parse .env: %w", err))
119+
}
120+
if len(entries) == 0 {
121+
fmt.Println("No key=value pairs found in the provided .env input.")
122+
return
123+
}
124+
125+
// Deterministic iteration for stable updates.
126+
names := make([]string, 0, len(entries))
127+
for k := range entries {
128+
names = append(names, k)
129+
}
130+
sort.Strings(names)
131+
132+
if deployment.Metadata == nil {
133+
deployment.Metadata = make(map[string]string)
134+
}
135+
136+
var imported, updated int
137+
for _, name := range names {
138+
key, err := publicVarMetadataKey(name)
139+
if err != nil {
140+
cobra.CheckErr(err)
141+
}
142+
143+
if existing, ok := deployment.Metadata[key]; ok {
144+
if existing == entries[name] {
145+
continue
146+
}
147+
common.CheckForceErr(fmt.Errorf("the public variable named '%s' for deployment '%s' already exists", name, roflCommon.DeploymentName))
148+
updated++
149+
} else {
150+
imported++
151+
}
152+
deployment.Metadata[key] = entries[name]
153+
}
154+
155+
src := envFn
156+
if envFn == "-" {
157+
src = "stdin"
158+
}
159+
if imported == 0 && updated == 0 {
160+
fmt.Printf("No public variables were imported or updated using '%s'.\n", src)
161+
return
162+
}
163+
164+
// Update manifest.
165+
if err = manifest.Save(); err != nil {
166+
cobra.CheckErr(fmt.Errorf("failed to update manifest: %w", err))
167+
}
168+
169+
fmt.Printf("Imported %d public variables, updated %d existing from '%s'.\n", imported, updated, src)
170+
fmt.Printf("Run `oasis rofl update` to update your ROFL app's on-chain configuration.\n")
171+
},
172+
}
173+
174+
publicVarGetCmd = &cobra.Command{
175+
Use: "get <name>",
176+
Short: "Show the given public variable",
177+
Args: cobra.ExactArgs(1),
178+
Run: func(_ *cobra.Command, args []string) {
179+
publicVarName := args[0]
180+
181+
_, deployment, _ := roflCommon.LoadManifestAndSetNPA(&roflCommon.ManifestOptions{
182+
NeedAppID: false,
183+
NeedAdmin: false,
184+
})
185+
186+
key, err := publicVarMetadataKey(publicVarName)
187+
if err != nil {
188+
cobra.CheckErr(err)
189+
}
190+
value, ok := deployment.Metadata[key]
191+
if !ok {
192+
cobra.CheckErr(fmt.Errorf("public variable named '%s' does not exist for deployment '%s'", publicVarName, roflCommon.DeploymentName))
193+
return // Lint doesn't know that cobra.CheckErr never returns.
194+
}
195+
196+
fmt.Printf("Name: %s\n", publicVarName)
197+
fmt.Printf("Value: %s\n", value)
198+
fmt.Printf("Size: %d bytes\n", len(value))
199+
},
200+
}
201+
202+
publicVarRmCmd = &cobra.Command{
203+
Use: "rm <name>",
204+
Short: "Remove the given public variable from the manifest",
205+
Args: cobra.ExactArgs(1),
206+
Run: func(_ *cobra.Command, args []string) {
207+
publicVarName := args[0]
208+
209+
manifest, deployment, _ := roflCommon.LoadManifestAndSetNPA(&roflCommon.ManifestOptions{
210+
NeedAppID: false,
211+
NeedAdmin: false,
212+
})
213+
214+
key, err := publicVarMetadataKey(publicVarName)
215+
if err != nil {
216+
cobra.CheckErr(err)
217+
}
218+
if _, ok := deployment.Metadata[key]; !ok {
219+
cobra.CheckErr(fmt.Errorf("public variable named '%s' does not exist for deployment '%s'", publicVarName, roflCommon.DeploymentName))
220+
}
221+
delete(deployment.Metadata, key)
222+
if len(deployment.Metadata) == 0 {
223+
deployment.Metadata = nil
224+
}
225+
226+
// Update manifest.
227+
if err := manifest.Save(); err != nil {
228+
cobra.CheckErr(fmt.Errorf("failed to update manifest: %w", err))
229+
}
230+
231+
fmt.Printf("Run `oasis rofl update` to update your ROFL app's on-chain configuration.\n")
232+
},
233+
}
234+
)
235+
236+
func publicVarMetadataKey(name string) (string, error) {
237+
if err := validatePublicVarName(name); err != nil {
238+
return "", err
239+
}
240+
return publicVarMetadataPrefix + name, nil
241+
}
242+
243+
func validatePublicVarName(name string) error {
244+
if name == "" {
245+
return fmt.Errorf("public variable name cannot be empty")
246+
}
247+
for _, ch := range name {
248+
if (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_' {
249+
continue
250+
}
251+
return fmt.Errorf("public variable name '%s' is invalid, only ASCII letters, digits and '_' are allowed", name)
252+
}
253+
return nil
254+
}
255+
256+
func parsePublicVarValue(raw []byte) (string, error) {
257+
if !utf8.Valid(raw) {
258+
return "", fmt.Errorf("public variable values must be valid UTF-8")
259+
}
260+
return string(raw), nil
261+
}
262+
263+
func init() {
264+
publicVarSetCmd.Flags().AddFlagSet(roflCommon.DeploymentFlags)
265+
publicVarSetCmd.Flags().AddFlagSet(common.ForceFlag)
266+
publicVarCmd.AddCommand(publicVarSetCmd)
267+
268+
publicVarImportCmd.Flags().AddFlagSet(roflCommon.DeploymentFlags)
269+
publicVarImportCmd.Flags().AddFlagSet(common.ForceFlag)
270+
publicVarCmd.AddCommand(publicVarImportCmd)
271+
272+
publicVarGetCmd.Flags().AddFlagSet(roflCommon.DeploymentFlags)
273+
publicVarCmd.AddCommand(publicVarGetCmd)
274+
275+
publicVarRmCmd.Flags().AddFlagSet(roflCommon.DeploymentFlags)
276+
publicVarCmd.AddCommand(publicVarRmCmd)
277+
}

0 commit comments

Comments
 (0)