Blacklist → denylist, whitelist → allowlist (#20)

Since this is an incompatible change, also bump up version to 2.0.0
(major version change, to signal incompatible API).
This commit is contained in:
Andrea Spadaccini 2020-06-12 13:39:17 +01:00 коммит произвёл GitHub
Родитель 1a2e1f3261
Коммит 0742b2035b
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
10 изменённых файлов: 109 добавлений и 95 удалений

Просмотреть файл

@ -2,7 +2,7 @@
<package> <package>
<metadata> <metadata>
<id>FeatureFlags.PowerShell</id> <id>FeatureFlags.PowerShell</id>
<version>1.0.1</version> <version>2.0.0</version>
<title>PowerShell Feature Flags</title> <title>PowerShell Feature Flags</title>
<authors>Andrea Spadaccini,Nick Hara</authors> <authors>Andrea Spadaccini,Nick Hara</authors>
<owners>Andrea Spadaccini</owners> <owners>Andrea Spadaccini</owners>
@ -10,7 +10,7 @@
<projectUrl>https://github.com/microsoft/PowerShell-FeatureFlags/</projectUrl> <projectUrl>https://github.com/microsoft/PowerShell-FeatureFlags/</projectUrl>
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>PowerShell module containing a Feature Flags implementation based on a local config file.</description> <description>PowerShell module containing a Feature Flags implementation based on a local config file.</description>
<releaseNotes>Fixed bug with the "probability" condition (issue #18).</releaseNotes> <releaseNotes>Renamed blacklist to denylist, whitelist to allowlist. Incompatible with 1.0.x.</releaseNotes>
<copyright>Copyright 2020 Microsoft</copyright> <copyright>Copyright 2020 Microsoft</copyright>
<repository type="git" url="https://github.com/microsoft/PowerShell-FeatureFlags.git" branch="master" /> <repository type="git" url="https://github.com/microsoft/PowerShell-FeatureFlags.git" branch="master" />
</metadata> </metadata>

Двоичные данные
FeatureFlags.psd1

Двоичный файл не отображается.

Просмотреть файл

@ -219,24 +219,24 @@ function Test-FeatureConditions
) )
# Conditions are evaluated in the order they are presented in the configuration file. # Conditions are evaluated in the order they are presented in the configuration file.
foreach ($condition in $conditions) { foreach ($condition in $conditions) {
# Each condition object can have only one of the whitelist, blacklist or probability # Each condition object can have only one of the allowlist, denylist or probability
# attributes set. This invariant is enforced by the JSON schema, which uses the "oneof" # attributes set. This invariant is enforced by the JSON schema, which uses the "oneof"
# strategy to choose between whitelist, blacklist or probability and, for each of these # strategy to choose between allowlist, denylist or probability and, for each of these
# condition types, only allows the homonym attribute to be set. # condition types, only allows the homonym attribute to be set.
if ($null -ne $condition.whitelist) { if ($null -ne $condition.allowlist) {
Write-Verbose "Checking the whitelist condition" Write-Verbose "Checking the allowlist condition"
# The predicate must match any of the regexes in the whitelist in order to # The predicate must match any of the regexes in the allowlist in order to
# consider the whitelist condition satisfied. # consider the allowlist condition satisfied.
$matchesWhitelist = Test-RegexList $predicate @($condition.whitelist) $matchesallowlist = Test-RegexList $predicate @($condition.allowlist)
if (-not $matchesWhitelist) { if (-not $matchesallowlist) {
return $false return $false
} }
} elseif ($null -ne $condition.blacklist) { } elseif ($null -ne $condition.denylist) {
Write-Verbose "Checking the blacklist condition" Write-Verbose "Checking the denylist condition"
# The predicate must not match all of the regexes in the blacklist in order to # The predicate must not match all of the regexes in the denylist in order to
# consider the blacklist condition satisfied. # consider the denylist condition satisfied.
$matchesBlacklist = Test-RegexList $predicate @($condition.blacklist) $matchesdenylist = Test-RegexList $predicate @($condition.denylist)
if ($matchesBlacklist) { if ($matchesdenylist) {
return $false return $false
} }
} elseif ($null -ne $condition.probability) { } elseif ($null -ne $condition.probability) {
@ -250,7 +250,7 @@ function Test-FeatureConditions
return $false return $false
} }
} else { } else {
throw "${condition} is not a supported condition type (blacklist, whitelist or probability)." throw "${condition} is not a supported condition type (denylist, allowlist or probability)."
} }
} }
return $true return $true

Просмотреть файл

@ -49,14 +49,14 @@ Imagine to have a feature flag configuration file called `features.json`:
{ {
"stages": { "stages": {
"test": [ "test": [
{"whitelist": ["test.*", "dev.*"]} {"allowlist": ["test.*", "dev.*"]}
], ],
"canary": [ "canary": [
{"whitelist": ["prod-canary"]} {"allowlist": ["prod-canary"]}
], ],
"prod": [ "prod": [
{"whitelist": ["prod.*"]}, {"allowlist": ["prod.*"]},
{"blacklist": ["prod-canary"]} {"denylist": ["prod-canary"]}
] ]
}, },
"features": { "features": {
@ -122,13 +122,13 @@ An example lifecycle of a feature flag might be the following:
Here is how these example stages could be implemented: Here is how these example stages could be implemented:
* Stage 1 can be implemented with a `blacklist` condition with value `.*`. * Stage 1 can be implemented with a `denylist` condition with value `.*`.
* Stages 2 and 3 can be implemented with `whitelist` conditions. * Stages 2 and 3 can be implemented with `allowlist` conditions.
* Stages 4 and 5 can be implemented with `probability` conditions. * Stages 4 and 5 can be implemented with `probability` conditions.
## Conditions ## Conditions
There are two types of conditions: *deterministic* (whitelist and blacklist, There are two types of conditions: *deterministic* (allowlist and denylist,
regex-based) and *probabilistic* (probability, expressed as a number between regex-based) and *probabilistic* (probability, expressed as a number between
0 and 1). Conditions can be repeated if multiple instances are required. 0 and 1). Conditions can be repeated if multiple instances are required.
@ -138,9 +138,9 @@ in the configuration file, for the feature to be considered enabled.
If any condition is not met, evaluation of conditions stops and the feature If any condition is not met, evaluation of conditions stops and the feature
is considered disabled. is considered disabled.
### Whitelist ### Allow list
The `whitelist` condition allows to specify a list of regular expressions; if the 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 predicate matches any of the expressions, then the condition is met and the evaluation
moves to the next condition, if there is any. moves to the next condition, if there is any.
@ -150,9 +150,9 @@ unintended matches, it's recommended to always anchor the regex.
So, for example, `"^storage$"` will only match `"storage"` and not `"storage1"`. So, for example, `"^storage$"` will only match `"storage"` and not `"storage1"`.
### Blacklist ### Deny list
The `blacklist` condition is analogous to the whitelist condition, except that if 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 the predicate matches any of the expressions the condition is considered not met
and the evaluation stops. and the evaluation stops.
@ -172,25 +172,25 @@ the following example:
```json ```json
{ {
"stages": { "stages": {
"whitelist-first": [ "allowlist-first": [
{"whitelist": ["storage.*"]}, {"allowlist": ["storage.*"]},
{"probability": 0.1} {"probability": 0.1}
], ],
"probability-first": [ "probability-first": [
{"probability": 0.1} {"probability": 0.1}
{"whitelist": ["storage.*"]}, {"allowlist": ["storage.*"]},
] ]
} }
} }
``` ```
The first stage definition, `whitelist-first`, will evaluate the `probability` condition The first stage definition, `allowlist-first`, will evaluate the `probability` condition
only if the predicate first passes the whitelist. only if the predicate first passes the allowlist.
The second stage definition, `probability-first`, will instead first evaluate The second stage definition, `probability-first`, will instead first evaluate
the `probability` condition, and then apply the whitelist. the `probability` condition, and then apply the allowlist.
Assuming there are predicates that do not match the whitelist, the second stage definition 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 is more restrictive than the first one, leading to fewer positive evaluations of the
feature flag. feature flag.
@ -231,19 +231,19 @@ for comments. Don't add comments to your feature flag configuration file.
], ],
// Examples of deterministic stages. // Examples of deterministic stages.
"all-storage": [ "all-storage": [
{"whitelist": [".*Storage.*"]}, {"allowlist": [".*Storage.*"]},
], ],
"storage-except-important": [ "storage-except-important": [
{"whitelist": [".*Storage.*"]}, {"allowlist": [".*Storage.*"]},
{"blacklist": [".*StorageImportant.*"]}, {"denylist": [".*StorageImportant.*"]},
], ],
// Example of mixed roll-out stage. // Example of mixed roll-out stage.
// This stage will match on predicates containing the word "Storage" // This stage will match on predicates containing the word "Storage"
// but not the word "StorageImportant", and then will consider the feature // but not the word "StorageImportant", and then will consider the feature
// enabled in 50% of the cases. // enabled in 50% of the cases.
"50-percent-storage-except-StorageImportant": [ "50-percent-storage-except-StorageImportant": [
{"whitelist": [".*Storage.*"]}, {"allowlist": [".*Storage.*"]},
{"blacklist": ["StorageImportant"]}, {"denylist": ["StorageImportant"]},
{"probability": 0.5}, {"probability": 0.5},
], ],
}, },

Просмотреть файл

@ -1,14 +1,14 @@
{ {
"stages": { "stages": {
"test": [ "test": [
{"whitelist": ["test.*", "dev.*"]} {"allowlist": ["test.*", "dev.*"]}
], ],
"canary": [ "canary": [
{"whitelist": ["prod-canary"]} {"allowlist": ["prod-canary"]}
], ],
"prod": [ "prod": [
{"whitelist": ["prod.*"]}, {"allowlist": ["prod.*"]},
{"blacklist": ["prod-canary"]} {"denylist": ["prod-canary"]}
] ]
}, },
"features": { "features": {

Просмотреть файл

@ -29,30 +29,30 @@
"required": ["stages"], "required": ["stages"],
"additionalProperties": false, "additionalProperties": false,
"definitions": { "definitions": {
"whitelist": { "allowlist": {
"type": "object", "type": "object",
"properties": { "properties": {
"whitelist": { "allowlist": {
"type": "array", "type": "array",
"items": { "items": {
"type": "string" "type": "string"
} }
} }
}, },
"required": ["whitelist"], "required": ["allowlist"],
"additionalProperties": false "additionalProperties": false
}, },
"blacklist": { "denylist": {
"type": "object", "type": "object",
"properties": { "properties": {
"blacklist": { "denylist": {
"type": "array", "type": "array",
"items": { "items": {
"type": "string" "type": "string"
} }
} }
}, },
"required": ["blacklist"], "required": ["denylist"],
"additionalProperties": false "additionalProperties": false
}, },
"probability": { "probability": {
@ -70,11 +70,12 @@
"conditions": { "conditions": {
"type": "array", "type": "array",
"items": { "items": {
"oneOf": [{ "oneOf": [
"$ref": "#/definitions/whitelist" {
"$ref": "#/definitions/allowlist"
}, },
{ {
"$ref": "#/definitions/blacklist" "$ref": "#/definitions/denylist"
}, },
{ {
"$ref": "#/definitions/probability" "$ref": "#/definitions/probability"

Просмотреть файл

@ -75,6 +75,19 @@ Describe 'Confirm-FeatureFlagConfig' {
$cfg = @" $cfg = @"
{ {
"stags": { "stags": {
"storage": [
{"allowlist": [".*storage.*"]}
]
}
}
"@
Confirm-FeatureFlagConfig -EA 0 $cfg | Should -Be $false
}
It 'Fails if a deprecated condition is used (whitelist)' {
$cfg = @"
{
"stages": {
"storage": [ "storage": [
{"whitelist": [".*storage.*"]} {"whitelist": [".*storage.*"]}
] ]
@ -84,12 +97,12 @@ Describe 'Confirm-FeatureFlagConfig' {
Confirm-FeatureFlagConfig -EA 0 $cfg | Should -Be $false Confirm-FeatureFlagConfig -EA 0 $cfg | Should -Be $false
} }
It 'Fails if a condition name contains a typo (whtelist)' { It 'Fails if a condition name contains a typo (allwlist)' {
$cfg = @" $cfg = @"
{ {
"stages": { "stages": {
"storage": [ "storage": [
{"whtelist": [".*storage.*"]} {"allwlist": [".*storage.*"]}
] ]
} }
} }
@ -102,7 +115,7 @@ Describe 'Confirm-FeatureFlagConfig' {
{ {
"stages": { "stages": {
"storage": [ "storage": [
{"whitelist": [".*storage.*"]} {"allowlist": [".*storage.*"]}
] ]
}, },
"featurs": { "featurs": {
@ -118,7 +131,7 @@ Describe 'Confirm-FeatureFlagConfig' {
{ {
"stages": { "stages": {
"": [ "": [
{"whitelist": [".*storage.*"]} {"allowlist": [".*storage.*"]}
] ]
} }
} }
@ -131,7 +144,7 @@ Describe 'Confirm-FeatureFlagConfig' {
{ {
"stages": { "stages": {
" ": [ " ": [
{"whitelist": [".*storage.*"]} {"allowlist": [".*storage.*"]}
] ]
} }
} }
@ -141,12 +154,12 @@ Describe 'Confirm-FeatureFlagConfig' {
} }
Context 'Successful validation of simple stages' { Context 'Successful validation of simple stages' {
It 'Succeeds with a simple stage with two whitelists' { It 'Succeeds with a simple stage with two allowlists' {
$cfg = @" $cfg = @"
{ {
"stages": { "stages": {
"storage": [ "storage": [
{"whitelist": [".*storage.*", ".*compute.*"]} {"allowlist": [".*storage.*", ".*compute.*"]}
] ]
} }
} }
@ -154,13 +167,13 @@ Describe 'Confirm-FeatureFlagConfig' {
Confirm-FeatureFlagConfig $cfg | Should -Be $true Confirm-FeatureFlagConfig $cfg | Should -Be $true
} }
It 'Succeeds with a simple stage with a whitelist and a blacklist' { It 'Succeeds with a simple stage with a allowlist and a denylist' {
$cfg = @" $cfg = @"
{ {
"stages": { "stages": {
"storage": [ "storage": [
{"whitelist": [".*storage.*"]}, {"allowlist": [".*storage.*"]},
{"blacklist": ["ImportantStorage"]} {"denylist": ["ImportantStorage"]}
] ]
} }
} }
@ -187,7 +200,7 @@ Describe 'Confirm-FeatureFlagConfig' {
{ {
"stages": { "stages": {
"storage": [ "storage": [
{"whitelist": [".*storage.*"]} {"allowlist": [".*storage.*"]}
] ]
}, },
"features": { "features": {
@ -205,10 +218,10 @@ Describe 'Confirm-FeatureFlagConfig' {
{ {
"stages": { "stages": {
"all": [ "all": [
{"whitelist": [".*"]} {"allowlist": [".*"]}
], ],
"storage": [ "storage": [
{"whitelist": [".*storage.*"]} {"allowlist": [".*storage.*"]}
] ]
}, },
"features": { "features": {
@ -231,7 +244,7 @@ Describe 'Confirm-FeatureFlagConfig' {
{ {
"stages": { "stages": {
"storage": [ "storage": [
{"whitelist": [".*storage.*"]} {"allowlist": [".*storage.*"]}
] ]
}, },
"features": { "features": {
@ -255,13 +268,13 @@ Describe 'Get-FeatureFlagConfigFromFile' {
} }
Describe 'Test-FeatureFlag' { Describe 'Test-FeatureFlag' {
Context 'Whitelist condition' { Context 'allowlist condition' {
Context 'Simple whitelist configuration' { Context 'Simple allowlist configuration' {
$serializedConfig = @" $serializedConfig = @"
{ {
"stages": { "stages": {
"all": [ "all": [
{"whitelist": [".*"]} {"allowlist": [".*"]}
] ]
}, },
"features": { "features": {
@ -283,12 +296,12 @@ Describe 'Test-FeatureFlag' {
} }
} }
Context 'Chained whitelist configuration' { Context 'Chained allowlist configuration' {
$serializedConfig = @" $serializedConfig = @"
{ {
"stages": { "stages": {
"test-repo-and-branch": [ "test-repo-and-branch": [
{"whitelist": [ {"allowlist": [
"storage1/.*", "storage1/.*",
"storage2/dev-branch" "storage2/dev-branch"
]} ]}
@ -315,13 +328,13 @@ Describe 'Test-FeatureFlag' {
} }
} }
Context 'Blacklist condition' { Context 'denylist condition' {
Context 'Reject-all configuration' { Context 'Reject-all configuration' {
$serializedConfig = @" $serializedConfig = @"
{ {
"stages": { "stages": {
"none": [ "none": [
{"blacklist": [".*"]} {"denylist": [".*"]}
] ]
}, },
"features": { "features": {
@ -346,7 +359,7 @@ Describe 'Test-FeatureFlag' {
{ {
"stages": { "stages": {
"all-except-important": [ "all-except-important": [
{"blacklist": ["^important$"]} {"denylist": ["^important$"]}
] ]
}, },
"features": { "features": {
@ -360,7 +373,7 @@ Describe 'Test-FeatureFlag' {
Confirm-FeatureFlagConfig $serializedConfig Confirm-FeatureFlagConfig $serializedConfig
$config = ConvertFrom-Json $serializedConfig $config = ConvertFrom-Json $serializedConfig
# Given that the regex is ^important$, only the exact string "important" will match the blacklist. # Given that the regex is ^important$, only the exact string "important" will match the denylist.
It 'Allows the flag if the predicate does not match exactly' { It 'Allows the flag if the predicate does not match exactly' {
Test-FeatureFlag "some-feature" "Storage/master" $config | Should -Be $true Test-FeatureFlag "some-feature" "Storage/master" $config | Should -Be $true
Test-FeatureFlag "some-feature" "foo" $config | Should -Be $true Test-FeatureFlag "some-feature" "foo" $config | Should -Be $true
@ -378,7 +391,7 @@ Describe 'Test-FeatureFlag' {
{ {
"stages": { "stages": {
"all-except-important": [ "all-except-important": [
{"blacklist": ["storage-important/master", "storage-important2/master"]} {"denylist": ["storage-important/master", "storage-important2/master"]}
] ]
}, },
"features": { "features": {
@ -391,7 +404,7 @@ Describe 'Test-FeatureFlag' {
Confirm-FeatureFlagConfig $serializedConfig Confirm-FeatureFlagConfig $serializedConfig
$config = ConvertFrom-Json $serializedConfig $config = ConvertFrom-Json $serializedConfig
It 'Allows predicates not matching the blacklist' { It 'Allows predicates not matching the denylist' {
Test-FeatureFlag "some-feature" "storage1/master" $config | Should -Be $true Test-FeatureFlag "some-feature" "storage1/master" $config | Should -Be $true
Test-FeatureFlag "some-feature" "storage2/master" $config | Should -Be $true Test-FeatureFlag "some-feature" "storage2/master" $config | Should -Be $true
Test-FeatureFlag "some-feature" "storage-important/dev" $config | Should -Be $true Test-FeatureFlag "some-feature" "storage-important/dev" $config | Should -Be $true
@ -404,13 +417,13 @@ Describe 'Test-FeatureFlag' {
} }
} }
Context 'Mixed whitelist/blacklist configuration' { Context 'Mixed allowlist/denylist configuration' {
$serializedConfig = @" $serializedConfig = @"
{ {
"stages": { "stages": {
"all-storage-important": [ "all-storage-important": [
{"whitelist": ["storage.*"]}, {"allowlist": ["storage.*"]},
{"blacklist": ["storage-important/master", "storage-important2/master"]} {"denylist": ["storage-important/master", "storage-important2/master"]}
] ]
}, },
"features": { "features": {
@ -509,13 +522,13 @@ Describe 'Test-FeatureFlag' {
} }
} }
Context 'Complex whitelist + blacklist + probability configuration' { Context 'Complex allowlist + denylist + probability configuration' {
$serializedConfig = @" $serializedConfig = @"
{ {
"stages": { "stages": {
"all-storage-important-50pc": [ "all-storage-important-50pc": [
{"whitelist": ["storage.*"]}, {"allowlist": ["storage.*"]},
{"blacklist": ["storage-important/master", "storage-important2/master"]}, {"denylist": ["storage-important/master", "storage-important2/master"]},
{"probability": 0.5} {"probability": 0.5}
] ]
}, },
@ -589,7 +602,7 @@ Describe 'Out-EvaluatedFeaturesFiles' -Tag Features {
Mock -ModuleName $ModuleName Add-Content { ${global:featuresIniContent}.Add($Value) } -ParameterFilter { $Path.EndsWith("features.ini") } Mock -ModuleName $ModuleName Add-Content { ${global:featuresIniContent}.Add($Value) } -ParameterFilter { $Path.EndsWith("features.ini") }
Mock -ModuleName $ModuleName Add-Content { ${global:featuresEnvConfigContent}.Add($Value) } -ParameterFilter { $Path.EndsWith("features.env.config") } Mock -ModuleName $ModuleName Add-Content { ${global:featuresEnvConfigContent}.Add($Value) } -ParameterFilter { $Path.EndsWith("features.env.config") }
It 'Honors blacklist' { It 'Honors denylist' {
$features = Get-EvaluatedFeatureFlags -predicate "important" -config $config $features = Get-EvaluatedFeatureFlags -predicate "important" -config $config
$expectedFeaturesIniContent = @("filetracker`tfalse", "newestfeature`tfalse", "testfeature`tfalse") $expectedFeaturesIniContent = @("filetracker`tfalse", "newestfeature`tfalse", "testfeature`tfalse")
$expectedFeaturesEnvConfigContent = @() $expectedFeaturesEnvConfigContent = @()

Просмотреть файл

@ -2,28 +2,28 @@
"stages": { "stages": {
"all-except-important": [ "all-except-important": [
{ {
"blacklist": [ "denylist": [
"^important$" "^important$"
] ]
} }
], ],
"poc": [ "poc": [
{ {
"whitelist": [ "allowlist": [
"demo/.*demofeature.*" "demo/.*demofeature.*"
] ]
} }
], ],
"dev": [ "dev": [
{ {
"whitelist": [ "allowlist": [
"Dev.*" "Dev.*"
] ]
} }
], ],
"test": [ "test": [
{ {
"whitelist": [ "allowlist": [
"demo/.*test.*/.*Official.*", "demo/.*test.*/.*Official.*",
"demo2/.*test2.*" "demo2/.*test2.*"
] ]
@ -31,7 +31,7 @@
], ],
"prod": [ "prod": [
{ {
"whitelist": [ "allowlist": [
"production/.*" "production/.*"
] ]
} }

Просмотреть файл

@ -1,14 +1,14 @@
{ {
"stages": { "stages": {
"all-storage": [ "all-storage": [
{"whitelist": [".*Storage.*"]} {"allowlist": [".*Storage.*"]}
], ],
"specific-storage-repos": [ "specific-storage-repos": [
{"whitelist": ["Storage-repo1", "storage-repo2"]} {"allowlist": ["Storage-repo1", "storage-repo2"]}
], ],
"all-storage-except-important": [ "all-storage-except-important": [
{"whitelist": [".*Storage.*"]}, {"allowlist": [".*Storage.*"]},
{"blacklist": ["Storage-important"]} {"denylist": ["Storage-important"]}
] ]
} }
} }

Просмотреть файл

@ -1,7 +1,7 @@
{ {
"stages": { "stages": {
"all-storage": [ "all-storage": [
{"whitelist": [".*Storage.*"]} {"allowlist": [".*Storage.*"]}
] ]
} }
} }