diff --git a/packages/shared/pkg/featureflags/client.go b/packages/shared/pkg/featureflags/client.go index d4dd591c6c..7722228808 100644 --- a/packages/shared/pkg/featureflags/client.go +++ b/packages/shared/pkg/featureflags/client.go @@ -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), @@ -412,4 +416,4 @@ func (c *Client) allContexts(ctx context.Context, contexts []ldcontext.Context) } return append(all, contexts...) -} +} \ No newline at end of file diff --git a/packages/shared/pkg/featureflags/flags.go b/packages/shared/pkg/featureflags/flags.go index 592bd2e195..9ae0a20839 100644 --- a/packages/shared/pkg/featureflags/flags.go +++ b/packages/shared/pkg/featureflags/flags.go @@ -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)) @@ -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()) @@ -1828,4 +1854,4 @@ func GetExtraCompatibleNodeModels(ctx context.Context, ff *Client, contexts ...l } return extra -} +} \ No newline at end of file diff --git a/packages/shared/pkg/featureflags/flags_test.go b/packages/shared/pkg/featureflags/flags_test.go index e792bfab9c..b3e930a53a 100644 --- a/packages/shared/pkg/featureflags/flags_test.go +++ b/packages/shared/pkg/featureflags/flags_test.go @@ -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()) + }) +} \ No newline at end of file