From aac75779272186d4d025e5074a2fa862c80fa4e5 Mon Sep 17 00:00:00 2001 From: Ray Ozzie Date: Tue, 14 May 2024 17:17:15 -0400 Subject: [PATCH] feat: add the ability to get and set env vars in a batch way across devices and fleets I also added a -pretty so that the json objects output by these commands can be visually pleasing. // Get the env vars for various scopes, returning a JSON object containing them notehub -product net.ozzie.ray:t -pretty -scope fleet:60379c1da2401609b8e1fb3eec2a186e -get-vars notehub -product net.ozzie.ray:t -pretty -scope dev:864475040518622 -get-vars notehub -product net.ozzie.ray:t -pretty -scope "Ray's Fleet" -get-vars notehub -product net.ozzie.ray:t -pretty -scope "@Ray's Fleet" -get-vars // Get the env vars for a list of fleets stored in a file, returning a JSON object containing them fleets.txt Ray's Fleet fleet:60379c1da2401609b8e1fb3eec2a186e notehub -product net.ozzie.ray:t -pretty -scope @fleets.txt -get-vars // Get the env vars for a list of devices stored in a file, returning a JSON object containing them devices.txt @Ray's Fleet dev:864475040518622 notehub -product net.ozzie.ray:t -pretty -scope @devices.txt -get-vars -pretty // Set one env vars and remove one using a template directly on the command line notehub -product net.ozzie.ray:t -pretty -scope "@Ray's Fleet" -set-vars '{"var1":"val1","var2":"-"}' // Set env vars through a file-based template notehub -product net.ozzie.ray:t -pretty -scope "@Ray's Fleet" -set-vars @template --- notecard/explore.go | 12 ++- notecard/main.go | 10 +- notehub/app.go | 251 +++++++++++++++++++++++++++++++++++++++++++ notehub/examples.txt | 99 +++++++---------- notehub/explore.go | 12 ++- notehub/main.go | 171 +++++++++++++++++++++++------ notehub/req.go | 108 ++++++++++++++----- notehub/trace.go | 68 +----------- notehub/vars.go | 131 ++++++++++++++++++++++ 9 files changed, 674 insertions(+), 188 deletions(-) create mode 100644 notehub/app.go create mode 100644 notehub/vars.go diff --git a/notecard/explore.go b/notecard/explore.go index 9147e4f..f39b675 100644 --- a/notecard/explore.go +++ b/notecard/explore.go @@ -13,7 +13,7 @@ import ( ) // Explore the contents of this device -func explore(includeReserved bool) (err error) { +func explore(includeReserved bool, pretty bool) (err error) { // Get the list of notefiles req := notecard.Request{Req: notecard.ReqFileChanges} @@ -65,9 +65,15 @@ func explore(includeReserved bool) (err error) { } fmt.Printf("\n") if n.Body != nil { - bodyJSON, err := note.JSONMarshal(*n.Body) + var bodyJSON []byte + prefix := " " + if pretty { + bodyJSON, err = note.JSONMarshalIndent(*n.Body, prefix, " ") + } else { + bodyJSON, err = note.JSONMarshal(*n.Body) + } if err == nil { - fmt.Printf(" %s\n", string(bodyJSON)) + fmt.Printf("%s%s\n", prefix, string(bodyJSON)) } } if n.Payload != nil { diff --git a/notecard/main.go b/notecard/main.go index bda8630..cc65bcd 100644 --- a/notecard/main.go +++ b/notecard/main.go @@ -38,6 +38,8 @@ func main() { go signalHandler() // Process actions + var actionPretty bool + flag.BoolVar(&actionPretty, "pretty", false, "format JSON output indented") var actionRequest string flag.StringVar(&actionRequest, "req", "", "perform the specified request (in quotes)") var actionWhenConnected bool @@ -638,7 +640,11 @@ func main() { // Output the response to the console if !actionVerbose { if err == nil { - rspJSON, _ = note.JSONMarshal(rsp) + if actionPretty { + rspJSON, _ = note.JSONMarshalIndent(rsp, "", " ") + } else { + rspJSON, _ = note.JSONMarshal(rsp) + } fmt.Printf("%s\n", rspJSON) } } @@ -708,7 +714,7 @@ func main() { } if err == nil && actionExplore { - err = explore(actionReserved) + err = explore(actionReserved, actionPretty) } // Process errors diff --git a/notehub/app.go b/notehub/app.go new file mode 100644 index 0000000..2e3422e --- /dev/null +++ b/notehub/app.go @@ -0,0 +1,251 @@ +// Copyright 2024 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package main + +import ( + "bufio" + "bytes" + "fmt" + "io/ioutil" + "sort" + "strings" + + "github.com/blues/note-cli/lib" + notegoapi "github.com/blues/note-go/notehub/api" +) + +type Metadata struct { + Name string `json:"name,omitempty"` + UID string `json:"uid,omitempty"` + BA string `json:"billing_account_uid,omitempty"` +} + +type AppMetadata struct { + App Metadata `json:"app,omitempty"` + Fleets []Metadata `json:"fleets,omitempty"` + Routes []Metadata `json:"routes,omitempty"` + Products []Metadata `json:"products,omitempty"` +} + +// Load metadata for the app +func appGetMetadata(flagVerbose bool) (appMetadata AppMetadata, err error) { + + rsp := map[string]interface{}{} + err = reqHubV0(flagVerbose, lib.ConfigAPIHub(), []byte("{\"req\":\"hub.app.get\"}"), "", "", "", "", false, false, nil, &rsp) + if err != nil { + return + } + + // App info + appMetadata.App.UID = rsp["uid"].(string) + appMetadata.App.Name = rsp["label"].(string) + appMetadata.App.BA = rsp["billing_account_uid"].(string) + + // Fleet info + settings, exists := rsp["info"].(map[string]interface{}) + if exists { + fleets, exists := settings["fleet"].(map[string]interface{}) + if exists { + items := []Metadata{} + for k, v := range fleets { + vj, ok := v.(map[string]interface{}) + if ok { + i := Metadata{Name: vj["label"].(string), UID: k} + items = append(items, i) + } + } + appMetadata.Fleets = items + } + } + + // Enum routes + rsp = map[string]interface{}{} + err = reqHubV0(flagVerbose, lib.ConfigAPIHub(), []byte("{\"req\":\"hub.app.test.route\"}"), "", "", "", "", false, false, nil, &rsp) + if err == nil { + body, exists := rsp["body"].(map[string]interface{}) + if exists { + items := []Metadata{} + for k, v := range body { + vs, ok := v.(string) + if ok { + components := strings.Split(k, "/") + if len(components) > 1 { + i := Metadata{Name: vs, UID: components[1]} + items = append(items, i) + } + } + } + appMetadata.Routes = items + } + } + + // Products + rsp = map[string]interface{}{} + err = reqHubV1(flagVerbose, lib.ConfigAPIHub(), "GET", "/v1/projects/"+appMetadata.App.UID+"/products", nil, &rsp) + if err == nil { + pi, exists := rsp["products"].([]interface{}) + if exists { + items := []Metadata{} + for _, v := range pi { + p, ok := v.(map[string]interface{}) + if ok { + i := Metadata{Name: p["label"].(string), UID: p["uid"].(string)} + items = append(items, i) + } + appMetadata.Products = items + } + } + } + + // Done + return + +} + +// Get a device list given +func appGetScope(scope string, flagVerbose bool) (appMetadata AppMetadata, scopeDevices []string, scopeFleets []string, err error) { + + // Get the metadata before we begin, because at a minimum we need appUID + appMetadata, err = appGetMetadata(flagVerbose) + if err != nil { + return + } + + // On the command line (but not inside files) we allow comma-separated lists + if strings.Contains(scope, ",") { + scopeList := strings.Split(scope, ",") + for _, scope := range scopeList { + err = addScope(scope, &appMetadata, &scopeDevices, &scopeFleets, flagVerbose) + if err != nil { + return + } + } + } else { + err = addScope(scope, &appMetadata, &scopeDevices, &scopeFleets, flagVerbose) + if err != nil { + return + } + } + + // Remove duplicates + scopeDevices = sortAndRemoveDuplicates(scopeDevices) + scopeFleets = sortAndRemoveDuplicates(scopeFleets) + + // Done + return + +} + +// Recursively add scope +func addScope(scope string, appMetadata *AppMetadata, scopeDevices *[]string, scopeFleets *[]string, flagVerbose bool) (err error) { + + if strings.HasPrefix(scope, "dev:") { + *scopeDevices = append(*scopeDevices, scope) + return + } + + if strings.HasPrefix(scope, "imei:") { + // This is a pre-V1 legacy that still exists in some ancient fleets + *scopeDevices = append(*scopeDevices, scope) + return + } + + if strings.HasPrefix(scope, "fleet:") { + *scopeFleets = append(*scopeFleets, scope) + return + } + + // See if this is a fleet name, and translate it to an ID + if !strings.HasPrefix(scope, "@") { + for _, fleet := range (*appMetadata).Fleets { + if strings.EqualFold(scope, strings.TrimSpace(fleet.Name)) { + *scopeFleets = append(*scopeFleets, fleet.UID) + return + } + } + return fmt.Errorf("'%s' does not appear to be a device, fleet, @fleet indirection, or @file.ext indirection", scope) + } + indirectScope := strings.TrimPrefix(scope, "@") + + // Process a fleet indirection. First, find the fleet. + foundFleet := false + lookingFor := strings.TrimSpace(indirectScope) + for _, fleet := range (*appMetadata).Fleets { + if strings.EqualFold(lookingFor, strings.TrimSpace(fleet.UID)) || strings.EqualFold(lookingFor, strings.TrimSpace(fleet.Name)) { + foundFleet = true + + pageSize := 100 + pageNum := 0 + for { + pageNum++ + + devices := notegoapi.GetDevicesResponse{} + url := fmt.Sprintf("/v1/projects/%s/fleets/%s/devices?pageSize=%d&pageNum=%d", appMetadata.App.UID, fleet.UID, pageSize, pageNum) + err = reqHubV1(flagVerbose, lib.ConfigAPIHub(), "GET", url, nil, &devices) + if err != nil { + return + } + + for _, device := range devices.Devices { + err = addScope(device.UID, appMetadata, scopeDevices, scopeFleets, flagVerbose) + if err != nil { + return err + } + } + + if !devices.HasMore { + break + } + + } + + } + } + if foundFleet { + return + } + + // Process a file indirection + var contents []byte + contents, err = ioutil.ReadFile(indirectScope) + if err != nil { + return fmt.Errorf("%s: %s", indirectScope, err) + } + + scanner := bufio.NewScanner(bytes.NewReader(contents)) + scanner.Split(bufio.ScanLines) + + for scanner.Scan() { + line := scanner.Text() + if trimmedLine := strings.TrimSpace(line); trimmedLine != "" { + err = addScope(trimmedLine, appMetadata, scopeDevices, scopeFleets, flagVerbose) + if err != nil { + return err + } + } + } + + err = scanner.Err() + return + +} + +// Sort and remove duplicates in a string slice +func sortAndRemoveDuplicates(strings []string) []string { + + sort.Strings(strings) + + unique := make(map[string]struct{}) + var result []string + + for _, v := range strings { + if _, exists := unique[v]; !exists { + unique[v] = struct{}{} + result = append(result, v) + } + } + + return result +} diff --git a/notehub/examples.txt b/notehub/examples.txt index 949ade2..1e15e9c 100644 --- a/notehub/examples.txt +++ b/notehub/examples.txt @@ -1,77 +1,52 @@ -notehub '{"req":"hub.app.get"}' -notehub '{"req":"hub.device.get"}' +// General note-related +notehub -product net.ozzie.ray:t -device dev:94deb82a98d0 '{"req":"note.update","file":"hub.db","note":"testnote","body":{"testfield":"testvalue"}}' +notehub -product net.ozzie.ray:t -device dev:94deb82a98d0 '{"req":"note.get","file":"hub.db","note":"testnote"}' -notehub '{"req":"hub.app.data.query",query:{"columns":".modified;.payload;.body","limit":25,"format":"json"}}' +// Explore what notefiles exist on the notehub +notehub -product net.ozzie.ray:t -device dev:94deb82a98d0 -explore +notehub -product net.ozzie.ray:t -device dev:94deb82a98d0 -explore -reserved -notehub -out test.csv '{"req":"hub.app.data.query",query:{"columns":".serial;device_uid:.device;.file;.note;body:q(.body::text);.payload","format":"csv"}}' +// See what files have been uploaded to the app +notehub -product net.ozzie.ray:t '{"req":"hub.app.upload.query"}' -notehub '{"req":"hub.app.data.query",query:{"columns":".serial;.modified;.when;.where;.payload;.body","limit":25}}' +// Upload a file to an app +notehub -product net.ozzie.ray:t -upload test.bin -notehub '{"req":"hub.app.data.query",query:{"columns":".modified;.body;.payload","limit":25,"where":".body.cpm::float < 30"}}' +// Delete an uploaded file +notehub -product net.ozzie.ray:t '{"req":"hub.app.upload.delete","name":"test-20181002225319.bin"}' -notehub '{"req":"hub.app.data.query",query:{"columns":".modified;.payload;.body","limit":25,"where":".body.class::text <> '\''comms'\''"}}' +// Get the metadata of an uploaded file +notehub -product net.ozzie.ray:t '{"req":"hub.app.upload.get","name":"test-20181002225319.bin"}' -notehub '{"req":"hub.app.data.query",query:{"columns":".modified;.body;.payload","limit":25,"where":".modified>=now()-interval '\''1 day'\''"}}' +// Get the data from an uploaded file once you know the length from the metadata +notehub -product net.ozzie.ray:t '{"req":"hub.app.upload.get","name":"test-20181002225319.bin","offset":0,"length":10}' -notehub '{"req":"hub.app.data.query",query:{"columns":".body.severity;.modified,.body;.payload","limit":10,"where":".body.severity::int < 2"}}' +// Set the metadata on an uploaded file +notehub -product net.ozzie.ray:t '{"req":"hub.app.upload.set","name":"test-20181002225319.bin","body":{"testing":123},"contains":"Generate Python"}' -notehub '{"req":"hub.app.data.query",query:{"count":true,"where":".body.severity::int < 2"}}' +// Get the env vars for various scopes, returning a JSON object containing them +notehub -product net.ozzie.ray:t -pretty -scope fleet:60379c1da2401609b8e1fb3eec2a186e -get-vars +notehub -product net.ozzie.ray:t -pretty -scope dev:864475040518622 -get-vars +notehub -product net.ozzie.ray:t -pretty -scope "Ray's Fleet" -get-vars +notehub -product net.ozzie.ray:t -pretty -scope "@Ray's Fleet" -get-vars -notehub '{"req":"hub.app.data.query",query:{"columns":"device_uid:.device;when_captured:q(.when::text);loc_olc:q(.where::text);.body.lnd_7318u;.body.lnd_7128ec;.body.env_temp;.body.env_humid;.body.env_press;.body.bat_voltage;.body.bat_current;.body.bat_charge;.body.opc_pm01_0;.body.opc_pm02_5;.body.opc_pm10_0","limit":100}}' +// Get the env vars for a list of fleets stored in a file, returning a JSON object containing them +fleets.txt + Ray's Fleet + fleet:60379c1da2401609b8e1fb3eec2a186e +notehub -product net.ozzie.ray:t -pretty -scope @fleets.txt -get-vars -notehub '{"req":"hub.app.data.query",query:{"columns":"device_uid:.device;when_captured:q(to_char(.when, '\''YYYY-MM-DD\"T\"HH24:MI:SSZ'\''));loc_olc:q(.where::text)","limit":100}}' +// Get the env vars for a list of devices stored in a file, returning a JSON object containing them +devices.txt + @Ray's Fleet + dev:864475040518622 +notehub -product net.ozzie.ray:t -pretty -scope @devices.txt -get-vars -pretty -notehub '{"req":"hub.app.data.query",query:{"columns":".serial;.modified;.body","limit":25,"order":".serial"}}' +// Set one env vars and remove one using a template directly on the command line +notehub -product net.ozzie.ray:t -pretty -scope "@Ray's Fleet" -set-vars '{"var1":"val1","var2":"-"}' -notehub '{"req":"note.add","file":"geiger.q","body":{"testfield":"testvalue"}}' +// Set env vars through a file-based template +notehub -product net.ozzie.ray:t -pretty -scope "@Ray's Fleet" -set-vars @template -notehub '{"req":"hub.app.data.query",query:{"columns":".modified;.when;.body;.payload","order":".modified","where":".body.test::text = '\''iccid:89011703278123166574:1522628156'\''"}}' -notehub '{"req":"hub.app.data.query",query:{"columns":".modified;.where;.file;.payload;.body;.device","limit":5000,"order":".modified","descending":true,"format":"json"}}' - -notehub '{"req":"note.update","file":"hub.db","note":"testnote","body":{"testfield":"testvalue"}}' -notehub '{"req":"note.get","file":"hub.db","note":"testnote"}' - -notehub -in testapp.json -notehub -in testrpt.json -notehub -in testweb1.json -notehub -in testweb2.json -notehub -in testmqtt.json -notehub -in testm2x.json - - -notehub -upload test.bin - -notehub '{"req":"hub.app.upload.query"}' - -notehub '{"req":"hub.app.upload.delete","name":"test-20181002225319.bin"}' - -notehub '{"req":"hub.app.upload.get","name":"test-20181002225319.bin","offset":0,"length":10}' - -notehub '{"req":"hub.app.upload.set","name":"test-20181002225319.bin","body":{"testing":123},"contains":"Generate Python"}' - -notehub '{"req":"hub.app.upload.get","name":"test-20181002225319.bin"}' - -notehub '{"req":"hub.app.data.query",query:{"columns":".serial;.device;.modified;.when;.body;.payload","order":".modified","where":".device::text='\''imei:866425030050464'\'' or .device::text='\''imei:866425030050464:1543271941'\'' "}}' - -// To see what version of firmware is running on a notecard -notehub '{"req":"note.get","file":"_env.dbs","note":"device_vars"}' - -// For a DFU of Notecard -notehub -upload ~/desktop/notecard.bin -notehub -type firmware -upload ~/desktop/notecard.bin -notehub -type firmware -upload ~/desktop/test.txt -notehub -type notecard -upload ~/desktop/notecard.bin -notehub -type notecard -upload ~/desktop/test.txt -notehub '{"req":"hub.app.upload.query","type":"firmware"}' -notehub '{"req":"hub.app.upload.query","type":"notecard"}' -// Or for a DFU of an app -notehub -upload ~/dev/tmp/arduino/airnote.ino.bin -// ...then, optionaly, using the output from that command, substitute the "name" below and verify the contents build number, upload custom metadata, etc -notehub '{"req":"hub.app.upload.set","name":"notecard-20181008172411.bin","body":{"your":"metadata"},"contains":"Blues Wireless Notecard"}' -// ...or... -notehub '{"req":"hub.app.upload.set","name":"airnote-20190407201549.ino.bin","body":{"your":"metadata"},"contains":"Airnote version"}' -// ...then, modify the server's HubEnvVarCardFirmwareName (_fwc) with the "name" above. That's it. -// ...and if it fails for some reason, modify HubEnvVarCardFirmwareRetry (_fwc_retry) to bump it up by one, which will force a client retry -// ...and all the while the device status should be being uploaded and viewable on the service. diff --git a/notehub/explore.go b/notehub/explore.go index 469be9f..d30206c 100644 --- a/notehub/explore.go +++ b/notehub/explore.go @@ -14,7 +14,7 @@ import ( ) // Explore the contents of this device -func explore(includeReserved bool, verbose bool) (err error) { +func explore(includeReserved bool, verbose bool, pretty bool) (err error) { // Get the list of notefiles req := notehub.HubRequest{} @@ -68,9 +68,15 @@ func explore(includeReserved bool, verbose bool) (err error) { } fmt.Printf("\n") if n.Body != nil { - bodyJSON, err := note.JSONMarshal(*n.Body) + prefix := " " + var bodyJSON []byte + if pretty { + bodyJSON, err = note.JSONMarshalIndent(*n.Body, prefix, " ") + } else { + bodyJSON, err = note.JSONMarshal(*n.Body) + } if err == nil { - fmt.Printf(" %s\n", string(bodyJSON)) + fmt.Printf("%s%s\n", prefix, string(bodyJSON)) } } if n.Payload != nil { diff --git a/notehub/main.go b/notehub/main.go index cc7f7d3..e889cc8 100644 --- a/notehub/main.go +++ b/notehub/main.go @@ -12,6 +12,7 @@ import ( "strings" "github.com/blues/note-cli/lib" + "github.com/blues/note-go/note" ) // Exit codes @@ -32,10 +33,10 @@ func main() { // Process command line var flagReq string flag.StringVar(&flagReq, "req", "", "{json for device-like request}") - var flagJq bool - flag.BoolVar(&flagJq, "jq", false, "strip all non json lines from output so that jq can be used") - var flagIn string - flag.StringVar(&flagIn, "in", "", "input filename, enabling request to be contained in a file") + var flagPretty bool + flag.BoolVar(&flagPretty, "pretty", false, "pretty print json output") + var flagJson bool + flag.BoolVar(&flagJson, "json", false, "strip all non json lines from output") var flagUpload string flag.StringVar(&flagUpload, "upload", "", "filename to upload") var flagType string @@ -65,8 +66,14 @@ func main() { flag.StringVar(&flagApp, "project", "", "projectUID") flag.StringVar(&flagProduct, "product", "", "productUID") flag.StringVar(&flagDevice, "device", "", "deviceUID") - var actionVersion bool - flag.BoolVar(&actionVersion, "version", false, "print the current version of the CLI") + var flagVersion bool + flag.BoolVar(&flagVersion, "version", false, "print the current version of the CLI") + var flagScope string + flag.StringVar(&flagScope, "scope", "", "dev:xx or @fleet:xx or fleet:xx or @filename") + var flagVarsGet bool + flag.BoolVar(&flagVarsGet, "get-vars", false, "get environment vars") + var flagVarsSet string + flag.StringVar(&flagVarsSet, "set-vars", "", "set environment vars using a json template") // Parse these flags and also the note tool config flags err := lib.FlagParse(false, true) @@ -98,6 +105,11 @@ func main() { os.Exit(exitFail) } } + + // See if we did something + didSomething := false + + // Display the token if flagToken { var token, username string username, token, err = authToken() @@ -106,6 +118,7 @@ func main() { } else { fmt.Printf("To issue HTTP API requests on behalf of %s set header field X-Session-Token to:\n%s\n", username, token) } + didSomething = true } // Create an output function that will be used during -req processing @@ -126,15 +139,12 @@ func main() { os.Exit(exitFail) } - // Process input filename as a -req - if flagIn != "" { - if flagReq != "" { - fmt.Printf("It's redundant to specify both -in as well as a request. Do one or the other.\n") - os.Exit(exitFail) - } - contents, err := ioutil.ReadFile(flagIn) + // Process request starting with @ as a filename containing the request + if strings.HasPrefix(flagReq, "@") { + fn := strings.TrimPrefix(flagReq, "@") + contents, err := ioutil.ReadFile(fn) if err != nil { - fmt.Printf("Can't read input file: %s\n", err) + fmt.Printf("Can't read request file '%s': %s\n", fn, err) os.Exit(exitFail) } flagReq = string(contents) @@ -142,36 +152,137 @@ func main() { // Process requests if flagReq != "" || flagUpload != "" { - rsp, err := reqHubJSON(flagVerbose, lib.ConfigAPIHub(), []byte(flagReq), flagUpload, flagType, flagTags, flagNotes, flagOverwrite, flagJq, nil) - if err != nil { - fmt.Printf("Error processing request: %s\n", err) - os.Exit(exitFail) - } - if flagOut == "" { - fmt.Printf("%s", rsp) - } else { - outfile, err2 := os.Create(flagOut) - if err2 != nil { - fmt.Printf("Can't create output file: %s\n", err) - os.Exit(exitFail) + var rsp []byte + rsp, err = reqHubV0JSON(flagVerbose, lib.ConfigAPIHub(), []byte(flagReq), flagUpload, flagType, flagTags, flagNotes, flagOverwrite, flagJson, nil) + if err == nil { + if flagOut == "" { + if flagPretty { + var rspo map[string]interface{} + err = note.JSONUnmarshal(rsp, &rspo) + if err != nil { + fmt.Printf("%s", rsp) + } else { + rsp, _ = note.JSONMarshalIndent(rspo, "", " ") + fmt.Printf("%s", rsp) + } + } else { + fmt.Printf("%s", rsp) + } + } else { + outfile, err2 := os.Create(flagOut) + if err2 != nil { + fmt.Printf("Can't create output file: %s\n", err) + os.Exit(exitFail) + } + outfile.Write(rsp) + outfile.Close() } - outfile.Write(rsp) - outfile.Close() + didSomething = true } } // Explore the contents of the device if err == nil && flagExplore { - err = explore(flagReserved, flagVerbose) + err = explore(flagReserved, flagVerbose, flagPretty) + didSomething = true } // Enter trace mode if err == nil && flagTrace { err = trace() + didSomething = true } - if err == nil && actionVersion { + if err == nil && flagVersion { fmt.Printf("Notehub CLI Version: %s\n", version) + didSomething = true + } + + // Determine the scope of a later request + var scopeDevices, scopeFleets []string + var appMetadata AppMetadata + if err == nil && flagScope != "" { + appMetadata, scopeDevices, scopeFleets, err = appGetScope(flagScope, flagVerbose) + didSomething = true + if err == nil { + if len(scopeDevices) != 0 && len(scopeFleets) != 0 { + err = fmt.Errorf("'from' scope may include devices or fleets but not both") + fmt.Printf("%d devices and %d fleets\n%v\n%v\n", len(scopeDevices), len(scopeFleets), scopeDevices, scopeFleets) + } + if len(scopeDevices) == 0 && len(scopeFleets) == 0 { + err = fmt.Errorf("no devices or fleets found within the specified scope") + } + } + } + + // Perform actions based on scope + if err == nil && flagScope != "" && flagVarsGet { + var vars map[string]Vars + var varsJSON []byte + if len(scopeDevices) != 0 { + vars, err = varsGetFromDevices(appMetadata, scopeDevices, flagVerbose) + } else if len(scopeFleets) != 0 { + vars, err = varsGetFromFleets(appMetadata, scopeFleets, flagVerbose) + } + if err == nil { + if flagPretty { + varsJSON, err = note.JSONMarshalIndent(vars, "", " ") + } else { + varsJSON, err = note.JSONMarshal(vars) + } + if err == nil { + fmt.Printf("%s\n", varsJSON) + } + } + } + + // Perform actions based on scope + if err == nil && flagScope != "" && flagVarsSet != "" { + template := Vars{} + if strings.HasPrefix(flagVarsSet, "@") { + var templateJSON []byte + templateJSON, err = ioutil.ReadFile(strings.TrimPrefix(flagVarsSet, "@")) + if err == nil { + err = note.JSONUnmarshal(templateJSON, &template) + } + } else { + err = note.JSONUnmarshal([]byte(flagVarsSet), &template) + } + if err == nil { + var vars map[string]Vars + var varsJSON []byte + if len(scopeDevices) != 0 { + vars, err = varsSetFromDevices(appMetadata, scopeDevices, template, flagVerbose) + } else if len(scopeFleets) != 0 { + vars, err = varsSetFromFleets(appMetadata, scopeFleets, template, flagVerbose) + } + if err == nil { + if flagPretty { + varsJSON, err = note.JSONMarshalIndent(vars, "", " ") + } else { + varsJSON, err = note.JSONMarshal(vars) + } + if err == nil { + fmt.Printf("%s\n", varsJSON) + } + } + } + } + + // If we didn't do anything and we're just asking about an app, do it + if err == nil && !didSomething && (flagApp != "" || flagProduct != "") { + appMetadata, err = appGetMetadata(flagVerbose) + if err == nil { + var metaJSON []byte + if flagPretty { + metaJSON, err = note.JSONMarshalIndent(appMetadata, "", " ") + } else { + metaJSON, err = note.JSONMarshal(appMetadata) + } + if err == nil { + fmt.Printf("%s\n", metaJSON) + } + } } // Success diff --git a/notehub/req.go b/notehub/req.go index d865d50..5b832ef 100644 --- a/notehub/req.go +++ b/notehub/req.go @@ -35,41 +35,35 @@ func addQuery(in string, key string, value string) (out string) { return } -// Perform a hub transaction +// Perform a hub transaction, and promote the returned err response to an error to this method func hubTransactionRequest(request notehub.HubRequest, verbose bool) (rsp notehub.HubRequest, err error) { - return reqHub(verbose, lib.ConfigAPIHub(), request, "", "", "", "", false, false, nil) -} - -// Perform an HTTP requet, but do so using structs rather than bytes -func reqHub(verbose bool, hub string, request notehub.HubRequest, requestFile string, filetype string, filetags string, filenotes string, overwrite bool, dropNonJSON bool, outq chan string) (response notehub.HubRequest, err error) { - - reqJSON, err2 := note.JSONMarshal(request) - if err2 != nil { - err = err2 - return - } - - rspJSON, err2 := reqHubJSON(verbose, hub, reqJSON, requestFile, filetype, filetags, filenotes, overwrite, dropNonJSON, outq) - if err2 != nil { - err = err2 + var reqJSON []byte + reqJSON, err = note.JSONMarshal(request) + if err != nil { return } - - err = note.JSONUnmarshal(rspJSON, &response) + err = reqHubV0(verbose, lib.ConfigAPIHub(), reqJSON, "", "", "", "", false, false, nil, &rsp) if err != nil { return } - - if response.Err != "" { - err = fmt.Errorf("%s", response.Err) + if rsp.Err != "" { + err = fmt.Errorf("%s", rsp.Err) } - return +} +// Process a V0 HTTPS request and unmarshal into an object +func reqHubV0(verbose bool, hub string, request []byte, requestFile string, filetype string, filetags string, filenotes string, overwrite bool, dropNonJSON bool, outq chan string, object interface{}) (err error) { + var response []byte + response, err = reqHubV0JSON(verbose, hub, request, requestFile, filetype, filetags, filenotes, overwrite, dropNonJSON, outq) + if err != nil { + return + } + return note.JSONUnmarshal(response, object) } -// Perform an HTTP request -func reqHubJSON(verbose bool, hub string, request []byte, requestFile string, filetype string, filetags string, filenotes string, overwrite bool, dropNonJSON bool, outq chan string) (response []byte, err error) { +// Perform a V0 HTTP request +func reqHubV0JSON(verbose bool, hub string, request []byte, requestFile string, filetype string, filetags string, filenotes string, overwrite bool, dropNonJSON bool, outq chan string) (response []byte, err error) { fn := "" path := strings.Split(requestFile, "/") @@ -200,3 +194,69 @@ func reqHubJSON(verbose bool, hub string, request []byte, requestFile string, fi return } + +// Process a V1 HTTPS request and unmarshal into an object +func reqHubV1(verbose bool, hub string, verb string, url string, body []byte, object interface{}) (err error) { + var response []byte + response, err = reqHubV1JSON(verbose, hub, verb, url, body) + if err != nil { + return + } + return note.JSONUnmarshal(response, object) +} + +// Process an HTTPS request +func reqHubV1JSON(verbose bool, hub string, verb string, url string, body []byte) (response []byte, err error) { + + verb = strings.ToUpper(verb) + + httpurl := fmt.Sprintf("https://%s%s", hub, url) + buffer := &bytes.Buffer{} + if body != nil { + buffer = bytes.NewBuffer(body) + } + httpReq, err := http.NewRequest(verb, httpurl, buffer) + if err != nil { + return + } + httpReq.Header.Set("User-Agent", "notehub-client") + httpReq.Header.Set("Content-Type", "application/json") + err = lib.ConfigAuthenticationHeader(httpReq) + if err != nil { + return + } + + if verbose { + fmt.Printf("%s %s\n", verb, httpurl) + if len(body) != 0 { + fmt.Printf("%s\n", string(body)) + } + } + + httpClient := &http.Client{} + httpRsp, err2 := httpClient.Do(httpReq) + if err2 != nil { + err = err2 + return + } + if httpRsp.StatusCode == http.StatusUnauthorized { + err = fmt.Errorf("please use -signin to authenticate") + return + } + + if verbose { + fmt.Printf("STATUS %d\n", httpRsp.StatusCode) + } + + response, err = ioutil.ReadAll(httpRsp.Body) + if err != nil { + return + } + + if verbose && len(response) != 0 { + fmt.Printf("%s\n", string(response)) + } + + return + +} diff --git a/notehub/trace.go b/notehub/trace.go index 5be651e..ffdc785 100644 --- a/notehub/trace.go +++ b/notehub/trace.go @@ -6,10 +6,7 @@ package main import ( "bufio" - "bytes" "fmt" - "io/ioutil" - "net/http" "os" "regexp" "strings" @@ -61,7 +58,7 @@ traceloop: // Process JSON requests if strings.HasPrefix(cmd, "{") { - _, err := reqHubJSON(true, lib.ConfigAPIHub(), []byte(cmd), "", "", "", "", false, false, nil) + _, err := reqHubV0JSON(true, lib.ConfigAPIHub(), []byte(cmd), "", "", "", "", false, false, nil) if err != nil { fmt.Printf("error: %s\n", err) } @@ -160,20 +157,20 @@ traceloop: } // Perform the transaction - _, err := reqHubHTTP(true, lib.ConfigAPIHub(), args[0], url, bodyJSON) + _, err := reqHubV1JSON(true, lib.ConfigAPIHub(), args[0], url, bodyJSON) if err != nil { fmt.Printf("error: %s\n", err) return err } case "ping": - _, err := reqHubHTTP(true, lib.ConfigAPIHub(), "GET", "/ping", nil) + _, err := reqHubV1JSON(true, lib.ConfigAPIHub(), "GET", "/ping", nil) if err != nil { fmt.Printf("error: %s\n", err) return err } if cleanApp != "" { url := "/v1/products/" + cleanApp + "/products" - _, err = reqHubHTTP(true, lib.ConfigAPIHub(), "GET", url, nil) + _, err = reqHubV1JSON(true, lib.ConfigAPIHub(), "GET", url, nil) if err != nil { fmt.Printf("error: %s\n", err) return err @@ -189,60 +186,3 @@ traceloop: } return nil } - -// Process an HTTPS request -func reqHubHTTP(verbose bool, hub string, verb string, url string, body []byte) (response []byte, err error) { - - verb = strings.ToUpper(verb) - - httpurl := fmt.Sprintf("https://%s%s", hub, url) - buffer := &bytes.Buffer{} - if body != nil { - buffer = bytes.NewBuffer(body) - } - httpReq, err := http.NewRequest(verb, httpurl, buffer) - if err != nil { - return - } - httpReq.Header.Set("User-Agent", "notehub-client") - httpReq.Header.Set("Content-Type", "application/json") - err = lib.ConfigAuthenticationHeader(httpReq) - if err != nil { - return - } - - if verbose { - fmt.Printf("%s %s\n", verb, httpurl) - if len(body) != 0 { - fmt.Printf("%s\n", string(body)) - } - } - - httpClient := &http.Client{} - httpRsp, err2 := httpClient.Do(httpReq) - if err2 != nil { - err = err2 - return - } - if httpRsp.StatusCode == http.StatusUnauthorized { - err = fmt.Errorf("please use -signin to authenticate") - return - } - - if verbose { - fmt.Printf("STATUS %d\n", httpRsp.StatusCode) - } - - var rspJSON []byte - rspJSON, err = ioutil.ReadAll(httpRsp.Body) - if err != nil { - return - } - - if verbose && len(rspJSON) != 0 { - fmt.Printf("%s\n", string(rspJSON)) - } - - return - -} diff --git a/notehub/vars.go b/notehub/vars.go new file mode 100644 index 0000000..308fe8f --- /dev/null +++ b/notehub/vars.go @@ -0,0 +1,131 @@ +// Copyright 2024 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package main + +import ( + "fmt" + + "github.com/blues/note-cli/lib" + "github.com/blues/note-go/note" + notegoapi "github.com/blues/note-go/notehub/api" +) + +type Vars map[string]string + +// Load env vars into metadata from a list of devices +func varsGetFromDevices(appMetadata AppMetadata, uids []string, flagVerbose bool) (vars map[string]Vars, err error) { + + vars = map[string]Vars{} + + for _, deviceUID := range uids { + varsRsp := notegoapi.GetDeviceEnvironmentVariablesResponse{} + url := fmt.Sprintf("/v1/projects/%s/devices/%s/environment_variables", appMetadata.App.UID, deviceUID) + err = reqHubV1(flagVerbose, lib.ConfigAPIHub(), "GET", url, nil, &varsRsp) + if err != nil { + return + } + vars[deviceUID] = varsRsp.EnvironmentVariables + } + + return + +} + +// Load env vars into metadata from a list of fleets +func varsGetFromFleets(appMetadata AppMetadata, uids []string, flagVerbose bool) (vars map[string]Vars, err error) { + + vars = map[string]Vars{} + + for _, fleetUID := range uids { + varsRsp := notegoapi.GetFleetEnvironmentVariablesResponse{} + url := fmt.Sprintf("/v1/projects/%s/fleets/%s/environment_variables", appMetadata.App.UID, fleetUID) + err = reqHubV1(flagVerbose, lib.ConfigAPIHub(), "GET", url, nil, &varsRsp) + if err != nil { + return + } + vars[fleetUID] = varsRsp.EnvironmentVariables + } + + return +} + +// Load env vars into metadata from a list of devices and set their values +func varsSetFromDevices(appMetadata AppMetadata, uids []string, template Vars, flagVerbose bool) (vars map[string]Vars, err error) { + + vars = map[string]Vars{} + + for _, deviceUID := range uids { + + rspGet := notegoapi.GetDeviceEnvironmentVariablesResponse{} + url := fmt.Sprintf("/v1/projects/%s/devices/%s/environment_variables", appMetadata.App.UID, deviceUID) + err = reqHubV1(flagVerbose, lib.ConfigAPIHub(), "GET", url, nil, &rspGet) + if err != nil { + return + } + + req := notegoapi.PutDeviceEnvironmentVariablesRequest{} + req.EnvironmentVariables = rspGet.EnvironmentVariables + for k, v := range template { + req.EnvironmentVariables[k] = v + } + + var reqJSON []byte + reqJSON, err = note.JSONMarshal(req) + if err != nil { + return + } + + rspPut := notegoapi.PutDeviceEnvironmentVariablesResponse{} + err = reqHubV1(flagVerbose, lib.ConfigAPIHub(), "PUT", url, reqJSON, &rspPut) + if err != nil { + return + } + + vars[deviceUID] = rspPut.EnvironmentVariables + + } + + return + +} + +// Load env vars into metadata from a list of fleets and set their values +func varsSetFromFleets(appMetadata AppMetadata, uids []string, template Vars, flagVerbose bool) (vars map[string]Vars, err error) { + + vars = map[string]Vars{} + + for _, fleetUID := range uids { + + rspGet := notegoapi.GetFleetEnvironmentVariablesResponse{} + url := fmt.Sprintf("/v1/projects/%s/fleets/%s/environment_variables", appMetadata.App.UID, fleetUID) + err = reqHubV1(flagVerbose, lib.ConfigAPIHub(), "GET", url, nil, &rspGet) + if err != nil { + return + } + + req := notegoapi.PutFleetEnvironmentVariablesRequest{} + req.EnvironmentVariables = rspGet.EnvironmentVariables + for k, v := range template { + req.EnvironmentVariables[k] = v + } + + var reqJSON []byte + reqJSON, err = note.JSONMarshal(req) + if err != nil { + return + } + + rspPut := notegoapi.PutFleetEnvironmentVariablesResponse{} + err = reqHubV1(flagVerbose, lib.ConfigAPIHub(), "PUT", url, reqJSON, &rspPut) + if err != nil { + return + } + + vars[fleetUID] = rspPut.EnvironmentVariables + + } + + return +}