services/application/application: replace Put by PutX, add Profiles command
Since PutX only takes one profile at a time, we change putEnvelopeJSON
to internally loop over the profiles and call PutX repeatedly.
Also, fix cmdEdit to only allow one profile to be specified.
MultiPart: 1/2
Change-Id: Ic89c20d112ec337e0e88b2dfe03368b42da2f9bc
diff --git a/services/application/application/doc.go b/services/application/application/doc.go
index 59af378..dc803a7 100644
--- a/services/application/application/doc.go
+++ b/services/application/application/doc.go
@@ -14,6 +14,7 @@
The application commands are:
match Shows the first matching envelope that matches the given
profiles.
+ profiles Shows the profiles supported by the given application.
put Add the given envelope to the application for the given profiles.
remove removes the application envelope for the given profile.
edit edits the application envelope for the given profile.
@@ -70,21 +71,34 @@
Usage:
application match <application> <profiles>
-<application> is the full name of the application. <profiles> is a
+<application> is the full name of the application. <profiles> is a non-empty
comma-separated list of profiles.
+Application profiles
+
+Returns a comma-separated list of profiles supported by the given application.
+
+Usage:
+ application profiles <application>
+
+<application> is the full name of the application.
+
Application put
Add the given envelope to the application for the given profiles.
Usage:
- application put <application> <profiles> [<envelope>]
+ application put [flags] <application> <profiles> [<envelope>]
<application> is the full name of the application. <profiles> is a
comma-separated list of profiles. <envelope> is the file that contains a
JSON-encoded envelope. If this file is not provided, the user will be prompted
to enter the data manually.
+The application put flags are:
+ -overwrite=false
+ If true, put forces an overwrite of any existing envelope
+
Application remove
removes the application envelope for the given profile.
diff --git a/services/application/application/impl.go b/services/application/application/impl.go
index dfce079..d244b5e 100644
--- a/services/application/application/impl.go
+++ b/services/application/application/impl.go
@@ -31,10 +31,10 @@
cmdline.Main(cmdRoot)
}
-func getEnvelopeJSON(ctx *context.T, app repository.ApplicationClientMethods, profiles string) ([]byte, error) {
+func getEnvelopeJSON(ctx *context.T, app repository.ApplicationClientMethods, profiles []string) ([]byte, error) {
ctx, cancel := context.WithTimeout(ctx, time.Minute)
defer cancel()
- env, err := app.Match(ctx, strings.Split(profiles, ","))
+ env, err := app.Match(ctx, profiles)
if err != nil {
return nil, err
}
@@ -45,19 +45,57 @@
return j, nil
}
-func putEnvelopeJSON(ctx *context.T, app repository.ApplicationClientMethods, profiles string, j []byte) error {
- var env application.Envelope
- if err := json.Unmarshal(j, &env); err != nil {
+func putEnvelopeJSON(ctx *context.T, env *cmdline.Env, app repository.ApplicationClientMethods, profiles []string, j []byte, overwrite bool) error {
+ var envelope application.Envelope
+ if err := json.Unmarshal(j, &envelope); err != nil {
return fmt.Errorf("Unmarshal(%v) failed: %v", string(j), err)
}
ctx, cancel := context.WithTimeout(ctx, time.Minute)
defer cancel()
- if err := app.Put(ctx, strings.Split(profiles, ","), env); err != nil {
- return err
+ results := make(chan struct {
+ profile string
+ err error
+ }, len(profiles))
+ for _, profile := range profiles {
+ go func(profile string) {
+ results <- struct {
+ profile string
+ err error
+ }{profile, app.PutX(ctx, profile, envelope, overwrite)}
+ }(profile)
+ }
+ resultsMap := make(map[string]error, len(profiles))
+ for i := 0; i < len(profiles); i++ {
+ result := <-results
+ resultsMap[result.profile] = result.err
+ }
+ nErrors := 0
+ for _, p := range profiles {
+ if err := resultsMap[p]; err == nil {
+ fmt.Fprintf(env.Stdout, "Application envelope added for profile %s.\n", p)
+ } else {
+ fmt.Fprintf(env.Stderr, "Failed adding application envelope for profile %s: %v.\n", p, err)
+ nErrors++
+ }
+ }
+ if nErrors > 0 {
+ return fmt.Errorf("encountered %d errors", nErrors)
}
return nil
}
+func parseProfiles(s string) (ret []string) {
+ profiles := strings.Split(s, ",")
+ seen := make(map[string]bool)
+ for _, p := range profiles {
+ if p != "" && !seen[p] {
+ seen[p] = true
+ ret = append(ret, p)
+ }
+ }
+ return
+}
+
func promptUser(env *cmdline.Env, msg string) string {
fmt.Fprint(env.Stdout, msg)
var answer string
@@ -75,14 +113,14 @@
ArgsName: "<application> <profiles>",
ArgsLong: `
<application> is the full name of the application.
-<profiles> is a comma-separated list of profiles.`,
+<profiles> is a non-empty comma-separated list of profiles.`,
}
func runMatch(ctx *context.T, env *cmdline.Env, args []string) error {
if expected, got := 2, len(args); expected != got {
return env.UsageErrorf("match: incorrect number of arguments, expected %d, got %d", expected, got)
}
- name, profiles := args[0], args[1]
+ name, profiles := args[0], parseProfiles(args[1])
app := repository.ApplicationClient(name)
j, err := getEnvelopeJSON(ctx, app, profiles)
if err != nil {
@@ -92,6 +130,32 @@
return nil
}
+var cmdProfiles = &cmdline.Command{
+ Runner: v23cmd.RunnerFunc(runProfiles),
+ Name: "profiles",
+ Short: "Shows the profiles supported by the given application.",
+ Long: "Returns a comma-separated list of profiles supported by the given application.",
+ ArgsName: "<application>",
+ ArgsLong: `
+<application> is the full name of the application.`,
+}
+
+func runProfiles(ctx *context.T, env *cmdline.Env, args []string) error {
+ if expected, got := 1, len(args); expected != got {
+ return env.UsageErrorf("profiles: incorrect number of arguments, expected %d, got %d", expected, got)
+ }
+ name := args[0]
+ app := repository.ApplicationClient(name)
+ ctx, cancel := context.WithTimeout(ctx, time.Minute)
+ defer cancel()
+ if profiles, err := app.Profiles(ctx); err != nil {
+ return err
+ } else {
+ fmt.Fprintln(env.Stdout, strings.Join(profiles, ","))
+ return nil
+ }
+}
+
var cmdPut = &cmdline.Command{
Runner: v23cmd.RunnerFunc(runPut),
Name: "put",
@@ -105,11 +169,20 @@
not provided, the user will be prompted to enter the data manually.`,
}
+var overwriteFlag bool
+
+func init() {
+ cmdPut.Flags.BoolVar(&overwriteFlag, "overwrite", false, "If true, put forces an overwrite of any existing envelope")
+}
+
func runPut(ctx *context.T, env *cmdline.Env, args []string) error {
if got := len(args); got != 2 && got != 3 {
return env.UsageErrorf("put: incorrect number of arguments, expected 2 or 3, got %d", got)
}
- name, profiles := args[0], args[1]
+ name, profiles := args[0], parseProfiles(args[1])
+ if len(profiles) == 0 {
+ return env.UsageErrorf("put: no profiles specified")
+ }
app := repository.ApplicationClient(name)
if len(args) == 3 {
envelope := args[2]
@@ -117,10 +190,9 @@
if err != nil {
return fmt.Errorf("ReadFile(%v): %v", envelope, err)
}
- if err = putEnvelopeJSON(ctx, app, profiles, j); err != nil {
+ if err = putEnvelopeJSON(ctx, env, app, profiles, j, overwriteFlag); err != nil {
return err
}
- fmt.Fprintln(env.Stdout, "Application envelope added successfully.")
return nil
}
envelope := application.Envelope{Args: []string{}, Env: []string{}, Packages: application.Packages{}}
@@ -128,7 +200,7 @@
if err != nil {
return fmt.Errorf("MarshalIndent() failed: %v", err)
}
- if err := editAndPutEnvelopeJSON(ctx, env, app, profiles, j); err != nil {
+ if err := editAndPutEnvelopeJSON(ctx, env, app, profiles, j, overwriteFlag); err != nil {
return err
}
return nil
@@ -175,20 +247,23 @@
if expected, got := 2, len(args); expected != got {
return env.UsageErrorf("edit: incorrect number of arguments, expected %d, got %d", expected, got)
}
- name, profile := args[0], args[1]
+ name, profiles := args[0], parseProfiles(args[1])
+ if numProfiles := len(profiles); numProfiles != 1 {
+ return env.UsageErrorf("edit: incorrect number of profiles, expected 1, got %d", numProfiles)
+ }
app := repository.ApplicationClient(name)
- envData, err := getEnvelopeJSON(ctx, app, profile)
+ envData, err := getEnvelopeJSON(ctx, app, profiles)
if err != nil {
return err
}
- if err := editAndPutEnvelopeJSON(ctx, env, app, profile, envData); err != nil {
+ if err := editAndPutEnvelopeJSON(ctx, env, app, profiles, envData, true); err != nil {
return err
}
return nil
}
-func editAndPutEnvelopeJSON(ctx *context.T, env *cmdline.Env, app repository.ApplicationClientMethods, profile string, envData []byte) error {
+func editAndPutEnvelopeJSON(ctx *context.T, env *cmdline.Env, app repository.ApplicationClientMethods, profiles []string, envData []byte, overwrite bool) error {
f, err := ioutil.TempFile("", "application-edit-")
if err != nil {
return fmt.Errorf("TempFile() failed: %v", err)
@@ -223,7 +298,7 @@
fmt.Fprintln(env.Stdout, "Nothing changed")
return nil
}
- if err = putEnvelopeJSON(ctx, app, profile, newData); err != nil {
+ if err = putEnvelopeJSON(ctx, env, app, profiles, newData, overwrite); err != nil {
fmt.Fprintf(env.Stdout, "Error: %v\n", err)
if ans := promptUser(env, "Try again? [y/N] "); strings.ToUpper(ans) == "Y" {
continue
@@ -242,5 +317,5 @@
Long: `
Command application manages the Vanadium application repository.
`,
- Children: []*cmdline.Command{cmdMatch, cmdPut, cmdRemove, cmdEdit},
+ Children: []*cmdline.Command{cmdMatch, cmdProfiles, cmdPut, cmdRemove, cmdEdit},
}
diff --git a/services/application/application/impl_test.go b/services/application/application/impl_test.go
index 42b3cec..2f02939 100644
--- a/services/application/application/impl_test.go
+++ b/services/application/application/impl_test.go
@@ -6,6 +6,7 @@
import (
"bytes"
+ "fmt"
"io/ioutil"
"os"
"strings"
@@ -75,6 +76,8 @@
"Restarts": 0,
"RestartTimeWindow": 0
}`
+ profiles = "a,b,c,d"
+ serverOut bytes.Buffer
)
//go:generate v23 test generate
@@ -95,12 +98,13 @@
func (s *server) PutX(ctx *context.T, _ rpc.ServerCall, profile string, env application.Envelope, overwrite bool) error {
ctx.VI(2).Infof("%v.PutX(%v, %v, %t) was called", s.suffix, profile, env, overwrite)
+ fmt.Fprintf(&serverOut, "PutX(%s, ..., %t)\n", profile, overwrite)
return nil
}
func (s *server) Profiles(ctx *context.T, _ rpc.ServerCall) ([]string, error) {
ctx.VI(2).Infof("%v.Profiles() was called", s.suffix)
- return nil, nil
+ return strings.Split(profiles, ","), nil
}
func (s *server) Remove(ctx *context.T, _ rpc.ServerCall, profile string) error {
@@ -142,18 +146,24 @@
// Setup the command-line.
var stdout, stderr bytes.Buffer
+ resetOut := func() {
+ stdout.Reset()
+ stderr.Reset()
+ serverOut.Reset()
+ }
env := &cmdline.Env{Stdout: &stdout, Stderr: &stderr}
appName := naming.JoinAddressName(endpoint.String(), "myapp/1")
- profile := "myprofile"
+ oneProfile := "myprofile"
+ severalProfiles := "myprofile1,myprofile2,myprofile1"
// Test the 'Match' command.
- if err := v23cmd.ParseAndRunForTest(cmdRoot, ctx, env, []string{"match", appName, profile}); err != nil {
+ if err := v23cmd.ParseAndRunForTest(cmdRoot, ctx, env, []string{"match", appName, oneProfile}); err != nil {
t.Fatalf("%v", err)
}
if expected, got := jsonEnv, strings.TrimSpace(stdout.String()); got != expected {
t.Errorf("Unexpected output from match. Got %q, expected %q", got, expected)
}
- stdout.Reset()
+ resetOut()
// Test the 'put' command.
f, err := ioutil.TempFile("", "test")
@@ -168,40 +178,81 @@
if err = f.Close(); err != nil {
t.Fatalf("%v", err)
}
- if err := v23cmd.ParseAndRunForTest(cmdRoot, ctx, env, []string{"put", appName, profile, fileName}); err != nil {
+ if err := v23cmd.ParseAndRunForTest(cmdRoot, ctx, env, []string{"put", appName, severalProfiles, fileName}); err != nil {
t.Fatalf("%v", err)
}
- if expected, got := "Application envelope added successfully.", strings.TrimSpace(stdout.String()); got != expected {
+ if expected, got := "Application envelope added for profile myprofile1.\nApplication envelope added for profile myprofile2.", strings.TrimSpace(stdout.String()); got != expected {
t.Errorf("Unexpected output from put. Got %q, expected %q", got, expected)
}
- stdout.Reset()
+ if expected1, expected2, got := "PutX(myprofile1, ..., false)\nPutX(myprofile2, ..., false)", "PutX(myprofile2, ..., false)\nPutX(myprofile1, ..., false)", strings.TrimSpace(serverOut.String()); got != expected1 && got != expected2 {
+ t.Errorf("Unexpected output from mock server. Got %q, expected %q or %q", got, expected1, expected2)
+ }
+ resetOut()
+
+ // Test the 'put' command with overwrite = true.
+ if err := v23cmd.ParseAndRunForTest(cmdRoot, ctx, env, []string{"put", "--overwrite", appName, oneProfile, fileName}); err != nil {
+ t.Fatalf("%v", err)
+ }
+ if expected, got := "Application envelope added for profile myprofile.", strings.TrimSpace(stdout.String()); got != expected {
+ t.Errorf("Unexpected output from put. Got %q, expected %q", got, expected)
+ }
+ if expected, got := "PutX(myprofile, ..., true)", strings.TrimSpace(serverOut.String()); got != expected {
+ t.Errorf("Unexpected output from mock server. Got %q, expected %q", got, expected)
+ }
+ resetOut()
+
+ // Test the 'put' command with no profiles.
+ if err := v23cmd.ParseAndRunForTest(cmdRoot, ctx, env, []string{"put", appName, ",,,", fileName}); err == nil {
+ t.Errorf("Expected put with no profiles to fail")
+ } else if expected, got := "ERROR: put: no profiles specified", strings.TrimSpace(stderr.String()); !strings.HasPrefix(got, expected) {
+ t.Errorf("Unexpected stderr output from put. Got %q, expected %q", got, expected+" ...")
+ }
+ resetOut()
// Test the 'remove' command.
- if err := v23cmd.ParseAndRunForTest(cmdRoot, ctx, env, []string{"remove", appName, profile}); err != nil {
+ if err := v23cmd.ParseAndRunForTest(cmdRoot, ctx, env, []string{"remove", appName, oneProfile}); err != nil {
t.Fatalf("%v", err)
}
if expected, got := "Application envelope removed successfully.", strings.TrimSpace(stdout.String()); got != expected {
t.Errorf("Unexpected output from remove. Got %q, expected %q", got, expected)
}
- stdout.Reset()
+ resetOut()
// Test the 'edit' command. (nothing changed)
env.Vars = map[string]string{"EDITOR": "true"}
- if err := v23cmd.ParseAndRunForTest(cmdRoot, ctx, env, []string{"edit", appName, profile}); err != nil {
+ if err := v23cmd.ParseAndRunForTest(cmdRoot, ctx, env, []string{"edit", appName, oneProfile}); err != nil {
t.Fatalf("%v", err)
}
if expected, got := "Nothing changed", strings.TrimSpace(stdout.String()); got != expected {
t.Errorf("Unexpected output from edit. Got %q, expected %q", got, expected)
}
- stdout.Reset()
+ resetOut()
// Test the 'edit' command.
env.Vars = map[string]string{"EDITOR": "perl -pi -e 's/arg1/arg111/'"}
- if err := v23cmd.ParseAndRunForTest(cmdRoot, ctx, env, []string{"edit", appName, profile}); err != nil {
+ if err := v23cmd.ParseAndRunForTest(cmdRoot, ctx, env, []string{"edit", appName, oneProfile}); err != nil {
t.Fatalf("%v", err)
}
- if expected, got := "Application envelope updated successfully.", strings.TrimSpace(stdout.String()); got != expected {
+ if expected, got := "Application envelope added for profile myprofile.\nApplication envelope updated successfully.", strings.TrimSpace(stdout.String()); got != expected {
t.Errorf("Unexpected output from edit. Got %q, expected %q", got, expected)
}
- stdout.Reset()
+ resetOut()
+
+ // Test the 'edit' command with more than 1 profiles.
+ env.Vars = map[string]string{"EDITOR": "true"}
+ if err := v23cmd.ParseAndRunForTest(cmdRoot, ctx, env, []string{"edit", appName, severalProfiles}); err == nil {
+ t.Errorf("Expected edit with two profiles to fail")
+ } else if expected, got := "ERROR: edit: incorrect number of profiles, expected 1, got 2", strings.TrimSpace(stderr.String()); !strings.HasPrefix(got, expected) {
+ t.Errorf("Unexpected stderr output from edit. Got %q, expected %q", got, expected+" ...")
+ }
+ resetOut()
+
+ // Test the 'profiles' command.
+ if err := v23cmd.ParseAndRunForTest(cmdRoot, ctx, env, []string{"profiles", appName}); err != nil {
+ t.Fatalf("%v", err)
+ }
+ if expected, got := profiles, strings.TrimSpace(stdout.String()); got != expected {
+ t.Errorf("Unexpected output from profiles. Got %q, expected %q", got, expected)
+ }
+ resetOut()
}
diff --git a/services/device/mgmt_v23_test.go b/services/device/mgmt_v23_test.go
index 58ab3ef..5940bcc 100644
--- a/services/device/mgmt_v23_test.go
+++ b/services/device/mgmt_v23_test.go
@@ -358,7 +358,7 @@
defer os.Remove(appEnvelopeFilename)
output := applicationBin.Run("put", sampleAppName, deviceProfile, appEnvelopeFilename)
- if got, want := output, "Application envelope added successfully."; got != want {
+ if got, want := output, fmt.Sprintf("Application envelope added for profile %s.", deviceProfile); got != want {
i.Fatalf("got %q, want %q", got, want)
}