From bf4aa87e560300587a41676f3cd90ddcb8b37632 Mon Sep 17 00:00:00 2001 From: Paulo Bressan Date: Thu, 11 Jan 2024 14:13:41 -0300 Subject: [PATCH] feat: added secondary route with key on host * feat: added plugin to accept dmtr key on hostname * feat: added secondary route with key on hostname --- bootstrap/crds/main.tf | 22 ++- operator/src/controller.rs | 50 ++++-- operator/src/handlers/auth.rs | 297 +++++++++++++++---------------- operator/src/handlers/gateway.rs | 107 ++++++----- operator/src/handlers/mod.rs | 13 -- operator/src/helpers/mod.rs | 52 +++--- operator/yaml/crd.yaml | 18 +- 7 files changed, 277 insertions(+), 282 deletions(-) diff --git a/bootstrap/crds/main.tf b/bootstrap/crds/main.tf index c2fb93d..bfd23a4 100644 --- a/bootstrap/crds/main.tf +++ b/bootstrap/crds/main.tf @@ -41,8 +41,8 @@ resource "kubernetes_manifest" "customresourcedefinition_kupoports_demeter_run" "type" = "string" }, { - "jsonPath" = ".spec.authentication" - "name" = "Authentication" + "jsonPath" = ".status.endpoint_key_url" + "name" = "Endpoint Key URL" "type" = "string" }, { @@ -58,13 +58,6 @@ resource "kubernetes_manifest" "customresourcedefinition_kupoports_demeter_run" "properties" = { "spec" = { "properties" = { - "authentication" = { - "enum" = [ - "none", - "apiKey", - ] - "type" = "string" - } "network" = { "enum" = [ "mainnet", @@ -85,7 +78,6 @@ resource "kubernetes_manifest" "customresourcedefinition_kupoports_demeter_run" } } "required" = [ - "authentication", "network", "operatorVersion", "pruneUtxo", @@ -97,14 +89,20 @@ resource "kubernetes_manifest" "customresourcedefinition_kupoports_demeter_run" "nullable" = true "properties" = { "authToken" = { - "nullable" = true + "type" = "string" + } + "endpointKeyUrl" = { "type" = "string" } "endpointUrl" = { - "nullable" = true "type" = "string" } } + "required" = [ + "authToken", + "endpointKeyUrl", + "endpointUrl", + ] "type" = "object" } } diff --git a/operator/src/controller.rs b/operator/src/controller.rs index c04013c..34e73cb 100644 --- a/operator/src/controller.rs +++ b/operator/src/controller.rs @@ -2,7 +2,7 @@ use futures::StreamExt; use kube::{ api::ListParams, runtime::{controller::Action, watcher::Config as WatcherConfig, Controller}, - Api, Client, CustomResource, + Api, Client, CustomResource, CustomResourceExt, ResourceExt, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -11,8 +11,8 @@ use tracing::{error, info, instrument}; use crate::{ auth::handle_auth, - gateway::{handle_http_route, handle_reference_grant}, - Error, Metrics, Network, Result, State, + gateway::{handle_http_route, handle_http_route_key, handle_reference_grant}, + patch_resource_status, Error, Metrics, Network, Result, State, }; pub static KUPO_PORT_FINALIZER: &str = "kupoports.demeter.run"; @@ -27,13 +27,6 @@ impl Context { } } -#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub enum Authentication { - None, - ApiKey, -} - #[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)] #[kube( kind = "KupoPort", @@ -48,7 +41,7 @@ pub enum Authentication { {"name": "Pruned", "jsonPath": ".spec.pruneUtxo", "type": "boolean"}, {"name": "Throughput Tier", "jsonPath":".spec.throughputTier", "type": "string"}, {"name": "Endpoint URL", "jsonPath": ".status.endpointUrl", "type": "string"}, - {"name": "Authentication", "jsonPath": ".spec.authentication", "type": "string"}, + {"name": "Endpoint Key URL", "jsonPath": ".status.endpoint_key_url", "type": "string"}, {"name": "Auth Token", "jsonPath": ".status.authToken", "type": "string"} "#)] #[serde(rename_all = "camelCase")] @@ -58,21 +51,42 @@ pub struct KupoPortSpec { pub prune_utxo: bool, // throughput should be 0, 1, 2 pub throughput_tier: String, - pub authentication: Authentication, } #[derive(Deserialize, Serialize, Clone, Default, Debug, JsonSchema)] #[serde(rename_all = "camelCase")] pub struct KupoPortStatus { - pub endpoint_url: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub auth_token: Option, + pub endpoint_url: String, + pub endpoint_key_url: String, + pub auth_token: String, } async fn reconcile(crd: Arc, ctx: Arc) -> Result { - handle_reference_grant(ctx.client.clone(), &crd).await?; - handle_http_route(ctx.client.clone(), &crd).await?; - handle_auth(ctx.client.clone(), &crd).await?; + handle_reference_grant(&ctx.client, &crd).await?; + + let key = handle_auth(&ctx.client, &crd).await?; + let hostname = handle_http_route(&ctx.client, &crd).await?; + let hostname_key = handle_http_route_key(&ctx.client, &crd, &key).await?; + + let status = KupoPortStatus { + endpoint_url: format!("https://{hostname}"), + endpoint_key_url: format!("https://{hostname_key}"), + auth_token: key, + }; + + let namespace = crd.namespace().unwrap(); + let kupo_port = KupoPort::api_resource(); + + patch_resource_status( + ctx.client.clone(), + &namespace, + kupo_port, + &crd.name_any(), + serde_json::to_value(status)?, + ) + .await?; + + info!(resource = crd.name_any(), "Reconcile completed"); Ok(Action::await_change()) } diff --git a/operator/src/handlers/auth.rs b/operator/src/handlers/auth.rs index 596df5e..e532d66 100644 --- a/operator/src/handlers/auth.rs +++ b/operator/src/handlers/auth.rs @@ -6,9 +6,9 @@ use base64::{ use bech32::ToBase32; use k8s_openapi::{api::core::v1::Secret, apimachinery::pkg::apis::meta::v1::OwnerReference}; use kube::{ - api::{DeleteParams, Patch, PatchParams, PostParams}, + api::{Patch, PatchParams, PostParams}, core::ObjectMeta, - Api, Client, CustomResourceExt, Resource, ResourceExt, + Api, Client, Resource, ResourceExt, }; use serde::Deserialize; use serde_json::{json, Value as JsonValue}; @@ -16,193 +16,153 @@ use std::collections::BTreeMap; use tracing::info; use crate::{ - create_resource, delete_resource, get_acl_name, get_auth_name, get_config, get_resource, - kong_consumer, kong_plugin, patch_resource, replace_resource_status, Authentication, Error, - KupoPort, + create_resource, get_acl_name, get_auth_name, get_config, get_host_key_name, get_resource, + kong_consumer, kong_plugin, patch_resource, Error, KupoPort, }; -pub async fn handle_auth(client: Client, crd: &KupoPort) -> Result<(), Error> { - handle_auth_secret(client.clone(), crd).await?; - handle_auth_plugin(client.clone(), crd).await?; +pub async fn handle_auth(client: &Client, crd: &KupoPort) -> Result { + let key = generate_api_key(crd).await?; - handle_acl_secret(client.clone(), crd).await?; - handle_acl_plugin(client.clone(), crd).await?; + handle_auth_secret(client, crd, &key).await?; + handle_auth_plugin(client, crd).await?; + handle_host_key_plugin(client, crd).await?; - handle_consumer(client.clone(), crd).await?; + handle_acl_secret(client, crd).await?; + handle_acl_plugin(client, crd).await?; - Ok(()) + handle_consumer(client, crd).await?; + + Ok(key) } -async fn handle_auth_secret(client: Client, crd: &KupoPort) -> Result<(), Error> { +async fn handle_auth_secret(client: &Client, crd: &KupoPort, key: &str) -> Result<(), Error> { let namespace = crd.namespace().unwrap(); let name = get_auth_name(&crd.name_any()); - let kupo_port = KupoPort::api_resource(); + let secret = build_auth_secret(crd, key); let api = Api::::namespaced(client.clone(), &namespace); let result = api.get_opt(&name).await?; - let mut status = crd.status.clone().unwrap_or_default(); - - match crd.spec.authentication { - Authentication::ApiKey => { - let api_key = generate_api_key(&name, &namespace).await?; - let secret = build_auth_secret(&name, &api_key, crd.clone()); - - status.auth_token = Some(api_key); - - if result.is_some() { - info!(resource = crd.name_any(), "Updating auth secret"); - let patch_params = PatchParams::default(); - api.patch(&name, &patch_params, &Patch::Merge(secret)) - .await?; - } else { - info!(resource = crd.name_any(), "Creating auth secret"); - let post_params = PostParams::default(); - api.create(&post_params, &secret).await?; - } - } - Authentication::None => { - if result.is_some() { - info!(resource = crd.name_any(), "Deleting auth secret"); - api.delete(&name, &DeleteParams::default()).await?; - } - } + if result.is_some() { + info!(resource = crd.name_any(), "Updating auth secret"); + let patch_params = PatchParams::default(); + let patch_data = Patch::Merge(secret); + api.patch(&name, &patch_params, &patch_data).await?; + } else { + info!(resource = crd.name_any(), "Creating auth secret"); + let post_params = PostParams::default(); + api.create(&post_params, &secret).await?; } - replace_resource_status( - client.clone(), - &namespace, - kupo_port, - &crd.name_any(), - serde_json::to_value(status)?, - ) - .await?; - Ok(()) } -async fn handle_auth_plugin(client: Client, crd: &KupoPort) -> Result<(), Error> { +async fn handle_auth_plugin(client: &Client, crd: &KupoPort) -> Result<(), Error> { let namespace = crd.namespace().unwrap(); let name = get_auth_name(&crd.name_any()); let kong_plugin = kong_plugin(); + let (metadata, data, raw) = build_auth_plugin(crd)?; + let result = get_resource(client.clone(), &namespace, &kong_plugin, &name).await?; - match crd.spec.authentication { - Authentication::ApiKey => { - let (metadata, data, raw) = build_auth_plugin(crd.clone())?; - if result.is_some() { - info!(resource = crd.name_any(), "Updating auth plugin"); - patch_resource(client.clone(), &namespace, kong_plugin, &name, raw).await?; - } else { - info!(resource = crd.name_any(), "Creating auth plugin"); - create_resource(client.clone(), &namespace, kong_plugin, metadata, data).await?; - } - } - Authentication::None => { - if result.is_some() { - info!(resource = crd.name_any(), "Deleting auth plugin"); - delete_resource(client.clone(), &namespace, kong_plugin, &name).await?; - } - } + if result.is_some() { + info!(resource = crd.name_any(), "Updating auth plugin"); + patch_resource(client.clone(), &namespace, kong_plugin, &name, raw).await?; + } else { + info!(resource = crd.name_any(), "Creating auth plugin"); + create_resource(client.clone(), &namespace, kong_plugin, metadata, data).await?; + } + + Ok(()) +} + +async fn handle_host_key_plugin(client: &Client, crd: &KupoPort) -> Result<(), Error> { + let namespace = crd.namespace().unwrap(); + let name = get_host_key_name(&crd.name_any()); + let kong_plugin = kong_plugin(); + + let (metadata, data, raw) = build_host_key_plugin(crd)?; + + let result = get_resource(client.clone(), &namespace, &kong_plugin, &name).await?; + + if result.is_some() { + info!(resource = crd.name_any(), "Updating host key plugin"); + patch_resource(client.clone(), &namespace, kong_plugin, &name, raw).await?; + } else { + info!(resource = crd.name_any(), "Creating host key plugin"); + create_resource(client.clone(), &namespace, kong_plugin, metadata, data).await?; } Ok(()) } -async fn handle_acl_secret(client: Client, crd: &KupoPort) -> Result<(), Error> { +async fn handle_acl_secret(client: &Client, crd: &KupoPort) -> Result<(), Error> { let namespace = crd.namespace().unwrap(); let name = get_acl_name(&crd.name_any()); + let secret = build_acl_secret(crd); let api = Api::::namespaced(client.clone(), &namespace); - let result = api.get_opt(&name).await?; - match crd.spec.authentication { - Authentication::ApiKey => { - let secret = build_acl_secret(&name, crd.clone()); - - if result.is_some() { - info!(resource = crd.name_any(), "Updating acl secret"); - let patch_params = PatchParams::default(); - api.patch(&name, &patch_params, &Patch::Merge(secret)) - .await?; - } else { - info!(resource = crd.name_any(), "Creating acl secret"); - let post_params = PostParams::default(); - api.create(&post_params, &secret).await?; - } - } - Authentication::None => { - if result.is_some() { - info!(resource = crd.name_any(), "Deleting acl secret"); - api.delete(&name, &DeleteParams::default()).await?; - } - } + if result.is_some() { + info!(resource = crd.name_any(), "Updating acl secret"); + let patch_params = PatchParams::default(); + let patch_data = Patch::Merge(secret); + api.patch(&name, &patch_params, &patch_data).await?; + } else { + info!(resource = crd.name_any(), "Creating acl secret"); + let post_params = PostParams::default(); + api.create(&post_params, &secret).await?; } Ok(()) } -async fn handle_acl_plugin(client: Client, crd: &KupoPort) -> Result<(), Error> { +async fn handle_acl_plugin(client: &Client, crd: &KupoPort) -> Result<(), Error> { let namespace = crd.namespace().unwrap(); let name = get_acl_name(&crd.name_any()); let kong_plugin = kong_plugin(); + let (metadata, data, raw) = build_acl_plugin(crd)?; + let result = get_resource(client.clone(), &namespace, &kong_plugin, &name).await?; - let (metadata, data, raw) = build_acl_plugin(crd.clone())?; - - match crd.spec.authentication { - Authentication::ApiKey => { - if result.is_some() { - info!(resource = crd.name_any(), "Updating acl plugin"); - patch_resource(client.clone(), &namespace, kong_plugin, &name, raw).await?; - } else { - info!(resource = crd.name_any(), "Creating acl plugin"); - create_resource(client.clone(), &namespace, kong_plugin, metadata, data).await?; - } - } - Authentication::None => { - if result.is_some() { - info!(resource = crd.name_any(), "Deleting acl plugin"); - delete_resource(client.clone(), &namespace, kong_plugin, &name).await?; - } - } + + if result.is_some() { + info!(resource = crd.name_any(), "Updating acl plugin"); + patch_resource(client.clone(), &namespace, kong_plugin, &name, raw).await?; + } else { + info!(resource = crd.name_any(), "Creating acl plugin"); + create_resource(client.clone(), &namespace, kong_plugin, metadata, data).await?; } Ok(()) } -async fn handle_consumer(client: Client, crd: &KupoPort) -> Result<(), Error> { +async fn handle_consumer(client: &Client, crd: &KupoPort) -> Result<(), Error> { let namespace = crd.namespace().unwrap(); let name = get_auth_name(&crd.name_any()); let kong_consumer = kong_consumer(); + let (metadata, data, raw) = build_consumer(crd)?; + let result = get_resource(client.clone(), &namespace, &kong_consumer, &name).await?; - match crd.spec.authentication { - Authentication::ApiKey => { - let (metadata, data, raw) = build_consumer(crd.clone())?; - if result.is_some() { - info!(resource = crd.name_any(), "Updating consumer"); - patch_resource(client.clone(), &namespace, kong_consumer, &name, raw).await?; - } else { - info!(resource = crd.name_any(), "Creating consumer"); - create_resource(client.clone(), &namespace, kong_consumer, metadata, data).await?; - } - } - Authentication::None => { - if result.is_some() { - info!(resource = crd.name_any(), "Deleting consumer"); - delete_resource(client.clone(), &namespace, kong_consumer, &name).await?; - } - } + if result.is_some() { + info!(resource = crd.name_any(), "Updating consumer"); + patch_resource(client.clone(), &namespace, kong_consumer, &name, raw).await?; + } else { + info!(resource = crd.name_any(), "Creating consumer"); + create_resource(client.clone(), &namespace, kong_consumer, metadata, data).await?; } Ok(()) } -async fn generate_api_key(name: &str, namespace: &str) -> Result { +async fn generate_api_key(crd: &KupoPort) -> Result { + let namespace = crd.namespace().unwrap(); + let name = get_auth_name(&crd.name_any()); + let password = format!("{}{}", name, namespace).as_bytes().to_vec(); let config = get_config(); @@ -213,7 +173,6 @@ async fn generate_api_key(name: &str, namespace: &str) -> Result let argon2 = Argon2::default(); let _ = 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_kupo", base64.to_base32(), bech32::Variant::Bech32).unwrap(); @@ -221,7 +180,7 @@ async fn generate_api_key(name: &str, namespace: &str) -> Result Ok(with_bech) } -fn build_auth_secret(name: &str, api_key: &str, owner: KupoPort) -> Secret { +fn build_auth_secret(crd: &KupoPort, api_key: &str) -> Secret { let mut string_data = BTreeMap::new(); string_data.insert("key".into(), api_key.into()); @@ -229,12 +188,12 @@ fn build_auth_secret(name: &str, api_key: &str, owner: KupoPort) -> Secret { labels.insert("konghq.com/credential".into(), "key-auth".into()); let metadata = ObjectMeta { - name: Some(name.to_string()), + name: Some(get_auth_name(&crd.name_any())), owner_references: Some(vec![OwnerReference { api_version: KupoPort::api_version(&()).to_string(), kind: KupoPort::kind(&()).to_string(), - name: owner.name_any(), - uid: owner.uid().unwrap(), + name: crd.name_any(), + uid: crd.uid().unwrap(), ..Default::default() }]), labels: Some(labels), @@ -249,18 +208,17 @@ fn build_auth_secret(name: &str, api_key: &str, owner: KupoPort) -> Secret { } } -fn build_auth_plugin(owner: KupoPort) -> Result<(ObjectMeta, JsonValue, JsonValue), Error> { +fn build_auth_plugin(crd: &KupoPort) -> Result<(ObjectMeta, JsonValue, JsonValue), Error> { let kong_plugin = kong_plugin(); let metadata = ObjectMeta::deserialize(&json!({ - "name": get_auth_name(&owner.name_any()), - + "name": get_auth_name(&crd.name_any()), "ownerReferences": [ { "apiVersion": KupoPort::api_version(&()).to_string(), // @TODO: try to grab this from the owner "kind": KupoPort::kind(&()).to_string(), // @TODO: try to grab this from the owner - "name": owner.name_any(), - "uid": owner.uid() + "name": crd.name_any(), + "uid": crd.uid() } ] }))?; @@ -283,20 +241,51 @@ fn build_auth_plugin(owner: KupoPort) -> Result<(ObjectMeta, JsonValue, JsonValu Ok((metadata, data, raw)) } -fn build_acl_secret(name: &str, owner: KupoPort) -> Secret { +fn build_host_key_plugin(crd: &KupoPort) -> Result<(ObjectMeta, JsonValue, JsonValue), Error> { + let kong_plugin = kong_plugin(); + + let metadata = ObjectMeta::deserialize(&json!({ + "name": get_host_key_name(&crd.name_any()), + "ownerReferences": [ + { + "apiVersion": KupoPort::api_version(&()).to_string(), + "kind": KupoPort::kind(&()).to_string(), + "name": crd.name_any(), + "uid": crd.uid() + } + ] + }))?; + + let data = json!({ + "plugin": "key-to-header", + "config": {} + }); + + 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 build_acl_secret(crd: &KupoPort) -> Secret { let mut string_data = BTreeMap::new(); - string_data.insert("group".into(), owner.name_any()); + string_data.insert("group".into(), crd.name_any()); let mut labels = BTreeMap::new(); labels.insert("konghq.com/credential".into(), "acl".into()); let metadata = ObjectMeta { - name: Some(name.to_string()), + name: Some(get_acl_name(&crd.name_any())), owner_references: Some(vec![OwnerReference { api_version: KupoPort::api_version(&()).to_string(), kind: KupoPort::kind(&()).to_string(), - name: owner.name_any(), - uid: owner.uid().unwrap(), + name: crd.name_any(), + uid: crd.uid().unwrap(), ..Default::default() }]), labels: Some(labels), @@ -311,17 +300,17 @@ fn build_acl_secret(name: &str, owner: KupoPort) -> Secret { } } -fn build_acl_plugin(owner: KupoPort) -> Result<(ObjectMeta, JsonValue, JsonValue), Error> { +fn build_acl_plugin(crd: &KupoPort) -> Result<(ObjectMeta, JsonValue, JsonValue), Error> { let kong_plugin = kong_plugin(); let metadata = ObjectMeta::deserialize(&json!({ - "name": get_acl_name(&owner.name_any()), + "name": get_acl_name(&crd.name_any()), "ownerReferences": [ { "apiVersion": KupoPort::api_version(&()).to_string(), // @TODO: try to grab this from the owner "kind": KupoPort::kind(&()).to_string(), // @TODO: try to grab this from the owner - "name": owner.name_any(), - "uid": owner.uid() + "name": crd.name_any(), + "uid": crd.uid() } ] }))?; @@ -329,7 +318,7 @@ fn build_acl_plugin(owner: KupoPort) -> Result<(ObjectMeta, JsonValue, JsonValue let data = json!({ "plugin": "acl", "config": { - "allow": [owner.name_any()] + "allow": [crd.name_any()] } }); @@ -344,12 +333,12 @@ fn build_acl_plugin(owner: KupoPort) -> Result<(ObjectMeta, JsonValue, JsonValue Ok((metadata, data, raw)) } -fn build_consumer(owner: KupoPort) -> Result<(ObjectMeta, JsonValue, JsonValue), Error> { +fn build_consumer(crd: &KupoPort) -> 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()), + "name": get_auth_name(&crd.name_any()), "annotations": { "kubernetes.io/ingress.class": config.ingress_class, }, @@ -358,15 +347,15 @@ fn build_consumer(owner: KupoPort) -> Result<(ObjectMeta, JsonValue, JsonValue), { "apiVersion": KupoPort::api_version(&()).to_string(), // @TODO: try to grab this from the owner "kind": KupoPort::kind(&()).to_string(), // @TODO: try to grab this from the owner - "name": owner.name_any(), - "uid": owner.uid() + "name": crd.name_any(), + "uid": crd.uid() } ] }))?; let data = json!({ - "username": owner.name_any(), - "credentials": [get_auth_name(&owner.name_any()), get_acl_name(&owner.name_any())] + "username": crd.name_any(), + "credentials": [get_auth_name(&crd.name_any()), get_acl_name(&crd.name_any())] }); let raw = json!({ diff --git a/operator/src/handlers/gateway.rs b/operator/src/handlers/gateway.rs index 7d1777e..1c133d5 100644 --- a/operator/src/handlers/gateway.rs +++ b/operator/src/handlers/gateway.rs @@ -1,26 +1,25 @@ -use kube::{core::ObjectMeta, Client, CustomResourceExt, Resource, ResourceExt}; +use kube::{core::ObjectMeta, Client, 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_rate_limit_name, get_resource, - http_route, patch_resource, patch_resource_status, reference_grant, Error, KupoPort, - KupoPortStatus, kupo_service_name, + create_resource, get_acl_name, get_auth_name, get_config, get_http_route_key_name, + get_http_route_name, get_rate_limit_name, get_resource, http_route, kupo_service_name, + patch_resource, reference_grant, Error, KupoPort, }; -pub async fn handle_http_route(client: Client, crd: &KupoPort) -> Result<(), Error> { +pub async fn handle_http_route(client: &Client, crd: &KupoPort) -> Result { let namespace = crd.namespace().unwrap(); let kupo_service = kupo_service_name(&crd.spec.network, crd.spec.prune_utxo); - let name = format!("kupo-{}", crd.name_any()); - let host_name = build_host(&crd.name_any(), &namespace_to_slug(&namespace)); + let name = get_http_route_name(&crd.name_any()); + let host_name = build_hostname(&crd.name_any(), &project_id(&namespace), None); let http_route = http_route(); - let kupo_port = KupoPort::api_resource(); let result = get_resource(client.clone(), &namespace, &http_route, &name).await?; - let (metadata, data, raw) = route(&name, &host_name, crd, &kupo_service)?; + let (metadata, data, raw) = build_route(&name, &host_name, crd, &kupo_service)?; if result.is_some() { info!(resource = crd.name_any(), "Updating http route"); @@ -30,22 +29,37 @@ pub async fn handle_http_route(client: Client, crd: &KupoPort) -> Result<(), Err create_resource(client.clone(), &namespace, http_route, metadata, data).await?; } - let status = KupoPortStatus { - endpoint_url: Some(format!("https://{}", host_name)), - ..Default::default() - }; - patch_resource_status( - client.clone(), - &namespace, - kupo_port, - &crd.name_any(), - serde_json::to_value(status)?, - ) - .await?; - Ok(()) + Ok(host_name) +} + +pub async fn handle_http_route_key( + client: &Client, + crd: &KupoPort, + key: &str, +) -> Result { + let namespace = crd.namespace().unwrap(); + let kupo_service = kupo_service_name(&crd.spec.network, crd.spec.prune_utxo); + + let name = get_http_route_key_name(&crd.name_any()); + let host_name = build_hostname(&crd.name_any(), &project_id(&namespace), Some(key)); + let http_route = http_route(); + + let result = get_resource(client.clone(), &namespace, &http_route, &name).await?; + + let (metadata, data, raw) = build_route(&name, &host_name, crd, &kupo_service)?; + + if result.is_some() { + info!(resource = crd.name_any(), "Updating http route"); + patch_resource(client.clone(), &namespace, http_route, &name, raw).await?; + } else { + info!(resource = crd.name_any(), "Creating http route"); + create_resource(client.clone(), &namespace, http_route, metadata, data).await?; + } + + Ok(host_name) } -pub async fn handle_reference_grant(client: Client, crd: &KupoPort) -> Result<(), Error> { +pub async fn handle_reference_grant(client: &Client, crd: &KupoPort) -> Result<(), Error> { let namespace = crd.namespace().unwrap(); let kupo_service = kupo_service_name(&crd.spec.network, crd.spec.prune_utxo); @@ -55,7 +69,7 @@ pub async fn handle_reference_grant(client: Client, crd: &KupoPort) -> Result<() let result = get_resource(client.clone(), &config.namespace, &reference_grant, &name).await?; - let (metadata, data, raw) = grant(&name, &kupo_service, &namespace)?; + let (metadata, data, raw) = build_grant(&name, &kupo_service, &namespace)?; if result.is_some() { info!(resource = crd.name_any(), "Updating reference grant"); @@ -69,7 +83,6 @@ pub async fn handle_reference_grant(client: Client, crd: &KupoPort) -> Result<() .await?; } else { info!(resource = crd.name_any(), "Creating reference grant"); - // we need to get the deserialized payload create_resource( client.clone(), &config.namespace, @@ -79,35 +92,39 @@ pub async fn handle_reference_grant(client: Client, crd: &KupoPort) -> Result<() ) .await?; } + Ok(()) } -fn build_host(name: &str, project_slug: &str) -> String { +fn build_hostname(crd_name: &str, project_id: &str, key: Option<&str>) -> String { let config = get_config(); + let ingress_class = &config.ingress_class; + let dns_zone = &config.dns_zone; + + if let Some(key) = key { + return format!("{key}.{crd_name}-{project_id}.{ingress_class}.{dns_zone}"); + } - format!( - "{}-{}.{}.{}", - name, project_slug, config.ingress_class, config.dns_zone - ) + format!("{crd_name}-{project_id}.{ingress_class}.{dns_zone}") } -fn namespace_to_slug(namespace: &str) -> String { +fn project_id(namespace: &str) -> String { namespace.split_once('-').unwrap().1.to_string() } -fn route( +fn build_route( name: &str, hostname: &str, - owner: &KupoPort, - private_dns_service_name: &str, + crd: &KupoPort, + kupo_service: &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()), - get_rate_limit_name(&owner.spec.throughput_tier), + get_auth_name(&crd.name_any()), + get_acl_name(&crd.name_any()), + get_rate_limit_name(&crd.spec.throughput_tier), ); let metadata = ObjectMeta::deserialize(&json!({ @@ -124,8 +141,8 @@ fn route( { "apiVersion": KupoPort::api_version(&()).to_string(), // @TODO: try to grab this from the owner "kind": KupoPort::kind(&()).to_string(), // @TODO: try to grab this from the owner - "name": owner.name_any(), - "uid": owner.uid() + "name": crd.name_any(), + "uid": crd.uid() } ] }))?; @@ -144,7 +161,7 @@ fn route( "backendRefs": [ { "kind": "Service", - "name": private_dns_service_name, + "name": kupo_service, "port": config.http_port.parse::()?, "namespace": config.namespace } @@ -164,10 +181,10 @@ fn route( Ok((metadata, data, raw)) } -fn grant( +fn build_grant( name: &str, - private_dns_service_name: &str, - project_namespace: &str, + kupo_service: &str, + namespace: &str, ) -> Result<(ObjectMeta, JsonValue, JsonValue), Error> { let reference_grant = reference_grant(); let http_route = http_route(); @@ -182,14 +199,14 @@ fn grant( { "group": http_route.group, "kind": http_route.kind, - "namespace": project_namespace, + "namespace": namespace, }, ], "to": [ { "group": "", "kind": "Service", - "name": private_dns_service_name, + "name": kupo_service, }, ], } diff --git a/operator/src/handlers/mod.rs b/operator/src/handlers/mod.rs index 42ade78..ddea910 100644 --- a/operator/src/handlers/mod.rs +++ b/operator/src/handlers/mod.rs @@ -1,15 +1,2 @@ pub mod auth; pub mod gateway; - -// helpers -pub fn get_auth_name(name: &str) -> String { - format!("kupo-auth-{}", name) -} - -pub fn get_rate_limit_name(tier: &str) -> String { - format!("rate-limiting-kupo-tier-{}", tier) -} - -pub fn get_acl_name(name: &str) -> String { - format!("kupo-acl-{}", name) -} diff --git a/operator/src/helpers/mod.rs b/operator/src/helpers/mod.rs index 3163676..564c34d 100644 --- a/operator/src/helpers/mod.rs +++ b/operator/src/helpers/mod.rs @@ -1,5 +1,5 @@ use kube::{ - api::{DeleteParams, Patch, PatchParams, PostParams}, + api::{Patch, PatchParams, PostParams}, core::{DynamicObject, ObjectMeta}, discovery::ApiResource, Api, Client, @@ -108,41 +108,33 @@ pub async fn patch_resource_status( Ok(()) } -pub async fn replace_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 post_params = PostParams::default(); +pub fn kupo_service_name(network: &Network, prune_utxo: bool) -> String { + if prune_utxo { + return format!("kupo-{}-pruned", network); + } + format!("kupo-{}", network) +} - api.replace_status(name, &post_params, status.to_string().into_bytes()) - .await?; +pub fn get_http_route_name(name: &str) -> String { + format!("kupo-http-route-{}", name) +} - Ok(()) +pub fn get_http_route_key_name(name: &str) -> String { + format!("kupo-http-route-key-{}", name) } -pub async fn delete_resource( - client: Client, - namespace: &str, - api_resource: ApiResource, - name: &str, -) -> Result<(), kube::Error> { - let api: Api = Api::namespaced_with(client, namespace, &api_resource); +pub fn get_auth_name(name: &str) -> String { + format!("kupo-auth-{}", name) +} - api.delete(name, &DeleteParams::default()).await?; +pub fn get_host_key_name(name: &str) -> String { + format!("kupo-host-key-{}", name) +} - Ok(()) +pub fn get_rate_limit_name(tier: &str) -> String { + format!("rate-limiting-kupo-tier-{}", tier) } -pub fn kupo_service_name(network: &Network, prune_utxo: bool) -> String { - if prune_utxo { - return format!("kupo-{}-pruned", network); - } - format!("kupo-{}", network) +pub fn get_acl_name(name: &str) -> String { + format!("kupo-acl-{}", name) } diff --git a/operator/yaml/crd.yaml b/operator/yaml/crd.yaml index 146d235..4bc0289 100644 --- a/operator/yaml/crd.yaml +++ b/operator/yaml/crd.yaml @@ -26,8 +26,8 @@ spec: - jsonPath: .status.endpointUrl name: Endpoint URL type: string - - jsonPath: .spec.authentication - name: Authentication + - jsonPath: .status.endpoint_key_url + name: Endpoint Key URL type: string - jsonPath: .status.authToken name: Auth Token @@ -39,11 +39,6 @@ spec: properties: spec: properties: - authentication: - enum: - - none - - apiKey - type: string network: enum: - mainnet @@ -58,7 +53,6 @@ spec: throughputTier: type: string required: - - authentication - network - operatorVersion - pruneUtxo @@ -68,11 +62,15 @@ spec: nullable: true properties: authToken: - nullable: true + type: string + endpointKeyUrl: type: string endpointUrl: - nullable: true type: string + required: + - authToken + - endpointKeyUrl + - endpointUrl type: object required: - spec