├── .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 [](https://travis-ci.org/softprops/scuttlebutt) [](https://coveralls.io/github/softprops/scuttlebutt) [](LICENSE) [](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 |
--------------------------------------------------------------------------------