-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add options to sync environment at dev-server startup (#463)
* Add options for start and sync * Add initial project sync * Fix imports * Add sync * Add context * Update sync * Add sync task * Refactor settings * Add overrides * Add test cases * Move to model * PR feedback * Fix doc
- Loading branch information
Showing
7 changed files
with
307 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,5 +2,6 @@ package dev_server | |
|
||
const ( | ||
ContextFlag = "context" | ||
OverrideFlag = "override" | ||
SourceEnvironmentFlag = "source" | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ package dev_server | |
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"errors" | ||
"log" | ||
"os/exec" | ||
|
@@ -10,10 +11,12 @@ import ( | |
"github.com/spf13/cobra" | ||
"github.com/spf13/viper" | ||
|
||
"github.com/launchdarkly/go-sdk-common/v3/ldcontext" | ||
"github.com/launchdarkly/ldcli/cmd/cliflags" | ||
resourcescmd "github.com/launchdarkly/ldcli/cmd/resources" | ||
"github.com/launchdarkly/ldcli/cmd/validators" | ||
"github.com/launchdarkly/ldcli/internal/dev_server" | ||
"github.com/launchdarkly/ldcli/internal/dev_server/model" | ||
) | ||
|
||
func NewStartServerCmd(client dev_server.Client) *cobra.Command { | ||
|
@@ -28,17 +31,61 @@ func NewStartServerCmd(client dev_server.Client) *cobra.Command { | |
|
||
cmd.SetUsageTemplate(resourcescmd.SubcommandUsageTemplate()) | ||
|
||
cmd.Flags().String(cliflags.ProjectFlag, "", "The project key") | ||
_ = viper.BindPFlag(cliflags.ProjectFlag, cmd.Flags().Lookup(cliflags.ProjectFlag)) | ||
|
||
cmd.Flags().String(SourceEnvironmentFlag, "", "environment to copy flag values from") | ||
_ = viper.BindPFlag(SourceEnvironmentFlag, cmd.Flags().Lookup(SourceEnvironmentFlag)) | ||
|
||
cmd.Flags().String(ContextFlag, "", `Stringified JSON representation of your context object ex. {"kind": "multi", "user": { "email": "[email protected]", "username": "foo", "key": "bar"}`) | ||
_ = viper.BindPFlag(ContextFlag, cmd.Flags().Lookup(ContextFlag)) | ||
|
||
cmd.Flags().String(OverrideFlag, "", `Stringified JSON representation of flag overrides ex. {"flagName": true, "stringFlagName": "test" }`) | ||
_ = viper.BindPFlag(OverrideFlag, cmd.Flags().Lookup(OverrideFlag)) | ||
|
||
return cmd | ||
} | ||
|
||
func startServer(client dev_server.Client) func(*cobra.Command, []string) error { | ||
return func(cmd *cobra.Command, args []string) error { | ||
ctx := context.Background() | ||
|
||
var initialSetting model.InitialProjectSettings | ||
|
||
if viper.IsSet(cliflags.ProjectFlag) && viper.IsSet(SourceEnvironmentFlag) { | ||
|
||
initialSetting = model.InitialProjectSettings{ | ||
Enabled: true, | ||
ProjectKey: viper.GetString(cliflags.ProjectFlag), | ||
EnvKey: viper.GetString(SourceEnvironmentFlag), | ||
} | ||
if viper.IsSet(ContextFlag) { | ||
var c ldcontext.Context | ||
contextString := viper.GetString(ContextFlag) | ||
err := c.UnmarshalJSON([]byte(contextString)) | ||
if err != nil { | ||
return err | ||
} | ||
initialSetting.Context = &c | ||
} | ||
|
||
if viper.IsSet(OverrideFlag) { | ||
var override map[string]model.FlagValue | ||
overrideString := viper.GetString(OverrideFlag) | ||
err := json.Unmarshal([]byte(overrideString), &override) | ||
if err != nil { | ||
return err | ||
} | ||
initialSetting.Overrides = override | ||
} | ||
} | ||
|
||
params := dev_server.ServerParams{ | ||
AccessToken: viper.GetString(cliflags.AccessTokenFlag), | ||
BaseURI: viper.GetString(cliflags.BaseURIFlag), | ||
DevStreamURI: viper.GetString(cliflags.DevStreamURIFlag), | ||
Port: viper.GetString(cliflags.PortFlag), | ||
AccessToken: viper.GetString(cliflags.AccessTokenFlag), | ||
BaseURI: viper.GetString(cliflags.BaseURIFlag), | ||
DevStreamURI: viper.GetString(cliflags.DevStreamURIFlag), | ||
Port: viper.GetString(cliflags.PortFlag), | ||
InitialProjectSettings: initialSetting, | ||
} | ||
|
||
client.RunServer(ctx, params) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package adapters | ||
|
||
import ( | ||
"context" | ||
ldapi "github.com/launchdarkly/api-client-go/v14" | ||
) | ||
|
||
func WithApiAndSdk(ctx context.Context, client ldapi.APIClient, streamingUrl string) context.Context { | ||
ctx = WithSdk(ctx, newSdk(streamingUrl)) | ||
ctx = WithApi(ctx, NewApi(client)) | ||
return ctx | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
package model | ||
|
||
import ( | ||
"context" | ||
"log" | ||
|
||
"github.com/pkg/errors" | ||
|
||
"github.com/launchdarkly/go-sdk-common/v3/ldcontext" | ||
"github.com/launchdarkly/go-sdk-common/v3/ldvalue" | ||
) | ||
|
||
type FlagValue = ldvalue.Value | ||
|
||
type InitialProjectSettings struct { | ||
Enabled bool | ||
ProjectKey string | ||
EnvKey string | ||
Context *ldcontext.Context `json:"context,omitempty"` | ||
Overrides map[string]FlagValue `json:"overrides,omitempty"` | ||
} | ||
|
||
func CreateOrSyncProject(ctx context.Context, settings InitialProjectSettings) error { | ||
if !settings.Enabled { | ||
return nil | ||
} | ||
|
||
log.Printf("Initial project [%s] with env [%s]", settings.ProjectKey, settings.EnvKey) | ||
var project Project | ||
project, createError := CreateProject(ctx, settings.ProjectKey, settings.EnvKey, settings.Context) | ||
if createError != nil { | ||
if errors.Is(createError, ErrAlreadyExists) { | ||
log.Printf("Project [%s] exists, refreshing data", settings.ProjectKey) | ||
var updateErr error | ||
project, updateErr = UpdateProject(ctx, settings.ProjectKey, settings.Context, &settings.EnvKey) | ||
if updateErr != nil { | ||
return updateErr | ||
} | ||
|
||
} else { | ||
return createError | ||
} | ||
} | ||
for flagKey, val := range settings.Overrides { | ||
_, err := UpsertOverride(ctx, settings.ProjectKey, flagKey, val) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
|
||
log.Printf("Successfully synced Initial project [%s]", project.Key) | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
package model_test | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
|
||
"github.com/pkg/errors" | ||
"github.com/stretchr/testify/assert" | ||
"go.uber.org/mock/gomock" | ||
|
||
ldapi "github.com/launchdarkly/api-client-go/v14" | ||
"github.com/launchdarkly/go-sdk-common/v3/ldcontext" | ||
"github.com/launchdarkly/go-sdk-common/v3/ldvalue" | ||
"github.com/launchdarkly/go-server-sdk/v7/interfaces/flagstate" | ||
adapters_mocks "github.com/launchdarkly/ldcli/internal/dev_server/adapters/mocks" | ||
"github.com/launchdarkly/ldcli/internal/dev_server/model" | ||
"github.com/launchdarkly/ldcli/internal/dev_server/model/mocks" | ||
) | ||
|
||
func TestInitialSync(t *testing.T) { | ||
|
||
ctx := context.Background() | ||
mockController := gomock.NewController(t) | ||
observers := model.NewObservers() | ||
ctx, api, sdk := adapters_mocks.WithMockApiAndSdk(ctx, mockController) | ||
store := mocks.NewMockStore(mockController) | ||
ctx = model.ContextWithStore(ctx, store) | ||
ctx = model.SetObserversOnContext(ctx, observers) | ||
projKey := "proj" | ||
sourceEnvKey := "env" | ||
sdkKey := "thing" | ||
|
||
allFlagsState := flagstate.NewAllFlagsBuilder(). | ||
AddFlag("boolFlag", flagstate.FlagState{Value: ldvalue.Bool(true)}). | ||
Build() | ||
|
||
trueVariationId, falseVariationId := "true", "false" | ||
allFlags := []ldapi.FeatureFlag{{ | ||
Name: "bool flag", | ||
Kind: "bool", | ||
Key: "boolFlag", | ||
Variations: []ldapi.Variation{ | ||
{ | ||
Id: &trueVariationId, | ||
Value: true, | ||
}, | ||
{ | ||
Id: &falseVariationId, | ||
Value: false, | ||
}, | ||
}, | ||
}} | ||
|
||
t.Run("Returns no error if disabled", func(t *testing.T) { | ||
input := model.InitialProjectSettings{ | ||
Enabled: false, | ||
ProjectKey: projKey, | ||
EnvKey: sourceEnvKey, | ||
Context: nil, | ||
Overrides: nil, | ||
} | ||
err := model.CreateOrSyncProject(ctx, input) | ||
assert.NoError(t, err) | ||
}) | ||
|
||
t.Run("Returns error if it cant fetch flag state", func(t *testing.T) { | ||
api.EXPECT().GetSdkKey(gomock.Any(), projKey, sourceEnvKey).Return("", errors.New("fetch flag state fails")) | ||
input := model.InitialProjectSettings{ | ||
Enabled: true, | ||
ProjectKey: projKey, | ||
EnvKey: sourceEnvKey, | ||
Context: nil, | ||
Overrides: nil, | ||
} | ||
err := model.CreateOrSyncProject(ctx, input) | ||
assert.NotNil(t, err) | ||
assert.Equal(t, "fetch flag state fails", err.Error()) | ||
}) | ||
|
||
t.Run("Returns error if it can't fetch flags", func(t *testing.T) { | ||
api.EXPECT().GetSdkKey(gomock.Any(), projKey, sourceEnvKey).Return(sdkKey, nil) | ||
sdk.EXPECT().GetAllFlagsState(gomock.Any(), gomock.Any(), sdkKey).Return(allFlagsState, nil) | ||
api.EXPECT().GetAllFlags(gomock.Any(), projKey).Return(nil, errors.New("fetch flags failed")) | ||
input := model.InitialProjectSettings{ | ||
Enabled: true, | ||
ProjectKey: projKey, | ||
EnvKey: sourceEnvKey, | ||
Context: nil, | ||
Overrides: nil, | ||
} | ||
err := model.CreateOrSyncProject(ctx, input) | ||
assert.NotNil(t, err) | ||
assert.Equal(t, "fetch flags failed", err.Error()) | ||
}) | ||
|
||
t.Run("Returns error if it fails to insert the project", func(t *testing.T) { | ||
api.EXPECT().GetSdkKey(gomock.Any(), projKey, sourceEnvKey).Return(sdkKey, nil) | ||
sdk.EXPECT().GetAllFlagsState(gomock.Any(), gomock.Any(), sdkKey).Return(allFlagsState, nil) | ||
api.EXPECT().GetAllFlags(gomock.Any(), projKey).Return(allFlags, nil) | ||
store.EXPECT().InsertProject(gomock.Any(), gomock.Any()).Return(errors.New("insert fails")) | ||
|
||
input := model.InitialProjectSettings{ | ||
Enabled: true, | ||
ProjectKey: projKey, | ||
EnvKey: sourceEnvKey, | ||
Context: nil, | ||
Overrides: nil, | ||
} | ||
err := model.CreateOrSyncProject(ctx, input) | ||
assert.NotNil(t, err) | ||
assert.Equal(t, "insert fails", err.Error()) | ||
}) | ||
|
||
t.Run("Successfully creates project", func(t *testing.T) { | ||
api.EXPECT().GetSdkKey(gomock.Any(), projKey, sourceEnvKey).Return(sdkKey, nil) | ||
sdk.EXPECT().GetAllFlagsState(gomock.Any(), gomock.Any(), sdkKey).Return(allFlagsState, nil) | ||
api.EXPECT().GetAllFlags(gomock.Any(), projKey).Return(allFlags, nil) | ||
store.EXPECT().InsertProject(gomock.Any(), gomock.Any()).Return(nil) | ||
|
||
input := model.InitialProjectSettings{ | ||
Enabled: true, | ||
ProjectKey: projKey, | ||
EnvKey: sourceEnvKey, | ||
Context: nil, | ||
Overrides: nil, | ||
} | ||
err := model.CreateOrSyncProject(ctx, input) | ||
|
||
assert.NoError(t, err) | ||
}) | ||
t.Run("Successfully creates project with override", func(t *testing.T) { | ||
override := model.Override{ | ||
ProjectKey: projKey, | ||
FlagKey: "boolFlag", | ||
Value: ldvalue.Bool(true), | ||
Active: true, | ||
Version: 1, | ||
} | ||
|
||
proj := model.Project{ | ||
Key: projKey, | ||
SourceEnvironmentKey: sourceEnvKey, | ||
Context: ldcontext.New(t.Name()), | ||
AllFlagsState: map[string]model.FlagState{ | ||
"boolFlag": { | ||
Version: 0, | ||
Value: ldvalue.Bool(false), | ||
}, | ||
}, | ||
} | ||
|
||
api.EXPECT().GetSdkKey(gomock.Any(), projKey, sourceEnvKey).Return(sdkKey, nil) | ||
sdk.EXPECT().GetAllFlagsState(gomock.Any(), gomock.Any(), sdkKey).Return(allFlagsState, nil) | ||
api.EXPECT().GetAllFlags(gomock.Any(), projKey).Return(allFlags, nil) | ||
store.EXPECT().InsertProject(gomock.Any(), gomock.Any()).Return(nil) | ||
store.EXPECT().UpsertOverride(gomock.Any(), override).Return(override, nil) | ||
store.EXPECT().GetDevProject(gomock.Any(), projKey).Return(&proj, nil) | ||
|
||
input := model.InitialProjectSettings{ | ||
Enabled: true, | ||
ProjectKey: projKey, | ||
EnvKey: sourceEnvKey, | ||
Context: nil, | ||
Overrides: map[string]model.FlagValue{ | ||
"boolFlag": ldvalue.Bool(true), | ||
}, | ||
} | ||
err := model.CreateOrSyncProject(ctx, input) | ||
|
||
assert.NoError(t, err) | ||
}) | ||
|
||
} |