diff --git a/cmd/root.go b/cmd/root.go index f62eb6a2..34ffdca9 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -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" @@ -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) diff --git a/cmd/sdk_active/sdk_active.go b/cmd/sdk_active/sdk_active.go new file mode 100644 index 00000000..fafae077 --- /dev/null +++ b/cmd/sdk_active/sdk_active.go @@ -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, + ) + 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)) +} diff --git a/cmd/sdk_active/sdk_active_test.go b/cmd/sdk_active/sdk_active_test.go new file mode 100644 index 00000000..602ea073 --- /dev/null +++ b/cmd/sdk_active/sdk_active_test.go @@ -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") +}