504e372b05
Tests are failing on pwsh 7.4 because it stopped accepting `'System.Collections.ArrayList()'` as an argument to `New-Object`. Removing the extra parentheses should fix the issue. |
||
---|---|---|
.devcontainer | ||
.github | ||
examples | ||
test | ||
tools | ||
.gitignore | ||
AUTHORS.md | ||
FeatureFlags.PowerShell.nuspec | ||
FeatureFlags.psd1 | ||
FeatureFlags.psm1 | ||
LICENSE | ||
PowerShell-FeatureFlags.csproj | ||
README.md | ||
SECURITY.md | ||
featureflags.schema.json |
README.md
PowerShell Feature Flags
This package contains a simple, low-dependencies implementation of feature flags for PowerShell, which relies on a local configuration file to verify if a given feature should be enabled or not.
The configuration file contains two sections:
- stages: a section where roll-out stages are defined;
- features: a section where each feature can be associated to a roll-out stage.
A roll-out stage is defined by a name and an array of conditions that the predicate must match in the order they are presented for the feature associated to the given stage to be enabled.
Stage names and feature names must be non-empty and must consist of non-space characters.
A feature can be assigned an array of stages that it applies to. In addition, it can also accept an environment variable array, and can optionally output an environment configuration file.
For more general information about feature flags, please visit featureflags.io.
Installation
This module is available from the PowerShell Gallery. Therefore, to install it for all users on the machine type the following from an administrator PowerShell prompt:
PS > Install-Module FeatureFlags
To install as an unprivileged user, type the following from any PowerShell prompt:
PS > Install-Module FeatureFlags -Scope CurrentUser
Simple example
Imagine to have a feature flag configuration file called features.json
:
{
"stages": {
"test": [
{"allowlist": ["test.*", "dev.*"]}
],
"canary": [
{"allowlist": ["prod-canary"]}
],
"prod": [
{"allowlist": ["prod.*"]},
{"denylist": ["prod-canary"]}
]
},
"features": {
"experimental-feature": {
"stages": ["test"]
},
"well-tested-feature": {
"stages": ["test", "canary", "prod"]
}
}
}
This file defines 3 stages: test
, canary
and prod
, and 2 features: experimental-feature
and well-tested-feature
.
The intent of the configuration is to enable experimental-feature
in test
only (all predicates starting with test
or dev
),
and to enable well-tested-feature
in all stages.
Let's first read the configuration:
$cfg = Get-FeatureFlagConfigFromFile features.json
This step would fail if there is any I/O error (e.g., file doesn't exist), if the file is not valid JSON or if the file does not conform with the feature flags schema.
Let's now test a couple of predicates to verify that the configuration does what we expect:
PS > Test-FeatureFlag -config $cfg -Feature "well-tested-feature" -predicate "test1"
True
PS > Test-FeatureFlag -config $cfg -Feature "well-tested-feature" -predicate "test2"
True
PS > Test-FeatureFlag -config $cfg -Feature "well-tested-feature" -predicate "dev1"
True
PS > Test-FeatureFlag -config $cfg -Feature "well-tested-feature" -predicate "prod-canary1"
True
PS > Test-FeatureFlag -config $cfg -Feature "experimental-feature" -predicate "prod-canary1"
False
PS > Test-FeatureFlag -config $cfg -Feature "experimental-feature" -predicate "test1"
True
PS > Test-FeatureFlag -config $cfg -Feature "experimental-feature" -predicate "prod1"
False
For more complex examples, please look at test cases. More examples will be added in the future (Issue #6).
Life of a feature flag
Feature flags are expected to be in use while a feature is rolled out to production, or in case there is a need to conditionally enable or disable features.
An example lifecycle of a feature flag might be the following:
- A new feature is checked in production after testing, in a disabled state;
- The feature is enabled for a particular customer;
- The feature is enabled for a small set of customers;
- The feature is gradually rolled out to increasingly large percentages of customers (e.g., 5%, 10%, 30%, 50%);
- The feature is rolled out to all customers (100%)
- The test for the feature flag is removed from the code, and the feature flag configuration is removed as well.
Here is how these example stages could be implemented:
- Stage 1 can be implemented with a
denylist
condition with value.*
. - Stages 2 and 3 can be implemented with
allowlist
conditions. - Stages 4 and 5 can be implemented with
probability
conditions.
Conditions
There are two types of conditions: deterministic (allowlist and denylist, regex-based) and probabilistic (probability, expressed as a number between 0 and 1). Conditions can be repeated if multiple instances are required.
All conditions in each stage must be satisfied, in the order they are listed in the configuration file, for the feature to be considered enabled.
If any condition is not met, evaluation of conditions stops and the feature is considered disabled.
Allow list
The allowlist
condition allows to specify a list of regular expressions; if the
predicate matches any of the expressions, then the condition is met and the evaluation
moves to the next condition, if there is any.
The regular expression is not anchored. This means that a regex of "storage"
will
match both the predicate "storage"
and the predicate "storage1"
. To prevent
unintended matches, it's recommended to always anchor the regex.
So, for example, "^storage$"
will only match "storage"
and not "storage1"
.
Deny list
The denylist
condition is analogous to the allowlist condition, except that if
the predicate matches any of the expressions the condition is considered not met
and the evaluation stops.
Probability
The probability
condition allows the user to specify a percentage of invocations
that will lead to the condition to be met, expressed as a floating point number
between 0 and 1.
So, if the user specifies a value of 0.3
, roughly 30% of times the condition is
checked it will be considered met, while for the remaining 70% of times
it will be considered unmet.
The position of the probability
condition is very important. Let's look at
the following example:
{
"stages": {
"allowlist-first": [
{"allowlist": ["storage.*"]},
{"probability": 0.1}
],
"probability-first": [
{"probability": 0.1}
{"allowlist": ["storage.*"]},
]
}
}
The first stage definition, allowlist-first
, will evaluate the probability
condition
only if the predicate first passes the allowlist.
The second stage definition, probability-first
, will instead first evaluate
the probability
condition, and then apply the allowlist.
Assuming there are predicates that do not match the allowlist, the second stage definition is more restrictive than the first one, leading to fewer positive evaluations of the feature flag.
Cmdlets
This package provides five PowerShell cmdlets:
Test-FeatureFlag
, which checks if a given feature is enabled by testing a predicate against the given feature flag configuration;Confirm-FeatureFlagConfig
, which validates the given feature flag configuration by first validating it against the feature flags JSON schema and then by applying some further validation rules;Get-FeatureFlagConfigFromFile
, which parses and validates a feature flag configuration from a given file.Get-EvaluatedFeatureFlags
, which can be used to determine the collection of feature flags, from the feature flags config, that apply given the specified predicate.Out-EvaluatedFeaturesFiles
, which will write out feature flag files (.json, .ini, env.config) which will indicate which features are enabled, and which environment variables should be set.
A more complex example
NOTE: comments are added in-line in this example, but the JSON format does not allow for comments. Don't add comments to your feature flag configuration file.
{
// Definition of roll-out stages.
"stages": {
// Examples of probabilistic stages.
"1percent": [
{"probability": 0.01},
],
"10percent": [
{"probability": 0.1},
],
"all": [
{"probability": 1},
],
// Examples of deterministic stages.
"all-storage": [
{"allowlist": [".*Storage.*"]},
],
"storage-except-important": [
{"allowlist": [".*Storage.*"]},
{"denylist": [".*StorageImportant.*"]},
],
// Example of mixed roll-out stage.
// This stage will match on predicates containing the word "Storage"
// but not the word "StorageImportant", and then will consider the feature
// enabled in 50% of the cases.
"50-percent-storage-except-StorageImportant": [
{"allowlist": [".*Storage.*"]},
{"denylist": ["StorageImportant"]},
{"probability": 0.5},
],
},
// Roll out status of different features:
"features": {
"msbuild-cache": {
"stages": ["all-storage"],
"environmentVariables": [
{ "Use_MsBuildCache": "1" }
]
},
"experimental-feature": {
"stages": ["1percent"]
// Environment Variables are optional
},
"well-tested-feature": {
"stages": ["all"],
"environmentVariables": [
{ "Use_TestedFeature": "1" }
]
},
}
}
Why JSON?
The configuration file uses JSON, despite its shortcomings, for the following reasons:
- it's supported natively by PowerShell, therefore it makes this package free from dependencies;
- it's familiar to most PowerShell developers.
Other formats, such as Protocol Buffers, while being technically superior, have been excluded for the above reasons.
Relationship to similar projects
There are some projects that allow to use Feature Flags in PowerShell:
-
microsoft/featurebits: this package uses a SQL database to store feature flags value. While the features provided by this project are similar, our
FeatureFlags
package does not need any external dependency to run, as features are stored in a local file. -
SaaS (Software-as-a-Service) solutions: using an external service for feature flags has its pros and cons. They typically are much easier to manage and offer rich interfaces to manage the flags; however, the specific use case for which this library was born is to enable feature flags for PowerShell code which might not be able to open network connections: this requires the library and the feature flags definition to be co-located with the code (hermeticity).
Contributing
This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.microsoft.com.
When you submit a pull request, a CLA-bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA.
This project has adopted the Microsoft Open Source Code of Conduct. For more information see the Code of Conduct FAQ or contact opencode@microsoft.com with any additional questions or comments.