From 13b959e1e6f5bf901f68e0b80108866e90572ee3 Mon Sep 17 00:00:00 2001 From: paulobressan Date: Fri, 22 Dec 2023 15:57:26 -0300 Subject: [PATCH 01/16] feat: implemented reconcile ogmios port resources --- operator/Cargo.lock | 55 +++++ operator/Cargo.toml | 5 + operator/src/config.rs | 31 +++ operator/src/controller.rs | 30 ++- operator/src/handlers/auth.rs | 340 +++++++++++++++++++++++++++++++ operator/src/handlers/gateway.rs | 207 +++++++++++++++++++ operator/src/handlers/mod.rs | 2 + operator/src/helpers/mod.rs | 121 +++++++++++ operator/src/lib.rs | 67 ++++++ 9 files changed, 853 insertions(+), 5 deletions(-) create mode 100644 operator/src/config.rs create mode 100644 operator/src/handlers/auth.rs create mode 100644 operator/src/handlers/gateway.rs create mode 100644 operator/src/handlers/mod.rs create mode 100644 operator/src/helpers/mod.rs diff --git a/operator/Cargo.lock b/operator/Cargo.lock index 1515b08..fa47fd6 100644 --- a/operator/Cargo.lock +++ b/operator/Cargo.lock @@ -254,6 +254,18 @@ dependencies = [ "libc", ] +[[package]] +name = "argon2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ba4cac0a46bc1d2912652a751c47f2a9f3a7fe89bcae2275d418f5270402f9" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "async-trait" version = "0.1.74" @@ -303,6 +315,18 @@ version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bech32" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" + [[package]] name = "bitflags" version = "1.3.2" @@ -315,6 +339,15 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -532,6 +565,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -572,10 +606,14 @@ name = "ext-cardano-ogmios" version = "0.1.0" dependencies = [ "actix-web", + "argon2", + "base64", + "bech32", "dotenv", "futures", "k8s-openapi", "kube", + "lazy_static", "prometheus", "schemars", "serde", @@ -1260,6 +1298,17 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "paste" version = "1.0.14" @@ -1749,6 +1798,12 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + [[package]] name = "syn" version = "1.0.109" diff --git a/operator/Cargo.toml b/operator/Cargo.toml index 77130d3..3e5adad 100644 --- a/operator/Cargo.toml +++ b/operator/Cargo.toml @@ -2,15 +2,20 @@ name = "ext-cardano-ogmios" version = "0.1.0" edition = "2021" +default-run = "controller" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] actix-web = "4.4.0" +argon2 = "0.5.2" +base64 = "0.21.5" +bech32 = "0.9.1" dotenv = "0.15.0" futures = "0.3.29" k8s-openapi = { version = "0.20.0", features = ["latest"] } kube = { version = "0.87.1", features = ["runtime", "client", "derive"] } +lazy_static = "1.4.0" prometheus = "0.13.3" schemars = "0.8.16" serde = { version = "1.0.193", features = ["derive"] } diff --git a/operator/src/config.rs b/operator/src/config.rs new file mode 100644 index 0000000..f71b1c3 --- /dev/null +++ b/operator/src/config.rs @@ -0,0 +1,31 @@ +use lazy_static::lazy_static; +use std::env; + +lazy_static! { + static ref CONTROLLER_CONFIG: Config = Config::from_env(); +} + +pub fn get_config() -> &'static Config { + &CONTROLLER_CONFIG +} + +#[derive(Debug, Clone)] +pub struct Config { + pub dns_zone: String, + pub namespace: String, + pub ingress_class: String, + pub api_key_salt: String, + pub http_port: String, +} + +impl Config { + pub fn from_env() -> Self { + Self { + dns_zone: env::var("DNS_ZONE").unwrap_or("demeter.run".into()), + namespace: env::var("NAMESPACE").unwrap_or("ftr-ogmios-v1".into()), + ingress_class: env::var("INGRESS_CLASS").unwrap_or("ogmios-v1".into()), + api_key_salt: env::var("API_KEY_SALT").unwrap_or("ogmios-salt".into()), + http_port: "1337".into(), + } + } +} diff --git a/operator/src/controller.rs b/operator/src/controller.rs index 460a333..6f5fa16 100644 --- a/operator/src/controller.rs +++ b/operator/src/controller.rs @@ -12,7 +12,12 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::{sync::Arc, time::Duration}; -use crate::{Error, Metrics, Result, State}; +use crate::{ + auth::handle_auth, + build_private_dns_service_name, + gateway::{handle_http_route, handle_reference_grant}, + Error, Metrics, Network, Result, State, +}; pub static OGMIOS_PORT_FINALIZER: &str = "ogmiosports.demeter.run"; @@ -24,14 +29,29 @@ pub static OGMIOS_PORT_FINALIZER: &str = "ogmiosports.demeter.run"; namespaced )] #[kube(status = "OgmiosPortStatus")] - -pub struct OgmiosPortSpec {} +#[serde(rename_all = "camelCase")] +pub struct OgmiosPortSpec { + pub network: Network, +} #[derive(Deserialize, Serialize, Clone, Default, Debug, JsonSchema)] -pub struct OgmiosPortStatus {} +#[serde(rename_all = "camelCase")] +pub struct OgmiosPortStatus { + #[serde(skip_serializing_if = "Option::is_none")] + pub endpoint_url: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub auth_token: Option, +} impl OgmiosPort { - async fn reconcile(&self, _ctx: Arc) -> Result { + async fn reconcile(&self, ctx: Arc) -> Result { + let client = ctx.client.clone(); + let namespace = self.namespace().unwrap(); + + let private_dns_service_name = build_private_dns_service_name(&self.spec.network); + handle_reference_grant(client.clone(), &namespace, self, &private_dns_service_name).await?; + handle_http_route(client.clone(), &namespace, self, &private_dns_service_name).await?; + handle_auth(client.clone(), &namespace, self).await?; Ok(Action::requeue(Duration::from_secs(5 * 60))) } diff --git a/operator/src/handlers/auth.rs b/operator/src/handlers/auth.rs new file mode 100644 index 0000000..d91d954 --- /dev/null +++ b/operator/src/handlers/auth.rs @@ -0,0 +1,340 @@ +use argon2::Argon2; +use base64::{ + engine::general_purpose::{self}, + Engine, +}; +use bech32::ToBase32; +use k8s_openapi::{api::core::v1::Secret, apimachinery::pkg::apis::meta::v1::OwnerReference}; +use kube::{ + api::{Patch, PatchParams, PostParams}, + core::ObjectMeta, + Api, Client, CustomResourceExt, Resource, ResourceExt, +}; +use serde::Deserialize; +use serde_json::{json, Value as JsonValue}; +use std::collections::BTreeMap; + +use crate::{ + create_resource, get_config, get_resource, kong_consumer, kong_plugin, patch_resource, + patch_resource_status, Error, OgmiosPort, OgmiosPortStatus, get_auth_name, get_acl_name, +}; + +pub async fn handle_auth( + client: Client, + namespace: &str, + resource: &OgmiosPort, +) -> Result<(), Error> { + handle_auth_secret(client.clone(), namespace, resource).await?; + handle_auth_plugin(client.clone(), namespace, resource).await?; + handle_acl_secret(client.clone(), namespace, resource).await?; + handle_acl_plugin(client.clone(), namespace, resource).await?; + handle_consumer(client.clone(), namespace, resource).await?; + Ok(()) +} + +async fn handle_auth_secret( + client: Client, + namespace: &str, + resource: &OgmiosPort, +) -> Result<(), Error> { + let name = get_auth_name(&resource.name_any()); + let api_key = generate_api_key(&name, namespace).await?; + let ogmios_port = OgmiosPort::api_resource(); + + let api = Api::::namespaced(client.clone(), namespace); + + let secret = auth_secret(&name, &api_key, resource.clone()); + let result = api.get_opt(&name).await?; + + if result.is_some() { + println!("Updating auth secret for {}", resource.name_any()); + let patch_params = PatchParams::default(); + api.patch(&name, &patch_params, &Patch::Merge(secret)) + .await?; + } else { + println!("Creating auth secret for {}", resource.name_any()); + let post_params = PostParams::default(); + api.create(&post_params, &secret).await?; + } + + let status = OgmiosPortStatus { + auth_token: Some(api_key), + ..Default::default() + }; + + patch_resource_status( + client.clone(), + namespace, + ogmios_port, + &resource.name_any(), + serde_json::to_value(status)?, + ) + .await?; + Ok(()) +} + +async fn handle_auth_plugin( + client: Client, + namespace: &str, + resource: &OgmiosPort, +) -> Result<(), Error> { + let name = get_auth_name(&resource.name_any()); + let kong_plugin = kong_plugin(); + + let result = get_resource(client.clone(), namespace, &kong_plugin, &name).await?; + let (metadata, data, raw) = auth_plugin(resource.clone())?; + + if result.is_some() { + println!("Updating auth plugin for: {}", resource.name_any()); + patch_resource(client.clone(), namespace, kong_plugin, &name, raw).await?; + } else { + println!("Creating auth plugin for: {}", resource.name_any()); + create_resource(client.clone(), namespace, kong_plugin, metadata, data).await?; + } + Ok(()) +} + +async fn handle_acl_secret( + client: Client, + namespace: &str, + resource: &OgmiosPort, +) -> Result<(), Error> { + let name = get_acl_name(&resource.name_any()); + + let api = Api::::namespaced(client.clone(), namespace); + + let secret = acl_secret(&name, resource.clone()); + let result = api.get_opt(&name).await?; + + if result.is_some() { + println!("Updating acl secret for {}", resource.name_any()); + let patch_params = PatchParams::default(); + api.patch(&name, &patch_params, &Patch::Merge(secret)) + .await?; + } else { + println!("Creating acl secret for {}", resource.name_any()); + let post_params = PostParams::default(); + api.create(&post_params, &secret).await?; + } + + Ok(()) +} + +async fn handle_acl_plugin( + client: Client, + namespace: &str, + resource: &OgmiosPort, +) -> Result<(), Error> { + let name = get_acl_name(&resource.name_any()); + let kong_plugin = kong_plugin(); + + let result = get_resource(client.clone(), namespace, &kong_plugin, &name).await?; + let (metadata, data, raw) = acl_plugin(resource.clone())?; + + if result.is_some() { + println!("Updating acl plugin for: {}", resource.name_any()); + patch_resource(client.clone(), namespace, kong_plugin, &name, raw).await?; + } else { + println!("Creating acl plugin for: {}", resource.name_any()); + create_resource(client.clone(), namespace, kong_plugin, metadata, data).await?; + } + Ok(()) +} + +async fn handle_consumer( + client: Client, + namespace: &str, + resource: &OgmiosPort, +) -> Result<(), Error> { + let name = get_auth_name(&resource.name_any()); + let kong_consumer = kong_consumer(); + + let result = get_resource(client.clone(), namespace, &kong_consumer, &name).await?; + let (metadata, data, raw) = consumer(resource.clone())?; + + if result.is_some() { + println!("Updating consumer for: {}", resource.name_any()); + patch_resource(client.clone(), namespace, kong_consumer, &name, raw).await?; + } else { + println!("Creating consumer for: {}", resource.name_any()); + create_resource(client.clone(), namespace, kong_consumer, metadata, data).await?; + } + Ok(()) +} + +async fn generate_api_key(name: &str, namespace: &str) -> Result { + let password = format!("{}{}", name, namespace).as_bytes().to_vec(); + + let config = get_config(); + let salt = config.api_key_salt.as_bytes(); + + let mut output = vec![0; 16]; + + let argon2 = Argon2::default(); + argon2.hash_password_into(password.as_slice(), salt, &mut output)?; + + // Encode the hash using Bech32 + let base64 = general_purpose::URL_SAFE_NO_PAD.encode(output); + let with_bech = bech32::encode("dmtr_ogmios", base64.to_base32(), bech32::Variant::Bech32)?; + + Ok(with_bech) +} + +fn auth_secret(name: &str, api_key: &str, owner: OgmiosPort) -> Secret { + let mut string_data = BTreeMap::new(); + string_data.insert("key".into(), api_key.into()); + + let mut labels = BTreeMap::new(); + labels.insert("konghq.com/credential".into(), "key-auth".into()); + + let metadata = ObjectMeta { + name: Some(name.to_string()), + owner_references: Some(vec![OwnerReference { + api_version: OgmiosPort::api_version(&()).to_string(), + kind: OgmiosPort::kind(&()).to_string(), + name: owner.name_any(), + uid: owner.uid().unwrap(), + ..Default::default() + }]), + labels: Some(labels), + ..Default::default() + }; + + Secret { + type_: Some(String::from("Opaque")), + metadata, + string_data: Some(string_data), + ..Default::default() + } +} + +fn auth_plugin(owner: OgmiosPort) -> Result<(ObjectMeta, JsonValue, JsonValue), Error> { + let kong_plugin = kong_plugin(); + + let metadata = ObjectMeta::deserialize(&json!({ + "name": get_auth_name(&owner.name_any()), + + "ownerReferences": [ + { + "apiVersion": OgmiosPort::api_version(&()).to_string(), // @TODO: try to grab this from the owner + "kind": OgmiosPort::kind(&()).to_string(), // @TODO: try to grab this from the owner + "name": owner.name_any(), + "uid": owner.uid() + } + ] + }))?; + + let data = json!({ + "plugin": "key-auth", + "config": { + "key_names": ["dmtr-api-key"], + } + }); + + let raw = json!({ + "apiVersion": kong_plugin.api_version, + "kind": kong_plugin.kind, + "metadata": metadata, + "plugin": data["plugin"], + "config": data["config"] + }); + + Ok((metadata, data, raw)) +} + +fn acl_secret(name: &str, owner: OgmiosPort) -> Secret { + let mut string_data = BTreeMap::new(); + string_data.insert("group".into(), owner.name_any()); + + let mut labels = BTreeMap::new(); + labels.insert("konghq.com/credential".into(), "acl".into()); + + let metadata = ObjectMeta { + name: Some(name.to_string()), + owner_references: Some(vec![OwnerReference { + api_version: OgmiosPort::api_version(&()).to_string(), + kind: OgmiosPort::kind(&()).to_string(), + name: owner.name_any(), + uid: owner.uid().unwrap(), + ..Default::default() + }]), + labels: Some(labels), + ..Default::default() + }; + + Secret { + type_: Some(String::from("Opaque")), + metadata, + string_data: Some(string_data), + ..Default::default() + } +} + +fn acl_plugin(owner: OgmiosPort) -> Result<(ObjectMeta, JsonValue, JsonValue), Error> { + let kong_plugin = kong_plugin(); + + let metadata = ObjectMeta::deserialize(&json!({ + "name": get_acl_name(&owner.name_any()), + "ownerReferences": [ + { + "apiVersion": OgmiosPort::api_version(&()).to_string(), // @TODO: try to grab this from the owner + "kind": OgmiosPort::kind(&()).to_string(), // @TODO: try to grab this from the owner + "name": owner.name_any(), + "uid": owner.uid() + } + ] + }))?; + + let data = json!({ + "plugin": "acl", + "config": { + "allow": [owner.name_any()] + } + }); + + let raw = json!({ + "apiVersion": kong_plugin.api_version, + "kind": kong_plugin.kind, + "metadata": metadata, + "plugin": data["plugin"], + "config": data["config"] + }); + + Ok((metadata, data, raw)) +} + +fn consumer(owner: OgmiosPort) -> Result<(ObjectMeta, JsonValue, JsonValue), Error> { + let kong_consumer = kong_consumer(); + let config = get_config(); + + let metadata = ObjectMeta::deserialize(&json!({ + "name": get_auth_name(&owner.name_any()), + "annotations": { + "kubernetes.io/ingress.class": config.ingress_class, + }, + + "ownerReferences": [ + { + "apiVersion": OgmiosPort::api_version(&()).to_string(), // @TODO: try to grab this from the owner + "kind": OgmiosPort::kind(&()).to_string(), // @TODO: try to grab this from the owner + "name": owner.name_any(), + "uid": owner.uid() + } + ] + }))?; + + let data = json!({ + "username": owner.name_any(), + "credentials": [get_auth_name(&owner.name_any()), get_acl_name(&owner.name_any())] + }); + + let raw = json!({ + "apiVersion": kong_consumer.api_version, + "kind": kong_consumer.kind, + "metadata": metadata, + "username": data["username"], + "credentials": data["credentials"] + }); + + Ok((metadata, data, raw)) +} diff --git a/operator/src/handlers/gateway.rs b/operator/src/handlers/gateway.rs new file mode 100644 index 0000000..99a9707 --- /dev/null +++ b/operator/src/handlers/gateway.rs @@ -0,0 +1,207 @@ +use kube::{core::ObjectMeta, Client, CustomResourceExt, Resource, ResourceExt}; +use serde::Deserialize; +use serde_json::{json, Value as JsonValue}; + +use crate::{ + create_resource, get_acl_name, get_auth_name, get_config, get_resource, http_route, + patch_resource, patch_resource_status, reference_grant, Error, OgmiosPort, OgmiosPortStatus, +}; + +pub async fn handle_http_route( + client: Client, + namespace: &str, + resource: &OgmiosPort, + private_dns_service_name: &str, +) -> Result<(), Error> { + let name = format!("ogmios-{}", resource.name_any()); + let host_name = build_host(&resource.name_any(), &namespace_to_slug(namespace)); + let http_route = http_route(); + let ogmios_port = OgmiosPort::api_resource(); + + let result = get_resource(client.clone(), namespace, &http_route, &name).await?; + + let (metadata, data, raw) = route(&name, &host_name, resource, private_dns_service_name)?; + + if result.is_some() { + println!("Updating http route for {}", resource.name_any()); + patch_resource(client.clone(), namespace, http_route, &name, raw).await?; + } else { + println!("Creating http route for {}", resource.name_any()); + create_resource(client.clone(), namespace, http_route, metadata, data).await?; + } + + let status = OgmiosPortStatus { + endpoint_url: Some(format!("https://{}", host_name)), + ..Default::default() + }; + patch_resource_status( + client.clone(), + namespace, + ogmios_port, + &resource.name_any(), + serde_json::to_value(status)?, + ) + .await?; + Ok(()) +} + +pub async fn handle_reference_grant( + client: Client, + namespace: &str, + resource: &OgmiosPort, + private_dns_service_name: &str, +) -> Result<(), Error> { + let name = format!("{}-{}-http", namespace, resource.name_any()); + let reference_grant = reference_grant(); + let config = get_config(); + + let result = get_resource(client.clone(), &config.namespace, &reference_grant, &name).await?; + + let (metadata, data, raw) = grant(&name, private_dns_service_name, namespace)?; + + if result.is_some() { + println!("Updating reference grant for {}", resource.name_any()); + patch_resource( + client.clone(), + &config.namespace, + reference_grant, + &name, + raw, + ) + .await?; + } else { + println!("Creating reference grant for {}", resource.name_any()); + // we need to get the deserialized payload + create_resource( + client.clone(), + &config.namespace, + reference_grant, + metadata, + data, + ) + .await?; + } + Ok(()) +} + +fn build_host(name: &str, project_slug: &str) -> String { + let config = get_config(); + + format!( + "{}-{}.{}.{}", + name, project_slug, config.ingress_class, config.dns_zone + ) +} + +fn namespace_to_slug(namespace: &str) -> String { + namespace.split_once('-').unwrap().1.to_string() +} + +fn route( + name: &str, + hostname: &str, + owner: &OgmiosPort, + private_dns_service_name: &str, +) -> Result<(ObjectMeta, JsonValue, JsonValue), Error> { + let config = get_config(); + let http_route = http_route(); + let plugins = format!( + "{},{}", + get_auth_name(&owner.name_any()), + get_acl_name(&owner.name_any()), + ); + + let metadata = ObjectMeta::deserialize(&json!({ + "name": name, + "labels": { + "demeter.run/instance": name, + "demeter.run/tenancy": "project", + "demeter.run/kind": "http-route" + }, + "annotations": { + "konghq.com/plugins": plugins, + }, + "ownerReferences": [ + { + "apiVersion": OgmiosPort::api_version(&()).to_string(), // @TODO: try to grab this from the owner + "kind": OgmiosPort::kind(&()).to_string(), // @TODO: try to grab this from the owner + "name": owner.name_any(), + "uid": owner.uid() + } + ] + }))?; + + let data = json!({ + "spec": { + "hostnames": [hostname], + "parentRefs": [ + { + "name": config.ingress_class, + "namespace": config.namespace + } + ], + "rules": [ + { + "backendRefs": [ + { + "kind": "Service", + "name": private_dns_service_name, + "port": config.http_port.parse::()?, + "namespace": config.namespace + } + ] + } + ] + } + }); + + let raw = json!({ + "apiVersion": http_route.api_version, + "kind": http_route.kind, + "metadata": metadata, + "spec": data["spec"] + }); + + Ok((metadata, data, raw)) +} + +fn grant( + name: &str, + private_dns_service_name: &str, + project_namespace: &str, +) -> Result<(ObjectMeta, JsonValue, JsonValue), Error> { + let reference_grant = reference_grant(); + let http_route = http_route(); + + let metadata = ObjectMeta::deserialize(&json!({ + "name": name, + }))?; + + let data = json!({ + "spec": { + "from": [ + { + "group": http_route.group, + "kind": http_route.kind, + "namespace": project_namespace, + }, + ], + "to": [ + { + "group": "", + "kind": "Service", + "name": private_dns_service_name, + }, + ], + } + }); + + let raw = json!({ + "apiVersion": reference_grant.api_version, + "kind": reference_grant.kind, + "metadata": metadata, + "spec": data["spec"] + }); + + Ok((metadata, data, raw)) +} diff --git a/operator/src/handlers/mod.rs b/operator/src/handlers/mod.rs new file mode 100644 index 0000000..ddea910 --- /dev/null +++ b/operator/src/handlers/mod.rs @@ -0,0 +1,2 @@ +pub mod auth; +pub mod gateway; diff --git a/operator/src/helpers/mod.rs b/operator/src/helpers/mod.rs new file mode 100644 index 0000000..5bd1c21 --- /dev/null +++ b/operator/src/helpers/mod.rs @@ -0,0 +1,121 @@ +use kube::{ + api::{Patch, PatchParams, PostParams}, + core::{DynamicObject, ObjectMeta}, + discovery::ApiResource, + Api, Client, +}; +use serde_json::json; + +use crate::Network; + +pub fn http_route() -> ApiResource { + ApiResource { + group: "gateway.networking.k8s.io".into(), + version: "v1".into(), + api_version: "gateway.networking.k8s.io/v1".into(), + kind: "HTTPRoute".into(), + plural: "httproutes".into(), + } +} + +pub fn reference_grant() -> ApiResource { + ApiResource { + group: "gateway.networking.k8s.io".into(), + version: "v1beta1".into(), + api_version: "gateway.networking.k8s.io/v1beta1".into(), + kind: "ReferenceGrant".into(), + plural: "referencegrants".into(), + } +} + +pub fn kong_plugin() -> ApiResource { + ApiResource { + group: "configuration.konghq.com".into(), + version: "v1".into(), + api_version: "configuration.konghq.com/v1".into(), + kind: "KongPlugin".into(), + plural: "kongplugins".into(), + } +} + +pub fn kong_consumer() -> ApiResource { + ApiResource { + group: "configuration.konghq.com".into(), + version: "v1".into(), + api_version: "configuration.konghq.com/v1".into(), + kind: "KongConsumer".into(), + plural: "kongconsumers".into(), + } +} + +pub async fn get_resource( + client: Client, + namespace: &str, + api_resource: &ApiResource, + name: &str, +) -> Result, kube::Error> { + let api: Api = Api::namespaced_with(client, namespace, api_resource); + + api.get_opt(name).await +} + +pub async fn create_resource( + client: Client, + namespace: &str, + api_resource: ApiResource, + metadata: ObjectMeta, + data: serde_json::Value, +) -> Result<(), kube::Error> { + let api: Api = Api::namespaced_with(client, namespace, &api_resource); + + let post_params = PostParams::default(); + + let mut dynamic = DynamicObject::new("", &api_resource); + dynamic.data = data; + dynamic.metadata = metadata; + api.create(&post_params, &dynamic).await?; + Ok(()) +} + +pub async fn patch_resource( + client: Client, + namespace: &str, + api_resource: ApiResource, + name: &str, + payload: serde_json::Value, +) -> Result<(), kube::Error> { + let api: Api = Api::namespaced_with(client, namespace, &api_resource); + + let patch_params = PatchParams::default(); + api.patch(name, &patch_params, &Patch::Merge(payload)) + .await?; + Ok(()) +} + +pub async fn patch_resource_status( + client: Client, + namespace: &str, + api_resource: ApiResource, + name: &str, + payload: serde_json::Value, +) -> Result<(), kube::Error> { + let api: Api = Api::namespaced_with(client, namespace, &api_resource); + + let status = json!({ "status": payload }); + let patch_params = PatchParams::default(); + api.patch_status(name, &patch_params, &Patch::Merge(status)) + .await?; + Ok(()) +} + +pub fn get_auth_name(name: &str) -> String { + format!("ogmios-auth-{}", name) +} + +pub fn get_acl_name(name: &str) -> String { + format!("ogmios-acl-{}", name) +} + +pub fn build_private_dns_service_name(network: &Network) -> String { + format!("ogmios-{}", network) +} diff --git a/operator/src/lib.rs b/operator/src/lib.rs index 5b97457..da54d37 100644 --- a/operator/src/lib.rs +++ b/operator/src/lib.rs @@ -1,4 +1,8 @@ +use std::fmt::{self, Display, Formatter}; + use prometheus::Registry; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; use thiserror::Error; #[derive(Error, Debug)] @@ -8,6 +12,18 @@ pub enum Error { #[error("Finalizer Error: {0}")] FinalizerError(#[source] Box>), + + #[error("Argon Error: {0}")] + ArgonError(String), + + #[error("Bech32 Error: {0}")] + Bech32Error(#[source] bech32::Error), + + #[error("Deserialize Error: {0}")] + DeserializeError(#[source] serde_json::Error), + + #[error("Parse Int error: {0}")] + ParseIntError(#[source] std::num::ParseIntError), } impl Error { pub fn metric_label(&self) -> String { @@ -19,6 +35,26 @@ impl From for Error { Error::KubeError(value) } } +impl From for Error { + fn from(value: argon2::Error) -> Self { + Error::ArgonError(value.to_string()) + } +} +impl From for Error { + fn from(value: serde_json::Error) -> Self { + Error::DeserializeError(value) + } +} +impl From for Error { + fn from(value: std::num::ParseIntError) -> Self { + Error::ParseIntError(value) + } +} +impl From for Error { + fn from(value: bech32::Error) -> Self { + Error::Bech32Error(value) + } +} #[derive(Clone, Default)] pub struct State { @@ -37,6 +73,28 @@ impl State { } } +#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] +pub enum Network { + #[serde(rename = "mainnet")] + Mainnet, + #[serde(rename = "preprod")] + Preprod, + #[serde(rename = "preview")] + Preview, + #[serde(rename = "sanchonet")] + Sanchonet, +} +impl Display for Network { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Network::Mainnet => write!(f, "mainnet"), + Network::Preprod => write!(f, "preprod"), + Network::Preview => write!(f, "preview"), + Network::Sanchonet => write!(f, "sanchonet"), + } + } +} + pub type Result = std::result::Result; pub mod controller; @@ -44,3 +102,12 @@ pub use crate::controller::*; pub mod metrics; pub use metrics::*; + +mod helpers; +pub use helpers::*; + +mod handlers; +pub use handlers::*; + +mod config; +pub use config::*; From ee2e37e006dc336d4c3de6d97ddb6dedf0b8e09e Mon Sep 17 00:00:00 2001 From: paulobressan Date: Fri, 22 Dec 2023 17:07:46 -0300 Subject: [PATCH 02/16] chore: removed finalizer --- operator/src/controller.rs | 42 ++++++++------------------------------ 1 file changed, 9 insertions(+), 33 deletions(-) diff --git a/operator/src/controller.rs b/operator/src/controller.rs index 6f5fa16..2e09f4f 100644 --- a/operator/src/controller.rs +++ b/operator/src/controller.rs @@ -1,11 +1,6 @@ use futures::StreamExt; use kube::{ - runtime::{ - controller::Action, - finalizer::{finalizer, Event}, - watcher::Config as WatcherConfig, - Controller, - }, + runtime::{controller::Action, watcher::Config as WatcherConfig, Controller}, Api, Client, CustomResource, ResourceExt, }; use schemars::JsonSchema; @@ -43,23 +38,6 @@ pub struct OgmiosPortStatus { pub auth_token: Option, } -impl OgmiosPort { - async fn reconcile(&self, ctx: Arc) -> Result { - let client = ctx.client.clone(); - let namespace = self.namespace().unwrap(); - - let private_dns_service_name = build_private_dns_service_name(&self.spec.network); - handle_reference_grant(client.clone(), &namespace, self, &private_dns_service_name).await?; - handle_http_route(client.clone(), &namespace, self, &private_dns_service_name).await?; - handle_auth(client.clone(), &namespace, self).await?; - Ok(Action::requeue(Duration::from_secs(5 * 60))) - } - - async fn cleanup(&self, _: Arc) -> Result { - Ok(Action::await_change()) - } -} - struct Context { pub client: Client, pub metrics: Metrics, @@ -71,17 +49,15 @@ impl Context { } async fn reconcile(crd: Arc, ctx: Arc) -> Result { - let ns = crd.namespace().unwrap(); - let crds: Api = Api::namespaced(ctx.client.clone(), &ns); + let client = ctx.client.clone(); + let namespace = crd.namespace().unwrap(); + + let private_dns_service_name = build_private_dns_service_name(&crd.spec.network); + handle_reference_grant(client.clone(), &namespace, &crd, &private_dns_service_name).await?; + handle_http_route(client.clone(), &namespace, &crd, &private_dns_service_name).await?; + handle_auth(client.clone(), &namespace, &crd).await?; - finalizer(&crds, OGMIOS_PORT_FINALIZER, crd, |event| async { - match event { - Event::Apply(crd) => crd.reconcile(ctx.clone()).await, - Event::Cleanup(crd) => crd.cleanup(ctx.clone()).await, - } - }) - .await - .map_err(|e| Error::FinalizerError(Box::new(e))) + Ok(Action::await_change()) } fn error_policy(crd: Arc, err: &Error, ctx: Arc) -> Action { From c1a7b340940bbbf8e6493eddfa48b23ca992ba0a Mon Sep 17 00:00:00 2001 From: paulobressan Date: Fri, 22 Dec 2023 17:10:49 -0300 Subject: [PATCH 03/16] chore: added license --- operator/LICENSE | 201 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 operator/LICENSE diff --git a/operator/LICENSE b/operator/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/operator/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. From 07b8b57f1828c43be3dd572276c6cbdfded53245 Mon Sep 17 00:00:00 2001 From: paulobressan Date: Fri, 22 Dec 2023 17:12:10 -0300 Subject: [PATCH 04/16] chore: added dockerfile --- operator/Dockerfile | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 operator/Dockerfile diff --git a/operator/Dockerfile b/operator/Dockerfile new file mode 100644 index 0000000..1091c66 --- /dev/null +++ b/operator/Dockerfile @@ -0,0 +1,15 @@ +FROM rust:1.74-slim-buster as build + +WORKDIR /app + +COPY ./Cargo.lock ./Cargo.lock +COPY ./Cargo.toml ./Cargo.toml +COPY ./src ./src + +RUN cargo build --release + +FROM rust:1.74-slim-buster + +COPY --from=build /app/target/release/controller . + +CMD ["./controller"] \ No newline at end of file From 7cffc1cad8b5d382092d77a5de1a98eb23d71f8d Mon Sep 17 00:00:00 2001 From: paulobressan Date: Fri, 22 Dec 2023 17:14:26 -0300 Subject: [PATCH 05/16] docs: updated readme --- operator/.gitignore | 1 + README.md => operator/README.md | 11 +++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) rename README.md => operator/README.md (59%) diff --git a/operator/.gitignore b/operator/.gitignore index ea8c4bf..0b745e2 100644 --- a/operator/.gitignore +++ b/operator/.gitignore @@ -1 +1,2 @@ /target +.env \ No newline at end of file diff --git a/README.md b/operator/README.md similarity index 59% rename from README.md rename to operator/README.md index e88caed..10bfb39 100644 --- a/README.md +++ b/operator/README.md @@ -4,10 +4,13 @@ This project allow demeter to run and expose ogmios ## Environment -| Key | Value | -| ---- | ------------ | -| ADDR | 0.0.0.0:5000 | - +| Key | Value | +| ------------- | ------------- | +| ADDR | 0.0.0.0:5000 | +| DNS_ZONE | demeter.run | +| NAMESPACE | ftr-ogmios-v1 | +| INGRESS_CLASS | ogmios-v1 | +| API_KEY_SALT | ogmios-salt | ## Commands From 869402493f7bf5055da19e7284e4bd196ea5732b Mon Sep 17 00:00:00 2001 From: paulobressan Date: Fri, 22 Dec 2023 17:18:54 -0300 Subject: [PATCH 06/16] chore: added ci clippy job --- .github/workflows/clippy.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/clippy.yml diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml new file mode 100644 index 0000000..a2d5d57 --- /dev/null +++ b/.github/workflows/clippy.yml @@ -0,0 +1,22 @@ +name: Clippy + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + lint: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./operator + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Clippy check lints + run: cargo clippy -- -D warnings From 9b5dae87fbad2e58bf4df19070dab64417640500 Mon Sep 17 00:00:00 2001 From: paulobressan Date: Fri, 22 Dec 2023 17:21:17 -0300 Subject: [PATCH 07/16] chore: added column descriptions --- operator/src/controller.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/operator/src/controller.rs b/operator/src/controller.rs index 2e09f4f..c2938fc 100644 --- a/operator/src/controller.rs +++ b/operator/src/controller.rs @@ -24,6 +24,11 @@ pub static OGMIOS_PORT_FINALIZER: &str = "ogmiosports.demeter.run"; namespaced )] #[kube(status = "OgmiosPortStatus")] +#[kube(printcolumn = r#" + {"name":"Network", "jsonPath": ".spec.network", "type": "string"}, + {"name": "Endpoint URL", "jsonPath": ".status.endpointUrl", "type": "string"}, + {"name": "Auth Token", "jsonPath": ".status.authToken", "type": "string"} + "#)] #[serde(rename_all = "camelCase")] pub struct OgmiosPortSpec { pub network: Network, From 62cc69700e5ec6161aa750095a73543c18e260d4 Mon Sep 17 00:00:00 2001 From: paulobressan Date: Fri, 22 Dec 2023 17:26:07 -0300 Subject: [PATCH 08/16] docs: added root readme --- README.md | 9 +++++++++ operator/README.md | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..af2c856 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# Ext Cardano Ogmios + +The approach of this project is to allow a CRD to Ogmios on the K8S cluster and an operator will enable the required resources to expose an Ogmios port. + +## Folder structure + +* bootstrap: contains terraform resources +* operator: rust application integrated with the cluster +* scripts: useful scripts \ No newline at end of file diff --git a/operator/README.md b/operator/README.md index 10bfb39..a37e768 100644 --- a/operator/README.md +++ b/operator/README.md @@ -1,6 +1,6 @@ -# Ext Cardano Ogmios +# Ext Cardano Ogmios Operator -This project allow demeter to run and expose ogmios +This operator allow demeter to run and expose ogmios ## Environment From 89a7ed3d2a3ac631ad891637d08e032cfa3ddd04 Mon Sep 17 00:00:00 2001 From: paulobressan Date: Fri, 22 Dec 2023 17:27:17 -0300 Subject: [PATCH 09/16] chore: generated ogmios crd --- bootstrap/crds/main.tf | 90 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 bootstrap/crds/main.tf diff --git a/bootstrap/crds/main.tf b/bootstrap/crds/main.tf new file mode 100644 index 0000000..15a11ff --- /dev/null +++ b/bootstrap/crds/main.tf @@ -0,0 +1,90 @@ +resource "kubernetes_manifest" "customresourcedefinition_ogmiosports_demeter_run" { + manifest = { + "apiVersion" = "apiextensions.k8s.io/v1" + "kind" = "CustomResourceDefinition" + "metadata" = { + "name" = "ogmiosports.demeter.run" + } + "spec" = { + "group" = "demeter.run" + "names" = { + "categories" = [] + "kind" = "OgmiosPort" + "plural" = "ogmiosports" + "shortNames" = [] + "singular" = "ogmiosport" + } + "scope" = "Namespaced" + "versions" = [ + { + "additionalPrinterColumns" = [ + { + "jsonPath" = ".spec.network" + "name" = "Network" + "type" = "string" + }, + { + "jsonPath" = ".status.endpointUrl" + "name" = "Endpoint URL" + "type" = "string" + }, + { + "jsonPath" = ".status.authToken" + "name" = "Auth Token" + "type" = "string" + }, + ] + "name" = "v1alpha1" + "schema" = { + "openAPIV3Schema" = { + "description" = "Auto-generated derived type for OgmiosPortSpec via `CustomResource`" + "properties" = { + "spec" = { + "properties" = { + "network" = { + "enum" = [ + "mainnet", + "preprod", + "preview", + "sanchonet", + ] + "type" = "string" + } + } + "required" = [ + "network", + ] + "type" = "object" + } + "status" = { + "nullable" = true + "properties" = { + "authToken" = { + "nullable" = true + "type" = "string" + } + "endpointUrl" = { + "nullable" = true + "type" = "string" + } + } + "type" = "object" + } + } + "required" = [ + "spec", + ] + "title" = "OgmiosPort" + "type" = "object" + } + } + "served" = true + "storage" = true + "subresources" = { + "status" = {} + } + }, + ] + } + } +} From 7623e97be077ae93c16c6495d9a1169da8f689a1 Mon Sep 17 00:00:00 2001 From: paulobressan Date: Sat, 23 Dec 2023 09:52:48 -0300 Subject: [PATCH 10/16] chore: added ogmios version --- operator/src/controller.rs | 7 +++++-- operator/src/helpers/mod.rs | 8 ++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/operator/src/controller.rs b/operator/src/controller.rs index c2938fc..a00e719 100644 --- a/operator/src/controller.rs +++ b/operator/src/controller.rs @@ -25,13 +25,15 @@ pub static OGMIOS_PORT_FINALIZER: &str = "ogmiosports.demeter.run"; )] #[kube(status = "OgmiosPortStatus")] #[kube(printcolumn = r#" - {"name":"Network", "jsonPath": ".spec.network", "type": "string"}, + {"name": "Network", "jsonPath": ".spec.network", "type": "string"}, + {"name": "Version", "jsonPath": ".spec.network", "type": "number"}, {"name": "Endpoint URL", "jsonPath": ".status.endpointUrl", "type": "string"}, {"name": "Auth Token", "jsonPath": ".status.authToken", "type": "string"} "#)] #[serde(rename_all = "camelCase")] pub struct OgmiosPortSpec { pub network: Network, + pub version: u8, } #[derive(Deserialize, Serialize, Clone, Default, Debug, JsonSchema)] @@ -57,7 +59,8 @@ async fn reconcile(crd: Arc, ctx: Arc) -> Result { let client = ctx.client.clone(); let namespace = crd.namespace().unwrap(); - let private_dns_service_name = build_private_dns_service_name(&crd.spec.network); + let private_dns_service_name = + build_private_dns_service_name(&crd.spec.network, &crd.spec.version); handle_reference_grant(client.clone(), &namespace, &crd, &private_dns_service_name).await?; handle_http_route(client.clone(), &namespace, &crd, &private_dns_service_name).await?; handle_auth(client.clone(), &namespace, &crd).await?; diff --git a/operator/src/helpers/mod.rs b/operator/src/helpers/mod.rs index 5bd1c21..a1bb53d 100644 --- a/operator/src/helpers/mod.rs +++ b/operator/src/helpers/mod.rs @@ -109,13 +109,13 @@ pub async fn patch_resource_status( } pub fn get_auth_name(name: &str) -> String { - format!("ogmios-auth-{}", name) + format!("ogmios-auth-{name}") } pub fn get_acl_name(name: &str) -> String { - format!("ogmios-acl-{}", name) + format!("ogmios-acl-{name}") } -pub fn build_private_dns_service_name(network: &Network) -> String { - format!("ogmios-{}", network) +pub fn build_private_dns_service_name(network: &Network, version: &u8) -> String { + format!("ogmios-{network}-{version}") } From 7b65ea93affc5ac9bcab6a4cf7f433255d64a70f Mon Sep 17 00:00:00 2001 From: paulobressan Date: Sat, 23 Dec 2023 09:54:25 -0300 Subject: [PATCH 11/16] chore: updated crd tf --- bootstrap/crds/main.tf | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/bootstrap/crds/main.tf b/bootstrap/crds/main.tf index 15a11ff..7c6e16d 100644 --- a/bootstrap/crds/main.tf +++ b/bootstrap/crds/main.tf @@ -23,6 +23,11 @@ resource "kubernetes_manifest" "customresourcedefinition_ogmiosports_demeter_run "name" = "Network" "type" = "string" }, + { + "jsonPath" = ".spec.network" + "name" = "Version" + "type" = "number" + }, { "jsonPath" = ".status.endpointUrl" "name" = "Endpoint URL" @@ -50,9 +55,15 @@ resource "kubernetes_manifest" "customresourcedefinition_ogmiosports_demeter_run ] "type" = "string" } + "version" = { + "format" = "uint8" + "minimum" = 0 + "type" = "integer" + } } "required" = [ "network", + "version", ] "type" = "object" } From 9aba4852cd89de218be18eeebd6c203cd4a5774f Mon Sep 17 00:00:00 2001 From: paulobressan Date: Sat, 23 Dec 2023 10:03:13 -0300 Subject: [PATCH 12/16] chore: fix ogmios version jsonPath --- bootstrap/crds/main.tf | 2 +- operator/src/controller.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bootstrap/crds/main.tf b/bootstrap/crds/main.tf index 7c6e16d..fbf5809 100644 --- a/bootstrap/crds/main.tf +++ b/bootstrap/crds/main.tf @@ -24,7 +24,7 @@ resource "kubernetes_manifest" "customresourcedefinition_ogmiosports_demeter_run "type" = "string" }, { - "jsonPath" = ".spec.network" + "jsonPath" = ".spec.version" "name" = "Version" "type" = "number" }, diff --git a/operator/src/controller.rs b/operator/src/controller.rs index a00e719..6605114 100644 --- a/operator/src/controller.rs +++ b/operator/src/controller.rs @@ -26,7 +26,7 @@ pub static OGMIOS_PORT_FINALIZER: &str = "ogmiosports.demeter.run"; #[kube(status = "OgmiosPortStatus")] #[kube(printcolumn = r#" {"name": "Network", "jsonPath": ".spec.network", "type": "string"}, - {"name": "Version", "jsonPath": ".spec.network", "type": "number"}, + {"name": "Version", "jsonPath": ".spec.version", "type": "number"}, {"name": "Endpoint URL", "jsonPath": ".status.endpointUrl", "type": "string"}, {"name": "Auth Token", "jsonPath": ".status.authToken", "type": "string"} "#)] From 787792c923f52fffcad598baef46f84e3fb1fa76 Mon Sep 17 00:00:00 2001 From: paulobressan Date: Tue, 26 Dec 2023 15:06:03 -0300 Subject: [PATCH 13/16] chore: added build controller ci --- .github/workflows/controller.yml | 40 ++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/controller.yml diff --git a/.github/workflows/controller.yml b/.github/workflows/controller.yml new file mode 100644 index 0000000..a273964 --- /dev/null +++ b/.github/workflows/controller.yml @@ -0,0 +1,40 @@ +name: Controller + +on: + push: + branches: + - "main" + paths: + - ".github/workflows/controller.yml" + - "operator/**" + +jobs: + build-images: + strategy: + fail-fast: false + matrix: + include: + - context: operator + file: operator/Dockerfile + endpoint: demeter-run/ext-cardano-ogmios + + continue-on-error: true + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2.3.3 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v2 + with: + context: ${{ matrix.context }} + file: ${{ matrix.file }} + platforms: linux/amd64 + push: true + tags: ghcr.io/${{ matrix.endpoint }},ghcr.io/${{ matrix.endpoint }}:${{ github.sha }} From 94c7c4aa9de0abd13db9c9b137b66db1dcb76ef7 Mon Sep 17 00:00:00 2001 From: paulobressan Date: Fri, 29 Dec 2023 11:33:38 -0300 Subject: [PATCH 14/16] chore: added tracing logs --- operator/src/controller.rs | 4 ++++ operator/src/handlers/auth.rs | 25 +++++++++++++------------ operator/src/handlers/gateway.rs | 9 +++++---- operator/src/lib.rs | 3 --- operator/src/main.rs | 16 ++++++++++++---- operator/src/metrics.rs | 10 +--------- 6 files changed, 35 insertions(+), 32 deletions(-) diff --git a/operator/src/controller.rs b/operator/src/controller.rs index 6605114..bbcab30 100644 --- a/operator/src/controller.rs +++ b/operator/src/controller.rs @@ -6,6 +6,7 @@ use kube::{ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::{sync::Arc, time::Duration}; +use tracing::{info, instrument}; use crate::{ auth::handle_auth, @@ -73,7 +74,10 @@ fn error_policy(crd: Arc, err: &Error, ctx: Arc) -> Action Action::requeue(Duration::from_secs(5)) } +#[instrument("controller run", skip_all)] pub async fn run(state: Arc) -> Result<(), Error> { + info!("listening crds running"); + let client = Client::try_default().await?; let crds = Api::::all(client.clone()); let ctx = Context::new(client, state.metrics.clone()); diff --git a/operator/src/handlers/auth.rs b/operator/src/handlers/auth.rs index d91d954..de1781c 100644 --- a/operator/src/handlers/auth.rs +++ b/operator/src/handlers/auth.rs @@ -13,10 +13,11 @@ use kube::{ use serde::Deserialize; use serde_json::{json, Value as JsonValue}; use std::collections::BTreeMap; +use tracing::info; use crate::{ - create_resource, get_config, get_resource, kong_consumer, kong_plugin, patch_resource, - patch_resource_status, Error, OgmiosPort, OgmiosPortStatus, get_auth_name, get_acl_name, + create_resource, get_acl_name, get_auth_name, get_config, get_resource, kong_consumer, + kong_plugin, patch_resource, patch_resource_status, Error, OgmiosPort, OgmiosPortStatus, }; pub async fn handle_auth( @@ -47,12 +48,12 @@ async fn handle_auth_secret( let result = api.get_opt(&name).await?; if result.is_some() { - println!("Updating auth secret for {}", resource.name_any()); + info!(resource = resource.name_any(), "Updating auth secret"); let patch_params = PatchParams::default(); api.patch(&name, &patch_params, &Patch::Merge(secret)) .await?; } else { - println!("Creating auth secret for {}", resource.name_any()); + info!(resource = resource.name_any(), "Creating auth secret"); let post_params = PostParams::default(); api.create(&post_params, &secret).await?; } @@ -85,10 +86,10 @@ async fn handle_auth_plugin( let (metadata, data, raw) = auth_plugin(resource.clone())?; if result.is_some() { - println!("Updating auth plugin for: {}", resource.name_any()); + info!(resource = resource.name_any(), "Updating auth plugin"); patch_resource(client.clone(), namespace, kong_plugin, &name, raw).await?; } else { - println!("Creating auth plugin for: {}", resource.name_any()); + info!(resource = resource.name_any(), "Creating auth plugin"); create_resource(client.clone(), namespace, kong_plugin, metadata, data).await?; } Ok(()) @@ -107,12 +108,12 @@ async fn handle_acl_secret( let result = api.get_opt(&name).await?; if result.is_some() { - println!("Updating acl secret for {}", resource.name_any()); + info!(resource = resource.name_any(), "Updating acl secret"); let patch_params = PatchParams::default(); api.patch(&name, &patch_params, &Patch::Merge(secret)) .await?; } else { - println!("Creating acl secret for {}", resource.name_any()); + info!(resource = resource.name_any(), "Creating acl secret"); let post_params = PostParams::default(); api.create(&post_params, &secret).await?; } @@ -132,10 +133,10 @@ async fn handle_acl_plugin( let (metadata, data, raw) = acl_plugin(resource.clone())?; if result.is_some() { - println!("Updating acl plugin for: {}", resource.name_any()); + info!(resource = resource.name_any(), "Updating acl plugin"); patch_resource(client.clone(), namespace, kong_plugin, &name, raw).await?; } else { - println!("Creating acl plugin for: {}", resource.name_any()); + info!(resource = resource.name_any(), "Creating acl plugin"); create_resource(client.clone(), namespace, kong_plugin, metadata, data).await?; } Ok(()) @@ -153,10 +154,10 @@ async fn handle_consumer( let (metadata, data, raw) = consumer(resource.clone())?; if result.is_some() { - println!("Updating consumer for: {}", resource.name_any()); + info!(resource = resource.name_any(), "Updating consumer"); patch_resource(client.clone(), namespace, kong_consumer, &name, raw).await?; } else { - println!("Creating consumer for: {}", resource.name_any()); + info!(resource = resource.name_any(), "Creating consumer"); create_resource(client.clone(), namespace, kong_consumer, metadata, data).await?; } Ok(()) diff --git a/operator/src/handlers/gateway.rs b/operator/src/handlers/gateway.rs index 99a9707..6de8ad1 100644 --- a/operator/src/handlers/gateway.rs +++ b/operator/src/handlers/gateway.rs @@ -1,6 +1,7 @@ use kube::{core::ObjectMeta, Client, CustomResourceExt, Resource, ResourceExt}; use serde::Deserialize; use serde_json::{json, Value as JsonValue}; +use tracing::info; use crate::{ create_resource, get_acl_name, get_auth_name, get_config, get_resource, http_route, @@ -23,10 +24,10 @@ pub async fn handle_http_route( let (metadata, data, raw) = route(&name, &host_name, resource, private_dns_service_name)?; if result.is_some() { - println!("Updating http route for {}", resource.name_any()); + info!(resource = resource.name_any(), "Updating http route"); patch_resource(client.clone(), namespace, http_route, &name, raw).await?; } else { - println!("Creating http route for {}", resource.name_any()); + info!(resource = resource.name_any(), "Creating http route"); create_resource(client.clone(), namespace, http_route, metadata, data).await?; } @@ -60,7 +61,7 @@ pub async fn handle_reference_grant( let (metadata, data, raw) = grant(&name, private_dns_service_name, namespace)?; if result.is_some() { - println!("Updating reference grant for {}", resource.name_any()); + info!(resource = resource.name_any(), "Updating reference grant"); patch_resource( client.clone(), &config.namespace, @@ -70,7 +71,7 @@ pub async fn handle_reference_grant( ) .await?; } else { - println!("Creating reference grant for {}", resource.name_any()); + info!(resource = resource.name_any(), "Creating reference grant"); // we need to get the deserialized payload create_resource( client.clone(), diff --git a/operator/src/lib.rs b/operator/src/lib.rs index da54d37..f0900a3 100644 --- a/operator/src/lib.rs +++ b/operator/src/lib.rs @@ -10,9 +10,6 @@ pub enum Error { #[error("Kube Error: {0}")] KubeError(#[source] kube::Error), - #[error("Finalizer Error: {0}")] - FinalizerError(#[source] Box>), - #[error("Argon Error: {0}")] ArgonError(String), diff --git a/operator/src/main.rs b/operator/src/main.rs index b7e7aef..c7b43a8 100644 --- a/operator/src/main.rs +++ b/operator/src/main.rs @@ -4,8 +4,9 @@ use actix_web::{ use dotenv::dotenv; use prometheus::{Encoder, TextEncoder}; use std::{io, sync::Arc}; +use tracing::{info, Level}; -use ext_cardano_ogmios::{controller, metrics as metrics_collector, State}; +use ext_cardano_ogmios::{controller, State}; #[get("/metrics")] async fn metrics(c: Data>, _req: HttpRequest) -> impl Responder { @@ -25,10 +26,11 @@ async fn health(_: HttpRequest) -> impl Responder { async fn main() -> io::Result<()> { dotenv().ok(); + tracing_subscriber::fmt().with_max_level(Level::INFO).init(); + let state = Arc::new(State::default()); let controller = tokio::spawn(controller::run(state.clone())); - let metrics_collector = tokio::spawn(metrics_collector::run_metrics_collector(state.clone())); let addr = std::env::var("ADDR").unwrap_or("0.0.0.0:8080".into()); @@ -39,9 +41,15 @@ async fn main() -> io::Result<()> { .service(health) .service(metrics) }) - .bind(addr)?; + .bind(&addr)?; + info!({ addr }, "metrics server running"); + + let signal = tokio::spawn(async { + tokio::signal::ctrl_c().await.expect("Fail to exit"); + std::process::exit(0); + }); - tokio::join!(controller, metrics_collector, server.run()).2?; + tokio::join!(controller, server.run(), signal).2?; Ok(()) } diff --git a/operator/src/metrics.rs b/operator/src/metrics.rs index 0f4b82e..d18e3d4 100644 --- a/operator/src/metrics.rs +++ b/operator/src/metrics.rs @@ -1,9 +1,7 @@ -use std::{sync::Arc, thread::sleep, time::Duration}; - use kube::ResourceExt; use prometheus::{opts, IntCounterVec, Registry}; -use crate::{Error, OgmiosPort, State}; +use crate::{Error, OgmiosPort}; #[derive(Clone)] pub struct Metrics { @@ -38,9 +36,3 @@ impl Metrics { .inc() } } - -pub async fn run_metrics_collector(_state: Arc) -> Result<(), Error> { - loop { - sleep(Duration::from_secs(6)) - } -} From 7563a1f9748a692b34c4945282084c718bea9d0f Mon Sep 17 00:00:00 2001 From: paulobressan Date: Fri, 29 Dec 2023 14:28:22 -0300 Subject: [PATCH 15/16] chore: updated position tasks on tokio join macro --- operator/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/operator/src/main.rs b/operator/src/main.rs index c7b43a8..621b6c2 100644 --- a/operator/src/main.rs +++ b/operator/src/main.rs @@ -49,7 +49,7 @@ async fn main() -> io::Result<()> { std::process::exit(0); }); - tokio::join!(controller, server.run(), signal).2?; + tokio::join!(server.run(), controller, signal).0?; Ok(()) } From 0ec6491092a6df6fa8a89d4aa17a8fa8ec278a18 Mon Sep 17 00:00:00 2001 From: paulobressan Date: Fri, 29 Dec 2023 14:37:51 -0300 Subject: [PATCH 16/16] chore: added error log on reconcile error handler --- operator/src/controller.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/operator/src/controller.rs b/operator/src/controller.rs index bbcab30..c9698e5 100644 --- a/operator/src/controller.rs +++ b/operator/src/controller.rs @@ -6,7 +6,7 @@ use kube::{ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::{sync::Arc, time::Duration}; -use tracing::{info, instrument}; +use tracing::{error, info, instrument}; use crate::{ auth::handle_auth, @@ -70,6 +70,7 @@ async fn reconcile(crd: Arc, ctx: Arc) -> Result { } fn error_policy(crd: Arc, err: &Error, ctx: Arc) -> Action { + error!(error = err.to_string(), "reconcile failed"); ctx.metrics.reconcile_failure(&crd, err); Action::requeue(Duration::from_secs(5)) }