-
Notifications
You must be signed in to change notification settings - Fork 173
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(flags): Add OpenFeature integration
This adds an OpenFeature-based feature flags module along with a built-in provider based on flagd.
- Loading branch information
Showing
29 changed files
with
996 additions
and
40 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
44 changes: 44 additions & 0 deletions
44
kork-core/src/main/java/com/netflix/spinnaker/kork/BootstrapComponents.java
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,44 @@ | ||
/* | ||
* Copyright 2024 Apple, Inc. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package com.netflix.spinnaker.kork; | ||
|
||
import com.netflix.spinnaker.kork.dynamicconfig.OpenFeatureConfigConfiguration; | ||
import com.netflix.spinnaker.kork.dynamicconfig.TransientConfigConfiguration; | ||
import com.netflix.spinnaker.kork.version.ServiceVersion; | ||
import com.netflix.spinnaker.kork.version.SpringPackageVersionResolver; | ||
import com.netflix.spinnaker.kork.version.VersionResolver; | ||
import java.util.List; | ||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; | ||
import org.springframework.context.ApplicationContext; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Import; | ||
|
||
@Import({OpenFeatureConfigConfiguration.class, TransientConfigConfiguration.class}) | ||
public class BootstrapComponents { | ||
@Bean | ||
@ConditionalOnMissingBean(VersionResolver.class) | ||
public static VersionResolver versionResolver(ApplicationContext applicationContext) { | ||
return new SpringPackageVersionResolver(applicationContext); | ||
} | ||
|
||
@Bean | ||
@ConditionalOnMissingBean(ServiceVersion.class) | ||
public static ServiceVersion serviceVersion( | ||
ApplicationContext applicationContext, List<VersionResolver> versionResolvers) { | ||
return new ServiceVersion(applicationContext, versionResolvers); | ||
} | ||
} |
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
39 changes: 39 additions & 0 deletions
39
...rc/main/java/com/netflix/spinnaker/kork/dynamicconfig/OpenFeatureConfigConfiguration.java
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,39 @@ | ||
/* | ||
* Copyright 2024 Apple, Inc. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
* | ||
*/ | ||
|
||
package com.netflix.spinnaker.kork.dynamicconfig; | ||
|
||
import com.netflix.spinnaker.kork.flags.EnableOpenFeature; | ||
import dev.openfeature.sdk.OpenFeatureAPI; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.core.env.Environment; | ||
import org.springframework.stereotype.Component; | ||
|
||
/** | ||
* Configuration to enable OpenFeature and set up a {@link DynamicConfigService} bean based on it. | ||
*/ | ||
@Component | ||
@EnableOpenFeature | ||
public class OpenFeatureConfigConfiguration { | ||
@Bean | ||
public OpenFeatureDynamicConfigService openFeatureDynamicConfigService( | ||
OpenFeatureAPI api, Environment environment) { | ||
var fallback = new SpringDynamicConfigService(); | ||
fallback.setEnvironment(environment); | ||
return new OpenFeatureDynamicConfigService(api.getClient(), fallback); | ||
} | ||
} |
120 changes: 120 additions & 0 deletions
120
...c/main/java/com/netflix/spinnaker/kork/dynamicconfig/OpenFeatureDynamicConfigService.java
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,120 @@ | ||
/* | ||
* Copyright 2024 Apple, Inc. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
* | ||
*/ | ||
|
||
package com.netflix.spinnaker.kork.dynamicconfig; | ||
|
||
import com.netflix.spinnaker.kork.annotations.NonnullByDefault; | ||
import dev.openfeature.sdk.ErrorCode; | ||
import dev.openfeature.sdk.EvaluationContext; | ||
import dev.openfeature.sdk.Features; | ||
import dev.openfeature.sdk.FlagEvaluationDetails; | ||
import dev.openfeature.sdk.MutableContext; | ||
import dev.openfeature.sdk.Value; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.log4j.Log4j2; | ||
|
||
/** | ||
* Dynamic configuration service using {@linkplain dev.openfeature.sdk.OpenFeatureAPI OpenFeature} | ||
* as the primary lookup mechanism with the ability to fall back to using another {@link | ||
* DynamicConfigService}. | ||
*/ | ||
@Log4j2 | ||
@NonnullByDefault | ||
@RequiredArgsConstructor | ||
public class OpenFeatureDynamicConfigService implements DynamicConfigService { | ||
private final Features features; | ||
private final DynamicConfigService fallbackDynamicConfigService; | ||
|
||
@Override | ||
@SuppressWarnings("unchecked") | ||
public <T> T getConfig(Class<T> configType, String configName, T defaultValue) { | ||
FlagEvaluationDetails<?> details; | ||
T value; | ||
if (configType == Boolean.class) { | ||
details = features.getBooleanDetails(configName, (Boolean) defaultValue); | ||
value = (T) details.getValue(); | ||
} else if (configType == Integer.class) { | ||
details = features.getIntegerDetails(configName, (Integer) defaultValue); | ||
value = (T) details.getValue(); | ||
} else if (configType == Long.class) { | ||
// TODO(jvz): https://github.com/open-feature/java-sdk/issues/501 | ||
var doubleDetails = | ||
features.getDoubleDetails(configName, ((Long) defaultValue).doubleValue()); | ||
details = doubleDetails; | ||
var longValue = Long.valueOf(doubleDetails.getValue().longValue()); | ||
value = (T) longValue; | ||
} else if (configType == Double.class) { | ||
details = features.getDoubleDetails(configName, (Double) defaultValue); | ||
value = (T) details.getValue(); | ||
} else if (configType == String.class) { | ||
details = features.getStringDetails(configName, (String) defaultValue); | ||
value = (T) details.getValue(); | ||
} else { | ||
var objectDetails = features.getObjectDetails(configName, Value.objectToValue(defaultValue)); | ||
details = objectDetails; | ||
value = (T) objectDetails.getValue().asObject(); | ||
} | ||
var errorCode = details.getErrorCode(); | ||
if (errorCode == ErrorCode.FLAG_NOT_FOUND) { | ||
return fallbackDynamicConfigService.getConfig(configType, configName, defaultValue); | ||
} | ||
if (errorCode != null) { | ||
log.warn("Unable to resolve configuration key '{}': {}", configName, details); | ||
} | ||
return value; | ||
} | ||
|
||
@Override | ||
public boolean isEnabled(String flagName, boolean defaultValue) { | ||
var details = features.getBooleanDetails(flagPropertyName(flagName), defaultValue); | ||
var errorCode = details.getErrorCode(); | ||
if (errorCode == ErrorCode.FLAG_NOT_FOUND) { | ||
return fallbackDynamicConfigService.isEnabled(flagName, defaultValue); | ||
} | ||
if (errorCode != null) { | ||
log.warn("Unable to resolve configuration flag '{}': {}", flagName, details); | ||
} | ||
return details.getValue(); | ||
} | ||
|
||
@Override | ||
public boolean isEnabled(String flagName, boolean defaultValue, ScopedCriteria criteria) { | ||
var context = convertCriteria(criteria); | ||
var details = features.getBooleanDetails(flagPropertyName(flagName), defaultValue, context); | ||
var errorCode = details.getErrorCode(); | ||
if (errorCode == ErrorCode.FLAG_NOT_FOUND) { | ||
return fallbackDynamicConfigService.isEnabled(flagName, defaultValue, criteria); | ||
} | ||
if (errorCode != null) { | ||
log.warn( | ||
"Unable to resolve configuration flag '{}' with {}: {}", flagName, criteria, details); | ||
} | ||
return details.getValue(); | ||
} | ||
|
||
private static String flagPropertyName(String flagName) { | ||
return flagName.endsWith(".enabled") ? flagName : flagName + ".enabled"; | ||
} | ||
|
||
private static EvaluationContext convertCriteria(ScopedCriteria criteria) { | ||
return new MutableContext() | ||
.add("region", criteria.region) | ||
.add("account", criteria.account) | ||
.add("cloudProvider", criteria.cloudProvider) | ||
.add("application", criteria.application); | ||
} | ||
} |
63 changes: 63 additions & 0 deletions
63
...roovy/com/netflix/spinnaker/kork/dynamicconfig/OpenFeatureDynamicConfigServiceSpec.groovy
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,63 @@ | ||
/* | ||
* Copyright 2024 Apple, Inc. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
* | ||
*/ | ||
|
||
package com.netflix.spinnaker.kork.dynamicconfig | ||
|
||
import dev.openfeature.sdk.OpenFeatureAPI | ||
import dev.openfeature.sdk.providers.memory.Flag | ||
import dev.openfeature.sdk.providers.memory.InMemoryProvider | ||
import org.springframework.mock.env.MockEnvironment | ||
import spock.lang.Specification | ||
import spock.lang.Subject | ||
|
||
class OpenFeatureDynamicConfigServiceSpec extends Specification { | ||
def environment = new MockEnvironment() | ||
.withProperty('flag.enabled', 'true') | ||
.withProperty('some.string', 'hello') | ||
.withProperty('some.int', '42') | ||
def fallbackDynamicConfigService = new SpringDynamicConfigService(environment: environment) | ||
def flags = [ | ||
'bool.enabled': Flag.builder() | ||
.variant('on', true) | ||
.variant('off', false) | ||
.defaultVariant('on') | ||
.build(), | ||
'color' : Flag.builder() | ||
.variant('red', 0xff0000) | ||
.variant('green', 0x00ff00) | ||
.variant('blue', 0x0000ff) | ||
.defaultVariant('blue') | ||
.build() | ||
] | ||
def api = OpenFeatureAPI.instance.tap { setProviderAndWait(new InMemoryProvider(flags)) } | ||
|
||
@Subject | ||
def service = new OpenFeatureDynamicConfigService(api.client, fallbackDynamicConfigService) | ||
|
||
def "can resolve basic flags"() { | ||
expect: | ||
service.isEnabled('flag', false) | ||
service.isEnabled('bool', false) | ||
} | ||
|
||
def "can resolve basic configs"() { | ||
expect: | ||
service.getConfig(Integer, 'color', 0) == 0xff | ||
service.getConfig(Integer, 'some.int', 0) == 42 | ||
service.getConfig(String, 'some.string', '') == 'hello' | ||
} | ||
} |
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,14 @@ | ||
# flagd OpenFeature Provider | ||
|
||
Spinnaker provides integration with [flagd](https://flagd.dev), an open source feature flag daemon that integrates with [OpenFeature](https://openfeature.dev). | ||
This module sets up a flagd `FeatureProvider` bean as required by `kork-flags` to be used in `OpenFeatureAPI`. | ||
This module supports extensions which can poll for or otherwise handle feature flag configuration update notifications to inform flagd to load the updated flags. | ||
|
||
## Configuration | ||
|
||
* `flags.providers.flagd.enabled`: toggles whether this provider is enabled. | ||
* `flags.providers.flagd.offline.enabled`: toggles offline mode; if enabled, requires a flag source path to be defined. | ||
* `flags.providers.flagd.offline.flag-source-path`: path to source of feature flags to periodically reload. | ||
* `flags.providers.flagd.sync.enabled`: toggles sync mode; if enabled, sets up an in-process gRPC provider for the flagd `FlagSyncService`. Extensions should emit a `SyncFlagsEvent` event on startup and as needed to inform flagd of the current flags configuration. | ||
* `flags.providers.flagd.sync.socket-path`: if defined, uses a unix socket for gRPC communication (requires platform support for Netty's epoll API) | ||
* `flags.providers.flagd.sync.port`: specifies the local port to use for the gRPC service (default is 8015). Note that this port binding is made to a loopback interface. |
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,23 @@ | ||
/* | ||
* Copyright 2024 Apple, Inc. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
apply from: "$rootDir/gradle/kotlin.gradle" | ||
|
||
dependencies { | ||
api platform(project(':spinnaker-dependencies')) | ||
api project(':kork-flags') | ||
implementation 'dev.openfeature.contrib.providers:flagd' | ||
} |
Oops, something went wrong.