Skip to content

Commit

Permalink
Add Support for HPA Object (#28)
Browse files Browse the repository at this point in the history
* chore: add initial struct for HPA

* chore: add default min replica

* chore: add hpa to main nimble Object

* chore: remove replicas field from manifest

reason behind is that when using with HPA it is

ideal that we don't define replicas

* docs: add example for hpa usecase

* feat: add controller for hpa

* chore: add requests field for hpa usecase

* chore: update mods

* feat: add hpa controller to main

* chore: update crd
  • Loading branch information
ivaltryek authored Mar 10, 2024
1 parent 9adcb57 commit 8ef1778
Show file tree
Hide file tree
Showing 13 changed files with 310 additions and 20 deletions.
49 changes: 45 additions & 4 deletions crd/nimble.ivaltryek.github.com.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ spec:
spec:
properties:
deployment:
description: Spec for Deployment Object
properties:
annotations:
additionalProperties:
Expand Down Expand Up @@ -278,16 +279,56 @@ spec:
type: string
description: Labels to be applied to the deployment and its pods.
type: object
replicas:
required:
- containers
- labels
type: object
hpa:
description: Spec for Autoscaling (HPA) Object
nullable: true
properties:
annotations:
additionalProperties:
type: string
default:
app.kubernetes.io/managed-by: kube-nimble
description: Annotations to be applied to the HPA object
nullable: true
type: object
max:
description: maxReplicas is the upper limit for the number of replicas to which the autoscaler can scale up. It cannot be less that minReplicas.
format: int32
type: integer
min:
default: 1
description: Number of desired replicas for the deployment.
description: minReplicas is the lower limit for the number of replicas to which the autoscaler can scale down. It defaults to 1 pod. minReplicas is allowed to be 0 if the alpha feature gate HPAScaleToZero is enabled and at least one Object or External metric is configured. Scaling is active as long as at least one metric value is available.
format: int32
nullable: true
type: integer
resourcePolicy:
description: resource refers to a resource metric (such as those specified in requests and limits) known to Kubernetes describing each pod in the current scale target (e.g. CPU or memory).
nullable: true
properties:
avgUtil:
description: avgUtil is the target value of the average of the resource metric across all relevant pods, represented as a percentage of the requested value of the resource for the pods. Currently only valid for Resource metric source type
format: int32
nullable: true
type: integer
name:
description: name is the name of the resource in question.
type: string
type:
description: type represents whether the metric type is Utilization, Value, or AverageValue
type: string
required:
- name
- type
type: object
required:
- containers
- labels
- max
type: object
service:
description: Spec for Service Object
nullable: true
properties:
annotations:
Expand Down
27 changes: 27 additions & 0 deletions examples/deployment-service-hpa.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
apiVersion: ivaltryek.github.com/v1
kind: Nimble
metadata:
name: demo-deployment-service-hpa
namespace: test
spec:
deployment:
containers:
- image: nginx:stable
name: nginx-stable
requests:
cpu: 50m
labels:
test: hpa
service:
ports:
- name: http
port: 80
targetPort: 80
hpa:
min: 2
max: 4
resourcePolicy:
name: cpu
type: Utilization
avgUtil: 30

2 changes: 2 additions & 0 deletions examples/deployment-with-service.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ spec:
containers:
- image: nginx:stable
name: nginx-stable
requests:
cpu: 50m
labels:
app: nginx
env: test
Expand Down
1 change: 0 additions & 1 deletion src/controllers/dpcontroller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ pub async fn reconcile(nimble: Arc<Nimble>, ctx: Arc<ContextData>) -> Result<Act
..ObjectMeta::default()
},
spec: Some(DeploymentSpec {
replicas: Some(nimble.spec.deployment.replicas),
selector: LabelSelector {
match_expressions: None,
match_labels: Some(labels.clone()),
Expand Down
147 changes: 147 additions & 0 deletions src/controllers/hpacontroller.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};

use k8s_openapi::api::autoscaling::v2::{
CrossVersionObjectReference, HorizontalPodAutoscaler, HorizontalPodAutoscalerSpec,
};
use kube::{
api::{ObjectMeta, Patch, PatchParams},
runtime::{controller::Action, watcher::Config, Controller},
Api, Resource,
};
use tracing::{error, info};

use crate::{
common::client::{error_policy, ContextData, Error},
crds::nimble::Nimble,
transformers::hpa::transform_metrics,
};

use futures::StreamExt;
use tokio::time::Duration;

static DOES_HPA_EXIST: AtomicBool = AtomicBool::new(false);

/**
* Reconciles the HPA of a Nimble instance.
*
* This function orchestrates the deployment of a Nimble instance based on the provided context data.
* It creates or updates a Kubernetes Deployment object with the specified configuration.
*
* # Arguments
* - `nimble`: An Arc reference to the Nimble instance to reconcile.
* - `ctx`: An Arc reference to the context data needed for reconciliation.
*
* # Returns
* An Ok(Action) containing the requeue action with a specified duration on successful reconciliation,
* or an Err(Error) if the reconciliation process encounters any errors.
*
* # Errors
* - Returns an Error::MissingObjectKey if required object keys are missing.
* - Returns an Error::NimbleObjectCreationFailed if the creation or update of the Nimble object fails.
*/
pub async fn reconcile(nimble: Arc<Nimble>, ctx: Arc<ContextData>) -> Result<Action, Error> {
match nimble.spec.hpa.clone() {
Some(hpa_spec) => {
let client = &ctx.client;

let oref = nimble.controller_owner_ref(&()).unwrap();

let hpa: HorizontalPodAutoscaler = HorizontalPodAutoscaler {
metadata: ObjectMeta {
annotations: hpa_spec.annotations.clone(),
owner_references: Some(vec![oref]),
name: nimble.metadata.name.clone(),
..ObjectMeta::default()
},
spec: Some(HorizontalPodAutoscalerSpec {
max_replicas: hpa_spec.max,
min_replicas: hpa_spec.min,
scale_target_ref: CrossVersionObjectReference {
api_version: Some("apps/v1".to_owned()),
kind: "Deployment".to_owned(),
name: nimble.metadata.name.clone().unwrap(),
},
metrics: transform_metrics(nimble.spec.hpa.clone()),
..HorizontalPodAutoscalerSpec::default()
}),
..HorizontalPodAutoscaler::default()
};

let hpa_api = Api::<HorizontalPodAutoscaler>::namespaced(
client.clone(),
nimble
.metadata
.namespace
.as_ref()
.ok_or_else(|| Error::MissingObjectKey(".metadata.namespace"))?,
);

hpa_api
.patch(
hpa.metadata
.name
.as_ref()
.ok_or_else(|| Error::MissingObjectKey(".metadata.name"))?,
&PatchParams::apply("nimble.ivaltryek.github.com"),
&Patch::Apply(&hpa),
)
.await
.map_err(Error::NimbleObjectCreationFailed)?;

DOES_HPA_EXIST.store(true, Ordering::Relaxed);

Ok(Action::requeue(Duration::from_secs(30)))
}
_ => {
DOES_HPA_EXIST.store(false, Ordering::Relaxed);
Ok(Action::await_change())
}
}
}

/**
* Starts the main loop for the Nimble HPA controller.
*
* This function initiates the main event loop for the Nimble controller, responsible for monitoring and reconciling Nimble resources in the Kubernetes cluster.
*
* Args:
* - crd_api (Api<Nimble>): Reference to the Kubernetes API client for Nimble resources.
* - context (Arc<ContextData>): Reference-counted handle to the controller context data.
*
* Returns:
* - Future: Represents the completion of the controller loop.
*
* Process:
* 1. Creates a new controller instance using the provided API client and default configuration.
* 2. Configures the controller to shut down gracefully on receiving specific signals.
* 3. Starts the controller loop, running the `reconcile` function for each Nimble resource change it detects.
* 4. Within the loop, handles reconciliation results:
* - On success: logs a message with resource information.
* - On error: logs an error message with details.
* 5. Waits for the loop to complete.
*/
pub async fn run_hpa_controller(crd_api: Api<Nimble>, context: Arc<ContextData>) {
Controller::new(crd_api.clone(), Config::default())
.shutdown_on_signal()
.run(reconcile, error_policy, context)
.for_each(|reconcilation_result| async move {
match reconcilation_result {
Ok((nimble_resource, _)) => {
// Log the reconciliation message only if service field exist in object manifest.
if DOES_HPA_EXIST.load(Ordering::Relaxed) {
info!(msg = "HPA reconciliation successful.",
resource_name = ?nimble_resource.name,
namespace = ?nimble_resource.namespace.unwrap(),
);
}
}
Err(reconciliation_err) => {
error!("Service reconciliation error: {:?}", reconciliation_err)
}
}
})
.await;
}
1 change: 1 addition & 0 deletions src/controllers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod dpcontroller;
pub mod hpacontroller;
pub mod servicecontroller;
13 changes: 0 additions & 13 deletions src/crds/deploymentspec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ use serde::{Deserialize, Serialize};
pub struct DeploySpec {
#[doc = "Containers to run in the deployment."]
pub containers: Vec<ContainerSpec>,
#[doc = "Number of desired replicas for the deployment."]
#[serde(default = "default_replicas")]
pub replicas: i32,
#[doc = "Labels to be applied to the deployment and its pods."]
pub labels: BTreeMap<String, String>,
#[doc = "Annotations to be applied to the deployment and its pods."]
Expand Down Expand Up @@ -132,16 +129,6 @@ pub struct EnvFromSpec {
pub secret_ref: Option<String>,
}

/**
* This function returns the default value for the number of replicas.
* In this specific case, the default is set to 1.
*
* You can customize this value by modifying the function body.
*/
pub fn default_replicas() -> i32 {
1
}

/* This function creates a default `Option<BTreeMap<String, String>>` containing a single key-value pair:
* - "app.kubernetes.io/managed-by": "kube-nimble"
*
Expand Down
50 changes: 50 additions & 0 deletions src/crds/hpaspec.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
use std::collections::BTreeMap;

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use super::deploymentspec::default_annotations;

#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, JsonSchema)]
pub struct HPASpec {
#[doc = "Annotations to be applied to the HPA object"]
#[serde(default = "default_hpa_annotations")]
pub annotations: Option<BTreeMap<String, String>>,
#[doc = "maxReplicas is the upper limit for the number of replicas to which the autoscaler can scale up.
It cannot be less that minReplicas."]
pub max: i32,
#[doc = "minReplicas is the lower limit for the number of replicas to which the autoscaler can scale down.
It defaults to 1 pod.
minReplicas is allowed to be 0 if the alpha feature gate HPAScaleToZero is enabled and at least one Object or External metric is configured.
Scaling is active as long as at least one metric value is available."]
#[serde(default = "default_min_replicas")]
pub min: Option<i32>,
#[doc = "resource refers to a resource metric (such as those specified in requests and limits)
known to Kubernetes describing each pod in the current scale target (e.g. CPU or memory)."]
#[serde(rename = "resourcePolicy")]
pub resource_policy: Option<ResourceMetricSpec>,
}

#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, JsonSchema)]
pub struct ResourceMetricSpec {
#[doc = "name is the name of the resource in question."]
pub name: String,
#[doc = "type represents whether the metric type is Utilization, Value, or AverageValue"]
#[serde(rename = "type")]
pub type_: String,
#[doc = "avgUtil is the target value of the average of the resource metric across all relevant pods,
represented as a percentage of the requested value of the resource for the pods.
Currently only valid for Resource metric source type"]
#[serde(rename = "avgUtil")]
pub average_utilization: Option<i32>,
}

// Return default annotations to applied to an object.
fn default_hpa_annotations() -> Option<BTreeMap<String, String>> {
default_annotations()
}

// Return default min replica value.
fn default_min_replicas() -> Option<i32> {
Some(1)
}
1 change: 1 addition & 0 deletions src/crds/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod deploymentspec;
pub mod hpaspec;
pub mod nimble;
pub mod servicespec;
6 changes: 5 additions & 1 deletion src/crds/nimble.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use super::{deploymentspec::DeploySpec, servicespec::SvcSpec};
use super::{deploymentspec::DeploySpec, hpaspec::HPASpec, servicespec::SvcSpec};

#[derive(kube::CustomResource, Serialize, Deserialize, Debug, PartialEq, Clone, JsonSchema)]
#[kube(
Expand All @@ -14,6 +14,10 @@ use super::{deploymentspec::DeploySpec, servicespec::SvcSpec};
)]

pub struct NimbleSpec {
#[doc = "Spec for Deployment Object"]
pub deployment: DeploySpec,
#[doc = "Spec for Service Object"]
pub service: Option<SvcSpec>,
#[doc = "Spec for Autoscaling (HPA) Object"]
pub hpa: Option<HPASpec>,
}
4 changes: 3 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use tracing::info;

use crate::common::client::ContextData;
use crate::controllers::dpcontroller::run_dp_controller;
use crate::controllers::hpacontroller::run_hpa_controller;
use crate::controllers::servicecontroller::run_svc_controller;
use crate::crds::nimble::Nimble;

Expand All @@ -23,9 +24,10 @@ async fn main() {

info!("starting nimble controller");

let (_, _) = futures::join!(
let (_, _, _) = futures::join!(
run_dp_controller(crd_api.clone(), context.clone()),
run_svc_controller(crd_api.clone(), context.clone()),
run_hpa_controller(crd_api.clone(), context.clone())
);

info!("controller terminated");
Expand Down
Loading

0 comments on commit 8ef1778

Please sign in to comment.