Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
flagscmd "github.com/launchdarkly/ldcli/cmd/flags"
logincmd "github.com/launchdarkly/ldcli/cmd/login"
memberscmd "github.com/launchdarkly/ldcli/cmd/members"
sdkactivecmd "github.com/launchdarkly/ldcli/cmd/sdk_active"
resourcecmd "github.com/launchdarkly/ldcli/cmd/resources"
signupcmd "github.com/launchdarkly/ldcli/cmd/signup"
sourcemapscmd "github.com/launchdarkly/ldcli/cmd/sourcemaps"
Expand Down Expand Up @@ -227,6 +228,9 @@ func NewRootCommand(
if c.Name() == "members" {
c.AddCommand(memberscmd.NewMembersInviteCmd(clients.ResourcesClient))
}
if c.Name() == "environments" {
c.AddCommand(sdkactivecmd.NewSdkActiveCmd(clients.ResourcesClient))
}
}

rootCmd.Commands = append(rootCmd.Commands, configCmd)
Expand Down
108 changes: 108 additions & 0 deletions cmd/sdk_active/sdk_active.go
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"`
}
Copy link
Copy Markdown

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 sdkActiveResponse struct uses the JSON tag json:"active" but the PR description states the API response field is sdkActive. When the real API returns {"sdkActive": true, "lastSeenAt": "..."}, json.Unmarshal won't match the active tag to the sdkActive key, so resp.Active will always be false. The test masks this by mocking the response as {"active": true} instead of the real API shape {"sdkActive": true}.

Additional Locations (1)
Fix in Cursor Fix in Web

Copy link
Copy Markdown
Contributor Author

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, not sdkActive. I verified this directly in gonfalon's UsageSdkActiveRep struct at internal/reps/usage_sdk_active_rep.go:

type UsageSdkActiveRep struct {
    Active bool `json:"active"`
}

The second commit (fix: correct response field from sdkActive to active) specifically corrected this — the original code had sdkActive which was wrong. The current code and tests use the correct field name.


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,
)
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))
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Global viper binding overwrites break existing commands

High Severity

Calling viper.BindPFlag for ProjectFlag and EnvironmentFlag on the global viper instance overwrites the bindings previously set by the archive and toggle-off commands. Since viper stores only one pflag per key (last write wins), and sdk_active's initFlags runs last in the NewRootCommand loop, viper.GetString("project") and viper.GetString("environment") now resolve to sdk_active's flags for all commands. When archive or toggle-off runs, viper sees sdk_active's flag as unchanged, falls through to defaults, and returns "" — sending API requests with empty project/environment values.

Additional Locations (1)
Fix in Cursor Fix in Web

Copy link
Copy Markdown
Contributor Author

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 same viper.BindPFlag pattern is used by toggle.go (lines 88-101) and archive.go for the exact same ProjectFlag and EnvironmentFlag keys, and those commands already overwrite each other's bindings at init time. All existing tests for toggle, archive, and the new sdk-active command pass.

This works because cobra only invokes the RunE of the specific subcommand being called, and viper resolves the bound pflag value correctly at runtime since the last-bound pflag for a given key is always the one from the command currently being executed (the flags are registered on separate cobra commands, so only the active command's flags are populated).

126 changes: 126 additions & 0 deletions cmd/sdk_active/sdk_active_test.go
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")
}