Skip to content

Commit b3c447b

Browse files
feat: add hand-rolled get-sdk-active command for environments (#671)
* feat: add hand-rolled get-sdk-active command for environments Adds a custom CLI command to check SDK active status for an environment. This endpoint (GET /api/v2/projects/{projectKey}/environments/{environmentKey}/sdk-active) is hidden in the API spec and cannot be synced via make openapi-spec-update, so it is implemented as a hand-rolled command registered under the environments parent command. The command accepts --project and --environment flags and supports both plaintext and JSON output formats. Co-Authored-By: Ari Salem <asalem@launchdarkly.com> * fix: correct response field from sdkActive to active The gonfalon UsageSdkActiveRep struct uses json:"active", not json:"sdkActive". Updated the response struct and tests to match the actual API response shape. Co-Authored-By: Ari Salem <asalem@launchdarkly.com> * feat: add optional sdk-name and sdk-wrapper-name filters The gonfalon endpoint supports sdk_name and sdk_wrapper_name query parameters to narrow the active check to a specific SDK. These are now exposed as optional --sdk-name and --sdk-wrapper-name flags. Co-Authored-By: Ari Salem <asalem@launchdarkly.com> * fix: pass query params via MakeRequest instead of URL embedding MakeRequest overwrites req.URL.RawQuery with the query argument, so embedding params in the URL string was silently stripping them. Now sdk_name and sdk_wrapper_name are passed via url.Values through the query parameter. Co-Authored-By: Ari Salem <asalem@launchdarkly.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Ari Salem <asalem@launchdarkly.com>
1 parent 01d5c46 commit b3c447b

File tree

3 files changed

+238
-0
lines changed

3 files changed

+238
-0
lines changed

cmd/root.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
flagscmd "github.com/launchdarkly/ldcli/cmd/flags"
2222
logincmd "github.com/launchdarkly/ldcli/cmd/login"
2323
memberscmd "github.com/launchdarkly/ldcli/cmd/members"
24+
sdkactivecmd "github.com/launchdarkly/ldcli/cmd/sdk_active"
2425
resourcecmd "github.com/launchdarkly/ldcli/cmd/resources"
2526
signupcmd "github.com/launchdarkly/ldcli/cmd/signup"
2627
sourcemapscmd "github.com/launchdarkly/ldcli/cmd/sourcemaps"
@@ -227,6 +228,9 @@ func NewRootCommand(
227228
if c.Name() == "members" {
228229
c.AddCommand(memberscmd.NewMembersInviteCmd(clients.ResourcesClient))
229230
}
231+
if c.Name() == "environments" {
232+
c.AddCommand(sdkactivecmd.NewSdkActiveCmd(clients.ResourcesClient))
233+
}
230234
}
231235

232236
rootCmd.Commands = append(rootCmd.Commands, configCmd)

cmd/sdk_active/sdk_active.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package sdk_active
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/url"
7+
8+
"github.com/spf13/cobra"
9+
"github.com/spf13/viper"
10+
11+
"github.com/launchdarkly/ldcli/cmd/cliflags"
12+
resourcescmd "github.com/launchdarkly/ldcli/cmd/resources"
13+
"github.com/launchdarkly/ldcli/cmd/validators"
14+
"github.com/launchdarkly/ldcli/internal/errors"
15+
"github.com/launchdarkly/ldcli/internal/output"
16+
"github.com/launchdarkly/ldcli/internal/resources"
17+
)
18+
19+
type sdkActiveResponse struct {
20+
Active bool `json:"active"`
21+
}
22+
23+
func NewSdkActiveCmd(client resources.Client) *cobra.Command {
24+
cmd := &cobra.Command{
25+
Args: validators.Validate(),
26+
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.",
27+
RunE: runGetSdkActive(client),
28+
Short: "Get SDK active status for an environment",
29+
Use: "get-sdk-active",
30+
}
31+
32+
cmd.SetUsageTemplate(resourcescmd.SubcommandUsageTemplate())
33+
initFlags(cmd)
34+
35+
return cmd
36+
}
37+
38+
const (
39+
sdkNameFlag = "sdk-name"
40+
sdkWrapperNameFlag = "sdk-wrapper-name"
41+
)
42+
43+
func runGetSdkActive(client resources.Client) func(*cobra.Command, []string) error {
44+
return func(cmd *cobra.Command, args []string) error {
45+
path, _ := url.JoinPath(
46+
viper.GetString(cliflags.BaseURIFlag),
47+
"api/v2/projects",
48+
viper.GetString(cliflags.ProjectFlag),
49+
"environments",
50+
viper.GetString(cliflags.EnvironmentFlag),
51+
"sdk-active",
52+
)
53+
54+
query := url.Values{}
55+
if v := viper.GetString(sdkNameFlag); v != "" {
56+
query.Set("sdk_name", v)
57+
}
58+
if v := viper.GetString(sdkWrapperNameFlag); v != "" {
59+
query.Set("sdk_wrapper_name", v)
60+
}
61+
62+
res, err := client.MakeRequest(
63+
viper.GetString(cliflags.AccessTokenFlag),
64+
"GET",
65+
path,
66+
"application/json",
67+
query,
68+
nil,
69+
false,
70+
)
71+
if err != nil {
72+
return output.NewCmdOutputError(err, cliflags.GetOutputKind(cmd))
73+
}
74+
75+
outputKind := cliflags.GetOutputKind(cmd)
76+
if outputKind == "json" {
77+
fmt.Fprint(cmd.OutOrStdout(), string(res)+"\n")
78+
return nil
79+
}
80+
81+
var resp sdkActiveResponse
82+
if err := json.Unmarshal(res, &resp); err != nil {
83+
return errors.NewError(err.Error())
84+
}
85+
86+
fmt.Fprintf(cmd.OutOrStdout(), "SDK active: %t\n", resp.Active)
87+
88+
return nil
89+
}
90+
}
91+
92+
func initFlags(cmd *cobra.Command) {
93+
cmd.Flags().String(cliflags.ProjectFlag, "", "The project key")
94+
_ = cmd.MarkFlagRequired(cliflags.ProjectFlag)
95+
_ = cmd.Flags().SetAnnotation(cliflags.ProjectFlag, "required", []string{"true"})
96+
_ = viper.BindPFlag(cliflags.ProjectFlag, cmd.Flags().Lookup(cliflags.ProjectFlag))
97+
98+
cmd.Flags().String(cliflags.EnvironmentFlag, "", "The environment key")
99+
_ = cmd.MarkFlagRequired(cliflags.EnvironmentFlag)
100+
_ = cmd.Flags().SetAnnotation(cliflags.EnvironmentFlag, "required", []string{"true"})
101+
_ = viper.BindPFlag(cliflags.EnvironmentFlag, cmd.Flags().Lookup(cliflags.EnvironmentFlag))
102+
103+
cmd.Flags().String(sdkNameFlag, "", "Filter by SDK name (e.g. go-server-sdk, node-server-sdk)")
104+
_ = viper.BindPFlag(sdkNameFlag, cmd.Flags().Lookup(sdkNameFlag))
105+
106+
cmd.Flags().String(sdkWrapperNameFlag, "", "Filter by SDK wrapper name")
107+
_ = viper.BindPFlag(sdkWrapperNameFlag, cmd.Flags().Lookup(sdkWrapperNameFlag))
108+
}

cmd/sdk_active/sdk_active_test.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package sdk_active_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/launchdarkly/ldcli/cmd"
10+
"github.com/launchdarkly/ldcli/internal/analytics"
11+
"github.com/launchdarkly/ldcli/internal/resources"
12+
)
13+
14+
func TestGetSdkActive(t *testing.T) {
15+
mockClient := &resources.MockClient{
16+
Response: []byte(`{"active": true}`),
17+
}
18+
args := []string{
19+
"environments", "get-sdk-active",
20+
"--access-token", "abcd1234",
21+
"--project", "test-proj",
22+
"--environment", "test-env",
23+
}
24+
output, err := cmd.CallCmd(
25+
t,
26+
cmd.APIClients{
27+
ResourcesClient: mockClient,
28+
},
29+
analytics.NoopClientFn{}.Tracker(),
30+
args,
31+
)
32+
33+
require.NoError(t, err)
34+
assert.Equal(t, "SDK active: true\n", string(output))
35+
}
36+
37+
func TestGetSdkActiveJSON(t *testing.T) {
38+
mockClient := &resources.MockClient{
39+
Response: []byte(`{"active": true}`),
40+
}
41+
args := []string{
42+
"environments", "get-sdk-active",
43+
"--access-token", "abcd1234",
44+
"--project", "test-proj",
45+
"--environment", "test-env",
46+
"--output", "json",
47+
}
48+
output, err := cmd.CallCmd(
49+
t,
50+
cmd.APIClients{
51+
ResourcesClient: mockClient,
52+
},
53+
analytics.NoopClientFn{}.Tracker(),
54+
args,
55+
)
56+
57+
require.NoError(t, err)
58+
assert.Contains(t, string(output), `"active"`)
59+
}
60+
61+
func TestGetSdkActiveWithSdkNameFilter(t *testing.T) {
62+
mockClient := &resources.MockClient{
63+
Response: []byte(`{"active": true}`),
64+
}
65+
args := []string{
66+
"environments", "get-sdk-active",
67+
"--access-token", "abcd1234",
68+
"--project", "test-proj",
69+
"--environment", "test-env",
70+
"--sdk-name", "go-server-sdk",
71+
}
72+
output, err := cmd.CallCmd(
73+
t,
74+
cmd.APIClients{
75+
ResourcesClient: mockClient,
76+
},
77+
analytics.NoopClientFn{}.Tracker(),
78+
args,
79+
)
80+
81+
require.NoError(t, err)
82+
assert.Equal(t, "SDK active: true\n", string(output))
83+
}
84+
85+
func TestGetSdkActiveWithSdkWrapperNameFilter(t *testing.T) {
86+
mockClient := &resources.MockClient{
87+
Response: []byte(`{"active": false}`),
88+
}
89+
args := []string{
90+
"environments", "get-sdk-active",
91+
"--access-token", "abcd1234",
92+
"--project", "test-proj",
93+
"--environment", "test-env",
94+
"--sdk-wrapper-name", "flutter-client-sdk",
95+
}
96+
output, err := cmd.CallCmd(
97+
t,
98+
cmd.APIClients{
99+
ResourcesClient: mockClient,
100+
},
101+
analytics.NoopClientFn{}.Tracker(),
102+
args,
103+
)
104+
105+
require.NoError(t, err)
106+
assert.Equal(t, "SDK active: false\n", string(output))
107+
}
108+
109+
func TestGetSdkActiveMissingRequiredFlags(t *testing.T) {
110+
mockClient := &resources.MockClient{}
111+
args := []string{
112+
"environments", "get-sdk-active",
113+
"--access-token", "abcd1234",
114+
}
115+
_, err := cmd.CallCmd(
116+
t,
117+
cmd.APIClients{
118+
ResourcesClient: mockClient,
119+
},
120+
analytics.NoopClientFn{}.Tracker(),
121+
args,
122+
)
123+
124+
require.Error(t, err)
125+
assert.Contains(t, err.Error(), "required")
126+
}

0 commit comments

Comments
 (0)