diff --git a/Cargo.lock b/Cargo.lock index 41e9158..4667651 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,6 +37,24 @@ dependencies = [ "subtle", ] +[[package]] +name = "aho-corasick" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "ansi_term" version = "0.12.1" @@ -59,7 +77,7 @@ dependencies = [ "num-traits", "rusticata-macros", "thiserror", - "time", + "time 0.3.14", ] [[package]] @@ -206,6 +224,22 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-integer", + "num-traits", + "serde", + "time 0.1.44", + "wasm-bindgen", + "winapi", +] + [[package]] name = "cipher" version = "0.3.0" @@ -235,7 +269,7 @@ dependencies = [ "rand", "sha2", "subtle", - "time", + "time 0.3.14", "version_check", ] @@ -368,6 +402,27 @@ dependencies = [ "subtle", ] +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "displaydoc" version = "0.2.3" @@ -384,7 +439,12 @@ name = "dynk8s-provisioner" version = "0.1.0" dependencies = [ "base64", + "chrono", + "k8s-openapi", + "kube", "log", + "rand", + "regex", "reqwest", "rocket", "rsa", @@ -471,6 +531,7 @@ checksum = "7f21eda599937fba36daeb58a22e8f5cee2d14c4a17b5b7739c7c8e5e3b8230c" dependencies = [ "futures-channel", "futures-core", + "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -493,12 +554,34 @@ version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf" +[[package]] +name = "futures-executor" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ff63c23854bee61b6e9cd331d523909f238fc7636290b96826e9cfa5faa00ab" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68" +[[package]] +name = "futures-macro" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42cd15d1c7456c04dbdf7e88bcd69760d74f3a798d6444e16974b505b0e62f17" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-sink" version = "0.3.24" @@ -520,6 +603,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -559,7 +643,7 @@ checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] @@ -652,6 +736,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range-header" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" + [[package]] name = "httparse" version = "1.8.0" @@ -688,6 +778,36 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-openssl" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6ee5d7a8f718585d1c3c61dfde28ef5b0bb14734b4db13f5ada856cdc6c612b" +dependencies = [ + "http", + "hyper", + "linked_hash_set", + "once_cell", + "openssl", + "openssl-sys", + "parking_lot", + "tokio", + "tokio-openssl", + "tower-layer", +] + +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper", + "pin-project-lite", + "tokio", + "tokio-io-timeout", +] + [[package]] name = "hyper-tls" version = "0.5.0" @@ -701,6 +821,19 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "iana-time-zone" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd911b35d940d2bd0bea0f9100068e5b97b51a1cbe13d13382f132e0365257a0" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "js-sys", + "wasm-bindgen", + "winapi", +] + [[package]] name = "idna" version = "0.2.3" @@ -759,6 +892,96 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonpath_lib" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaa63191d68230cccb81c5aa23abd53ed64d83337cacbb25a7b8c7979523774f" +dependencies = [ + "log", + "serde", + "serde_json", +] + +[[package]] +name = "k8s-openapi" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9455388f4977de4d0934efa9f7d36296295537d774574113a20f6082de03da" +dependencies = [ + "base64", + "bytes", + "chrono", + "http", + "percent-encoding", + "serde", + "serde-value", + "serde_json", + "url", +] + +[[package]] +name = "kube" +version = "0.75.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb19108692aeafebb108fd0a1c381c06ac4c03859652599420975165e939b8a" +dependencies = [ + "k8s-openapi", + "kube-client", + "kube-core", +] + +[[package]] +name = "kube-client" +version = "0.75.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97e1a80ecd1b1438a2fc004549e155d47250b9e01fbfcf4cfbe9c8b56a085593" +dependencies = [ + "base64", + "bytes", + "chrono", + "dirs-next", + "either", + "futures", + "http", + "http-body", + "hyper", + "hyper-openssl", + "hyper-timeout", + "jsonpath_lib", + "k8s-openapi", + "kube-core", + "openssl", + "pem", + "pin-project", + "secrecy", + "serde", + "serde_json", + "serde_yaml", + "thiserror", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tracing", +] + +[[package]] +name = "kube-core" +version = "0.75.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4d780f2bb048eeef64a4c6b2582d26a0fe19e30b4d3cc9e081616e1779c5d47" +dependencies = [ + "chrono", + "form_urlencoded", + "http", + "k8s-openapi", + "once_cell", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -780,6 +1003,21 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "292a948cd991e376cf75541fe5b97a1081d713c618b4f1b9500f8844e49eb565" +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linked_hash_set" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47186c6da4d81ca383c7c47c1bfc80f4b95f4720514d860a5407aaf4233f9588" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "lock_api" version = "0.4.8" @@ -855,7 +1093,7 @@ checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys", ] @@ -1051,6 +1289,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "ordered-float" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7940cf2ca942593318d07fcf2596cdca60a85c9e7fab408a5e21a4f9dcd40d87" +dependencies = [ + "num-traits", +] + [[package]] name = "parking_lot" version = "0.12.1" @@ -1097,6 +1344,15 @@ dependencies = [ "syn", ] +[[package]] +name = "pem" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c64931a1a212348ec4f3b4362585eca7159d0d09cbdf4a7f74f02173596fd4" +dependencies = [ + "base64", +] + [[package]] name = "pem-rfc7468" version = "0.3.1" @@ -1112,6 +1368,26 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +[[package]] +name = "pin-project" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.9" @@ -1240,6 +1516,17 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall", + "thiserror", +] + [[package]] name = "ref-cast" version = "1.0.9" @@ -1266,6 +1553,8 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" dependencies = [ + "aho-corasick", + "memchr", "regex-syntax", ] @@ -1360,7 +1649,7 @@ dependencies = [ "serde_json", "state", "tempfile", - "time", + "time 0.3.14", "tokio", "tokio-stream", "tokio-util", @@ -1407,7 +1696,7 @@ dependencies = [ "smallvec", "stable-pattern", "state", - "time", + "time 0.3.14", "tokio", "uncased", ] @@ -1475,6 +1764,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "serde", + "zeroize", +] + [[package]] name = "security-framework" version = "2.7.0" @@ -1507,6 +1806,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + [[package]] name = "serde_derive" version = "1.0.144" @@ -1524,6 +1833,7 @@ version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" dependencies = [ + "indexmap", "itoa", "ryu", "serde", @@ -1541,6 +1851,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" +dependencies = [ + "indexmap", + "ryu", + "serde", + "yaml-rust", +] + [[package]] name = "sha1" version = "0.10.2" @@ -1718,6 +2040,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + [[package]] name = "time" version = "0.3.14" @@ -1771,6 +2104,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "tokio-io-timeout" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" +dependencies = [ + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-macros" version = "1.8.0" @@ -1792,6 +2135,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-openssl" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08f9ffb7809f1b20c1b398d92acf4cc719874b3b2b2d9ea2f09b4a80350878a" +dependencies = [ + "futures-util", + "openssl", + "openssl-sys", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.9" @@ -1826,6 +2181,49 @@ dependencies = [ "serde", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c530c8675c1dbf98facee631536fa116b5fb6382d7dd6dc1b118d970eafe3ba" +dependencies = [ + "base64", + "bitflags", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-range-header", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343bc9466d3fe6b0f960ef45960509f84480bf4fd96f92901afe7ff3df9d3a62" + [[package]] name = "tower-service" version = "0.3.2" @@ -1839,6 +2237,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307" dependencies = [ "cfg-if", + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -2002,6 +2401,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -2216,7 +2621,16 @@ dependencies = [ "oid-registry", "rusticata-macros", "thiserror", - "time", + "time 0.3.14", +] + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 9e87ae5..56d3a42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,11 @@ edition = "2021" [dependencies] base64 = "0.13.0" +chrono = "0.4.22" +k8s-openapi = { version = "0.16.0", features = ["v1_22"] } +kube = "0.75.0" log = "0.4.17" +rand = "0.8.5" reqwest = "0.11.11" rocket = { version = "0.5.0-rc.2", features = ["json"] } rsa = "0.6.1" @@ -13,3 +17,6 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.85" sha1 = "0.10.2" x509-parser = "0.14.0" + +[dev-dependencies] +regex = "1.6.0" diff --git a/src/events.rs b/src/events.rs new file mode 100644 index 0000000..a65c7f0 --- /dev/null +++ b/src/events.rs @@ -0,0 +1,29 @@ +//! Event handlers +//! +//! Functions in this module are called to handle events from outside sources, +//! such as Amazon EventBridge events delivered via Amazon Simple Notification +//! Service. +use log::{debug, error}; + +use crate::k8s::create_bootstrap_token; +use crate::model::events::*; + +/// Handle an EC2 instance state change event +/// +/// This function manages the lifecycle of the Kubernetes Secret resources +/// associated with ephemeral nodes running as EC2 instances. +/// +/// When an instance starts: +/// 1. A Kubernetes bootstrap token is generated, to be used by `kubeadm` to +/// add the node to the cluster. +pub async fn on_ec2_instance_state_change(evt: Ec2InstanceStateChange) { + debug!("EC2 instance {} is now {}", &evt.instance_id, &evt.state); + if evt.state == "running" { + if let Err(e) = create_bootstrap_token(&evt.instance_id).await { + error!( + "Failed to create bootstrap token for instance {}: {}", + &evt.instance_id, e + ) + }; + } +} diff --git a/src/k8s.rs b/src/k8s.rs new file mode 100644 index 0000000..3b494e6 --- /dev/null +++ b/src/k8s.rs @@ -0,0 +1,244 @@ +//! Kubernetes Integration +use std::collections::btree_map::BTreeMap; + +use chrono::offset::Utc; +use chrono::{DateTime, Duration}; +use k8s_openapi::api::core::v1::Secret; +use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta; +use kube::core::params::PostParams; +use kube::{Api, Client}; +use log::info; +use rand::seq::SliceRandom; + +/// The set of characters allowed to appear in bootstrap tokens +const TOKEN_CHARS: [char; 36] = [ + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', + 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', + '4', '5', '6', '7', '8', '9', +]; + +/// Kubernetes Bootstrap Token +/// +/// Bootstrap tokens are typically used to add new nodes to the cluster using +/// `kubeadm join`. They are bearer tokens consisting of two parts: a token ID +/// and a secret. For additional information, see [Authenticating with +/// Bootstrap Tokens][0]. +/// +/// The Dynk8s Provisioner allocates bootstrap tokens for ephemeral nodes. +/// Each token is assigned to a specific EC2 instance; the instance ID is +/// stored in the token Secret's metadata, using the +/// `dynk8s.du5t1n.me/ec2-instance-id` label. +/// +/// [0]: https://kubernetes.io/docs/reference/access-authn-authz/bootstrap-tokens/ +#[derive(Clone, Debug)] +struct BootstrapToken { + /// The token ID (generated) + token_id: String, + /// The token secret (generated) + secret: String, + /// The date and time the token expires + expiration: DateTime, + /// The EC2 instance ID to which the token is assigned + instance_id: Option, +} + +#[allow(dead_code)] +impl BootstrapToken { + /// Generate a new token + /// + /// Initially, the token is *not* assigned to an EC2 instance. Use + /// [`Self::instance_id`] to set the associated instance ID. + pub fn new() -> Self { + let mut rng = rand::thread_rng(); + let mut token_id = String::with_capacity(6); + while token_id.len() < 6 { + token_id.push(TOKEN_CHARS.choose(&mut rng).unwrap().clone()); + } + let mut secret = String::with_capacity(16); + while secret.len() < 16 { + secret.push(TOKEN_CHARS.choose(&mut rng).unwrap().clone()); + } + let expiration = Utc::now() + Duration::hours(1); + Self { + token_id, + secret, + expiration, + instance_id: None, + } + } + + /// Return the expiration date/time + pub fn expiration(&self) -> &DateTime { + &self.expiration + } + + /// Set the ID of the EC2 instance associated with the token + pub fn instance_id(mut self, instance_id: String) -> Self { + self.instance_id = Some(instance_id); + self + } + + /// Return the secret part of the token + pub fn secret(&self) -> &str { + &self.secret + } + + /// Set the expiration date/time from a time-to-live duration + pub fn set_ttl(mut self, ttl: Duration) -> Self { + self.expiration = Utc::now() + ttl; + self + } + + /// Set the expiration date/time + pub fn set_expiration(mut self, expiration: DateTime) -> Self { + self.expiration = expiration; + self + } + + /// Return the ID part of the token + pub fn token_id(&self) -> &str { + &self.token_id + } + + /// Return the token as a string + pub fn token(&self) -> String { + format!("{}.{}", self.token_id, self.secret) + } +} + +impl Into for BootstrapToken { + /// Create a [`Secret`] for the token + /// + /// Converting a [`BootstrapToken`] into a [`Secret`] populates the fields + /// necessary to store the token in Kubernetes. The `Secret` can be passed + /// to e.g. [`kube::Api::create`]. + fn into(self) -> Secret { + let mut data = BTreeMap::::new(); + data.insert("token-id".into(), self.token_id.clone()); + data.insert("token-secret".into(), self.secret); + data.insert("expiration".into(), self.expiration.to_rfc3339()); + data.insert("usage-bootstrap-authentication".into(), "true".into()); + data.insert("usage-bootstrap-signing".into(), "true".into()); + data.insert( + "auth-extra-groups".into(), + "system:bootstrappers:kubeadm:default-node-token".into(), + ); + let mut labels = BTreeMap::::new(); + if let Some(instance_id) = self.instance_id { + labels.insert( + "dynk8s.du5t1n.me/ec2-instance-id".into(), + instance_id.into(), + ); + } + Secret { + data: None, + immutable: None, + metadata: ObjectMeta { + annotations: None, + cluster_name: None, + creation_timestamp: None, + deletion_grace_period_seconds: None, + deletion_timestamp: None, + finalizers: None, + generate_name: None, + generation: None, + labels: Some(labels), + managed_fields: None, + name: Some(format!("bootstrap-token-{}", self.token_id)), + namespace: None, + owner_references: None, + resource_version: None, + self_link: None, + uid: None, + }, + string_data: Some(data), + type_: Some("bootstrap.kubernetes.io/token".into()), + } + } +} + +/// Generate and store a bootstrap token for the specified EC2 instance +/// +/// This function generates a new bootstrap token and stores it as a Kubernetes +/// Secret resource. The token is assigned to the given EC2 instance, and will +/// be provided to that instance when it is ready to join the cluster. +pub async fn create_bootstrap_token>( + instance_id: I, +) -> Result<(), kube::Error> { + let instance_id = instance_id.as_ref(); + info!("Creating bootstrap token for instance {}", instance_id); + let token = BootstrapToken::new().instance_id(instance_id.into()); + let client = Client::try_default().await?; + let secrets: Api = Api::namespaced(client, "kube-system"); + let pp: PostParams = Default::default(); + let secret = secrets.create(&pp, &token.into()).await?; + info!("Successfully created secret {:?}", &secret.metadata.name); + Ok(()) +} + +#[cfg(test)] +mod test { + use super::*; + use regex::Regex; + + #[test] + fn test_bootstrap_token_new() { + let token = BootstrapToken::new(); + let id_re = Regex::new(r"^[a-z0-9]{6}$").unwrap(); + let secret_re = Regex::new(r"^[a-z0-9]{16}$").unwrap(); + let token_re = Regex::new(r"[a-z0-9]{6}\.[a-z0-9]{16}$").unwrap(); + assert!(id_re.is_match(&token.token_id())); + assert!(secret_re.is_match(&token.secret())); + assert!(token_re.is_match(&token.token())); + } + + #[test] + fn test_bootstrap_token_into_secret() { + let token = BootstrapToken::new(); + let secret: Secret = token.clone().into(); + let data = secret.string_data.unwrap(); + assert_eq!( + &secret.metadata.name, + &Some(format!("bootstrap-token-{}", token.token_id)) + ); + assert_eq!(data.get("token-id").unwrap(), &token.token_id); + assert_eq!(data.get("token-secret").unwrap(), &token.secret); + assert_eq!( + data.get("expiration").unwrap(), + &token.expiration.to_rfc3339() + ); + assert!(secret + .metadata + .labels + .unwrap() + .get("dynk8s.du5t1n.me/ec2-instance-id") + .is_none()); + } + + #[test] + fn test_bootstrap_token_into_secret_instance_id() { + let token = + BootstrapToken::new().instance_id("i-0a1b2c3d4e5f6f7f8".into()); + let secret: Secret = token.clone().into(); + let data = secret.string_data.unwrap(); + assert_eq!( + &secret.metadata.name, + &Some(format!("bootstrap-token-{}", token.token_id)) + ); + assert_eq!(data.get("token-id").unwrap(), &token.token_id); + assert_eq!(data.get("token-secret").unwrap(), &token.secret); + assert_eq!( + data.get("expiration").unwrap(), + &token.expiration.to_rfc3339() + ); + assert_eq!( + secret + .metadata + .labels + .unwrap() + .get("dynk8s.du5t1n.me/ec2-instance-id") + .unwrap(), + "i-0a1b2c3d4e5f6f7f8" + ); + } +} diff --git a/src/main.rs b/src/main.rs index 36b4db5..463e3cf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,6 @@ mod error; +mod events; +mod k8s; mod model; mod routes; mod sns; diff --git a/src/model/events.rs b/src/model/events.rs new file mode 100644 index 0000000..9113dd5 --- /dev/null +++ b/src/model/events.rs @@ -0,0 +1,46 @@ +//! Amazon EventBridge event types +//! +//! These data structures are sent by [Amazon EventBridge][0], encapsulated in +//! SNS notification messages. +//! +//! [0]: https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-what-is.html +use serde::{Deserialize, Serialize}; + +/// EC2 Instance State-change Notification +/// +/// EventBridge event emitted when an EC2 instance changes state +#[derive(Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct Ec2InstanceStateChange { + pub instance_id: String, + pub state: String, +} + +/// Enumeration of EventBridge detail objects +/// +/// EventBridge events sent by AWS services include a `detail` property, the +/// contents of which vary depending on the `detail-type` field. +#[derive(Deserialize, Serialize)] +#[serde(untagged)] +pub enum EventDetail { + Ec2InstanceStateChange(Ec2InstanceStateChange), +} + +/// EventBridge event +/// +/// See also: [Amazon EventBridge events][0] +/// +/// [0]: https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-events.html +#[derive(Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct Event { + pub version: String, + pub id: String, + pub detail_type: String, + pub source: String, + pub account: String, + pub time: String, + pub region: String, + pub resources: Vec, + pub detail: EventDetail, +} diff --git a/src/model/mod.rs b/src/model/mod.rs index 0f400a7..470c8dc 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -1,2 +1,3 @@ //! The dynk8s provisioner data model +pub mod events; pub mod sns; diff --git a/src/sns/mod.rs b/src/sns/mod.rs index f341775..1657681 100644 --- a/src/sns/mod.rs +++ b/src/sns/mod.rs @@ -8,6 +8,8 @@ use log::{debug, error, info}; use reqwest::Url; use serde::Serialize; +use crate::events; +use crate::model::events::*; use crate::model::sns::*; use error::SnsError; use sig::SignatureVerifier; @@ -38,11 +40,23 @@ pub async fn handle_unsubscribe( /// Handle an notification message /// -/// After verifying the message signature, the message contents are written to -/// a file for later inspection. +/// This function handles varions SNS notification messages based on their +/// contents/sub-type. pub async fn handle_notify(msg: NotificationMessage) -> Result<(), SnsError> { verify(&msg, &msg.signing_cert_url).await?; save_message(&msg.topic_arn, &msg.timestamp, &msg.message_id, &msg); + let event: Event = match serde_json::from_str(&msg.message) { + Ok(evt) => evt, + Err(e) => { + error!("Failed to deserialize notification message: {}", e); + return Ok(()); + } + }; + match event.detail { + EventDetail::Ec2InstanceStateChange(d) => { + events::on_ec2_instance_state_change(d).await; + } + }; Ok(()) }