Compare commits
1 Commits
main
...
add-env-te
Author | SHA1 | Date | |
---|---|---|---|
502013bb5b |
@ -1,47 +0,0 @@
|
|||||||
# Build a container image
|
|
||||||
when:
|
|
||||||
event:
|
|
||||||
- push
|
|
||||||
services:
|
|
||||||
docker:
|
|
||||||
image: docker:dind
|
|
||||||
commands:
|
|
||||||
- echo "1" > /proc/sys/net/ipv4/ip_forward
|
|
||||||
- dockerd -H tcp://0.0.0.0:2375 --tls=false
|
|
||||||
privileged: true
|
|
||||||
ports:
|
|
||||||
- 2375
|
|
||||||
- 16443
|
|
||||||
backend_options:
|
|
||||||
kubernetes:
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
memory: 500Mi
|
|
||||||
cpu: 200m
|
|
||||||
limits:
|
|
||||||
memory: 1000Mi
|
|
||||||
cpu: 1000m
|
|
||||||
steps:
|
|
||||||
build:
|
|
||||||
image: git.badhouseplants.net/badhouseplants/badhouseplants-builder:555262114ea81f6f286010474527f419b56d33a3
|
|
||||||
name: Build shoebill operator image
|
|
||||||
privileged: true
|
|
||||||
environment:
|
|
||||||
- PACKAGE_NAME=allanger/shoebill-operator
|
|
||||||
commands:
|
|
||||||
- |
|
|
||||||
if [[ "${CI_COMMIT_TAG}" ]]; then
|
|
||||||
export CUSTOM_TAG="${CI_COMMIT_TAG}";
|
|
||||||
fi
|
|
||||||
- build-container
|
|
||||||
secrets:
|
|
||||||
- gitea_token
|
|
||||||
backend_options:
|
|
||||||
kubernetes:
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
memory: 500Mi
|
|
||||||
cpu: 200m
|
|
||||||
limits:
|
|
||||||
memory: 1000Mi
|
|
||||||
cpu: 1000m
|
|
13
Makefile
Normal file
13
Makefile
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
ENVTEST_K8S_VERSION = 1.28.0
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
# -- A path to store binaries that are used in the Makefile
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
LOCALBIN ?= $(shell pwd)/bin
|
||||||
|
$(LOCALBIN):
|
||||||
|
mkdir -p $(LOCALBIN)
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: envtest
|
||||||
|
envtest: ## Download envtest-setup locally if necessary.
|
||||||
|
test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest
|
||||||
|
${LOCALBIN}/setup-envtest use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path
|
@ -4,8 +4,4 @@ use clap::{Args, Command, Parser, Subcommand};
|
|||||||
pub(crate) struct ManifestsArgs {
|
pub(crate) struct ManifestsArgs {
|
||||||
#[arg(long, short, default_value = "default")]
|
#[arg(long, short, default_value = "default")]
|
||||||
pub(crate) namespace: String,
|
pub(crate) namespace: String,
|
||||||
#[arg(long, short, default_value = "latest")]
|
|
||||||
pub(crate) tag: String,
|
|
||||||
#[arg(long, short, default_value = "shoebill")]
|
|
||||||
pub(crate) image: String,
|
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,15 @@
|
|||||||
use crate::api::v1alpha1::configsets_api::{
|
use crate::api::v1alpha1::configsets_api::ConfigSet;
|
||||||
ConfigSet, Input, InputWithName, TargetWithName, Templates,
|
|
||||||
};
|
|
||||||
use core::fmt;
|
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use handlebars::Handlebars;
|
use handlebars::Handlebars;
|
||||||
use k8s_openapi::api::core::v1::{ConfigMap, Secret};
|
use k8s_openapi::api::core::v1::{ConfigMap, Secret};
|
||||||
use k8s_openapi::apimachinery::pkg::apis::meta::v1::OwnerReference;
|
use k8s_openapi::ByteString;
|
||||||
use k8s_openapi::{ByteString, NamespaceResourceScope};
|
|
||||||
use kube::api::{ListParams, PostParams};
|
use kube::api::{ListParams, PostParams};
|
||||||
use kube::core::{Object, ObjectMeta};
|
use kube::core::{Object, ObjectMeta};
|
||||||
use kube::error::ErrorResponse;
|
use kube::error::ErrorResponse;
|
||||||
use kube::runtime::controller::Action;
|
use kube::runtime::controller::Action;
|
||||||
use kube::runtime::finalizer::Event as Finalizer;
|
|
||||||
use kube::runtime::watcher::Config;
|
use kube::runtime::watcher::Config;
|
||||||
use kube::runtime::{finalizer, Controller};
|
use kube::runtime::Controller;
|
||||||
use kube::{Api, Client, CustomResource};
|
use kube::{Api, Client, CustomResource};
|
||||||
use kube_client::core::DynamicObject;
|
|
||||||
use kube_client::{Resource, ResourceExt};
|
|
||||||
use log::*;
|
use log::*;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@ -25,12 +18,11 @@ use std::str::{from_utf8, Utf8Error};
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
static WATCHED_BY_SHU: &str = "badhouseplants.net/watched-by-shu";
|
|
||||||
static SHU_FINALIZER: &str = "badhouseplants.net/shu-cleanup";
|
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
|
#[error("SerializationError: {0}")]
|
||||||
|
SerializationError(#[source] serde_json::Error),
|
||||||
|
|
||||||
#[error("Kube Error: {0}")]
|
#[error("Kube Error: {0}")]
|
||||||
KubeError(#[source] kube::Error),
|
KubeError(#[source] kube::Error),
|
||||||
|
|
||||||
@ -39,11 +31,15 @@ pub enum Error {
|
|||||||
// so boxing this error to break cycles
|
// so boxing this error to break cycles
|
||||||
FinalizerError(#[source] Box<kube::runtime::finalizer::Error<Error>>),
|
FinalizerError(#[source] Box<kube::runtime::finalizer::Error<Error>>),
|
||||||
|
|
||||||
#[error("IllegalConfigSet: {0}")]
|
#[error("IllegalDocument")]
|
||||||
IllegalConfigSet(#[source] Box<dyn std::error::Error + Send + Sync>),
|
IllegalDocument,
|
||||||
|
}
|
||||||
|
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||||
|
impl Error {
|
||||||
|
pub fn metric_label(&self) -> String {
|
||||||
|
format!("{self:?}").to_lowercase()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) type Result<T, E = Error> = std::result::Result<T, E>;
|
|
||||||
|
|
||||||
// Context for our reconciler
|
// Context for our reconciler
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@ -53,39 +49,16 @@ pub struct Context {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn reconcile(csupstream: Arc<ConfigSet>, ctx: Arc<Context>) -> Result<Action> {
|
async fn reconcile(csupstream: Arc<ConfigSet>, ctx: Arc<Context>) -> Result<Action> {
|
||||||
let ns = csupstream.namespace().unwrap();
|
let cs = csupstream.clone();
|
||||||
let confset: Api<ConfigSet> = Api::namespaced(ctx.client.clone(), &ns);
|
info!(
|
||||||
finalizer(&confset, SHU_FINALIZER, csupstream.clone(), |event| async {
|
"reconciling {} - {}",
|
||||||
info!(
|
cs.metadata.name.clone().unwrap(),
|
||||||
"reconciling {} - {}",
|
cs.metadata.namespace.clone().unwrap()
|
||||||
csupstream.metadata.name.clone().unwrap(),
|
);
|
||||||
csupstream.metadata.namespace.clone().unwrap()
|
match cs.metadata.deletion_timestamp {
|
||||||
);
|
Some(_) => return cs.cleanup(ctx).await,
|
||||||
match event {
|
None => return cs.reconcile(ctx).await,
|
||||||
Finalizer::Apply(doc) => match csupstream.reconcile(ctx.clone()).await {
|
}
|
||||||
Ok(res) => {
|
|
||||||
info!("reconciled successfully");
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
error!("reconciliation has failed with error: {}", err);
|
|
||||||
Err(err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Finalizer::Cleanup(doc) => match csupstream.cleanup(ctx.clone()).await {
|
|
||||||
Ok(res) => {
|
|
||||||
info!("cleaned up successfully");
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
error!("cleanup has failed with error: {}", err);
|
|
||||||
Err(err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.map_err(|e| Error::FinalizerError(Box::new(e)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize the controller and shared state (given the crd is installed)
|
/// Initialize the controller and shared state (given the crd is installed)
|
||||||
@ -112,282 +85,6 @@ fn error_policy(doc: Arc<ConfigSet>, error: &Error, ctx: Arc<Context>) -> Action
|
|||||||
Action::requeue(Duration::from_secs(5 * 60))
|
Action::requeue(Duration::from_secs(5 * 60))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_secret_api(client: Client, namespace: String) -> Api<Secret> {
|
|
||||||
Api::namespaced(client, &namespace)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_configmap_api(client: Client, namespace: String) -> Api<ConfigMap> {
|
|
||||||
Api::namespaced(client, &namespace)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn gather_inputs(
|
|
||||||
client: Client,
|
|
||||||
namespace: String,
|
|
||||||
inputs: Vec<InputWithName>,
|
|
||||||
) -> Result<HashMap<String, String>> {
|
|
||||||
let mut result: HashMap<String, String> = HashMap::new();
|
|
||||||
for i in inputs {
|
|
||||||
info!("populating data from input {}", i.name);
|
|
||||||
match i.from.kind {
|
|
||||||
crate::api::v1alpha1::configsets_api::Kinds::Secret => {
|
|
||||||
let secret: String = match get_secret_api(client.clone(), namespace.clone())
|
|
||||||
.get(&i.from.name)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(s) => {
|
|
||||||
let data = s.data.clone().unwrap();
|
|
||||||
let value = match data.get(i.from.key.as_str()) {
|
|
||||||
Some(data) => match from_utf8(&data.0) {
|
|
||||||
Ok(data) => data,
|
|
||||||
Err(err) => return Err(Error::IllegalConfigSet(Box::from(err))),
|
|
||||||
},
|
|
||||||
None => {
|
|
||||||
return Err(Error::IllegalConfigSet(Box::from(format!(
|
|
||||||
"value is not set for the key: {}",
|
|
||||||
i.from.key
|
|
||||||
))))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
value.to_string()
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
error!("{err}");
|
|
||||||
return Err(Error::KubeError(err));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
result.insert(i.from.key, secret);
|
|
||||||
}
|
|
||||||
crate::api::v1alpha1::configsets_api::Kinds::ConfigMap => {
|
|
||||||
let configmap: String = match get_configmap_api(client.clone(), namespace.clone())
|
|
||||||
.get(&i.from.name)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(cm) => {
|
|
||||||
let data = cm.data.unwrap();
|
|
||||||
let value = match data.get(i.from.key.as_str()) {
|
|
||||||
Some(data) => data,
|
|
||||||
None => {
|
|
||||||
return Err(Error::IllegalConfigSet(Box::from(format!(
|
|
||||||
"value is not set for the key: {}",
|
|
||||||
i.from.key
|
|
||||||
))))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
value.to_string()
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
error!("{err}");
|
|
||||||
return Err(Error::KubeError(err));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
result.insert(i.name, configmap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn gather_targets(
|
|
||||||
client: Client,
|
|
||||||
namespace: String,
|
|
||||||
targets: Vec<TargetWithName>,
|
|
||||||
owner_reference: Vec<OwnerReference>,
|
|
||||||
) -> Result<(HashMap<String, Secret>, HashMap<String, ConfigMap>)> {
|
|
||||||
let mut target_secrets: HashMap<String, Secret> = HashMap::new();
|
|
||||||
let mut target_configmaps: HashMap<String, ConfigMap> = HashMap::new();
|
|
||||||
for target in targets {
|
|
||||||
match target.target.kind {
|
|
||||||
crate::api::v1alpha1::configsets_api::Kinds::Secret => {
|
|
||||||
let api = get_secret_api(client.clone(), namespace.clone());
|
|
||||||
match api.get_opt(&target.target.name).await {
|
|
||||||
Ok(sec_opt) => match sec_opt {
|
|
||||||
Some(sec) => target_secrets.insert(target.name, sec),
|
|
||||||
None => {
|
|
||||||
let empty_data: BTreeMap<String, ByteString> = BTreeMap::new();
|
|
||||||
let new_secret: Secret = Secret {
|
|
||||||
data: Some(empty_data),
|
|
||||||
metadata: ObjectMeta {
|
|
||||||
name: Some(target.target.name),
|
|
||||||
namespace: Some(namespace.clone()),
|
|
||||||
owner_references: Some(owner_reference.clone()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
match api.create(&PostParams::default(), &new_secret).await {
|
|
||||||
Ok(sec) => target_secrets.insert(target.name, sec),
|
|
||||||
Err(err) => {
|
|
||||||
error!("{err}");
|
|
||||||
return Err(Error::KubeError(err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(err) => {
|
|
||||||
error!("{err}");
|
|
||||||
return Err(Error::KubeError(err));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
crate::api::v1alpha1::configsets_api::Kinds::ConfigMap => {
|
|
||||||
let api = get_configmap_api(client.clone(), namespace.clone());
|
|
||||||
match api.get_opt(&target.target.name).await {
|
|
||||||
Ok(cm_opt) => match cm_opt {
|
|
||||||
Some(cm) => target_configmaps.insert(target.name, cm),
|
|
||||||
None => {
|
|
||||||
let empty_data: BTreeMap<String, String> = BTreeMap::new();
|
|
||||||
let new_configmap: ConfigMap = ConfigMap {
|
|
||||||
data: Some(empty_data),
|
|
||||||
metadata: ObjectMeta {
|
|
||||||
name: Some(target.target.name),
|
|
||||||
namespace: Some(namespace.clone()),
|
|
||||||
owner_references: Some(owner_reference.clone()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
match api.create(&PostParams::default(), &new_configmap).await {
|
|
||||||
Ok(cm) => target_configmaps.insert(target.name, cm),
|
|
||||||
Err(err) => {
|
|
||||||
error!("{err}");
|
|
||||||
return Err(Error::KubeError(err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(err) => {
|
|
||||||
error!("{err}");
|
|
||||||
return Err(Error::KubeError(err));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok((target_secrets, target_configmaps))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_owner_refenerce(object: ConfigSet) -> Vec<OwnerReference> {
|
|
||||||
let owner_reference = OwnerReference {
|
|
||||||
api_version: ConfigSet::api_version(&()).to_string(),
|
|
||||||
kind: ConfigSet::kind(&()).to_string(),
|
|
||||||
name: object.metadata.name.unwrap(),
|
|
||||||
uid: object.metadata.uid.unwrap(),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
vec![owner_reference]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_templates(
|
|
||||||
templates: Vec<Templates>,
|
|
||||||
target_secrets: &mut HashMap<String, Secret>,
|
|
||||||
target_configmaps: &mut HashMap<String, ConfigMap>,
|
|
||||||
targets: Vec<TargetWithName>,
|
|
||||||
inputs: HashMap<String, String>,
|
|
||||||
confset_name: String,
|
|
||||||
) -> Result<()> {
|
|
||||||
for template in templates {
|
|
||||||
let reg = Handlebars::new();
|
|
||||||
info!("building template {}", template.name);
|
|
||||||
let var = match reg.render_template(template.template.as_str(), &inputs) {
|
|
||||||
Ok(var) => var,
|
|
||||||
Err(err) => return Err(Error::IllegalConfigSet(Box::from(err))),
|
|
||||||
};
|
|
||||||
|
|
||||||
let target = match targets.iter().find(|target| target.name == template.target) {
|
|
||||||
Some(target) => target,
|
|
||||||
None => {
|
|
||||||
return Err(Error::IllegalConfigSet(Box::from(format!(
|
|
||||||
"target not found {}",
|
|
||||||
template.target
|
|
||||||
))));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match target.target.kind {
|
|
||||||
crate::api::v1alpha1::configsets_api::Kinds::Secret => {
|
|
||||||
let sec = target_secrets.get_mut(&template.target).unwrap();
|
|
||||||
let mut byte_var: ByteString = ByteString::default();
|
|
||||||
byte_var.0 = var.as_bytes().to_vec();
|
|
||||||
|
|
||||||
let mut existing_data = match sec.clone().data {
|
|
||||||
Some(sec) => sec,
|
|
||||||
None => BTreeMap::new(),
|
|
||||||
};
|
|
||||||
existing_data.insert(template.name, byte_var);
|
|
||||||
sec.data = Some(existing_data);
|
|
||||||
let mut existing_annotations = match sec.metadata.annotations.clone() {
|
|
||||||
Some(ann) => ann,
|
|
||||||
None => BTreeMap::new(),
|
|
||||||
};
|
|
||||||
existing_annotations.insert(WATCHED_BY_SHU.to_string(), confset_name.clone());
|
|
||||||
sec.metadata.annotations = Some(existing_annotations);
|
|
||||||
}
|
|
||||||
crate::api::v1alpha1::configsets_api::Kinds::ConfigMap => {
|
|
||||||
let cm = target_configmaps.get_mut(&template.target).unwrap();
|
|
||||||
let mut existing_data = match cm.clone().data {
|
|
||||||
Some(cm) => cm,
|
|
||||||
None => BTreeMap::new(),
|
|
||||||
};
|
|
||||||
existing_data.insert(template.name, var);
|
|
||||||
cm.data = Some(existing_data);
|
|
||||||
let mut existing_annotations = match cm.metadata.annotations.clone() {
|
|
||||||
Some(ann) => ann,
|
|
||||||
None => BTreeMap::new(),
|
|
||||||
};
|
|
||||||
existing_annotations.insert(WATCHED_BY_SHU.to_string(), confset_name.clone());
|
|
||||||
cm.metadata.annotations = Some(existing_annotations);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cleanup_templates(
|
|
||||||
templates: Vec<Templates>,
|
|
||||||
target_secrets: &mut HashMap<String, Secret>,
|
|
||||||
target_configmaps: &mut HashMap<String, ConfigMap>,
|
|
||||||
targets: Vec<TargetWithName>,
|
|
||||||
) -> Result<()> {
|
|
||||||
for template in templates {
|
|
||||||
info!("cleaning template {}", template.name);
|
|
||||||
let target = match targets.iter().find(|target| target.name == template.target) {
|
|
||||||
Some(target) => target,
|
|
||||||
None => {
|
|
||||||
return Err(Error::IllegalConfigSet(Box::from(format!(
|
|
||||||
"target not found {}",
|
|
||||||
template.target
|
|
||||||
))));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match target.target.kind {
|
|
||||||
crate::api::v1alpha1::configsets_api::Kinds::Secret => {
|
|
||||||
let sec = target_secrets.get_mut(&template.target).unwrap();
|
|
||||||
if let Some(mut existing_data) = sec.clone().data {
|
|
||||||
existing_data.remove(&template.name);
|
|
||||||
sec.data = Some(existing_data)
|
|
||||||
}
|
|
||||||
if let Some(mut existing_annotations) = sec.metadata.clone().annotations {
|
|
||||||
existing_annotations.remove(WATCHED_BY_SHU);
|
|
||||||
sec.metadata.annotations = Some(existing_annotations);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
crate::api::v1alpha1::configsets_api::Kinds::ConfigMap => {
|
|
||||||
let cm = target_configmaps.get_mut(&template.target).unwrap();
|
|
||||||
if let Some(mut existing_data) = cm.clone().data {
|
|
||||||
existing_data.remove(&template.name);
|
|
||||||
cm.data = Some(existing_data);
|
|
||||||
}
|
|
||||||
if let Some(mut existing_annotations) = cm.metadata.clone().annotations {
|
|
||||||
existing_annotations.remove(WATCHED_BY_SHU);
|
|
||||||
cm.metadata.annotations = Some(existing_annotations);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConfigSet {
|
impl ConfigSet {
|
||||||
// Reconcile (for non-finalizer related changes)
|
// Reconcile (for non-finalizer related changes)
|
||||||
async fn reconcile(&self, ctx: Arc<Context>) -> Result<Action> {
|
async fn reconcile(&self, ctx: Arc<Context>) -> Result<Action> {
|
||||||
@ -396,36 +93,170 @@ impl ConfigSet {
|
|||||||
* Then use them to build new values with templates
|
* Then use them to build new values with templates
|
||||||
* And then write those values to targets
|
* And then write those values to targets
|
||||||
*/
|
*/
|
||||||
|
let mut inputs: HashMap<String, String> = HashMap::new();
|
||||||
|
for input in self.spec.inputs.clone() {
|
||||||
|
info!("populating data from input {}", input.name);
|
||||||
|
match input.from.kind {
|
||||||
|
crate::api::v1alpha1::configsets_api::Kinds::Secret => {
|
||||||
|
let secrets: Api<Secret> = Api::namespaced(
|
||||||
|
ctx.client.clone(),
|
||||||
|
self.metadata.namespace.clone().unwrap().as_str(),
|
||||||
|
);
|
||||||
|
let secret: String = match secrets.get(&input.from.name).await {
|
||||||
|
Ok(s) => from_utf8(&s.data.clone().unwrap()[input.from.key.as_str()].0)
|
||||||
|
.unwrap()
|
||||||
|
.to_string(),
|
||||||
|
Err(err) => {
|
||||||
|
error!("{err}");
|
||||||
|
return Err(Error::KubeError(err));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
inputs.insert(input.from.key, secret);
|
||||||
|
}
|
||||||
|
crate::api::v1alpha1::configsets_api::Kinds::ConfigMap => {
|
||||||
|
let configmaps: Api<ConfigMap> = Api::namespaced(
|
||||||
|
ctx.client.clone(),
|
||||||
|
self.metadata.namespace.clone().unwrap().as_str(),
|
||||||
|
);
|
||||||
|
let configmap: String = match configmaps.get(&input.from.name).await {
|
||||||
|
Ok(cm) => {
|
||||||
|
let data = &cm.data.unwrap()[input.from.key.as_str()];
|
||||||
|
data.to_string()
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("{err}");
|
||||||
|
return Err(Error::KubeError(err));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
inputs.insert(input.name, configmap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let inputs: HashMap<String, String> = gather_inputs(
|
let mut target_secrets: HashMap<String, Secret> = HashMap::new();
|
||||||
ctx.client.clone(),
|
let mut target_configmaps: HashMap<String, ConfigMap> = HashMap::new();
|
||||||
self.metadata.namespace.clone().unwrap(),
|
|
||||||
self.spec.inputs.clone(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let owner_reference = build_owner_refenerce(self.clone());
|
for target in self.spec.targets.clone() {
|
||||||
|
match target.target.kind {
|
||||||
|
crate::api::v1alpha1::configsets_api::Kinds::Secret => {
|
||||||
|
let secrets: Api<Secret> = Api::namespaced(
|
||||||
|
ctx.client.clone(),
|
||||||
|
self.metadata.namespace.clone().unwrap().as_str(),
|
||||||
|
);
|
||||||
|
match secrets.get_opt(&target.target.name).await {
|
||||||
|
Ok(sec_opt) => match sec_opt {
|
||||||
|
Some(sec) => target_secrets.insert(target.name, sec),
|
||||||
|
None => {
|
||||||
|
let empty_data: BTreeMap<String, ByteString> = BTreeMap::new();
|
||||||
|
let new_secret: Secret = Secret {
|
||||||
|
data: Some(empty_data),
|
||||||
|
metadata: ObjectMeta {
|
||||||
|
name: Some(target.target.name),
|
||||||
|
namespace: self.metadata.namespace.clone(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
match secrets.create(&PostParams::default(), &new_secret).await {
|
||||||
|
Ok(sec) => target_secrets.insert(target.name, sec),
|
||||||
|
Err(err) => {
|
||||||
|
error!("{err}");
|
||||||
|
return Err(Error::KubeError(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
error!("{err}");
|
||||||
|
return Err(Error::KubeError(err));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
crate::api::v1alpha1::configsets_api::Kinds::ConfigMap => {
|
||||||
|
let configmaps: Api<ConfigMap> = Api::namespaced(
|
||||||
|
ctx.client.clone(),
|
||||||
|
self.metadata.namespace.clone().unwrap().as_str(),
|
||||||
|
);
|
||||||
|
match configmaps.get_opt(&target.target.name).await {
|
||||||
|
Ok(cm_opt) => match cm_opt {
|
||||||
|
Some(cm) => target_configmaps.insert(target.name, cm),
|
||||||
|
None => {
|
||||||
|
let empty_data: BTreeMap<String, String> = BTreeMap::new();
|
||||||
|
let new_configmap: ConfigMap = ConfigMap {
|
||||||
|
data: Some(empty_data),
|
||||||
|
metadata: ObjectMeta {
|
||||||
|
name: Some(target.target.name),
|
||||||
|
namespace: self.metadata.namespace.clone(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
match configmaps
|
||||||
|
.create(&PostParams::default(), &new_configmap)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(cm) => target_configmaps.insert(target.name, cm),
|
||||||
|
Err(err) => {
|
||||||
|
error!("{err}");
|
||||||
|
return Err(Error::KubeError(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
error!("{err}");
|
||||||
|
return Err(Error::KubeError(err));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let (mut target_secrets, mut target_configmaps) = gather_targets(
|
let mut templates: HashMap<String, String> = HashMap::new();
|
||||||
ctx.client.clone(),
|
for template in self.spec.templates.clone() {
|
||||||
self.metadata.namespace.clone().unwrap(),
|
let reg = Handlebars::new();
|
||||||
self.spec.targets.clone(),
|
info!("building template {}", template.name);
|
||||||
owner_reference,
|
let var = reg
|
||||||
)
|
.render_template(template.template.as_str(), &inputs)
|
||||||
.await?;
|
.unwrap();
|
||||||
|
match self
|
||||||
|
.spec
|
||||||
|
.targets
|
||||||
|
.iter()
|
||||||
|
.find(|target| target.name == template.target)
|
||||||
|
.unwrap()
|
||||||
|
.target
|
||||||
|
.kind
|
||||||
|
{
|
||||||
|
crate::api::v1alpha1::configsets_api::Kinds::Secret => {
|
||||||
|
let sec = target_secrets.get_mut(&template.target).unwrap();
|
||||||
|
let mut byte_var: ByteString = ByteString::default();
|
||||||
|
byte_var.0 = var.as_bytes().to_vec();
|
||||||
|
|
||||||
build_templates(
|
let mut existing_data = match sec.clone().data {
|
||||||
self.spec.templates.clone(),
|
Some(sec) => sec,
|
||||||
&mut target_secrets,
|
None => BTreeMap::new(),
|
||||||
&mut target_configmaps,
|
};
|
||||||
self.spec.targets.clone(),
|
existing_data.insert(template.name, byte_var);
|
||||||
inputs.clone(),
|
sec.data = Some(existing_data);
|
||||||
self.metadata.name.clone().unwrap(),
|
}
|
||||||
)?;
|
crate::api::v1alpha1::configsets_api::Kinds::ConfigMap => {
|
||||||
|
let cm = target_configmaps.get_mut(&template.target).unwrap();
|
||||||
|
let mut existing_data = match cm.clone().data {
|
||||||
|
Some(cm) => cm,
|
||||||
|
None => BTreeMap::new(),
|
||||||
|
};
|
||||||
|
existing_data.insert(template.name, var);
|
||||||
|
cm.data = Some(existing_data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (_, value) in target_secrets {
|
for (_, value) in target_secrets {
|
||||||
let secrets =
|
let secrets: Api<Secret> = Api::namespaced(
|
||||||
get_secret_api(ctx.client.clone(), self.metadata.namespace.clone().unwrap());
|
ctx.client.clone(),
|
||||||
|
self.metadata.namespace.clone().unwrap().as_str(),
|
||||||
|
);
|
||||||
match secrets
|
match secrets
|
||||||
.replace(
|
.replace(
|
||||||
value.metadata.name.clone().unwrap().as_str(),
|
value.metadata.name.clone().unwrap().as_str(),
|
||||||
@ -444,8 +275,10 @@ impl ConfigSet {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
for (_, value) in target_configmaps {
|
for (_, value) in target_configmaps {
|
||||||
let configmaps =
|
let configmaps: Api<ConfigMap> = Api::namespaced(
|
||||||
get_configmap_api(ctx.client.clone(), self.metadata.namespace.clone().unwrap());
|
ctx.client.clone(),
|
||||||
|
self.metadata.namespace.clone().unwrap().as_str(),
|
||||||
|
);
|
||||||
match configmaps
|
match configmaps
|
||||||
.replace(
|
.replace(
|
||||||
value.metadata.name.clone().unwrap().as_str(),
|
value.metadata.name.clone().unwrap().as_str(),
|
||||||
@ -468,69 +301,6 @@ impl ConfigSet {
|
|||||||
|
|
||||||
// Finalizer cleanup (the object was deleted, ensure nothing is orphaned)
|
// Finalizer cleanup (the object was deleted, ensure nothing is orphaned)
|
||||||
async fn cleanup(&self, ctx: Arc<Context>) -> Result<Action> {
|
async fn cleanup(&self, ctx: Arc<Context>) -> Result<Action> {
|
||||||
let inputs: HashMap<String, String> = gather_inputs(
|
|
||||||
ctx.client.clone(),
|
|
||||||
self.metadata.namespace.clone().unwrap(),
|
|
||||||
self.spec.inputs.clone(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let owner_reference = build_owner_refenerce(self.clone());
|
|
||||||
|
|
||||||
let (mut target_secrets, mut target_configmaps) = gather_targets(
|
|
||||||
ctx.client.clone(),
|
|
||||||
self.metadata.namespace.clone().unwrap(),
|
|
||||||
self.spec.targets.clone(),
|
|
||||||
owner_reference,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
cleanup_templates(
|
|
||||||
self.spec.templates.clone(),
|
|
||||||
&mut target_secrets,
|
|
||||||
&mut target_configmaps,
|
|
||||||
self.spec.targets.clone(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
for (_, value) in target_secrets {
|
|
||||||
let secrets =
|
|
||||||
get_secret_api(ctx.client.clone(), self.metadata.namespace.clone().unwrap());
|
|
||||||
match secrets
|
|
||||||
.replace(
|
|
||||||
value.metadata.name.clone().unwrap().as_str(),
|
|
||||||
&PostParams::default(),
|
|
||||||
&value,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(sec) => {
|
|
||||||
info!("secret {} is updated", sec.metadata.name.unwrap());
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
error!("{}", err);
|
|
||||||
return Err(Error::KubeError(err));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
for (_, value) in target_configmaps {
|
|
||||||
let configmaps =
|
|
||||||
get_configmap_api(ctx.client.clone(), self.metadata.namespace.clone().unwrap());
|
|
||||||
match configmaps
|
|
||||||
.replace(
|
|
||||||
value.metadata.name.clone().unwrap().as_str(),
|
|
||||||
&PostParams::default(),
|
|
||||||
&value,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(sec) => {
|
|
||||||
info!("secret {} is updated", sec.metadata.name.unwrap());
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
error!("{}", err);
|
|
||||||
return Err(Error::KubeError(err));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Ok::<Action, Error>(Action::await_change())
|
Ok::<Action, Error>(Action::await_change())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ use kube::{core::ObjectMeta, CustomResourceExt, ResourceExt};
|
|||||||
|
|
||||||
use crate::api::v1alpha1::configsets_api::ConfigSet;
|
use crate::api::v1alpha1::configsets_api::ConfigSet;
|
||||||
|
|
||||||
pub fn generate_kube_manifests(namespace: String, image: String, image_tag: String) {
|
pub fn generate_kube_manifests(namespace: String) {
|
||||||
print!("---\n{}", serde_yaml::to_string(&ConfigSet::crd()).unwrap());
|
print!("---\n{}", serde_yaml::to_string(&ConfigSet::crd()).unwrap());
|
||||||
print!(
|
print!(
|
||||||
"---\n{}",
|
"---\n{}",
|
||||||
@ -29,12 +29,7 @@ pub fn generate_kube_manifests(namespace: String, image: String, image_tag: Stri
|
|||||||
|
|
||||||
print!(
|
print!(
|
||||||
"---\n{}",
|
"---\n{}",
|
||||||
serde_yaml::to_string(&prepare_deployment(
|
serde_yaml::to_string(&prepare_deployment(namespace.clone())).unwrap()
|
||||||
namespace.clone(),
|
|
||||||
image.clone(),
|
|
||||||
image_tag.clone()
|
|
||||||
))
|
|
||||||
.unwrap()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,7 +119,7 @@ fn prepare_cluster_role_binding(namespace: String) -> ClusterRoleBinding {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_deployment(namespace: String, image: String, image_tag: String) -> Deployment {
|
fn prepare_deployment(namespace: String) -> Deployment {
|
||||||
let mut labels: BTreeMap<String, String> = BTreeMap::new();
|
let mut labels: BTreeMap<String, String> = BTreeMap::new();
|
||||||
labels.insert("container".to_string(), "shoebill-controller".to_string());
|
labels.insert("container".to_string(), "shoebill-controller".to_string());
|
||||||
|
|
||||||
@ -150,8 +145,8 @@ fn prepare_deployment(namespace: String, image: String, image_tag: String) -> De
|
|||||||
containers: vec![Container {
|
containers: vec![Container {
|
||||||
command: Some(vec!["/shoebill".to_string()]),
|
command: Some(vec!["/shoebill".to_string()]),
|
||||||
args: Some(vec!["controller".to_string()]),
|
args: Some(vec!["controller".to_string()]),
|
||||||
image: Some(format!("{}:{}", image, image_tag)),
|
image: Some("shoebill".to_string()),
|
||||||
image_pull_policy: Some("IfNotPresent".to_string()),
|
image_pull_policy: Some("Never".to_string()),
|
||||||
name: "shoebill-controller".to_string(),
|
name: "shoebill-controller".to_string(),
|
||||||
env: Some(vec![EnvVar {
|
env: Some(vec![EnvVar {
|
||||||
name: "RUST_LOG".to_string(),
|
name: "RUST_LOG".to_string(),
|
||||||
|
@ -25,11 +25,9 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
match &cli.command {
|
match &cli.command {
|
||||||
Commands::Manifests(args) => helpers::manifests::generate_kube_manifests(
|
Commands::Manifests(args) => {
|
||||||
args.namespace.clone(),
|
helpers::manifests::generate_kube_manifests(args.namespace.clone())
|
||||||
args.image.clone(),
|
}
|
||||||
args.tag.clone(),
|
|
||||||
),
|
|
||||||
Commands::Controller(args) => {
|
Commands::Controller(args) => {
|
||||||
// Initiatilize Kubernetes controller state
|
// Initiatilize Kubernetes controller state
|
||||||
let controller = configsets_controller::setup();
|
let controller = configsets_controller::setup();
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
#!/bin/env sh
|
|
||||||
|
|
||||||
# Run shoebill generate and apply
|
|
||||||
TAG=$(git rev-parse HEAD)
|
|
||||||
IMAGE="git.badhouseplants.net/allanger/shoebill-operator"
|
|
||||||
NAMESPACE="test-shoebill-operator"
|
|
||||||
|
|
||||||
kubectl create namespace $NAMESPACE
|
|
||||||
shoebill manifests -i $IMAGE -t $TAG -n $NAMESPACE > /tmp/manifests.yaml
|
|
||||||
kubectl apply -f /tmp/manifests.yaml
|
|
||||||
kubectl rollout status -n $NAMESPACE deployment shoebill-controller
|
|
||||||
|
|
||||||
kubectl delete -f /tmp/manifests.yaml
|
|
||||||
kubectl delete namespace $NAMESPACE
|
|
@ -9,16 +9,7 @@ spec:
|
|||||||
target:
|
target:
|
||||||
kind: Secret
|
kind: Secret
|
||||||
name: app-connection-string
|
name: app-connection-string
|
||||||
- name: existing-target
|
|
||||||
target:
|
|
||||||
kind: Secret
|
|
||||||
name: database-secret
|
|
||||||
inputs:
|
inputs:
|
||||||
- name: PROTO
|
|
||||||
from:
|
|
||||||
kind: ConfigMap
|
|
||||||
name: database-configmap
|
|
||||||
key: PROTOCOL
|
|
||||||
- name: PASSWORD
|
- name: PASSWORD
|
||||||
from:
|
from:
|
||||||
kind: Secret
|
kind: Secret
|
||||||
@ -34,13 +25,15 @@ spec:
|
|||||||
kind: Secret
|
kind: Secret
|
||||||
name: database-secret
|
name: database-secret
|
||||||
key: DATABASE
|
key: DATABASE
|
||||||
|
- name: PROTO
|
||||||
|
from:
|
||||||
|
kind: ConfigMap
|
||||||
|
name: database-configmap
|
||||||
|
key: PROTOCOL
|
||||||
templates:
|
templates:
|
||||||
- name: CONNECTION
|
- name: CONNECTION
|
||||||
template: "{{ PROTO }}:{{ USERNAME }}:{{ PASSWORD }}/{{ DATABASE }}"
|
template: "{{ PROTO }}:{{ USERNAME }}:{{ PASSWORD }}/{{ DATABASE }}"
|
||||||
target: app-connection-string
|
target: app-connection-string
|
||||||
- name: EXISTING
|
|
||||||
template: TEST
|
|
||||||
target: existing-target
|
|
||||||
- name: IS_POSTGRES
|
- name: IS_POSTGRES
|
||||||
template: |
|
template: |
|
||||||
{{#if (eq PROTO "postgresql") }}
|
{{#if (eq PROTO "postgresql") }}
|
Loading…
x
Reference in New Issue
Block a user