Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions config/crds/fleet-addon-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,37 @@ spec:
description: feature gates controlling experimental features
nullable: true
properties:
configMap:
description: FeaturesConfigMap references a ConfigMap where to apply feature flags. If a ConfigMap is referenced, the controller will update it instead of upgrading the Fleet chart.
nullable: true
properties:
ref:
description: ObjectReference contains enough information to let you inspect or modify the referred object.
nullable: true
properties:
apiVersion:
description: API version of the referent.
type: string
fieldPath:
description: 'If referring to a piece of an object instead of an entire object, this string should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. For example, if the object reference is to a container within a pod, this would take on a value like: "spec.containers{name}" (where "name" refers to the name of the container that triggered the event) or if no container name is specified "spec.containers[2]" (container with index 2 in this pod). This syntax is chosen only to have some well-defined way of referencing a part of an object.'
type: string
kind:
description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
name:
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names'
type: string
namespace:
description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/'
type: string
resourceVersion:
description: 'Specific resourceVersion to which this reference is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency'
type: string
uid:
description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids'
type: string
type: object
type: object
experimentalHelmOps:
description: Enables experimental Helm operations support.
type: boolean
Expand Down
21 changes: 21 additions & 0 deletions docs/src/02_getting_started/02_configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,24 @@ spec:

**By default, if the `featureGates` field is not present, these feature gates are *enabled*. To disable these need to explicitly be set to `false`.**

Optionally, the `featureGates` flags can be synced to a `ConfigMap` object.
This is useful when Fleet is installed and managed by Rancher.
When a `ConfigMap` reference is defined, the controller will just sync the `featureGates` to it, without making any changes to the Fleet helm chart.

```yaml
apiVersion: addons.cluster.x-k8s.io/v1alpha1
kind: FleetAddonConfig
metadata:
name: fleet-addon-config
spec:
config:
featureGates:
experimentalOciStorage: true # Enables experimental OCI storage support
experimentalHelmOps: true # Enables experimental Helm operations support
configMap:
ref:
apiVersion: v1
kind: ConfigMap
name: rancher-config
namespace: cattle-system
```
1 change: 1 addition & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ deploy features="": _download-kustomize
just generate {{features}}
just build-and-load
kustomize build config/default | kubectl apply -f -
kubectl -n caapf-system rollout restart deployment/caapf-controller-manager
kubectl --context kind-dev apply -f testdata/config.yaml
kubectl wait fleetaddonconfigs fleet-addon-config --timeout=150s --for=condition=Ready=true

Expand Down
164 changes: 162 additions & 2 deletions src/api/fleet_addon_config.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use std::collections::BTreeMap;

use fleet_api_rs::fleet_cluster::{ClusterAgentEnvVars, ClusterAgentTolerations};
use k8s_openapi::{
api::core::v1::ObjectReference,
api::core::v1::{ConfigMap, ObjectReference},
apimachinery::pkg::apis::meta::v1::{Condition, LabelSelector},
};
use kube::{
Expand All @@ -9,8 +11,11 @@ use kube::{
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_yaml::{Value, Error};

pub const AGENT_NAMESPACE: &str = "fleet-addon-agent";
pub const EXPERIMENTAL_OCI_STORAGE: &str = "EXPERIMENTAL_OCI_STORAGE";
pub const EXPERIMENTAL_HELM_OPS: &str = "EXPERIMENTAL_HELM_OPS";

/// This provides a config for fleet addon functionality
#[derive(CustomResource, Deserialize, Serialize, Clone, Default, Debug, CELSchema)]
Expand Down Expand Up @@ -239,6 +244,61 @@ pub struct FeatureGates {

/// Enables experimental Helm operations support.
pub experimental_helm_ops: bool,

// Enables syncing of feature gates to a ConfigMap.
pub config_map: Option<FeaturesConfigMap>,
}

impl FeatureGates {
/// Returns true if a ConfigMap reference is defined.
pub(crate) fn has_config_map_ref(&self) -> bool {
self.config_map.as_ref().and_then(|c | c.reference.as_ref()).is_some()
}

/// Syncs the `fleet` data key on the ConfigMap with FeatureGates
pub(crate) fn sync_configmap(&self, config_map: &mut ConfigMap) -> Result<(), Error> {
let mut empty_data: BTreeMap<String,String> = BTreeMap::new();
let data = config_map.data.as_mut().unwrap_or(&mut empty_data);
match data.get("fleet") {
Some(fleet_data) => {
data.insert(String::from("fleet"), self.merge_features(Some(fleet_data))?);
}
None => {
data.insert(String::from("fleet"), self.merge_features(None)?);
}
}
config_map.data = Some(data.clone());
Ok(())
}

/// Merge the feature gates environment variables with a provided optional input.
fn merge_features(&self, input: Option<&String>) -> Result<String, Error> {
let mut extra_env_map: BTreeMap<String, String> = BTreeMap::new();
let mut values = FleetChartValues {
..Default::default()
};

if let Some(input) = input {
values = serde_yaml::from_str(input)?;
// If there are existing extraEnv entries, convert them to a map.
if let Some(extra_env) = values.extra_env {
extra_env_map = extra_env.into_iter().map(|var| (var.name.unwrap_or_default(), var.value.unwrap_or_default())).collect();
}
}

// Sync the feature flags to the map.
extra_env_map.insert(String::from(EXPERIMENTAL_HELM_OPS), self.experimental_helm_ops.to_string());
extra_env_map.insert(String::from(EXPERIMENTAL_OCI_STORAGE), self.experimental_oci_storage.to_string());

// Convert the map back to list.
values.extra_env = Some(extra_env_map.iter()
.map(|(key, value)|
EnvironmentVariable{name: Some(key.clone()), value: Some(value.clone())})
.collect());

//Return the serialized updated values.
serde_yaml::to_string(&values)
}
}

impl Default for FeatureGates {
Expand All @@ -247,10 +307,38 @@ impl Default for FeatureGates {
// Unless is set otherwise, these features are enabled by CAAPF
experimental_oci_storage: true,
experimental_helm_ops: true,
config_map: None,
}
}
}

/// FeaturesConfigMap references a ConfigMap where to apply feature flags.
/// If a ConfigMap is referenced, the controller will update it instead of upgrading the Fleet chart.
#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct FeaturesConfigMap {
// The reference to a ConfigMap resource
#[serde(rename = "ref")]
pub reference: Option<ObjectReference>,
}

/// FleetChartValues represents Fleet chart values.
#[derive(Clone, Default, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FleetChartValues {
pub extra_env: Option<Vec<EnvironmentVariable>>,
#[serde(flatten)]
pub other: Value
}

/// EnvironmentVariable is a simple name/value pair.
#[derive(Clone, Default, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EnvironmentVariable {
pub name: Option<String>,
pub value: Option<String>
}

#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct FleetInstall {
Expand Down Expand Up @@ -381,7 +469,11 @@ impl FleetAddonConfig {

#[cfg(test)]
mod tests {
use crate::api::fleet_addon_config::NamingStrategy;
use std::collections::BTreeMap;

use k8s_openapi::api::core::v1::ConfigMap;

use crate::api::fleet_addon_config::{FeatureGates, NamingStrategy};

#[tokio::test]
async fn test_naming_strategy() {
Expand Down Expand Up @@ -429,4 +521,72 @@ mod tests {
.apply(None)
);
}

#[tokio::test]
async fn test_sync_config_map() {
let want_fleet_data = r#"extraEnv:
- name: EXPERIMENTAL_HELM_OPS
value: 'true'
- name: EXPERIMENTAL_OCI_STORAGE
value: 'true'
- name: foo
value: bar
foo:
bar: foobar
"#;
let fleet_data = r#"foo:
bar: foobar
extraEnv:
- name: foo
value: bar
- name: EXPERIMENTAL_OCI_STORAGE
value: "false"
- name: EXPERIMENTAL_HELM_OPS
value: "false"
"#;
let mut data: BTreeMap<String, String> = BTreeMap::new();
data.insert(String::from("fleet"), String::from(fleet_data));
let mut config_map = ConfigMap {
data: Some(data),
..Default::default()
};
let feature_gates = FeatureGates {
experimental_oci_storage: true,
experimental_helm_ops: true,
config_map: None
};
let _ = feature_gates.sync_configmap(&mut config_map);

let synced_fleet_data = config_map.data.as_ref().and_then(|d | d.get("fleet")).unwrap();
assert_eq!(
want_fleet_data.to_string(),
synced_fleet_data.clone()
)
}

#[tokio::test]
async fn test_sync_empty_config_map() {
let want_fleet_data = r#"extraEnv:
- name: EXPERIMENTAL_HELM_OPS
value: 'false'
- name: EXPERIMENTAL_OCI_STORAGE
value: 'false'
"#;
let mut config_map = ConfigMap {
data: None,
..Default::default()
};
let feature_gates = FeatureGates {
experimental_oci_storage: false,
experimental_helm_ops: false,
config_map: None
};
let _ = feature_gates.sync_configmap(&mut config_map);

let synced_fleet_data = config_map.data.as_ref().and_then(|d | d.get("fleet")).unwrap();
assert_eq!(
want_fleet_data.to_string(),
synced_fleet_data.clone()
)
}
}
51 changes: 49 additions & 2 deletions src/controllers/addon_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ use crate::{
};

use super::{
controller::Context,
controller::{patch, Context},
helm::{
self,
install::{ChartSearch, FleetChart, FleetOptions, HelmOperation},
},
}, GetOrCreateError, PatchError,
};

#[derive(Resource, Serialize, Deserialize, Default, Clone, Debug)]
Expand Down Expand Up @@ -96,6 +96,13 @@ impl FleetAddonConfig {
#[instrument(skip_all, fields(reconcile_id, name = self.name_any(), namespace = self.namespace()))]
pub async fn reconcile_helm(&mut self, ctx: Arc<Context>) -> crate::Result<Action> {
let _current = Span::current().record("reconcile_id", display(telemetry::get_trace_id()));
if let Some(feature_gates) = self.spec.config.as_ref().and_then(|c | c.feature_gates.as_ref()) {
if feature_gates.has_config_map_ref() {
self.update_config_map(ctx.clone()).await?;
return Ok(Action::await_change())
}
}

let chart = FleetChart {
repo: "https://rancher.github.io/fleet-helm-charts/".into(),
namespace: "cattle-fleet-system".into(),
Expand Down Expand Up @@ -454,6 +461,29 @@ impl FleetAddonConfig {

Ok(None)
}

async fn update_config_map(&self, ctx: Arc<Context>) -> ConfigMapSyncResult<()> {
let feature_gates = self
.spec
.config
.as_ref()
.map(|c| c.feature_gates.clone().unwrap_or_default())
.unwrap_or_default();

if let Some(reference) = feature_gates.config_map.as_ref()
.and_then(|c |c.reference.as_ref()) {
let mut config_map: ConfigMap = ctx.client.fetch(reference).await?;
feature_gates.sync_configmap(&mut config_map)?;
patch(
ctx,
&mut config_map,
&PatchParams::apply("addon-provider-fleet").force(),
)
.await?;
}

Ok(())
}
}

pub fn to_dynamic_event<R>(
Expand Down Expand Up @@ -533,6 +563,23 @@ pub enum DynamicWatcherError {
SelectorParseError(#[from] kube::core::ParseExpressionError),
}

pub type ConfigMapSyncResult<T> = std::result::Result<T, ConfigMapSyncError>;

#[derive(Error, Debug)]
pub enum ConfigMapSyncError {
#[error("ConfigMap fetch error: {0}")]
FetchConfigMap(#[from] kube::Error),

#[error("ConfigMap get or create error: {0}")]
GetOrCreate(#[from] GetOrCreateError),

#[error("ConfigMap patch error: {0}")]
Patch(#[from] PatchError),

#[error("ConfigMap serialization error: {0}")]
Serialize(#[from] serde_yaml::Error),
}

mod tests {
#[test]
fn test() {
Expand Down
5 changes: 4 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::io;

use controllers::{
addon_config::{AddonConfigSyncError, DynamicWatcherError, FleetPatchError},
addon_config::{AddonConfigSyncError, DynamicWatcherError, FleetPatchError, ConfigMapSyncError},
helm, BundleError, SyncError,
};
use futures::channel::mpsc::TrySendError;
Expand Down Expand Up @@ -49,6 +49,9 @@ pub enum Error {

#[error("IllegalDocument")]
IllegalDocument,

#[error("ConfigMap sync error: {0}")]
ConfigMapSyncError(#[from] ConfigMapSyncError),
}

pub type Result<T, E = Error> = std::result::Result<T, E>;
Expand Down