-
Notifications
You must be signed in to change notification settings - Fork 13
feat: add hand-rolled get-sdk-active command for environments #671
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8a5da58
6c6c26c
9669e40
67097e9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| package sdk_active | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "fmt" | ||
| "net/url" | ||
|
|
||
| "github.com/spf13/cobra" | ||
| "github.com/spf13/viper" | ||
|
|
||
| "github.com/launchdarkly/ldcli/cmd/cliflags" | ||
| resourcescmd "github.com/launchdarkly/ldcli/cmd/resources" | ||
| "github.com/launchdarkly/ldcli/cmd/validators" | ||
| "github.com/launchdarkly/ldcli/internal/errors" | ||
| "github.com/launchdarkly/ldcli/internal/output" | ||
| "github.com/launchdarkly/ldcli/internal/resources" | ||
| ) | ||
|
|
||
| type sdkActiveResponse struct { | ||
| Active bool `json:"active"` | ||
| } | ||
|
|
||
| func NewSdkActiveCmd(client resources.Client) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Args: validators.Validate(), | ||
| Long: "Get SDK active status for an environment. Returns information about whether any SDKs have initialized in the given environment within the past seven days.", | ||
| RunE: runGetSdkActive(client), | ||
| Short: "Get SDK active status for an environment", | ||
| Use: "get-sdk-active", | ||
| } | ||
|
|
||
| cmd.SetUsageTemplate(resourcescmd.SubcommandUsageTemplate()) | ||
| initFlags(cmd) | ||
|
|
||
| return cmd | ||
| } | ||
|
|
||
| const ( | ||
| sdkNameFlag = "sdk-name" | ||
| sdkWrapperNameFlag = "sdk-wrapper-name" | ||
| ) | ||
|
|
||
| func runGetSdkActive(client resources.Client) func(*cobra.Command, []string) error { | ||
| return func(cmd *cobra.Command, args []string) error { | ||
| path, _ := url.JoinPath( | ||
| viper.GetString(cliflags.BaseURIFlag), | ||
| "api/v2/projects", | ||
| viper.GetString(cliflags.ProjectFlag), | ||
| "environments", | ||
| viper.GetString(cliflags.EnvironmentFlag), | ||
| "sdk-active", | ||
| ) | ||
|
|
||
| query := url.Values{} | ||
| if v := viper.GetString(sdkNameFlag); v != "" { | ||
| query.Set("sdk_name", v) | ||
| } | ||
| if v := viper.GetString(sdkWrapperNameFlag); v != "" { | ||
| query.Set("sdk_wrapper_name", v) | ||
| } | ||
|
|
||
| res, err := client.MakeRequest( | ||
| viper.GetString(cliflags.AccessTokenFlag), | ||
| "GET", | ||
| path, | ||
| "application/json", | ||
| query, | ||
| nil, | ||
| false, | ||
| ) | ||
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if err != nil { | ||
| return output.NewCmdOutputError(err, cliflags.GetOutputKind(cmd)) | ||
| } | ||
|
|
||
| outputKind := cliflags.GetOutputKind(cmd) | ||
| if outputKind == "json" { | ||
| fmt.Fprint(cmd.OutOrStdout(), string(res)+"\n") | ||
| return nil | ||
| } | ||
|
|
||
| var resp sdkActiveResponse | ||
| if err := json.Unmarshal(res, &resp); err != nil { | ||
| return errors.NewError(err.Error()) | ||
| } | ||
|
|
||
| fmt.Fprintf(cmd.OutOrStdout(), "SDK active: %t\n", resp.Active) | ||
|
|
||
| return nil | ||
| } | ||
| } | ||
|
|
||
| func initFlags(cmd *cobra.Command) { | ||
| cmd.Flags().String(cliflags.ProjectFlag, "", "The project key") | ||
| _ = cmd.MarkFlagRequired(cliflags.ProjectFlag) | ||
| _ = cmd.Flags().SetAnnotation(cliflags.ProjectFlag, "required", []string{"true"}) | ||
| _ = viper.BindPFlag(cliflags.ProjectFlag, cmd.Flags().Lookup(cliflags.ProjectFlag)) | ||
|
|
||
| cmd.Flags().String(cliflags.EnvironmentFlag, "", "The environment key") | ||
| _ = cmd.MarkFlagRequired(cliflags.EnvironmentFlag) | ||
| _ = cmd.Flags().SetAnnotation(cliflags.EnvironmentFlag, "required", []string{"true"}) | ||
| _ = viper.BindPFlag(cliflags.EnvironmentFlag, cmd.Flags().Lookup(cliflags.EnvironmentFlag)) | ||
|
|
||
| cmd.Flags().String(sdkNameFlag, "", "Filter by SDK name (e.g. go-server-sdk, node-server-sdk)") | ||
| _ = viper.BindPFlag(sdkNameFlag, cmd.Flags().Lookup(sdkNameFlag)) | ||
|
|
||
| cmd.Flags().String(sdkWrapperNameFlag, "", "Filter by SDK wrapper name") | ||
| _ = viper.BindPFlag(sdkWrapperNameFlag, cmd.Flags().Lookup(sdkWrapperNameFlag)) | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Global viper binding overwrites break existing commandsHigh Severity Calling Additional Locations (1)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a false positive. The same This works because cobra only invokes the |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| package sdk_active_test | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
|
|
||
| "github.com/launchdarkly/ldcli/cmd" | ||
| "github.com/launchdarkly/ldcli/internal/analytics" | ||
| "github.com/launchdarkly/ldcli/internal/resources" | ||
| ) | ||
|
|
||
| func TestGetSdkActive(t *testing.T) { | ||
| mockClient := &resources.MockClient{ | ||
| Response: []byte(`{"active": true}`), | ||
| } | ||
| args := []string{ | ||
| "environments", "get-sdk-active", | ||
| "--access-token", "abcd1234", | ||
| "--project", "test-proj", | ||
| "--environment", "test-env", | ||
| } | ||
| output, err := cmd.CallCmd( | ||
| t, | ||
| cmd.APIClients{ | ||
| ResourcesClient: mockClient, | ||
| }, | ||
| analytics.NoopClientFn{}.Tracker(), | ||
| args, | ||
| ) | ||
|
|
||
| require.NoError(t, err) | ||
| assert.Equal(t, "SDK active: true\n", string(output)) | ||
| } | ||
|
|
||
| func TestGetSdkActiveJSON(t *testing.T) { | ||
| mockClient := &resources.MockClient{ | ||
| Response: []byte(`{"active": true}`), | ||
| } | ||
| args := []string{ | ||
| "environments", "get-sdk-active", | ||
| "--access-token", "abcd1234", | ||
| "--project", "test-proj", | ||
| "--environment", "test-env", | ||
| "--output", "json", | ||
| } | ||
| output, err := cmd.CallCmd( | ||
| t, | ||
| cmd.APIClients{ | ||
| ResourcesClient: mockClient, | ||
| }, | ||
| analytics.NoopClientFn{}.Tracker(), | ||
| args, | ||
| ) | ||
|
|
||
| require.NoError(t, err) | ||
| assert.Contains(t, string(output), `"active"`) | ||
| } | ||
|
|
||
| func TestGetSdkActiveWithSdkNameFilter(t *testing.T) { | ||
| mockClient := &resources.MockClient{ | ||
| Response: []byte(`{"active": true}`), | ||
| } | ||
| args := []string{ | ||
| "environments", "get-sdk-active", | ||
| "--access-token", "abcd1234", | ||
| "--project", "test-proj", | ||
| "--environment", "test-env", | ||
| "--sdk-name", "go-server-sdk", | ||
| } | ||
| output, err := cmd.CallCmd( | ||
| t, | ||
| cmd.APIClients{ | ||
| ResourcesClient: mockClient, | ||
| }, | ||
| analytics.NoopClientFn{}.Tracker(), | ||
| args, | ||
| ) | ||
|
|
||
| require.NoError(t, err) | ||
| assert.Equal(t, "SDK active: true\n", string(output)) | ||
| } | ||
|
|
||
| func TestGetSdkActiveWithSdkWrapperNameFilter(t *testing.T) { | ||
| mockClient := &resources.MockClient{ | ||
| Response: []byte(`{"active": false}`), | ||
| } | ||
| args := []string{ | ||
| "environments", "get-sdk-active", | ||
| "--access-token", "abcd1234", | ||
| "--project", "test-proj", | ||
| "--environment", "test-env", | ||
| "--sdk-wrapper-name", "flutter-client-sdk", | ||
| } | ||
| output, err := cmd.CallCmd( | ||
| t, | ||
| cmd.APIClients{ | ||
| ResourcesClient: mockClient, | ||
| }, | ||
| analytics.NoopClientFn{}.Tracker(), | ||
| args, | ||
| ) | ||
|
|
||
| require.NoError(t, err) | ||
| assert.Equal(t, "SDK active: false\n", string(output)) | ||
| } | ||
|
|
||
| func TestGetSdkActiveMissingRequiredFlags(t *testing.T) { | ||
| mockClient := &resources.MockClient{} | ||
| args := []string{ | ||
| "environments", "get-sdk-active", | ||
| "--access-token", "abcd1234", | ||
| } | ||
| _, err := cmd.CallCmd( | ||
| t, | ||
| cmd.APIClients{ | ||
| ResourcesClient: mockClient, | ||
| }, | ||
| analytics.NoopClientFn{}.Tracker(), | ||
| args, | ||
| ) | ||
|
|
||
| require.Error(t, err) | ||
| assert.Contains(t, err.Error(), "required") | ||
| } |


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wrong JSON tag causes field to always be false
High Severity
The
sdkActiveResponsestruct uses the JSON tagjson:"active"but the PR description states the API response field issdkActive. When the real API returns{"sdkActive": true, "lastSeenAt": "..."},json.Unmarshalwon't match theactivetag to thesdkActivekey, soresp.Activewill always befalse. The test masks this by mocking the response as{"active": true}instead of the real API shape{"sdkActive": true}.Additional Locations (1)
cmd/sdk_active/sdk_active_test.go#L14-L17There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a false positive. The API response field is
active, notsdkActive. I verified this directly in gonfalon'sUsageSdkActiveRepstruct atinternal/reps/usage_sdk_active_rep.go:The second commit (
fix: correct response field from sdkActive to active) specifically corrected this — the original code hadsdkActivewhich was wrong. The current code and tests use the correct field name.