Skip to content
Open
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
6 changes: 5 additions & 1 deletion packages/shared/pkg/featureflags/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ func NewClientWithLogLevel(deploymentEnvironment, serviceName, serviceVersion st
return nil, firecrackerVersionsEnvErr
}

if extraCompatibleNodeModelsEnvErr != nil {
return nil, extraCompatibleNodeModelsEnvErr
}

cfg := ldclient.Config{
ApplicationInfo: applicationInfo(deploymentEnvironment, serviceName, serviceVersion),
Logging: ldcomponents.Logging().MinLevel(logLevel),
Expand Down Expand Up @@ -412,4 +416,4 @@ func (c *Client) allContexts(ctx context.Context, contexts []ldcontext.Context)
}

return append(all, contexts...)
}
}
30 changes: 28 additions & 2 deletions packages/shared/pkg/featureflags/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -1182,6 +1182,32 @@ func FirecrackerVersionsFallback(base map[string]string, raw string) (map[string
// A malformed override keeps the baked map here and fails NewClient, so the service stops with the message instead of a package-init panic.
var firecrackerVersionsFallback, firecrackerVersionsEnvErr = FirecrackerVersionsFallback(FirecrackerVersionMap, env.GetEnv(FirecrackerVersionsEnv, ""))

// ExtraCompatibleNodeModelsEnv overrides the extra-compatible-node-models fallback where
// there is no LaunchDarkly: a JSON object of build CPU model to the node CPU models that
// build may run on, e.g. {"106":["173"]}. Pairs are one-way and architecture and family
// must still match at placement; this only decides where the value comes from, not the rule.
// LaunchDarkly, where present, still overrides it.
const ExtraCompatibleNodeModelsEnv = "EXTRA_COMPATIBLE_NODE_MODELS"

// parseExtraCompatibleNodeModels turns the ExtraCompatibleNodeModelsEnv raw value into the
// flag fallback. Empty is null (no extra pairs). A malformed value fails NewClient via
// extraCompatibleNodeModelsEnvErr, like FirecrackerVersions, so the service stops with the
// message instead of silently placing on the base compatibility rule.
func parseExtraCompatibleNodeModels(raw string) (ldvalue.Value, error) {
if strings.TrimSpace(raw) == "" {
return ldvalue.Null(), nil
}

var m map[string][]string
if err := json.Unmarshal([]byte(raw), &m); err != nil {
return ldvalue.Null(), fmt.Errorf("%s: expected a JSON object of build model to node models: %w", ExtraCompatibleNodeModelsEnv, err)
}

return ldvalue.FromJSONMarshal(m), nil
}

var extraCompatibleNodeModelsValue, extraCompatibleNodeModelsEnvErr = parseExtraCompatibleNodeModels(env.GetEnv(ExtraCompatibleNodeModelsEnv, ""))

// BuildIoEngine Sync is used by default as there seems to be a bad interaction between Async and a lot of io operations.
var (
BuildFirecrackerVersion = NewStringFlag("build-firecracker-version", env.GetEnv("DEFAULT_FIRECRACKER_VERSION", DefaultFirecrackerVersion))
Expand Down Expand Up @@ -1311,7 +1337,7 @@ var (
//
// Null (the default) or a malformed value adds nothing. It applies to every
// placement, and FsOnlyResumeCPUModelFlag still narrows on top of it.
ExtraCompatibleNodeModelsFlag = NewJSONFlag("extra-compatible-node-models", ldvalue.Null())
ExtraCompatibleNodeModelsFlag = NewJSONFlag("extra-compatible-node-models", extraCompatibleNodeModelsValue)

DefaultPersistentVolumeType = NewStringFlag("default-persistent-volume-type", "")
BuildNodeInfo = NewJSONFlag("preferred-build-node", ldvalue.Null())
Expand Down Expand Up @@ -1828,4 +1854,4 @@ func GetExtraCompatibleNodeModels(ctx context.Context, ff *Client, contexts ...l
}

return extra
}
}
40 changes: 40 additions & 0 deletions packages/shared/pkg/featureflags/flags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -498,3 +498,43 @@ func TestResolveFirecrackerVersion_OverriddenLineServesNewRelease(t *testing.T)
assert.Equal(t, "v1.14-0.3.1", ResolveFirecrackerVersion(t.Context(), client, "v1.14-0.3.1"))
assert.Equal(t, "v1.14.1_431f1fc", ResolveFirecrackerVersion(t.Context(), client, "v1.14.1_431f1fc"))
}

func TestParseExtraCompatibleNodeModels(t *testing.T) {
t.Parallel()

t.Run("empty is null without error", func(t *testing.T) {
t.Parallel()
v, err := parseExtraCompatibleNodeModels("")
require.NoError(t, err)
assert.True(t, v.IsNull())
})

t.Run("whitespace is null without error", func(t *testing.T) {
t.Parallel()
v, err := parseExtraCompatibleNodeModels(" \n ")
require.NoError(t, err)
assert.True(t, v.IsNull())
})

t.Run("object parses to the flag value", func(t *testing.T) {
t.Parallel()
v, err := parseExtraCompatibleNodeModels(`{"106":["173"]}`)
require.NoError(t, err)

td := ldtestdata.DataSource()
td.Update(td.Flag(ExtraCompatibleNodeModelsFlag.Key()).ValueForAll(v))
client, err := NewClientWithDatasource(td)
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, client.Close(context.WithoutCancel(t.Context()))) })

assert.Equal(t, map[string][]string{"106": {"173"}}, GetExtraCompatibleNodeModels(t.Context(), client))
})

t.Run("malformed JSON errors and is null", func(t *testing.T) {
t.Parallel()
v, err := parseExtraCompatibleNodeModels(`{"106":`)
require.Error(t, err)
assert.Contains(t, err.Error(), ExtraCompatibleNodeModelsEnv)
assert.True(t, v.IsNull())
})
}