├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── rustfmt.toml └── src ├── lib.rs └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | *.rs.bk 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: rust 3 | matrix: 4 | fast_finish: true 5 | include: 6 | - rust: nightly 7 | - rust: beta 8 | - rust: stable 9 | script: 10 | - cargo clean -v 11 | - RUSTFLAGS="$RUSTFLAGS -C link-dead-code" cargo test 12 | cache: 13 | cargo: true 14 | apt: true 15 | directories: 16 | - target/debug/deps 17 | - target/debug/build 18 | addons: 19 | apt: 20 | packages: 21 | - libcurl4-openssl-dev 22 | - libelf-dev 23 | - libdw-dev 24 | - cmake 25 | - gcc 26 | - binutils-dev 27 | after_success: | 28 | [ $TRAVIS_RUST_VERSION = stable ] && 29 | [ $TRAVIS_BRANCH = master ] && 30 | [ $TRAVIS_PULL_REQUEST = false ] && 31 | curl -L https://github.com/SimonKagstrom/kcov/archive/master.tar.gz | tar xz && 32 | mkdir kcov-master/build && 33 | cd kcov-master/build && 34 | cmake .. && 35 | make && 36 | make install DESTDIR=../tmp && 37 | cd ../.. && 38 | ls target/debug && 39 | for file in target/debug/scuttlebutt-*; do mkdir -p "target/cov/$(basename $file)"; ./kcov-master/tmp/usr/local/bin/kcov --exclude-pattern=/.cargo,/usr/lib --verify "target/cov/$(basename $file)" "$file"; done && 40 | echo "covered" && 41 | cargo doc --no-deps && 42 | echo "" > target/doc/index.html && 43 | pip install --user ghp-import && 44 | /home/travis/.local/bin/ghp-import -n target/doc && 45 | git push -fq https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git gh-pages && 46 | echo "documented" 47 | env: 48 | global: 49 | secure: DzjCwZae6k/GyYLJ5x3wgDIjO8SjzkAq3eI8RvRT4aqSfVai2x3y4bPk5QoX+lt6TLaJxmoc1Vq6HZJbNlKokpGAvyzwJ0qmCBWYCUcRyTUyAVmH8VEY/wnBF0k8lYiQmfxOeGhiQb8nHZSpIIhItfIUirKMXyCW3ArkVV5HzQa53T1mO3unY9AEnDgyHfmwke0QM6MYdHAhNjfg45dvckU6Euu2oAXZ0tQpf0+uDPpU78Mg3dJ523/REuhKgRjreHt3S99R8rmnlKY9ZQeK0Zg4ZE4e+xxeC0H67CIhLWZiIwqZxauA7zB11UXI7GZ6fZoPsvZN3U3TlJwaAU/RHzGBczZVxGqgujwsbrne2WRYFtnFirs8D15uRp4ix4UIyKaUeqy5rUoZcLAzKIWDNLTJ5bQvyhDN641eF1auPURr0z7g+tQc8uEEKmuJXR+PplHPzvGOTz54dQa603XeEoERjL/59XqcbJFCoW3FqIxlLMosBOyQBfE1eGpkisFUdZ6YYODEFFOggF1JWF5psP9DUJ9CX852EdPpgIzojvmo9KqeeBiHw/I02rTRa6QgsIT0uea5HlV6eNo0le9AeT3fMYfVU/wzDJxH/2mt42SCvLSbbXl9Nam1KUtEvjc5cGhD4vjnX6G2hAiY/xPXuvmlZ8btwlaykeI//wdvq7o= 50 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.2.0 2 | 3 | * upgraded to hyper 0.10 and serde 0.9 4 | 5 | # 0.1.1 6 | 7 | * ObjectMeta deletion_timestamp is now an Option 8 | * EventSource host is now an Option 9 | 10 | # 0.1.0 11 | 12 | * initial release 13 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "scuttlebutt" 3 | version = "0.2.0" 4 | authors = ["softprops "] 5 | description = "A Rust interface for kubernetes events" 6 | documentation = "https://softprops.github.io/scuttlebutt" 7 | homepage = "https://github.com/softprops/scuttlebutt" 8 | repository = "https://github.com/softprops/scuttlebutt" 9 | keywords = ["kubernetes", "k8s", "hyper"] 10 | license = "MIT" 11 | 12 | [dependencies] 13 | log = "0.3" 14 | hyper = "0.10" 15 | serde = "0.9" 16 | serde_json = "0.9" 17 | serde_derive = "0.9" 18 | 19 | [[bin]] 20 | doc = false 21 | name = "scuttlebutt" 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-2017 Doug Tangren 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | docker run -it --rm \ 3 | -v $(PWD):/source \ 4 | jimmycuadra/rust \ 5 | cargo test 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scuttlebutt [![Build Status](https://travis-ci.org/softprops/scuttlebutt.svg?branch=master)](https://travis-ci.org/softprops/scuttlebutt) [![Coverage Status](https://coveralls.io/repos/github/softprops/scuttlebutt/badge.svg)](https://coveralls.io/github/softprops/scuttlebutt) [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE) [![crates.io](http://meritbadge.herokuapp.com/scuttlebutt)](https://crates.io/crates/scuttlebutt) 2 | 3 | 4 | > Listen in on all the gossip going on in your [kubernetes](http://kubernetes.io/) cluster 5 | 6 | [Documentation](https://softprops.github.io/scuttlebutt) 7 | 8 | ## install 9 | 10 | Add the following to your Cargo.toml file 11 | 12 | ```toml 13 | [dependencies] 14 | scuttlebutt = "0.2" 15 | ``` 16 | 17 | ## usage 18 | 19 | Central to scuttlebutt is a cluster. Clusters provide an interface for feeding off of kubernetes events 20 | via a [Receiver](https://doc.rust-lang.org/std/sync/mpsc/struct.Receiver.html). 21 | 22 | The default use case is to connect to a kubernetes cluster behind [kube-proxy](http://kubernetes.io/docs/admin/kube-proxy/). This interface may be extended to run 23 | outside a cluster with a set of [kubeconfig credentials](https://github.com/softprops/kubecfg) in the future. 24 | 25 | ```rust 26 | extern crate scuttlebutt; 27 | use scuttlebutt::{Cluster, Events}; 28 | 29 | fn main() { 30 | match Cluster::new().events() { 31 | Ok(events) => { 32 | for e in events.into_iter() { 33 | println!("{:#?}", e) 34 | } 35 | } 36 | Err(e) => println!("{:#?}", e), 37 | } 38 | } 39 | ``` 40 | 41 | Doug Tangren (softprops) 2016-2017 42 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | reorder_imports = true 2 | fn_args_layout = "Block" 3 | array_layout = "Block" 4 | where_style = "Rfc" 5 | generics_indent = "Block" 6 | fn_call_style = "Block" 7 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Scuttlebutt 2 | //! 3 | //! Scuttlebutt is an interface for extending kubernetes by feeding off a stream of kubernetes 4 | //! cluster events 5 | 6 | #[macro_use] 7 | extern crate log; 8 | extern crate hyper; 9 | extern crate serde_json; 10 | #[macro_use] 11 | extern crate serde_derive; 12 | 13 | use hyper::{Client, Error as HttpError, Url}; 14 | use std::env; 15 | use std::io::{self, Read}; 16 | use std::sync::mpsc::{channel, Receiver}; 17 | use std::thread; 18 | 19 | // Kubernets cluster event 20 | #[derive(Serialize, Deserialize, Debug)] 21 | pub struct Event { 22 | pub object: Object, 23 | #[serde(rename = "type")] 24 | pub event_type: String, 25 | } 26 | 27 | /// A description of the event 28 | #[derive(Serialize, Deserialize, Debug)] 29 | pub struct Object { 30 | /// APIVersion defines the versioned schema of this representation of an object. 31 | /// Servers should convert recognized schemas to the latest internal value, 32 | /// and may reject unrecognized values. More info: 33 | /// http://releases.k8s.io/release-1.3/docs/devel/api-conventions.md#resources 34 | #[serde(rename = "apiVersion")] 35 | pub api_version: String, 36 | /// The number of times this event has occurred. 37 | pub count: usize, 38 | /// The time at which the event was first recorded. (Time of server receipt is in TypeMeta.) 39 | #[serde(rename = "firstTimestamp")] 40 | pub first_timestamp: String, 41 | /// The time at which the most recent occurrence of this event was recorded. 42 | #[serde(rename = "lastTimestamp")] 43 | pub last_timestamp: String, 44 | /// The object that this event is about. 45 | #[serde(rename = "involvedObject")] 46 | pub involved_object: ObjectReference, 47 | /// Kind is a string value representing the REST resource this object represents. 48 | /// Servers may infer this from the endpoint the client submits requests to. 49 | /// Cannot be updated. In CamelCase. More info: 50 | /// http://releases.k8s.io/release-1.3/docs/devel/api-conventions.md#types-kinds 51 | pub kind: String, 52 | /// A human-readable description of the status of this operation. 53 | pub message: String, 54 | /// Standard object’s metadata. More info: 55 | /// http://releases.k8s.io/release-1.3/docs/devel/api-conventions.md#metadata 56 | pub metadata: ObjectMeta, 57 | /// This should be a short, machine understandable string that gives the reason for the 58 | /// transition into the object’s current status. 59 | pub reason: String, 60 | /// The component reporting this event. Should be a short machine understandable string. 61 | pub source: EventSource, 62 | /// Type of this event (Normal, Warning), new types could be added in the future 63 | #[serde(rename = "type")] 64 | pub object_type: String, 65 | } 66 | 67 | /// ObjectMeta is metadata that all persisted resources must have, which includes all 68 | /// objects users must create. 69 | #[derive(Serialize, Deserialize, Debug)] 70 | pub struct ObjectMeta { 71 | /// CreationTimestamp is a timestamp representing the server time when this object was 72 | // created. It is not guaranteed to be set in happens-before order across separate operations. 73 | // Clients may not set this value. It is represented in RFC3339 form and is in UTC. 74 | /// Populated by the system. Read-only. Null for lists. More info: 75 | /// http://releases.k8s.io/release-1.3/docs/devel/api-conventions.md#metadata 76 | #[serde(rename = "creationTimestamp")] 77 | pub creation_timestamp: String, 78 | /// DeletionTimestamp is RFC 3339 date and time at which this resource will be deleted. 79 | /// This field is set by the server when a graceful deletion is requested by the user, 80 | // and is not directly settable by a client. The resource will be deleted (no longer visible 81 | // from resource lists, and not reachable by name) after the time in this field. Once set, 82 | /// this value may not be unset or be set further into the future, although it may be shortened 83 | /// or the resource may be deleted prior to this time. For example, a user may request that a 84 | /// pod is deleted in 30 seconds. The Kubelet will react by sending a graceful termination 85 | /// signal to the containers in the pod. Once the resource is deleted in the API, the Kubelet 86 | /// will send a hard termination signal to the container. If not set, graceful deletion of 87 | /// the object has not been requested. 88 | /// Populated by the system when a graceful deletion is requested. Read-only. More info: 89 | /// http://releases.k8s.io/release-1.3/docs/devel/api-conventions.md#metadata 90 | #[serde(rename = "deletionTimestamp")] 91 | pub deletion_timestamp: Option, 92 | /// Name must be unique within a namespace. Is required when creating resources, although 93 | /// some resources may allow a client to request the generation of an appropriate name 94 | /// automatically. Name is primarily intended for creation idempotence and configuration 95 | /// definition. Cannot be updated. More info: 96 | /// http://releases.k8s.io/release-1.3/docs/user-guide/identifiers.md#names 97 | pub name: String, 98 | /// Namespace defines the space within each name must be unique. An empty namespace is 99 | /// equivalent to the "default" namespace, but "default" is the canonical representation. 100 | /// Not all objects are required to be scoped to a namespace - the value of this field for 101 | /// those objects will be empty. 102 | /// Must be a DNS_LABEL. Cannot be updated. More info: 103 | /// http://releases.k8s.io/release-1.3/docs/user-guide/namespaces.md 104 | pub namespace: String, 105 | /// An opaque value that represents the internal version of this object that can be used 106 | /// by clients to determine when objects have changed. May be used for optimistic concurrency, 107 | /// change detection, and the watch operation on a resource or set of resources. 108 | /// Clients must treat these values as opaque and passed unmodified back to the server. 109 | /// They may only be valid for a particular resource or set of resources. 110 | /// Populated by the system. Read-only. Value must be treated as opaque by clients 111 | #[serde(rename = "resourceVersion")] 112 | pub resource_version: String, 113 | /// SelfLink is a URL representing this object. Populated by the system. Read-only. 114 | #[serde(rename = "selfLink")] 115 | pub self_link: String, 116 | /// UID is the unique in time and space value for this object. It is typically generated by 117 | /// the server on successful creation of a resource and is not allowed to change on PUT 118 | /// operations. 119 | /// Populated by the system. Read-only. More info: 120 | /// http://releases.k8s.io/release-1.3/docs/user-guide/identifiers.md#uids 121 | pub uid: String, 122 | } 123 | 124 | /// EventSource contains information for an event. 125 | #[derive(Serialize, Deserialize, Debug)] 126 | pub struct EventSource { 127 | /// Component from which the event is generated. 128 | pub component: String, 129 | /// Host name on which the event is generated. 130 | pub host: Option, 131 | } 132 | 133 | /// ObjectReference contains enough information to let you inspect or modify the referred object. 134 | #[derive(Serialize, Deserialize, Debug)] 135 | pub struct ObjectReference { 136 | /// API version of the referent. 137 | #[serde(rename = "apiVersion")] 138 | pub api_version: String, 139 | /// Specific resourceVersion to which this reference is made, if any. 140 | #[serde(rename = "resourceVersion")] 141 | pub resource_version: String, 142 | /// UID of the referent. More info: 143 | /// http://releases.k8s.io/release-1.3/docs/user-guide/identifiers.md#uids 144 | pub uid: String, 145 | /// If referring to a piece of an object instead of an entire object, 146 | /// this string should contain a valid JSON/Go field access statement, 147 | /// such as desiredState.manifest.containers[2]. For example, if the object reference 148 | /// is to a container within a pod, this would take on a value like: "spec.containers{name}" 149 | /// (where "name" refers to the name of the container that triggered the event) or if no 150 | /// container name is specified "spec.containers[2]" (container with index 2 in this pod). 151 | /// This syntax is chosen only to have some well-defined way of referencing a part of an 152 | /// object. 153 | #[serde(rename = "fieldPath")] 154 | pub field_path: Option, 155 | /// Kind of the referent. More info: 156 | /// http://releases.k8s.io/release-1.3/docs/devel/api-conventions.md#types-kinds 157 | pub kind: String, 158 | /// Name of the referent. More info: 159 | /// http://releases.k8s.io/release-1.3/docs/user-guide/identifiers.md#names 160 | pub name: String, 161 | /// Namespace of the referent. More info: 162 | /// http://releases.k8s.io/release-1.3/docs/user-guide/namespaces.md 163 | pub namespace: String, 164 | } 165 | 166 | const DEFAULT_HOST: &'static str = "http://localhost:8001"; 167 | 168 | pub type Result = std::result::Result; 169 | 170 | /// An enumeratation of potential errors 171 | #[derive(Debug)] 172 | pub enum Error { 173 | Transport(HttpError), 174 | } 175 | 176 | impl From for Error { 177 | fn from(error: HttpError) -> Error { 178 | Error::Transport(error) 179 | } 180 | } 181 | 182 | /// A cluster contains an address 183 | /// for interacting with a kubernetes Cluster 184 | /// of nodes 185 | pub struct Cluster { 186 | host: Url, 187 | } 188 | 189 | /// Events provides a means for generating 190 | /// a receiver for events 191 | pub trait Events { 192 | fn events(&mut self) -> Result>; 193 | 194 | fn generator(&self, bytes: Bytes) -> Result> 195 | where 196 | Bytes: 'static + Iterator>, 197 | Bytes: Send, 198 | { 199 | let (tx, rx) = channel(); 200 | let stream = serde_json::Deserializer::from_iter(bytes).into_iter::(); 201 | thread::spawn( 202 | move || for e in stream { 203 | match e { 204 | Ok(event) => { 205 | if let Err(e) = tx.send(event) { 206 | debug!("{:#?}", e); 207 | break; 208 | } 209 | } 210 | Err(e) => { 211 | debug!("{:#?}", e); 212 | break; 213 | } 214 | } 215 | }, 216 | ); 217 | Ok(rx) 218 | } 219 | } 220 | 221 | impl Cluster { 222 | pub fn new() -> Cluster { 223 | let kubernetes_api_host: &str = &env::var("KUBERNETES_API_HOST").unwrap_or(DEFAULT_HOST.to_string()); 224 | Cluster { host: Url::parse(kubernetes_api_host).unwrap() } 225 | } 226 | } 227 | 228 | impl Events for Cluster { 229 | fn events(&mut self) -> Result> { 230 | let res = try!( 231 | Client::new() 232 | .get(self.host.join("/api/v1/events?watch=true").unwrap()) 233 | .send() 234 | ); 235 | self.generator(res.bytes()) 236 | } 237 | } 238 | 239 | #[cfg(test)] 240 | mod tests { 241 | use super::*; 242 | use std::sync::mpsc::Receiver; 243 | #[test] 244 | fn events_generator() { 245 | impl Events for &'static str { 246 | fn events(&mut self) -> Result> { 247 | self.generator(self.bytes().into_iter().map(|b| Ok(b))) 248 | } 249 | } 250 | let events = r#"{ 251 | "object":{ 252 | "apiVersion": "1", 253 | "count": 1, 254 | "firstTimestamp": "...", 255 | "lastTimestamp": "...", 256 | "kind":"Event", 257 | "message":"test", 258 | "involvedObject": { 259 | "apiVersion": "1", 260 | "resourceVersion": "2", 261 | "uid":"2", 262 | "kind": "POD", 263 | "name": "test_name", 264 | "namespace": "test_namespace" 265 | }, 266 | "metadata": { 267 | "creationTimestamp": "...", 268 | "deletionTimestamp": "...", 269 | "name": "test", 270 | "namespace":"default", 271 | "resourceVersion": "1", 272 | "selfLink": "...", 273 | "uid": "1" 274 | }, 275 | "reason": "started", 276 | "source": { 277 | "component": "test", 278 | "host": "foo.com" 279 | }, 280 | "type": "Normal" 281 | }, 282 | "type":"ADDED" 283 | }"# 284 | .events(); 285 | assert!( 286 | events 287 | .unwrap() 288 | .into_iter() 289 | .map(|e| e.object.involved_object.namespace) 290 | .nth(0) == Some("test_namespace".to_owned()) 291 | ) 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate scuttlebutt; 2 | use scuttlebutt::{Cluster, Events}; 3 | 4 | fn main() { 5 | match Cluster::new().events() { 6 | Ok(events) => { 7 | for e in events.into_iter() { 8 | println!("{:#?}", e) 9 | } 10 | } 11 | Err(e) => println!("{:#?}", e), 12 | } 13 | } 14 | --------------------------------------------------------------------------------