Skip to content

Commit

Permalink
plumb a new 'offline' bool throughout environment config
Browse files Browse the repository at this point in the history
  • Loading branch information
cwaldren-ld committed Jun 25, 2024
1 parent 513445d commit 7bfc26e
Show file tree
Hide file tree
Showing 12 changed files with 148 additions and 35 deletions.
1 change: 1 addition & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ type EnvConfig struct {
TTL ct.OptDuration `conf:"LD_TTL_"`
ProjKey string `conf:"LD_PROJ_KEY_"`
FilterKey FilterKey // injected based on [filters] section
Offline bool // set to true if this environment was created in offline mode
}

type FiltersConfig struct {
Expand Down
3 changes: 2 additions & 1 deletion integrationtests/api_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,13 +337,14 @@ func (a *apiHelper) createFlag(
flagPost.Variations = append(flagPost.Variations, ldapi.Variation{Value: &valueAsInterface})
}

_, _, err := a.apiClient.FeatureFlagsApi.
_, rsp, err := a.apiClient.FeatureFlagsApi.
PostFeatureFlag(a.apiContext, proj.key).
FeatureFlagBody(flagPost).
Execute()

err = a.logResult("Create flag "+flagKey+" in "+proj.key, err)
if err != nil {
fmt.Println(rsp.Body)
return err
}

Expand Down
16 changes: 8 additions & 8 deletions integrationtests/autoconfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func testPolicyUpdate(t *testing.T, manager *integrationTestManager) {
manager.awaitRelayStatus(t, func(status api.StatusRep) bool {
if len(status.Environments) == 1 {
if envStatus, ok := status.Environments[string(remainingEnv.id)]; ok {
verifyEnvProperties(t, testData.project, remainingEnv, envStatus, true)
verifyEnvProperties(t, testData.project, remainingEnv, envStatus, &envPropertyExpectations{nameAndKey: true})
return true
}
}
Expand All @@ -96,7 +96,7 @@ func testAddEnvironment(t *testing.T, manager *integrationTestManager) {
manager.awaitRelayStatus(t, func(status api.StatusRep) bool {
if len(status.Environments) == len(testData.environments)+1 {
if envStatus, ok := status.Environments[string(newEnv.id)]; ok {
verifyEnvProperties(t, testData.project, newEnv, envStatus, true)
verifyEnvProperties(t, testData.project, newEnv, envStatus, &envPropertyExpectations{nameAndKey: true})
return true
}
}
Expand Down Expand Up @@ -136,7 +136,7 @@ func testUpdatedSDKKeyWithoutExpiry(t *testing.T, manager *integrationTestManage

manager.awaitRelayStatus(t, func(status api.StatusRep) bool {
if envStatus, ok := status.Environments[string(envToUpdate.id)]; ok {
verifyEnvProperties(t, testData.project, updatedEnv, envStatus, true)
verifyEnvProperties(t, testData.project, updatedEnv, envStatus, &envPropertyExpectations{nameAndKey: true})
return last5(envStatus.SDKKey) == last5(string(newKey)) && envStatus.ExpiringSDKKey == ""
}
return false
Expand Down Expand Up @@ -166,7 +166,7 @@ func testUpdatedSDKKeyWithExpiry(t *testing.T, manager *integrationTestManager)
return false
}
if envStatus, ok := status.Environments[string(envToUpdate.id)]; ok {
verifyEnvProperties(t, testData.project, updatedEnv, envStatus, true)
verifyEnvProperties(t, testData.project, updatedEnv, envStatus, &envPropertyExpectations{nameAndKey: true})
return last5(envStatus.SDKKey) == last5(string(newKey)) &&
last5(envStatus.ExpiringSDKKey) == last5(string(oldKey))
}
Expand Down Expand Up @@ -204,13 +204,13 @@ func testUpdatedSDKKeyWithExpiryBeforeStartingRelay(t *testing.T, manager *integ
})
defer manager.stopRelay(t)

manager.awaitEnvironments(t, projAndEnvs, false, func(proj projectInfo, env environmentInfo) string {
manager.awaitEnvironments(t, projAndEnvs, nil, func(proj projectInfo, env environmentInfo) string {
return string(env.id)
})

manager.awaitRelayStatus(t, func(status api.StatusRep) bool {
if envStatus, ok := status.Environments[string(envToUpdate.id)]; ok {
verifyEnvProperties(t, testData.project, updatedEnv, envStatus, true)
verifyEnvProperties(t, testData.project, updatedEnv, envStatus, &envPropertyExpectations{nameAndKey: true})
return last5(envStatus.SDKKey) == last5(string(newKey)) &&
last5(envStatus.ExpiringSDKKey) == last5(string(oldKey))
}
Expand Down Expand Up @@ -238,7 +238,7 @@ func testUpdatedMobileKey(t *testing.T, manager *integrationTestManager) {

manager.awaitRelayStatus(t, func(status api.StatusRep) bool {
if envStatus, ok := status.Environments[string(envToUpdate.id)]; ok {
verifyEnvProperties(t, testData.project, updatedEnv, envStatus, true)
verifyEnvProperties(t, testData.project, updatedEnv, envStatus, &envPropertyExpectations{nameAndKey: true})
return last5(envStatus.MobileKey) == last5(string(newKey))
}
return false
Expand Down Expand Up @@ -286,7 +286,7 @@ func withRelayAndTestData(t *testing.T, manager *integrationTestManager, action

func awaitInitialState(t *testing.T, manager *integrationTestManager, testData autoConfigTestData) {
projsAndEnvs := projsAndEnvs{testData.project: testData.environments}
manager.awaitEnvironments(t, projsAndEnvs, true, func(proj projectInfo, env environmentInfo) string {
manager.awaitEnvironments(t, projsAndEnvs, &envPropertyExpectations{nameAndKey: true}, func(proj projectInfo, env environmentInfo) string {
return string(env.id)
})
}
73 changes: 69 additions & 4 deletions integrationtests/offline_mode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"os"
"path/filepath"
"testing"
"time"

"github.com/launchdarkly/ld-relay/v8/config"

Expand All @@ -25,8 +26,13 @@ type offlineModeTestData struct {
autoConfigID autoConfigID
}

type apiParams struct {
numProjects int
numEnvironments int
}

func testOfflineMode(t *testing.T, manager *integrationTestManager) {
withOfflineModeTestData(t, manager, func(testData offlineModeTestData) {
withOfflineModeTestData(t, manager, apiParams{numEnvironments: 2, numProjects: 2}, func(testData offlineModeTestData) {
helpers.WithTempDir(func(dirPath string) {
fileName := "archive.tar.gz"
filePath := filepath.Join(manager.relaySharedDir, fileName)
Expand All @@ -40,16 +46,75 @@ func testOfflineMode(t *testing.T, manager *integrationTestManager) {
})
defer manager.stopRelay(t)

manager.awaitEnvironments(t, testData.projsAndEnvs, true, func(proj projectInfo, env environmentInfo) string {
manager.awaitEnvironments(t, testData.projsAndEnvs, &envPropertyExpectations{nameAndKey: true}, func(proj projectInfo, env environmentInfo) string {
return string(env.id)
})
manager.verifyFlagValues(t, testData.projsAndEnvs)
})
})

// Tests that if we download an archive with a primary SDK key, and then it is subsequently updated
// with a deprecated key, we become initialized with both keys present.
withOfflineModeTestData(t, manager, apiParams{numEnvironments: 2, numProjects: 1}, func(testData offlineModeTestData) {
helpers.WithTempDir(func(dirPath string) {
fileName := "archive.tar.gz"
filePath := filepath.Join(manager.relaySharedDir, fileName)

err := downloadRelayArchive(manager, testData.autoConfigKey, filePath)
manager.apiHelper.logResult("Download data archive from /relay/latest-all to "+filePath, err)
require.NoError(t, err)

manager.startRelay(t, map[string]string{
"FILE_DATA_SOURCE": filepath.Join(relayContainerSharedDir, fileName),
"EXPIRED_CREDENTIAL_CLEANUP_INTERVAL": "100ms",
})
defer manager.stopRelay(t)

manager.awaitEnvironments(t, testData.projsAndEnvs, &envPropertyExpectations{nameAndKey: true}, func(proj projectInfo, env environmentInfo) string {
return string(env.id)
})
manager.verifyFlagValues(t, testData.projsAndEnvs)

updated := manager.rotateSDKKeys(t, testData.projsAndEnvs, time.Now().Add(1*time.Hour))

err = downloadRelayArchive(manager, testData.autoConfigKey, filePath)
manager.apiHelper.logResult("Download data archive from /relay/latest-all to "+filePath, err)
require.NoError(t, err)

manager.awaitEnvironments(t, updated, &envPropertyExpectations{nameAndKey: true, expiringSDKKey: true}, func(proj projectInfo, env environmentInfo) string {
return string(env.id)
})
})
})

// Tests that upon startup, if an archive contains a primary and deprecated key, we become initialized with both keys.
withOfflineModeTestData(t, manager, apiParams{numEnvironments: 2, numProjects: 1}, func(testData offlineModeTestData) {
helpers.WithTempDir(func(dirPath string) {
fileName := "archive.tar.gz"
filePath := filepath.Join(manager.relaySharedDir, fileName)

updated := manager.rotateSDKKeys(t, testData.projsAndEnvs, time.Now().Add(1*time.Hour))

err := downloadRelayArchive(manager, testData.autoConfigKey, filePath)
manager.apiHelper.logResult("Download data archive from /relay/latest-all to "+filePath, err)
require.NoError(t, err)

manager.startRelay(t, map[string]string{
"FILE_DATA_SOURCE": filepath.Join(relayContainerSharedDir, fileName),
"EXPIRED_CREDENTIAL_CLEANUP_INTERVAL": "100ms",
})
defer manager.stopRelay(t)

manager.awaitEnvironments(t, updated, &envPropertyExpectations{nameAndKey: true, expiringSDKKey: true}, func(proj projectInfo, env environmentInfo) string {
return string(env.id)
})
manager.verifyFlagValues(t, testData.projsAndEnvs)
})
})
}

func withOfflineModeTestData(t *testing.T, manager *integrationTestManager, fn func(offlineModeTestData)) {
projsAndEnvs, err := manager.apiHelper.createProjectsAndEnvironments(2, 2)
func withOfflineModeTestData(t *testing.T, manager *integrationTestManager, cfg apiParams, fn func(offlineModeTestData)) {
projsAndEnvs, err := manager.apiHelper.createProjectsAndEnvironments(cfg.numProjects, cfg.numEnvironments)
require.NoError(t, err)
defer manager.apiHelper.deleteProjects(projsAndEnvs)

Expand Down
15 changes: 8 additions & 7 deletions integrationtests/projects_and_environments_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@ type projectInfo struct {
}

type environmentInfo struct {
id config.EnvironmentID
key string
name string
sdkKey config.SDKKey
mobileKey config.MobileKey
prefix string
projKey string
id config.EnvironmentID
key string
name string
sdkKey config.SDKKey
expiringSdkKey config.SDKKey
mobileKey config.MobileKey
prefix string
projKey string

// this is a synthetic field, set only when this environment is a filtered environment.
filterKey config.FilterKey
Expand Down
4 changes: 2 additions & 2 deletions integrationtests/standard_mode_payload_filters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func testStandardModeWithDefaultFilters(t *testing.T, manager *integrationTestMa
manager.startRelay(t, envVars)
defer manager.stopRelay(t)

manager.awaitEnvironments(t, testData.projsAndEnvs, false, func(proj projectInfo, env environmentInfo) string {
manager.awaitEnvironments(t, testData.projsAndEnvs, nil, func(proj projectInfo, env environmentInfo) string {
if env.filterKey == "" {
return env.key
}
Expand Down Expand Up @@ -105,7 +105,7 @@ func testStandardModeWithSpecificFilters(t *testing.T, manager *integrationTestM
manager.startRelay(t, envVars)
defer manager.stopRelay(t)

manager.awaitEnvironments(t, testData.projsAndEnvs, false, func(proj projectInfo, env environmentInfo) string {
manager.awaitEnvironments(t, testData.projsAndEnvs, nil, func(proj projectInfo, env environmentInfo) string {
if env.filterKey == "" {
return env.key
}
Expand Down
2 changes: 1 addition & 1 deletion integrationtests/standard_mode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func testStandardMode(t *testing.T, manager *integrationTestManager) {
manager.startRelay(t, envVars)
defer manager.stopRelay(t)

manager.awaitEnvironments(t, testData.projsAndEnvs, false, func(proj projectInfo, env environmentInfo) string {
manager.awaitEnvironments(t, testData.projsAndEnvs, nil, func(proj projectInfo, env environmentInfo) string {
return string(env.name)
})
manager.verifyFlagValues(t, testData.projsAndEnvs)
Expand Down
35 changes: 30 additions & 5 deletions integrationtests/test_manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,8 +303,7 @@ func (m *integrationTestManager) awaitRelayStatus(t *testing.T, fn func(api.Stat
return lastStatus, success
}

func (m *integrationTestManager) awaitEnvironments(t *testing.T, projsAndEnvs projsAndEnvs,
expectNameAndKey bool, envMapKeyFn func(proj projectInfo, env environmentInfo) string) {
func (m *integrationTestManager) awaitEnvironments(t *testing.T, projsAndEnvs projsAndEnvs, expectations *envPropertyExpectations, envMapKeyFn func(proj projectInfo, env environmentInfo) string) {
_, success := m.awaitRelayStatus(t, func(status api.StatusRep) bool {
if len(status.Environments) != projsAndEnvs.countEnvs() {
return false
Expand All @@ -313,7 +312,7 @@ func (m *integrationTestManager) awaitEnvironments(t *testing.T, projsAndEnvs pr
projsAndEnvs.enumerateEnvs(func(proj projectInfo, env environmentInfo) {
mapKey := envMapKeyFn(proj, env)
if envStatus, found := status.Environments[mapKey]; found {
verifyEnvProperties(t, proj, env, envStatus, expectNameAndKey)
verifyEnvProperties(t, proj, env, envStatus, expectations)
if envStatus.Status != "connected" {
ok = false
}
Expand All @@ -328,6 +327,21 @@ func (m *integrationTestManager) awaitEnvironments(t *testing.T, projsAndEnvs pr
}
}

func (m *integrationTestManager) rotateSDKKeys(t *testing.T, existing projsAndEnvs, expiry time.Time) projsAndEnvs {
updated := make(projsAndEnvs)
for proj, envs := range existing {
updated[proj] = make([]environmentInfo, 0)
for _, env := range envs {
newKey, err := m.apiHelper.rotateSDKKey(proj, env, expiry)
require.NoError(t, err, "failed to rotate SDK key for environment %s", env.id)
env.expiringSdkKey = env.sdkKey
env.sdkKey = newKey
updated[proj] = append(updated[proj], env)
}
}
return updated
}

// verifyFlagValues hits Relay's polling evaluation endpoint and verifies that it returns the expected
// flags and values, based on the standard way we create flags for our test environments in createFlag.
func (m *integrationTestManager) verifyFlagValues(t *testing.T, projsAndEnvs projsAndEnvs) {
Expand Down Expand Up @@ -454,14 +468,25 @@ func (m *integrationTestManager) withExtraContainer(
action(container)
}

func verifyEnvProperties(t *testing.T, project projectInfo, environment environmentInfo, envStatus api.EnvironmentStatusRep, expectNameAndKey bool) {
type envPropertyExpectations struct {
nameAndKey bool
expiringSDKKey bool
}

func verifyEnvProperties(t *testing.T, project projectInfo, environment environmentInfo, envStatus api.EnvironmentStatusRep, expectations *envPropertyExpectations) {
assert.Equal(t, string(environment.id), envStatus.EnvID)
if expectNameAndKey {
if expectations == nil {
return
}
if expectations.nameAndKey {
assert.Equal(t, environment.name, envStatus.EnvName)
assert.Equal(t, environment.key, envStatus.EnvKey)
assert.Equal(t, project.name, envStatus.ProjName)
assert.Equal(t, project.key, envStatus.ProjKey)
}
if expectations.expiringSDKKey {
assert.Equal(t, environment.expiringSdkKey.Masked(), config.SDKKey(envStatus.ExpiringSDKKey).Masked())
}
}

func flagKeyForProj(proj projectInfo) string {
Expand Down
1 change: 1 addition & 0 deletions internal/envfactory/env_config_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ func (f EnvConfigFactory) MakeEnvironmentConfig(params EnvironmentParams) config
AllowedHeader: f.AllowedHeader,
SecureMode: params.SecureMode,
FilterKey: params.Identifiers.FilterKey,
Offline: params.Offline,
}
if params.TTL != 0 {
ret.TTL = ct.NewOptDuration(params.TTL)
Expand Down
3 changes: 3 additions & 0 deletions internal/envfactory/env_params.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ type EnvironmentParams struct {

// SecureMode is true if secure mode is required for this environment.
SecureMode bool

// True if the environment was created from an offline file data source.
Offline bool
}

type ExpiringSDKKey struct {
Expand Down
6 changes: 5 additions & 1 deletion internal/filedata/archive_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,12 @@ func (ar *archiveReader) GetEnvironmentMetadata(envID config.EnvironmentID) (env
if err := json.Unmarshal(data, &rep); err != nil {
return environmentMetadata{}, err
}

params := rep.Env.ToParams()
params.Offline = true // Signify that this environment is from an offline mode source

return environmentMetadata{
params: rep.Env.ToParams(),
params: params,
version: rep.Env.Version,
dataID: rep.DataID,
}, nil
Expand Down
Loading

0 comments on commit 7bfc26e

Please sign in to comment.