Begin EC2 instance state event handler

The lifecycle of ephemeral Kubernetes worker nodes is driven by events
emitted by Amazon EventBridge and delivered via Amazon Simple
Notification Service.  These events trigger the *dynk8s* provisioner to
take the appropriate action based on the state of an EC2 instance.

In order to add a node to the cluster using `kubeadm`, a "bootstrap
token" needs to be created.  When manually adding a node, this would be
done e.g. using `kubeadm token create`.  Since bootstrap tokens are just
a special type of Secret, they can be easily created programmatically as
well.  When a new EC2 instance enters the "running" state, the
provisioner creates a new bootstrap token and associates it with the
instance by storing the instance ID in a label in the Secret resource's
metadata.

The initial implementation of the event handler is rather naïve.  It
generates a token for every instance, though some instances may not be
intended to be used as Kubernetes workers.  Ideally, the provisioner
would only allocate tokens for instances matching some configurable
criteria, such as AWS tags.  Further, a token is allocated every time
the instance enters the running state, even if a token already exists or
is not needed.
master
Dustin 2022-10-01 10:34:03 -05:00
parent 8e1165eb95
commit 25d7be004c
8 changed files with 766 additions and 9 deletions

428
Cargo.lock generated
View File

@ -37,6 +37,24 @@ dependencies = [
"subtle", "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]] [[package]]
name = "ansi_term" name = "ansi_term"
version = "0.12.1" version = "0.12.1"
@ -59,7 +77,7 @@ dependencies = [
"num-traits", "num-traits",
"rusticata-macros", "rusticata-macros",
"thiserror", "thiserror",
"time", "time 0.3.14",
] ]
[[package]] [[package]]
@ -206,6 +224,22 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 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]] [[package]]
name = "cipher" name = "cipher"
version = "0.3.0" version = "0.3.0"
@ -235,7 +269,7 @@ dependencies = [
"rand", "rand",
"sha2", "sha2",
"subtle", "subtle",
"time", "time 0.3.14",
"version_check", "version_check",
] ]
@ -368,6 +402,27 @@ dependencies = [
"subtle", "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]] [[package]]
name = "displaydoc" name = "displaydoc"
version = "0.2.3" version = "0.2.3"
@ -384,7 +439,12 @@ name = "dynk8s-provisioner"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"base64", "base64",
"chrono",
"k8s-openapi",
"kube",
"log", "log",
"rand",
"regex",
"reqwest", "reqwest",
"rocket", "rocket",
"rsa", "rsa",
@ -471,6 +531,7 @@ checksum = "7f21eda599937fba36daeb58a22e8f5cee2d14c4a17b5b7739c7c8e5e3b8230c"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-core", "futures-core",
"futures-executor",
"futures-io", "futures-io",
"futures-sink", "futures-sink",
"futures-task", "futures-task",
@ -493,12 +554,34 @@ version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf" 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]] [[package]]
name = "futures-io" name = "futures-io"
version = "0.3.24" version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68" 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]] [[package]]
name = "futures-sink" name = "futures-sink"
version = "0.3.24" version = "0.3.24"
@ -520,6 +603,7 @@ dependencies = [
"futures-channel", "futures-channel",
"futures-core", "futures-core",
"futures-io", "futures-io",
"futures-macro",
"futures-sink", "futures-sink",
"futures-task", "futures-task",
"memchr", "memchr",
@ -559,7 +643,7 @@ checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"wasi", "wasi 0.11.0+wasi-snapshot-preview1",
] ]
[[package]] [[package]]
@ -652,6 +736,12 @@ dependencies = [
"pin-project-lite", "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]] [[package]]
name = "httparse" name = "httparse"
version = "1.8.0" version = "1.8.0"
@ -688,6 +778,36 @@ dependencies = [
"want", "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]] [[package]]
name = "hyper-tls" name = "hyper-tls"
version = "0.5.0" version = "0.5.0"
@ -701,6 +821,19 @@ dependencies = [
"tokio-native-tls", "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]] [[package]]
name = "idna" name = "idna"
version = "0.2.3" version = "0.2.3"
@ -759,6 +892,96 @@ dependencies = [
"wasm-bindgen", "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]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.4.0" version = "1.4.0"
@ -780,6 +1003,21 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "292a948cd991e376cf75541fe5b97a1081d713c618b4f1b9500f8844e49eb565" 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]] [[package]]
name = "lock_api" name = "lock_api"
version = "0.4.8" version = "0.4.8"
@ -855,7 +1093,7 @@ checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf"
dependencies = [ dependencies = [
"libc", "libc",
"log", "log",
"wasi", "wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys", "windows-sys",
] ]
@ -1051,6 +1289,15 @@ dependencies = [
"vcpkg", "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]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.1" version = "0.12.1"
@ -1097,6 +1344,15 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "pem"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03c64931a1a212348ec4f3b4362585eca7159d0d09cbdf4a7f74f02173596fd4"
dependencies = [
"base64",
]
[[package]] [[package]]
name = "pem-rfc7468" name = "pem-rfc7468"
version = "0.3.1" version = "0.3.1"
@ -1112,6 +1368,26 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" 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]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.9" version = "0.2.9"
@ -1240,6 +1516,17 @@ dependencies = [
"bitflags", "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]] [[package]]
name = "ref-cast" name = "ref-cast"
version = "1.0.9" version = "1.0.9"
@ -1266,6 +1553,8 @@ version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b"
dependencies = [ dependencies = [
"aho-corasick",
"memchr",
"regex-syntax", "regex-syntax",
] ]
@ -1360,7 +1649,7 @@ dependencies = [
"serde_json", "serde_json",
"state", "state",
"tempfile", "tempfile",
"time", "time 0.3.14",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"tokio-util", "tokio-util",
@ -1407,7 +1696,7 @@ dependencies = [
"smallvec", "smallvec",
"stable-pattern", "stable-pattern",
"state", "state",
"time", "time 0.3.14",
"tokio", "tokio",
"uncased", "uncased",
] ]
@ -1475,6 +1764,16 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 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]] [[package]]
name = "security-framework" name = "security-framework"
version = "2.7.0" version = "2.7.0"
@ -1507,6 +1806,16 @@ dependencies = [
"serde_derive", "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]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.144" version = "1.0.144"
@ -1524,6 +1833,7 @@ version = "1.0.85"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44"
dependencies = [ dependencies = [
"indexmap",
"itoa", "itoa",
"ryu", "ryu",
"serde", "serde",
@ -1541,6 +1851,18 @@ dependencies = [
"serde", "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]] [[package]]
name = "sha1" name = "sha1"
version = "0.10.2" version = "0.10.2"
@ -1718,6 +2040,17 @@ dependencies = [
"once_cell", "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]] [[package]]
name = "time" name = "time"
version = "0.3.14" version = "0.3.14"
@ -1771,6 +2104,16 @@ dependencies = [
"winapi", "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]] [[package]]
name = "tokio-macros" name = "tokio-macros"
version = "1.8.0" version = "1.8.0"
@ -1792,6 +2135,18 @@ dependencies = [
"tokio", "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]] [[package]]
name = "tokio-stream" name = "tokio-stream"
version = "0.1.9" version = "0.1.9"
@ -1826,6 +2181,49 @@ dependencies = [
"serde", "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]] [[package]]
name = "tower-service" name = "tower-service"
version = "0.3.2" version = "0.3.2"
@ -1839,6 +2237,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307" checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"log",
"pin-project-lite", "pin-project-lite",
"tracing-attributes", "tracing-attributes",
"tracing-core", "tracing-core",
@ -2002,6 +2401,12 @@ dependencies = [
"try-lock", "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]] [[package]]
name = "wasi" name = "wasi"
version = "0.11.0+wasi-snapshot-preview1" version = "0.11.0+wasi-snapshot-preview1"
@ -2216,7 +2621,16 @@ dependencies = [
"oid-registry", "oid-registry",
"rusticata-macros", "rusticata-macros",
"thiserror", "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]] [[package]]

View File

@ -5,7 +5,11 @@ edition = "2021"
[dependencies] [dependencies]
base64 = "0.13.0" 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" log = "0.4.17"
rand = "0.8.5"
reqwest = "0.11.11" reqwest = "0.11.11"
rocket = { version = "0.5.0-rc.2", features = ["json"] } rocket = { version = "0.5.0-rc.2", features = ["json"] }
rsa = "0.6.1" rsa = "0.6.1"
@ -13,3 +17,6 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.85" serde_json = "1.0.85"
sha1 = "0.10.2" sha1 = "0.10.2"
x509-parser = "0.14.0" x509-parser = "0.14.0"
[dev-dependencies]
regex = "1.6.0"

29
src/events.rs Normal file
View File

@ -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
)
};
}
}

244
src/k8s.rs Normal file
View File

@ -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<Utc>,
/// The EC2 instance ID to which the token is assigned
instance_id: Option<String>,
}
#[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<Utc> {
&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<Utc>) -> 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<Secret> 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::<String, String>::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::<String, String>::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<I: AsRef<str>>(
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<Secret> = 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"
);
}
}

View File

@ -1,4 +1,6 @@
mod error; mod error;
mod events;
mod k8s;
mod model; mod model;
mod routes; mod routes;
mod sns; mod sns;

46
src/model/events.rs Normal file
View File

@ -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<String>,
pub detail: EventDetail,
}

View File

@ -1,2 +1,3 @@
//! The dynk8s provisioner data model //! The dynk8s provisioner data model
pub mod events;
pub mod sns; pub mod sns;

View File

@ -8,6 +8,8 @@ use log::{debug, error, info};
use reqwest::Url; use reqwest::Url;
use serde::Serialize; use serde::Serialize;
use crate::events;
use crate::model::events::*;
use crate::model::sns::*; use crate::model::sns::*;
use error::SnsError; use error::SnsError;
use sig::SignatureVerifier; use sig::SignatureVerifier;
@ -38,11 +40,23 @@ pub async fn handle_unsubscribe(
/// Handle an notification message /// Handle an notification message
/// ///
/// After verifying the message signature, the message contents are written to /// This function handles varions SNS notification messages based on their
/// a file for later inspection. /// contents/sub-type.
pub async fn handle_notify(msg: NotificationMessage) -> Result<(), SnsError> { pub async fn handle_notify(msg: NotificationMessage) -> Result<(), SnsError> {
verify(&msg, &msg.signing_cert_url).await?; verify(&msg, &msg.signing_cert_url).await?;
save_message(&msg.topic_arn, &msg.timestamp, &msg.message_id, &msg); 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(()) Ok(())
} }