-
-
Notifications
You must be signed in to change notification settings - Fork 19
feat(operator/client): Support feature gate retrieval #1207
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
b1199d3
feat(operator/client): Support feature gate retrieval
Techassi 4390242
test(operator): Add feature gate parsing unit tests
Techassi 733776c
chore(operator): Add changelog entry
Techassi b2264de
test(operator): Test with real snippet, remove positive tests
Techassi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or 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
This file contains hidden or 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
This file contains hidden or 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,229 @@ | ||
| use std::{collections::HashMap, str::FromStr}; | ||
|
|
||
| use snafu::{OptionExt as _, ResultExt as _, Snafu}; | ||
| use winnow::{ | ||
| Parser as _, | ||
| ascii::{alphanumeric0, alphanumeric1, digit1, space0, space1}, | ||
| combinator::{delimited, separated, separated_pair}, | ||
| }; | ||
|
|
||
| use crate::client::{ | ||
| Client, CreateRawRequestSnafu, ParseFeatureGateSnafu, PerformRawRequestSnafu, Result, | ||
| }; | ||
|
|
||
| impl Client { | ||
| /// Retrieves and parses all feature gates via a raw request to the `/metrics` endpoint. | ||
| /// | ||
| /// This list of feature gates in combination with [`KubeClient::apiserver_version`] can be used | ||
| /// to enable gated behaviour. | ||
| pub async fn get_feature_gates(&self) -> Result<Vec<FeatureGate>> { | ||
| let request = | ||
| http::Request::get("/metrics") | ||
| .body(vec![]) | ||
| .context(CreateRawRequestSnafu { | ||
| method: http::Method::GET, | ||
| })?; | ||
|
|
||
| let response = self | ||
| .client | ||
| .request_text(request) | ||
| .await | ||
| .context(PerformRawRequestSnafu)?; | ||
|
|
||
| response | ||
| .lines() | ||
| .filter(|l| l.starts_with("kubernetes_feature_enabled")) | ||
| .map(FeatureGate::from_str) | ||
| .collect::<Result<Vec<FeatureGate>, _>>() | ||
| .map_err(|error| ParseFeatureGateSnafu { error }.build()) | ||
| } | ||
|
|
||
| /// Retrieves enabled feature gates. | ||
| /// | ||
| /// Uses [`Client::get_feature_gates`] internally. | ||
| pub async fn get_enabled_feature_gates(&self) -> Result<Vec<FeatureGate>> { | ||
| let feature_gates = self.get_feature_gates().await?; | ||
| let enabled_feature_gates = feature_gates.into_iter().filter(|fg| fg.enabled).collect(); | ||
|
|
||
| Ok(enabled_feature_gates) | ||
| } | ||
|
|
||
| /// Retrieves disabled feature gates. | ||
| /// | ||
| /// Uses [`Client::get_feature_gates`] internally. | ||
| pub async fn get_disabled_feature_gates(&self) -> Result<Vec<FeatureGate>> { | ||
| let feature_gates = self.get_feature_gates().await?; | ||
| let disabled_feature_gates = feature_gates.into_iter().filter(|fg| !fg.enabled).collect(); | ||
|
|
||
| Ok(disabled_feature_gates) | ||
| } | ||
| } | ||
|
|
||
| #[derive(Debug, Snafu)] | ||
| enum FeatureGateParseError { | ||
| #[snafu(display("required feature gate metric label missing, expected 'name' and 'stage'"))] | ||
| MissingLabel, | ||
|
|
||
| #[snafu(display("failed to parse feature stage"))] | ||
| ParseStage { source: strum::ParseError }, | ||
|
|
||
| #[snafu(display("failed to parse string as integer"))] | ||
| ParseInt { source: std::num::ParseIntError }, | ||
| } | ||
|
|
||
| #[derive(Debug)] | ||
| pub struct FeatureGate { | ||
| /// The name of the feature gate, eg. `AllowDNSOnlyNodeCSR`. | ||
| pub name: String, | ||
|
|
||
| /// In which stage the feature is, eg. `ALPHA`. | ||
| pub stage: FeatureStage, | ||
|
|
||
| /// Whether the feature is enabled or disabled. | ||
| pub enabled: bool, | ||
| } | ||
|
|
||
| impl FromStr for FeatureGate { | ||
| type Err = String; | ||
|
|
||
| fn from_str(s: &str) -> std::prelude::v1::Result<Self, Self::Err> { | ||
| Self::parse_from_metric | ||
| .parse(s) | ||
| .map_err(|err| err.to_string()) | ||
| } | ||
| } | ||
|
|
||
| impl FeatureGate { | ||
| /// Parses a feature gate from the line-based `/metrics` response. | ||
| /// | ||
| /// This function expects feature gates to be passed as individual lines. | ||
| fn parse_from_metric(input: &mut &str) -> winnow::Result<Self> { | ||
| ( | ||
| Self::parse_metric_name, | ||
| delimited('{', Self::parse_labels, '}'), | ||
| // At least one space after the metric and the value | ||
| space1, | ||
| // The counter value | ||
| digit1, | ||
| ) | ||
| .try_map(|((), mut kv_pairs, _, count)| { | ||
| let name = kv_pairs | ||
| .remove("name") | ||
| .context(MissingLabelSnafu)? | ||
| .to_owned(); | ||
|
|
||
| let stage = kv_pairs | ||
| .remove("stage") | ||
| .context(MissingLabelSnafu)? | ||
| .parse() | ||
| .context(ParseStageSnafu)?; | ||
|
|
||
| let count = count.parse::<u8>().context(ParseIntSnafu)?; | ||
| // TODO (@Techassi): Potentially replace this with TryFrom instead. | ||
| // The TryFrom<u8> impl for bool is only available in Rust 1.95+ | ||
| let enabled = count != 0; | ||
|
|
||
| Ok::<Self, FeatureGateParseError>(Self { | ||
| name, | ||
| stage, | ||
| enabled, | ||
| }) | ||
| }) | ||
| .parse_next(input) | ||
| } | ||
|
|
||
| /// Parses (and removes) the well-known, static metric name. | ||
| fn parse_metric_name(input: &mut &str) -> winnow::Result<()> { | ||
| "kubernetes_feature_enabled".void().parse_next(input) | ||
| } | ||
|
|
||
| /// Parses and collects a list of labels contained within `{` and `}`. | ||
| fn parse_labels<'s>(input: &mut &'s str) -> winnow::Result<HashMap<&'s str, &'s str>> { | ||
| separated( | ||
| // We expect at least two labels: name and stage | ||
| 2.., | ||
| // The value of the label can be empty | ||
| separated_pair(alphanumeric1, '=', ('"', alphanumeric0, '"')) | ||
| .map(|(key, (_, value, _))| (key, value)), | ||
| // There might be spaces between labels (separated by comma) | ||
| (',', space0), | ||
| ) | ||
| .parse_next(input) | ||
| } | ||
| } | ||
|
|
||
| /// A feature can be in one of four different stages. | ||
| /// | ||
| /// See the [list of feature gates] and [feature stages] in the official documentation. | ||
| /// | ||
| /// [list of feature gates]: https://v1-35.docs.kubernetes.io/docs/reference/command-line-tools-reference/feature-gates/#feature-gates | ||
| /// [feature stages]: https://v1-35.docs.kubernetes.io/docs/reference/command-line-tools-reference/feature-gates/#feature-stages | ||
| #[derive(Debug, strum::Display, strum::EnumString)] | ||
| #[strum(serialize_all = "UPPERCASE")] | ||
| pub enum FeatureStage { | ||
| /// An Alpha feature. | ||
| /// | ||
| /// - Disabled by default. | ||
| /// - Might be buggy. Enabling the feature may expose bugs. | ||
| /// - Support for feature may be dropped at any time without notice. | ||
| /// - The API may change in incompatible ways in a later software release without notice. | ||
| /// - Recommended for use only in short-lived testing clusters, due to increased risk of bugs | ||
| /// and lack of long-term support. | ||
| /// | ||
| /// Taken from the Kubernetes documentation. | ||
| Alpha, | ||
|
|
||
| /// A Beta feature. | ||
| /// | ||
| /// - Usually enabled by default. Beta API groups are [disabled by default]. | ||
| /// - The feature is well tested. Enabling the feature is considered safe. | ||
| /// - Support for the overall feature will not be dropped, though details may change. | ||
| /// - The schema and/or semantics of objects may change in incompatible ways in a subsequent | ||
| /// beta or stable release. When this happens, we will provide instructions for migrating to | ||
| /// the next version. This may require deleting, editing, and re-creating API objects. The | ||
| /// editing process may require some thought. This may require downtime for applications that | ||
| /// rely on the feature. | ||
| /// - Recommended for only non-business-critical uses because of potential for incompatible | ||
| /// changes in subsequent releases. If you have multiple clusters that can be upgraded | ||
| /// independently, you may be able to relax this restriction. | ||
| /// | ||
| /// Taken from the Kubernetes documentation. | ||
| Beta, | ||
|
|
||
| /// A General Availability feature. | ||
| /// | ||
| /// - The feature is always enabled; you cannot disable it. | ||
| /// - The corresponding feature gate is no longer needed. | ||
| /// - Stable versions of features will appear in released software for many subsequent versions. | ||
| /// | ||
| /// Taken from the Kubernetes documentation. | ||
| #[strum(serialize = "")] | ||
| GeneralAvailability, | ||
|
|
||
| /// A feature is deprecated. | ||
| /// | ||
| /// The official documentation doesn't explain this stage at all, but it exists (in metrics). | ||
| Deprecated, | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use crate::client::{initialize_operator, tests::test_cluster_info_opts}; | ||
|
|
||
| #[tokio::test] | ||
| #[ignore = "Tests depending on Kubernetes are not ran by default"] | ||
| async fn k8s_test_feature_gates() { | ||
| let client = initialize_operator(None, &test_cluster_info_opts()) | ||
| .await | ||
| .expect("KUBECONFIG variable must be configured."); | ||
|
|
||
| let feature_gates = client | ||
| .get_feature_gates() | ||
| .await | ||
| .expect("list of feature gates must parse"); | ||
|
|
||
| for feature_gate in feature_gates { | ||
| println!("{feature_gate:?}"); | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or 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
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.