Skip to content

Commit ab97fb0

Browse files
authored
Commands to configure the default project (#146)
### Changelog None ### Docs None ### Description This PR introduces the new `config get {key}`, `config set {key}` and `config unset {key}` commands, e.g: * `foxglove config get project-id` * `foxglove config set project-id prj_abcd1234` * `foxglove config unset project-id` The only available keys today is `project-id` These store/clear the `default_project_id` value in `~/.foxgloverc` where the rest of our config (`auth_type`, `base_url` and `bearer_token`) lives. If set, the `default_project_id` will be automatically applied on all commands that take a `project-id` parameter. For reference, here's the map of all CLI commands. Sub-commands marked with `◆` are the new ones; `◉` marks commands that take a `--project-id` parameter and will use the default if available. `✖` is now deprecated (`foxglove imports` is deprecated in favor of `foxglove recordings list` ``` ├── attachments # Query and modify data attachments │ ├── download # Download an MCAP attachment by ID │ ╰── list # List MCAP attachments │ ├── auth # Manage authentication │ ├── configure-api-key # Configure an API key │ ├── info # Display info about the currently authenticated user │ ╰── login # Log in to Foxglove Data Platform │ ├─◆ config # Manage configuration │ ├─◆ get {key} # Get the value for a key (available keys are: project-id, api-key) │ ├─◆ set {key} # Set a value │ ╰─◆ unset {key} # Unset a value │ ├── data # Data access and management │ ├─◉ coverage # List coverage ranges │ ├── export # Export a data selection from Foxglove Data Platform │ ├─◉ import # Import a data file to Foxglove Data Platform │ ╰─✖ imports # Query and modify data imports │ ├── devices # List and manage devices │ ├─◉ add # Add a device for your organization │ ├── edit # Edit a device │ ╰─◉ list # List devices registered to your organization │ ├── events # List and manage events │ ├── add # Add an event │ ╰── list # List events │ ├── extensions # List and publish Studio extensions │ ├── list # List Studio extensions created for your organization │ ├── publish # Publish a Studio extension (.foxe) to your organization │ ╰── unpublish # Delete and unpublish a Studio extension from your organization │ ├── pending-imports # List pending imports │ ╰─◉ list # List the pending and errored import jobs for uploaded recordings │ ├── projects # List and manage projects │ ╰── list # List projects │ ├── recordings # Query recordings │ ├── delete # Delete a recording from your organization │ ╰─◉ list # List recordings │ ├── version # Print Foxglove CLI version ├── help # Help about any command ╰── completion # Generate the autocompletion script for the specified shell ```
1 parent 3e600b9 commit ab97fb0

File tree

8 files changed

+198
-32
lines changed

8 files changed

+198
-32
lines changed

foxglove/cmd/config.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"strings"
7+
8+
"github.com/spf13/cobra"
9+
"github.com/spf13/viper"
10+
)
11+
12+
func newConfigCommand() *cobra.Command {
13+
configCmd := &cobra.Command{
14+
Use: "config",
15+
Short: "Manage CLI configuration",
16+
Long: `Manage CLI configuration values.
17+
Available configuration keys:
18+
- project-id: Default project ID for commands`,
19+
}
20+
21+
configCmd.AddCommand(newConfigGetCommand())
22+
configCmd.AddCommand(newConfigSetCommand())
23+
configCmd.AddCommand(newConfigUnsetCommand())
24+
25+
return configCmd
26+
}
27+
28+
func newConfigGetCommand() *cobra.Command {
29+
getCmd := &cobra.Command{
30+
Use: "get [KEY]",
31+
Short: "Get a configuration value",
32+
Args: cobra.MaximumNArgs(1),
33+
Run: func(cmd *cobra.Command, args []string) {
34+
if len(args) == 0 {
35+
dief("No key provided. Valid keys are: %s", strings.Join(validConfigKeys, ", "))
36+
}
37+
key := args[0]
38+
if !isValidConfigKey(key) {
39+
dief("Invalid configuration key '%s'. Valid keys are: %s", key, strings.Join(validConfigKeys, ", "))
40+
}
41+
42+
viperKey := mapConfigKeyToViperKey(key)
43+
value := viper.GetString(viperKey)
44+
45+
if value == "" {
46+
if viper.IsSet(viperKey) {
47+
fmt.Println()
48+
} else {
49+
dief("No value set for key '%s'", key)
50+
}
51+
} else {
52+
fmt.Println(value)
53+
}
54+
},
55+
}
56+
return getCmd
57+
}
58+
59+
func newConfigSetCommand() *cobra.Command {
60+
setCmd := &cobra.Command{
61+
Use: "set [KEY] [VALUE]",
62+
Short: "Set a configuration value",
63+
Args: cobra.MaximumNArgs(2),
64+
Run: func(cmd *cobra.Command, args []string) {
65+
if len(args) < 1 {
66+
dief("No key provided. Valid keys are: %s", strings.Join(validConfigKeys, ", "))
67+
}
68+
key := args[0]
69+
if !isValidConfigKey(key) {
70+
dief("Invalid configuration key '%s'. Valid keys are: %s", key, strings.Join(validConfigKeys, ", "))
71+
}
72+
73+
if len(args) < 2 {
74+
dief("No value provided. Please provide a value for: %s", args[0])
75+
}
76+
value := args[1]
77+
78+
viperKey := mapConfigKeyToViperKey(key)
79+
80+
viper.Set(viperKey, value)
81+
82+
err := viper.WriteConfigAs(viper.ConfigFileUsed())
83+
if err != nil {
84+
dief("Failed to write config: %s", err)
85+
}
86+
87+
fmt.Fprintf(os.Stderr, "Configuration updated: %s = %s\n", key, value)
88+
},
89+
}
90+
return setCmd
91+
}
92+
93+
func newConfigUnsetCommand() *cobra.Command {
94+
unsetCmd := &cobra.Command{
95+
Use: "unset [KEY]",
96+
Short: "Remove a configuration value",
97+
Args: cobra.MaximumNArgs(1),
98+
Run: func(cmd *cobra.Command, args []string) {
99+
if len(args) == 0 {
100+
dief("No key provided. Valid keys are: %s", strings.Join(validConfigKeys, ", "))
101+
}
102+
key := args[0]
103+
if !isValidConfigKey(key) {
104+
dief("Invalid configuration key '%s'. Valid keys are: %s", key, strings.Join(validConfigKeys, ", "))
105+
}
106+
viperKey := mapConfigKeyToViperKey(key)
107+
if !viper.IsSet(viperKey) {
108+
dief("No value set for key '%s'", key)
109+
}
110+
configFile := viper.ConfigFileUsed()
111+
settings := viper.AllSettings()
112+
delete(settings, viperKey)
113+
114+
// Clear viper and reload with updated settings
115+
viper.Reset()
116+
viper.SetConfigType("yaml")
117+
viper.SetConfigFile(configFile)
118+
for k, v := range settings {
119+
viper.Set(k, v)
120+
}
121+
122+
err := viper.WriteConfigAs(configFile)
123+
if err != nil {
124+
dief("Failed to write config: %s", err)
125+
}
126+
fmt.Fprintf(os.Stderr, "Configuration removed: %s\n", key)
127+
},
128+
}
129+
return unsetCmd
130+
}
131+
132+
var validConfigKeys = []string{
133+
"project-id",
134+
}
135+
136+
func isValidConfigKey(key string) bool {
137+
for _, validKey := range validConfigKeys {
138+
if key == validKey {
139+
return true
140+
}
141+
}
142+
return false
143+
}
144+
145+
func mapConfigKeyToViperKey(key string) string {
146+
switch key {
147+
case "project-id":
148+
return "default_project_id"
149+
default:
150+
return key
151+
}
152+
}

foxglove/cmd/coverage.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55

66
"github.com/foxglove/foxglove-cli/foxglove/api"
77
"github.com/spf13/cobra"
8+
"github.com/spf13/viper"
89
)
910

1011
func newListCoverageCommand(params *baseParams) *cobra.Command {
@@ -59,7 +60,7 @@ func newListCoverageCommand(params *baseParams) *cobra.Command {
5960
},
6061
}
6162
coverageListCmd.InheritedFlags()
62-
coverageListCmd.PersistentFlags().StringVarP(&projectID, "project-id", "", "", "Project ID")
63+
coverageListCmd.PersistentFlags().StringVarP(&projectID, "project-id", "", viper.GetString("default_project_id"), "Project ID")
6364
coverageListCmd.PersistentFlags().StringVarP(&deviceID, "device-id", "", "", "Device ID")
6465
coverageListCmd.PersistentFlags().StringVarP(&deviceName, "device-name", "", "", "Device name")
6566
coverageListCmd.PersistentFlags().StringVarP(&recordingID, "recording-id", "", "", "Recording ID")

foxglove/cmd/devices.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/foxglove/foxglove-cli/foxglove/api"
88
"github.com/foxglove/foxglove-cli/foxglove/util"
99
"github.com/spf13/cobra"
10+
"github.com/spf13/viper"
1011
)
1112

1213
func newListDevicesCommand(params *baseParams) *cobra.Command {
@@ -37,7 +38,7 @@ func newListDevicesCommand(params *baseParams) *cobra.Command {
3738
},
3839
}
3940
deviceListCmd.InheritedFlags()
40-
deviceListCmd.PersistentFlags().StringVarP(&projectID, "project-id", "", "", "project ID")
41+
deviceListCmd.PersistentFlags().StringVarP(&projectID, "project-id", "", viper.GetString("default_project_id"), "Project ID")
4142
AddFormatFlag(deviceListCmd, &format)
4243
AddJsonFlag(deviceListCmd, &isJsonFormat)
4344
return deviceListCmd
@@ -80,7 +81,7 @@ func newAddDeviceCommand(params *baseParams) *cobra.Command {
8081
}
8182
addDeviceCmd.InheritedFlags()
8283
addDeviceCmd.PersistentFlags().StringVarP(&name, "name", "", "", "name of the device")
83-
addDeviceCmd.PersistentFlags().StringVarP(&projectID, "project-id", "", "", "project ID")
84+
addDeviceCmd.PersistentFlags().StringVarP(&projectID, "project-id", "", viper.GetString("default_project_id"), "Project ID")
8485
addDeviceCmd.PersistentFlags().StringVarP(&serialNumber, "serial-number", "", "", "Deprecated. Value will be ignored.")
8586
addDeviceCmd.PersistentFlags().StringArrayVarP(&propertyPairs, "property", "p", []string{}, "Custom property colon-separated key value pair. Multiple may be specified.")
8687
return addDeviceCmd

foxglove/cmd/import.go

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,21 @@ func importFromEdge(baseURL, clientID, token, userAgent, edgeRecordingID string)
4444
return nil
4545
}
4646

47-
func newImportCommand(params *baseParams, commandName string) (*cobra.Command, error) {
47+
func newImportCommand(params *baseParams, commandName string, deprecated *string) (*cobra.Command, error) {
4848
var projectID string
4949
var deviceID string
5050
var deviceName string
5151
var edgeRecordingID string
5252
var key string
53+
var deprecatedMsg string
54+
if deprecated != nil {
55+
deprecatedMsg = *deprecated
56+
}
5357
importCmd := &cobra.Command{
54-
Use: fmt.Sprintf("%s [FILE]", commandName),
55-
Short: "Import a data file to Foxglove Data Platform",
56-
Args: cobra.ExactArgs(1),
58+
Use: fmt.Sprintf("%s [FILE]", commandName),
59+
Short: "Import a data file to Foxglove Data Platform",
60+
Args: cobra.ExactArgs(1),
61+
Deprecated: deprecatedMsg,
5762
Run: func(cmd *cobra.Command, args []string) {
5863
if edgeRecordingID != "" {
5964
err := importFromEdge(params.baseURL, *params.clientID, params.token, params.userAgent, edgeRecordingID)
@@ -81,11 +86,11 @@ func newImportCommand(params *baseParams, commandName string) (*cobra.Command, e
8186
},
8287
}
8388
importCmd.InheritedFlags()
84-
importCmd.PersistentFlags().StringVarP(&projectID, "project-id", "", "", "project ID")
85-
importCmd.PersistentFlags().StringVarP(&deviceID, "device-id", "", "", "device ID")
86-
importCmd.PersistentFlags().StringVarP(&deviceName, "device-name", "", "", "device name")
87-
importCmd.PersistentFlags().StringVarP(&key, "key", "", "", "recording key")
88-
importCmd.PersistentFlags().StringVarP(&edgeRecordingID, "edge-recording-id", "", "", "edge recording ID")
89+
importCmd.PersistentFlags().StringVarP(&projectID, "project-id", "", viper.GetString("default_project_id"), "Project ID")
90+
importCmd.PersistentFlags().StringVarP(&deviceID, "device-id", "", "", "Device ID")
91+
importCmd.PersistentFlags().StringVarP(&deviceName, "device-name", "", "", "Device name")
92+
importCmd.PersistentFlags().StringVarP(&key, "key", "", "", "Recording key")
93+
importCmd.PersistentFlags().StringVarP(&edgeRecordingID, "edge-recording-id", "", "", "Edge recording ID")
8994
AddDeviceAutocompletion(importCmd, params)
9095
return importCmd, nil
9196
}

foxglove/cmd/imports.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ func newListImportsCommand(params *baseParams) *cobra.Command {
1818
var dataEnd string
1919
var isJsonFormat bool
2020
importsListCmd := &cobra.Command{
21-
Use: "list",
22-
Short: "List imports for a device",
21+
Use: "list",
22+
Short: "List imports for a device",
23+
Deprecated: "use 'recordings list' instead.",
2324
Run: func(cmd *cobra.Command, args []string) {
2425
client := api.NewRemoteFoxgloveClient(
2526
params.baseURL, *params.clientID,

foxglove/cmd/pending_imports.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66

77
"github.com/foxglove/foxglove-cli/foxglove/api"
88
"github.com/spf13/cobra"
9+
"github.com/spf13/viper"
910
)
1011

1112
func newPendingImportsCommand(params *baseParams) *cobra.Command {
@@ -66,7 +67,7 @@ func newPendingImportsCommand(params *baseParams) *cobra.Command {
6667
},
6768
}
6869
pendingImportsCmd.InheritedFlags()
69-
pendingImportsCmd.PersistentFlags().StringVarP(&projectID, "project-id", "", "", "Project ID")
70+
pendingImportsCmd.PersistentFlags().StringVarP(&projectID, "project-id", "", viper.GetString("default_project_id"), "Project ID")
7071
pendingImportsCmd.PersistentFlags().BoolVarP(&withoutProject, "without-project", "", false, "Filter to pending imports without a project")
7172
pendingImportsCmd.PersistentFlags().StringVarP(&requestId, "request-id", "", "", "Request ID")
7273
pendingImportsCmd.PersistentFlags().StringVarP(&deviceId, "device-id", "", "", "Device ID")

foxglove/cmd/recordings.go

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66

77
"github.com/foxglove/foxglove-cli/foxglove/api"
88
"github.com/spf13/cobra"
9+
"github.com/spf13/viper"
910
)
1011

1112
func newListRecordingsCommand(params *baseParams) *cobra.Command {
@@ -69,19 +70,19 @@ func newListRecordingsCommand(params *baseParams) *cobra.Command {
6970
},
7071
}
7172
recordingsListCmd.InheritedFlags()
72-
recordingsListCmd.PersistentFlags().StringVarP(&projectID, "project-id", "", "", "project ID")
73-
recordingsListCmd.PersistentFlags().StringVarP(&deviceID, "device-id", "", "", "device ID")
74-
recordingsListCmd.PersistentFlags().StringVarP(&deviceName, "device-name", "", "", "device name")
75-
recordingsListCmd.PersistentFlags().StringVarP(&start, "start", "", "", "start of data range (ISO8601 format)")
76-
recordingsListCmd.PersistentFlags().StringVarP(&end, "end", "", "", "end of data range (ISO8601 format)")
77-
recordingsListCmd.PersistentFlags().StringVarP(&path, "path", "", "", "recording file path")
78-
recordingsListCmd.PersistentFlags().StringVarP(&primarySiteID, "site-id", "", "", "primary site ID")
79-
recordingsListCmd.PersistentFlags().StringVarP(&edgeSiteID, "edge-site-id", "", "", "edge site ID")
80-
recordingsListCmd.PersistentFlags().StringVarP(&importStatus, "import-status", "", "", "import status")
81-
recordingsListCmd.PersistentFlags().IntVarP(&limit, "limit", "", 2000, "max number of recordings to return")
82-
recordingsListCmd.PersistentFlags().IntVarP(&offset, "offset", "", 0, "number of recordings to skip")
83-
recordingsListCmd.PersistentFlags().StringVarP(&sortBy, "sort-by", "", "", "sort recordings by a field")
84-
recordingsListCmd.PersistentFlags().StringVarP(&sortOrder, "sort-order", "", "", "sort order: 'asc' 'desc'")
73+
recordingsListCmd.PersistentFlags().StringVarP(&projectID, "project-id", "", viper.GetString("default_project_id"), "Project ID")
74+
recordingsListCmd.PersistentFlags().StringVarP(&deviceID, "device-id", "", "", "Device ID")
75+
recordingsListCmd.PersistentFlags().StringVarP(&deviceName, "device-name", "", "", "Device name")
76+
recordingsListCmd.PersistentFlags().StringVarP(&start, "start", "", "", "Start of data range (ISO8601 format)")
77+
recordingsListCmd.PersistentFlags().StringVarP(&end, "end", "", "", "End of data range (ISO8601 format)")
78+
recordingsListCmd.PersistentFlags().StringVarP(&path, "path", "", "", "Recording file path")
79+
recordingsListCmd.PersistentFlags().StringVarP(&primarySiteID, "site-id", "", "", "Primary site ID")
80+
recordingsListCmd.PersistentFlags().StringVarP(&edgeSiteID, "edge-site-id", "", "", "Edge site ID")
81+
recordingsListCmd.PersistentFlags().StringVarP(&importStatus, "import-status", "", "", "Import status")
82+
recordingsListCmd.PersistentFlags().IntVarP(&limit, "limit", "", 2000, "Max number of recordings to return")
83+
recordingsListCmd.PersistentFlags().IntVarP(&offset, "offset", "", 0, "Number of recordings to skip")
84+
recordingsListCmd.PersistentFlags().StringVarP(&sortBy, "sort-by", "", "", "Sort recordings by a field")
85+
recordingsListCmd.PersistentFlags().StringVarP(&sortOrder, "sort-order", "", "", "Sort order: 'asc' 'desc'")
8586
AddFormatFlag(recordingsListCmd, &format)
8687
AddDeviceAutocompletion(recordingsListCmd, params)
8788
AddJsonFlag(recordingsListCmd, &isJsonFormat)

foxglove/cmd/root.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,9 @@ func Execute(version string) {
127127
Short: "Data access and management",
128128
}
129129
importsCmd := &cobra.Command{
130-
Use: "imports",
131-
Short: "Query and modify data imports",
130+
Use: "imports",
131+
Short: "Query and modify data imports",
132+
Deprecated: "use 'recordings list' to list, and 'data import' to import data.",
132133
}
133134
attachmentsCmd := &cobra.Command{
134135
Use: "attachments",
@@ -162,6 +163,7 @@ func Execute(version string) {
162163
Use: "projects",
163164
Short: "List and manage projects",
164165
}
166+
configCmd := newConfigCommand()
165167

166168
var clientID, cfgFile string
167169
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "", "", "config file (default is $HOME/.foxglove.yaml)")
@@ -191,12 +193,13 @@ func Execute(version string) {
191193
baseURL: defaultString(viper.GetString("base_url"), defaultBaseURL),
192194
}
193195

194-
addImportCmd, err := newImportCommand(params, "add")
196+
deprecatedMsg := "use 'data import' instead."
197+
addImportCmd, err := newImportCommand(params, "add", &deprecatedMsg)
195198
if err != nil {
196199
fmt.Println(err)
197200
return
198201
}
199-
importShortcut, err := newImportCommand(params, "import")
202+
importShortcut, err := newImportCommand(params, "import", nil)
200203
if err != nil {
201204
fmt.Println(err)
202205
return
@@ -243,6 +246,7 @@ func Execute(version string) {
243246
eventsCmd,
244247
pendingImportsCmd,
245248
projectsCmd,
249+
configCmd,
246250
)
247251

248252
cobra.CheckErr(rootCmd.Execute())

0 commit comments

Comments
 (0)