├── .gitignore ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── cache_access.go ├── cache_impl.go ├── container.go ├── daemonset.go ├── deployment.go ├── elf_build.sh ├── get_test.go ├── gobuild.sh ├── k8sdeploy.yaml ├── kubeaccess.go ├── kubeiql.go ├── label.go ├── metadata.go ├── owner.go ├── pod.go ├── replicaset.go ├── resource.go ├── server.go ├── service.go ├── statefulset.go ├── testdata ├── daemonset.json ├── deployment.json ├── pod1.json ├── pod2.json ├── pod3.json ├── replicaset.json ├── service.json └── statefulset.json ├── testing.go ├── util.go ├── vendor └── vendor.json ├── volume.go └── watchers.go /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | coverage.* 3 | vendor/github.com/* 4 | vendor/golang.org/* 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribute to Kubeiql 2 | Kubeiql is maintained by Yipee.io, a CA, Inc. Acclerator project. External contributions are welcome and much appreciated. Just follow these easy steps to contribute. 3 | 4 | ## Code Standard and Guideline 5 | For consistency, we ask that you adhere to some basic code guidelines when contributing to the Kubeiql. Please run [gofmt](https://golang.org/cmd/gofmt/) on your code before submitting a pull request. 6 | 7 | ## Our Development Process 8 | 9 | ### Pull Requests 10 | We welcome and encourage pull requests. When we get a pull request, it is peer reviewed and sent to QA for integration and smoke testing. Next, a core team member signs off and performs the pull request merge. We'll provide updates and feedback throughout the process to keep you informed. 11 | 12 | Follow these steps for pull requests: 13 | 14 | 1. Fork the repo and create your branch from `master`. 15 | 1. For any new code, add unit tests. 16 | 1. Verify that the test suite passes. 17 | 1. Verify that your code follows the Code Standard Guideline 18 | 1. If you haven't already, complete the [Contributor License Agreement ("CLA")][cla]. 19 | 20 | ### Contributor License Agreement ("CLA") 21 | To get started, sign the Contributor License Agreement. 22 | 23 | ## Bugs 24 | We work hard to avoid them, but they still happen. We use GitHub issues to track bugs and ongoing work for Kubeiql. 25 | 26 | ### Known Issues 27 | We also use GitHub issues for updates to known issues, including alerts when fixes are in progress. 28 | 29 | ### Reporting New Issues 30 | Before filing a new issue, check Known Issues to see if your problem already exists. When reporting a new issue, provide as much detail as possible. The more information, the easier it is to debug and the faster you'll get a fix. 31 | 32 | **Tips:** 33 | 34 | * A description. What did you expect to happen? What actually happened? Why do you think the behavior was incorrect? 35 | * What Kubernetes version are you running? Have you seen the same issue/behavior on other versions too? 36 | * Anything else that seems relevant. 37 | 38 | ## License 39 | By contributing to kubeiql, you agree that your contributions will be licensed under its [license][license]. 40 | 41 | [cla]: https://www.clahub.com/agreements/yipeeio/kubeiql 42 | [license]: /LICENSE 43 | 44 | --- 45 | 46 | 47 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.8 2 | USER 496 3 | ADD --chown=496:496 ./kubeiql.elf /usr/local/bin/kubeiql 4 | #ADD --chown=496:496 https://storage.googleapis.com/kubernetes-release/release/${KUBECTL_VSN:-v1.11.2}/bin/linux/amd64/kubectl /usr/local/bin/kubectl 5 | #RUN chmod 777 /usr/local/bin/kubectl 6 | EXPOSE 8128 7 | 8 | CMD ["/usr/local/bin/kubeiql"] 9 | 10 | 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Repository Status 2 | **This repository exists here for historical purposes. The active repo is now hosted at: https://github.com/yipeeio/kubeiql** 3 | # Kubeiql 4 | A GraphQL interface for Kubernetes. 5 | 6 | The goal of this project is to provide an alternative GraphQL 7 | interface to a Kubernetes cluster. It is not intended to entirely replace the 8 | ReST APIs as some of them (particularly the watch APIs) don't map well 9 | onto GraphQL. 10 | 11 | Yipee.io is a CA, Inc. Accelerator project. 12 | 13 | ## Current Status 14 | 15 | pre-alpha 16 | 17 | * Queries are currently supported against Pods, Deployments, 18 | ReplicaSets, StatefulSets, and DaemonSets. 19 | * No mutations are yet implemented. 20 | * The retrieval of data from the cluster is accomplished via watchers 21 | on the kubernetes API. By default, we expect to access the API at 22 | localhost port 8080 (run "kubectl proxy --port=8080") 23 | * to access a kubernetes API without using the proxy, you can set 24 | environment variables: 25 | * API_HOST: host/port of API, e.g., https://kubernetes.default.svc 26 | from inside a cluster 27 | * API_SECRET_PATH: directory containing files 'ca.crt' and 28 | 'token', e.g. /var/run/secrets/kubernetes.io/serviceaccount from 29 | a pod inside a cluster 30 | * Tests are lacking 31 | * See https://hub.docker.com/r/yipeeio/kubeiql/ for a docker image 32 | 33 | 34 | ## Getting Started 35 | To experiment with the API: 36 | 37 | 1. Download the code 38 | 2. Type sh gobuild.sh 39 | 3. Start a proxy for the kubernetes API on port 8080 (e.g., kubectl 40 | proxy --port=8080) 41 | 4. Run ./kubeiql 42 | 43 | The server runs at port 8128. You can use curl to play with it as 44 | shown in the examples below via the /query endpoint, or point your 45 | browser at 'localhost:8128/' and experiment with the GraphiQL tool 46 | (much more user-friendly). 47 | 48 | ## Build an image 49 | If you're running on a non-linux machine, use the _elf-build.sh_ 50 | script to build an image suitable for use in a docker container. Then 51 | just build in the usual way: 52 | ``` 53 | docker build -t your-image-name . 54 | ``` 55 | ## Running inside a Kubernetes cluster 56 | To run kubeiql inside a cluster, simply apply the _k8sdeploy.yaml_ 57 | file: 58 | ``` 59 | kubectl apply -f k8sdeploy.yaml 60 | ``` 61 | 62 | ## Examples 63 | 64 | The query: 65 | 66 | ``` json 67 | { 68 | daemonSetByName(namespace: "kube-system", name: "kube-proxy") { 69 | metadata {name namespace labels {name value}} 70 | } 71 | } 72 | ``` 73 | 74 | 75 | curl -X POST -H"Content-Type: application/json" 76 | http://localhost:8128/query -d 77 | '{ "query": "{daemonSetByName(namespace: \"kube-system\", name: \"kube-proxy\") { metadata {name namespace labels {name value}} pods {metadata {name}}}}"}' 78 | 79 | 80 | 81 | returns: 82 | 83 | ```json 84 | { 85 | "data": { 86 | "daemonSetByName": { 87 | "metadata": { 88 | "name": "kube-proxy", 89 | "namespace": "kube-system", 90 | "labels": [ 91 | { 92 | "name": "k8s-app", 93 | "value": "kube-proxy" 94 | } 95 | ] 96 | }, 97 | "pods": [ 98 | { 99 | "metadata": { 100 | "name": "kube-proxy-7vhx5" 101 | } 102 | } 103 | ] 104 | } 105 | } 106 | } 107 | 108 | ``` 109 | 110 | and the query: 111 | 112 | ``` json 113 | { 114 | allPods() { 115 | owner {kind metadata {name}} 116 | rootOwner { kind metadata { name namespace } 117 | ... on StatefulSet { 118 | metadata { name } 119 | } 120 | ... on Deployment { 121 | replicaSets { 122 | metadata { name } 123 | pods { metadata { name } } } 124 | } 125 | } 126 | } 127 | } 128 | ``` 129 | 130 | 131 | curl -X POST -H"Content-Type: application/json" 132 | http://localhost:8128/query -d '{"query": "{allPods() {owner {kind 133 | metadata {name}} rootOwner { kind metadata { name namespace } ... on 134 | StatefulSet { metadata { name } } ... on Deployment { replicaSets { 135 | metadata { name } pods { metadata { name } } } } } } }" }' 136 | 137 | 138 | 139 | returns: 140 | 141 | ```json 142 | { 143 | "data": { 144 | "allPods": [ 145 | { 146 | "owner": { 147 | "kind": "ReplicaSet", 148 | "metadata": { 149 | "name": "backend-549447ccf" 150 | } 151 | }, 152 | "rootOwner": { 153 | "kind": "Deployment", 154 | "metadata": { 155 | "name": "backend", 156 | "namespace": "default" 157 | }, 158 | "replicaSets": [ 159 | { 160 | "metadata": { 161 | "name": "backend-549447ccf" 162 | }, 163 | "pods": [ 164 | { 165 | "metadata": { 166 | "name": "backend-549447ccf-4zphf" 167 | } 168 | }, 169 | { 170 | "metadata": { 171 | "name": "clunky-sabertooth-joomla-5d4ddc985d-fpddz" 172 | } 173 | }, 174 | { 175 | "metadata": { 176 | "name": "clunky-sabertooth-mariadb-0" 177 | } 178 | } 179 | // ... 180 | ] 181 | } 182 | ] 183 | } 184 | }, 185 | { 186 | "owner": { 187 | "kind": "ReplicaSet", 188 | "metadata": { 189 | "name": "clunky-sabertooth-joomla-5d4ddc985d" 190 | } 191 | }, 192 | "rootOwner": { 193 | "kind": "Deployment", 194 | "metadata": { 195 | "name": "clunky-sabertooth-joomla", 196 | "namespace": "default" 197 | }, 198 | "replicaSets": [ 199 | { 200 | "metadata": { 201 | "name": "backend-549447ccf" 202 | }, 203 | "pods": [ 204 | { 205 | "metadata": { 206 | "name": "backend-549447ccf-4zphf" 207 | } 208 | }, 209 | { 210 | "metadata": { 211 | "name": "clunky-sabertooth-joomla-5d4ddc985d-fpddz" 212 | } 213 | } 214 | //... 215 | ] 216 | } 217 | ] 218 | } 219 | }, 220 | { 221 | "owner": { 222 | "kind": "StatefulSet", 223 | "metadata": { 224 | "name": "clunky-sabertooth-mariadb" 225 | } 226 | }, 227 | "rootOwner": { 228 | "kind": "StatefulSet", 229 | "metadata": { 230 | "name": "clunky-sabertooth-mariadb", 231 | "namespace": "default" 232 | } 233 | } 234 | }, 235 | { 236 | "owner": { 237 | "kind": "ReplicaSet", 238 | "metadata": { 239 | "name": "ui-9c6c8d79" 240 | } 241 | }, 242 | "rootOwner": { 243 | "kind": "Deployment", 244 | "metadata": { 245 | "name": "ui", 246 | "namespace": "default" 247 | }, 248 | "replicaSets": [ 249 | { 250 | "metadata": { 251 | "name": "backend-549447ccf" 252 | }, 253 | "pods": [ 254 | { 255 | "metadata": { 256 | "name": "backend-549447ccf-4zphf" 257 | } 258 | }, 259 | { 260 | "metadata": { 261 | "name": "clunky-sabertooth-joomla-5d4ddc985d-fpddz" 262 | } 263 | } 264 | // ... 265 | ] 266 | } 267 | ] 268 | } 269 | }, 270 | { 271 | "owner": { 272 | "kind": "ReplicaSet", 273 | "metadata": { 274 | "name": "ui-9c6c8d79" 275 | } 276 | }, 277 | "rootOwner": { 278 | "kind": "Deployment", 279 | "metadata": { 280 | "name": "ui", 281 | "namespace": "default" 282 | }, 283 | "replicaSets": [ 284 | { 285 | "metadata": { 286 | "name": "backend-549447ccf" 287 | }, 288 | "pods": [ 289 | { 290 | "metadata": { 291 | "name": "backend-549447ccf-4zphf" 292 | } 293 | }, 294 | { 295 | "metadata": { 296 | "name": "clunky-sabertooth-joomla-5d4ddc985d-fpddz" 297 | } 298 | } 299 | // ... 300 | ] 301 | } 302 | ] 303 | } 304 | }, 305 | { 306 | "owner": { 307 | "kind": "Pod", 308 | "metadata": { 309 | "name": "etcd-minikube" 310 | } 311 | }, 312 | "rootOwner": { 313 | "kind": "Pod", 314 | "metadata": { 315 | "name": "etcd-minikube", 316 | "namespace": "kube-system" 317 | } 318 | } 319 | }, 320 | { 321 | "owner": { 322 | "kind": "Pod", 323 | "metadata": { 324 | "name": "kube-addon-manager-minikube" 325 | } 326 | }, 327 | "rootOwner": { 328 | "kind": "Pod", 329 | "metadata": { 330 | "name": "kube-addon-manager-minikube", 331 | "namespace": "kube-system" 332 | } 333 | } 334 | }, 335 | { 336 | "owner": { 337 | "kind": "Pod", 338 | "metadata": { 339 | "name": "kube-apiserver-minikube" 340 | } 341 | }, 342 | "rootOwner": { 343 | "kind": "Pod", 344 | "metadata": { 345 | "name": "kube-apiserver-minikube", 346 | "namespace": "kube-system" 347 | } 348 | } 349 | }, 350 | { 351 | "owner": { 352 | "kind": "Pod", 353 | "metadata": { 354 | "name": "kube-controller-manager-minikube" 355 | } 356 | }, 357 | "rootOwner": { 358 | "kind": "Pod", 359 | "metadata": { 360 | "name": "kube-controller-manager-minikube", 361 | "namespace": "kube-system" 362 | } 363 | } 364 | }, 365 | { 366 | "owner": { 367 | "kind": "ReplicaSet", 368 | "metadata": { 369 | "name": "kube-dns-86f4d74b45" 370 | } 371 | }, 372 | "rootOwner": { 373 | "kind": "Deployment", 374 | "metadata": { 375 | "name": "kube-dns", 376 | "namespace": "kube-system" 377 | }, 378 | "replicaSets": [ 379 | { 380 | "metadata": { 381 | "name": "backend-549447ccf" 382 | }, 383 | "pods": [ 384 | { 385 | "metadata": { 386 | "name": "backend-549447ccf-4zphf" 387 | } 388 | }, 389 | { 390 | "metadata": { 391 | "name": "clunky-sabertooth-joomla-5d4ddc985d-fpddz" 392 | } 393 | } 394 | // ... 395 | ] 396 | } 397 | ] 398 | } 399 | }, 400 | { 401 | "owner": { 402 | "kind": "DaemonSet", 403 | "metadata": { 404 | "name": "kube-proxy" 405 | } 406 | }, 407 | "rootOwner": { 408 | "kind": "DaemonSet", 409 | "metadata": { 410 | "name": "kube-proxy", 411 | "namespace": "kube-system" 412 | } 413 | } 414 | }, 415 | { 416 | "owner": { 417 | "kind": "Pod", 418 | "metadata": { 419 | "name": "kube-scheduler-minikube" 420 | } 421 | }, 422 | "rootOwner": { 423 | "kind": "Pod", 424 | "metadata": { 425 | "name": "kube-scheduler-minikube", 426 | "namespace": "kube-system" 427 | } 428 | } 429 | } 430 | // ... 431 | ] 432 | } 433 | } 434 | 435 | ``` 436 | ## License 437 | 438 | The work done has been licensed under Apache License 2.0. The license file can be found [here](LICENSE). You can find 439 | out more about the license at [www.apache.org/licenses/LICENSE-2.0](//www.apache.org/licenses/LICENSE-2.0). 440 | 441 | ## Questions? 442 | 443 | Feel free to [contact us](mailto:support@yipee.io). 444 | -------------------------------------------------------------------------------- /cache_access.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 CA. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | var client CacheClient 18 | 19 | type CacheOp func() interface{} 20 | 21 | type CacheRequest struct { 22 | operation CacheOp 23 | replyChan chan interface{} 24 | } 25 | 26 | type clientif interface { 27 | Lookup(key string) interface{} 28 | Remove(obj *JsonObject) 29 | Add(obj *JsonObject) 30 | } 31 | 32 | type CacheClient struct { 33 | serverMbox chan<- *CacheRequest 34 | } 35 | 36 | func initCacheClient(serverMbox chan *CacheRequest) { 37 | client = CacheClient{serverMbox} 38 | } 39 | 40 | func GetCache() *CacheClient { 41 | return &client 42 | } 43 | 44 | func (client *CacheClient) Lookup(key string) interface{} { 45 | replyChan := make(chan interface{}) 46 | req := &CacheRequest{ 47 | func() interface{} { 48 | return cacheLookup(key) 49 | }, replyChan, 50 | } 51 | client.serverMbox <- req 52 | retval := <-replyChan 53 | return retval 54 | } 55 | 56 | func (client *CacheClient) Remove(obj *JsonObject) { 57 | replyChan := make(chan interface{}) 58 | req := &CacheRequest{ 59 | func() interface{} { 60 | removeFromCache(obj) 61 | return true 62 | }, replyChan, 63 | } 64 | client.serverMbox <- req 65 | if retval := <-replyChan; retval != true { 66 | panic("bad return from cache Remove") 67 | } 68 | } 69 | 70 | func (client *CacheClient) Add(obj *JsonObject) { 71 | replyChan := make(chan interface{}) 72 | req := &CacheRequest{ 73 | func() interface{} { 74 | addToCache(obj) 75 | return true 76 | }, replyChan, 77 | } 78 | client.serverMbox <- req 79 | if retval := <-replyChan; retval != true { 80 | panic("bad return from cache Add") 81 | } 82 | } 83 | 84 | // compilation error if we don't implement the i/f properly 85 | var _ clientif = (*CacheClient)(nil) 86 | 87 | func cacheKey(kind, namespace, name string) string { 88 | return kind + "#" + namespace + "#" + name 89 | } 90 | 91 | func nsCacheKey(kind, namespace string) string { 92 | return kind + "#" + namespace 93 | } 94 | -------------------------------------------------------------------------------- /cache_impl.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 CA. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "fmt" 19 | ) 20 | 21 | // N.B.: this is the cache implementation whose functions are 22 | // not intended to be called directly. 23 | // The intended cache interface 24 | // is in cache_access.go: namely "Lookup", "Add", "Remove", 25 | // along with the key-building functions. 26 | // All access to the cache is intended to be via the server mailbox. 27 | // That is our serialization mechanism (akin to an erlang gen_server). 28 | // 29 | // (Deliberately resisting the "internal" package goo...) 30 | var cache map[string]interface{} 31 | 32 | func runServer(mbox <-chan *CacheRequest) { 33 | for { 34 | req, ok := <-mbox 35 | if ok { 36 | req.replyChan <- req.operation() 37 | } else { 38 | break 39 | } 40 | } 41 | } 42 | 43 | func initCache() { 44 | cache = make(map[string]interface{}) 45 | serverMbox := make(chan *CacheRequest) 46 | go runServer(serverMbox) 47 | initCacheClient(serverMbox) 48 | } 49 | 50 | func findInList(clist []*JsonObject, target *JsonObject) int { 51 | tname := getName(*target) 52 | tns := getNamespace(*target) 53 | tkind := getKind(*target) 54 | for idx, obj := range clist { 55 | name := getName(*obj) 56 | ns := getNamespace(*obj) 57 | kind := getKind(*obj) 58 | if tname == name && tns == ns && tkind == kind { 59 | return idx 60 | } 61 | } 62 | return -1 63 | } 64 | 65 | func deleteFromCacheList(key string, obj *JsonObject) { 66 | if val, ok := cache[key]; ok { 67 | clist := val.([]*JsonObject) 68 | idx := findInList(clist, obj) 69 | if idx > -1 { 70 | cache[key] = append(clist[:idx], clist[idx+1:]...) 71 | } 72 | } 73 | } 74 | 75 | func addToCacheList(key string, obj *JsonObject) { 76 | if val, ok := cache[key]; ok { 77 | clist := val.([]*JsonObject) 78 | idx := findInList(clist, obj) 79 | if idx == -1 { 80 | cache[key] = append(clist, obj) 81 | } else { 82 | clist[idx] = obj 83 | } 84 | } else { 85 | cache[key] = []*JsonObject{obj} 86 | } 87 | } 88 | 89 | func formattedName(obj *JsonObject) string { 90 | return cacheKey(getKind(*obj), getNamespace(*obj), getName(*obj)) 91 | } 92 | 93 | func addToCache(obj *JsonObject) { 94 | kind := getKind(*obj) 95 | ns := getNamespace(*obj) 96 | name := getName(*obj) 97 | cacheKey := cacheKey(kind, ns, name) 98 | nsCacheKey := nsCacheKey(kind, ns) 99 | cache[cacheKey] = obj 100 | addToCacheList(nsCacheKey, obj) 101 | addToCacheList(kind, obj) 102 | } 103 | 104 | func removeFromCache(obj *JsonObject) { 105 | kind := getKind(*obj) 106 | ns := getNamespace(*obj) 107 | name := getName(*obj) 108 | cacheKey := cacheKey(kind, ns, name) 109 | nsCacheKey := nsCacheKey(kind, ns) 110 | delete(cache, cacheKey) 111 | deleteFromCacheList(nsCacheKey, obj) 112 | deleteFromCacheList(kind, obj) 113 | } 114 | 115 | func cacheLookup(key string) interface{} { 116 | if val, ok := cache[key]; ok { 117 | if ref, ok := val.(*JsonObject); ok { 118 | return *ref 119 | } else if list, ok := val.([]*JsonObject); ok { 120 | // clone returned slice so that its "shape" can't be changed 121 | // while caller is holding it... 122 | retlist := make([]JsonObject, len(list)) 123 | for idx, item := range list { 124 | retlist[idx] = *item 125 | } 126 | return retlist 127 | } else { 128 | panic(fmt.Sprintf("invalid type in cache: %T", val)) 129 | } 130 | } else { 131 | return nil 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /container.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 CA. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | // "fmt" 20 | ) 21 | 22 | // Containers do the actual work in Kubernetes 23 | 24 | type container struct { 25 | } 26 | 27 | type containerResolver struct { 28 | ctx context.Context 29 | c container 30 | } 31 | 32 | // Translate unmarshalled json into a deployment object 33 | func mapToContainer(ctx context.Context, jsonObj JsonObject) container { 34 | return container{} 35 | } 36 | -------------------------------------------------------------------------------- /daemonset.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 CA. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | // "fmt" 20 | "sort" 21 | "strings" 22 | ) 23 | 24 | // DaemonSets place a pod on each server 25 | type daemonSet struct { 26 | Metadata metadata 27 | Owner resource 28 | RootOwner resource 29 | Pods *[]pod 30 | } 31 | 32 | type daemonSetResolver struct { 33 | ctx context.Context 34 | d daemonSet 35 | } 36 | 37 | // Translate unmarshalled json into a deployment object 38 | func mapToDaemonSet( 39 | ctx context.Context, 40 | jsonObj JsonObject) daemonSet { 41 | placeholder := &ownerRef{ctx, jsonObj, nil} 42 | owner := placeholder 43 | rootOwner := placeholder 44 | meta := 45 | mapToMetadata(ctx, getNamespace(jsonObj), mapItem(jsonObj, "metadata")) 46 | return daemonSet{meta, owner, rootOwner, nil} 47 | } 48 | 49 | // DaemonSets have pods as children 50 | func getDaemonSetPods(ctx context.Context, d daemonSet) *[]pod { 51 | dsName := *d.Metadata.Name 52 | dsNamePrefix := dsName + "-" 53 | dsNamespace := *d.Metadata.Namespace 54 | 55 | pset := getAllK8sObjsOfKindInNamespace( 56 | ctx, 57 | PodKind, 58 | dsNamespace, 59 | func(jobj JsonObject) bool { 60 | return (strings.HasPrefix(getName(jobj), dsNamePrefix) && 61 | hasMatchingOwner(jobj, dsName, DaemonSetKind)) 62 | }) 63 | 64 | results := make([]pod, len(pset)) 65 | 66 | for idx, p := range pset { 67 | pr := p.(*podResolver) 68 | results[idx] = pr.p 69 | } 70 | 71 | sort.Slice( 72 | results, 73 | func(i, j int) bool { 74 | return *results[i].Metadata.Name < *results[j].Metadata.Name 75 | }) 76 | 77 | return &results 78 | } 79 | 80 | // Resource method implementations 81 | func (r *daemonSetResolver) Kind() string { 82 | return DaemonSetKind 83 | } 84 | 85 | func (r *daemonSetResolver) Metadata() metadataResolver { 86 | return metadataResolver{r.ctx, r.d.Metadata} 87 | } 88 | 89 | func (r *daemonSetResolver) Owner() *resourceResolver { 90 | if oref, ok := r.d.Owner.(*ownerRef); ok { 91 | r.d.Owner = getOwner(oref.ctx, oref.ref) 92 | } 93 | return &resourceResolver{r.ctx, r.d.Owner} 94 | } 95 | 96 | func (r *daemonSetResolver) RootOwner() *resourceResolver { 97 | if oref, ok := r.d.Owner.(*ownerRef); ok { 98 | r.d.Owner = getOwner(oref.ctx, oref.ref) 99 | } 100 | return &resourceResolver{r.ctx, r.d.RootOwner} 101 | } 102 | 103 | // Resolve child Pods 104 | func (r *daemonSetResolver) Pods() []*podResolver { 105 | if r.d.Pods == nil { 106 | r.d.Pods = getDaemonSetPods(r.ctx, r.d) 107 | } 108 | 109 | var res []*podResolver 110 | for _, p := range *r.d.Pods { 111 | res = append(res, &podResolver{r.ctx, p}) 112 | } 113 | if res == nil { 114 | res = make([]*podResolver, 0) 115 | } 116 | return res 117 | } 118 | -------------------------------------------------------------------------------- /deployment.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 CA. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | // "fmt" 20 | "strings" 21 | ) 22 | 23 | // Top level Kubernetes replicated controller. Deployments are built out 24 | // of ReplicaSets. 25 | type deployment struct { 26 | Metadata metadata 27 | Spec deploymentSpec 28 | Owner resource 29 | RootOwner resource 30 | ReplicaSets *[]replicaSet 31 | } 32 | 33 | type deploymentSpec struct { 34 | MinReadySeconds int32 35 | Paused bool 36 | ProgressDeadlineSeconds int32 37 | Replicas int32 38 | RevisionHistoryLimit int32 39 | Selector *labelSelector 40 | Strategy *deploymentStrategy 41 | Template podTemplateSpec 42 | } 43 | 44 | type podTemplateSpec struct { 45 | Metadata metadata 46 | Spec podSpec 47 | } 48 | 49 | type labelSelector struct { 50 | MatchExpressions *[]labelSelectorRequirement 51 | MatchLabels *[]label 52 | } 53 | 54 | type labelSelectorRequirement struct { 55 | Key string 56 | Operator string 57 | Values []string 58 | } 59 | 60 | type deploymentStrategy struct { 61 | RollingUpdate *rollingUpdateDeployment 62 | Type *string 63 | } 64 | 65 | type rollingUpdateDeployment struct { 66 | MaxSurgeInt *int32 67 | MaxSurgeString *string 68 | MaxUnavailableInt *int32 69 | MaxUnavailableString *string 70 | } 71 | 72 | type deploymentResolver struct { 73 | ctx context.Context 74 | d deployment 75 | } 76 | 77 | type deploymentSpecResolver struct { 78 | ctx context.Context 79 | d deploymentSpec 80 | } 81 | 82 | type labelSelectorResolver struct { 83 | ctx context.Context 84 | l labelSelector 85 | } 86 | 87 | type deploymentStrategyResolver struct { 88 | ctx context.Context 89 | d *deploymentStrategy 90 | } 91 | 92 | type labelSelectorRequirementResolver struct { 93 | ctx context.Context 94 | l labelSelectorRequirement 95 | } 96 | 97 | type rollingUpdateDeploymentResolver struct { 98 | ctx context.Context 99 | r rollingUpdateDeployment 100 | } 101 | 102 | type podTemplateSpecResolver struct { 103 | ctx context.Context 104 | p podTemplateSpec 105 | } 106 | 107 | // Translate unmarshalled json into a deployment object 108 | func mapToDeployment( 109 | ctx context.Context, 110 | jsonObj JsonObject) deployment { 111 | ns := getNamespace(jsonObj) 112 | return deployment{ 113 | mapToMetadata(ctx, ns, mapItem(jsonObj, "metadata")), 114 | extractDeploymentSpec(ctx, ns, mapItem(jsonObj, "spec")), 115 | nil, 116 | nil, 117 | nil} 118 | } 119 | 120 | func extractDeploymentSpec(ctx context.Context, ns string, jsonObj JsonObject) deploymentSpec { 121 | jg := jgetter(jsonObj) 122 | template := mapItem(jsonObj, "template") 123 | return deploymentSpec{ 124 | jg.intItemOr("minReadySeconds", 0), 125 | jg.boolItemOr("paused", false), 126 | jg.intItemOr("progressDeadlineSeconds", 600), 127 | jg.intItemOr("replicas", 1), 128 | jg.intItemOr("revisionHistoryLimit", 10), 129 | mapToSelector(jg.objItemOr("selector", nil)), 130 | mapToStrategy(jg.objItemOr("strategy", nil)), 131 | podTemplateSpec{ 132 | mapToMetadata(ctx, ns, mapItem(template, "metadata")), 133 | mapToPodSpec(ctx, mapItem(template, "spec"))}} 134 | } 135 | 136 | func mapToSelector(sel *JsonObject) *labelSelector { 137 | if sel == nil { 138 | return nil 139 | } 140 | jg := jgetter(*sel) 141 | exprs := jg.arrayItemOr("matchExpressions", nil) 142 | labels := jg.objItemOr("matchLabels", nil) 143 | 144 | var exprsVal *[]labelSelectorRequirement 145 | 146 | if exprs != nil { 147 | eslice := make([]labelSelectorRequirement, len(*exprs)) 148 | exprsVal = &eslice 149 | for idx, lsr := range *exprs { 150 | jg := jgetter(lsr.(JsonObject)) 151 | vals := jg.arrayItem("values") 152 | strVals := make([]string, len(vals)) 153 | for sidx, sval := range vals { 154 | strVals[sidx] = sval.(string) 155 | } 156 | (*exprsVal)[idx] = labelSelectorRequirement{ 157 | jg.stringItem("key"), 158 | jg.stringItem("operator"), 159 | strVals} 160 | } 161 | } 162 | 163 | if labels != nil { 164 | lslice := make([]label, len(*labels)) 165 | i := 0 166 | for k, v := range *labels { 167 | lslice[i] = label{k, v.(string)} 168 | i = i + 1 169 | } 170 | 171 | return &labelSelector{exprsVal, &lslice} 172 | } 173 | 174 | empty := make([]label, 0) 175 | return &labelSelector{exprsVal, &empty} 176 | } 177 | 178 | func mapToStrategy(strat *JsonObject) *deploymentStrategy { 179 | if strat == nil { 180 | return nil 181 | } 182 | jg := jgetter(*strat) 183 | sType := jg.stringRefItemOr("type", nil) 184 | var updateItem *JsonObject 185 | 186 | if *sType == "RollingUpdate" { 187 | updateItem = jg.objItemOr("rollingUpdate", nil) 188 | } 189 | 190 | if updateItem == nil { 191 | return &deploymentStrategy{nil, sType} 192 | } 193 | 194 | sval, spresent := (*updateItem)["maxSurge"] 195 | uval, upresent := (*updateItem)["maxUnavailable"] 196 | var ss, su string 197 | var is, iu int32 198 | var ssptr *string = nil 199 | var suptr *string = nil 200 | var isptr *int32 = nil 201 | var iuptr *int32 = nil 202 | defval := "25%" 203 | 204 | if !spresent { 205 | ss = defval 206 | ssptr = &ss 207 | } else if ssval, ok := sval.(string); ok { 208 | ss = ssval 209 | ssptr = &ss 210 | } else { 211 | is = toGQLInt(sval) 212 | isptr = &is 213 | } 214 | 215 | if !upresent { 216 | su = defval 217 | suptr = &su 218 | } else if suval, ok := uval.(string); ok { 219 | su = suval 220 | suptr = &su 221 | } else { 222 | iu = toGQLInt(uval) 223 | iuptr = &iu 224 | } 225 | 226 | return &deploymentStrategy{ 227 | &rollingUpdateDeployment{isptr, ssptr, iuptr, suptr}, 228 | sType} 229 | } 230 | 231 | // Retrieve the ReplicaSets comprising the deployment 232 | func getReplicaSets(ctx context.Context, d deployment) *[]replicaSet { 233 | depName := *d.Metadata.Name 234 | depNamePrefix := depName + "-" 235 | depNamespace := *d.Metadata.Namespace 236 | 237 | rsets := getAllK8sObjsOfKindInNamespace( 238 | ctx, 239 | "ReplicaSet", 240 | depNamespace, 241 | func(jobj JsonObject) bool { 242 | return (strings.HasPrefix(getName(jobj), depNamePrefix) && 243 | hasMatchingOwner(jobj, depName, DeploymentKind)) 244 | }) 245 | 246 | results := make([]replicaSet, len(rsets)) 247 | 248 | for idx, rs := range rsets { 249 | rsr := rs.(*replicaSetResolver) 250 | results[idx] = rsr.r 251 | } 252 | 253 | return &results 254 | } 255 | 256 | func (r *labelSelectorResolver) MatchExpressions() *[]labelSelectorRequirementResolver { 257 | if r.l.MatchExpressions == nil { 258 | empty := make([]labelSelectorRequirementResolver, 0) 259 | return &empty 260 | } 261 | resolvers := make([]labelSelectorRequirementResolver, 262 | len(*r.l.MatchExpressions)) 263 | for idx, val := range *r.l.MatchExpressions { 264 | resolvers[idx] = labelSelectorRequirementResolver{r.ctx, val} 265 | } 266 | return &resolvers 267 | } 268 | 269 | func (r *labelSelectorResolver) MatchLabels() *[]labelResolver { 270 | if r.l.MatchLabels == nil { 271 | empty := make([]labelResolver, 0) 272 | return &empty 273 | } 274 | resolvers := make([]labelResolver, len(*r.l.MatchLabels)) 275 | for idx, val := range *r.l.MatchLabels { 276 | labelVal := val 277 | resolvers[idx] = labelResolver{r.ctx, &labelVal} 278 | } 279 | return &resolvers 280 | } 281 | 282 | func (r labelSelectorRequirementResolver) Key() string { 283 | return r.l.Key 284 | } 285 | 286 | func (r labelSelectorRequirementResolver) Operator() string { 287 | return r.l.Operator 288 | } 289 | 290 | func (r labelSelectorRequirementResolver) Values() []string { 291 | return r.l.Values 292 | } 293 | 294 | // Pod template spec implementations 295 | func (r podTemplateSpecResolver) Metadata() metadataResolver { 296 | return metadataResolver{r.ctx, r.p.Metadata} 297 | } 298 | 299 | func (r podTemplateSpecResolver) Spec() podSpecResolver { 300 | return podSpecResolver{r.ctx, r.p.Spec} 301 | } 302 | 303 | // Resource method implementations 304 | func (r *deploymentResolver) Kind() string { 305 | return DeploymentKind 306 | } 307 | 308 | func (r *deploymentResolver) Metadata() metadataResolver { 309 | return metadataResolver{r.ctx, r.d.Metadata} 310 | } 311 | 312 | func (r *deploymentResolver) Spec() deploymentSpecResolver { 313 | return deploymentSpecResolver{r.ctx, r.d.Spec} 314 | } 315 | 316 | func (r *deploymentResolver) Owner() *resourceResolver { 317 | return &resourceResolver{r.ctx, &deploymentResolver{r.ctx, r.d}} 318 | } 319 | 320 | func (r *deploymentResolver) RootOwner() *resourceResolver { 321 | return &resourceResolver{r.ctx, &deploymentResolver{r.ctx, r.d}} 322 | } 323 | 324 | // Deployment spec implementations 325 | func (r deploymentSpecResolver) MinReadySeconds() int32 { 326 | return r.d.MinReadySeconds 327 | } 328 | 329 | func (r deploymentSpecResolver) Paused() bool { 330 | return r.d.Paused 331 | } 332 | 333 | func (r deploymentSpecResolver) ProgressDeadlineSeconds() int32 { 334 | return r.d.ProgressDeadlineSeconds 335 | } 336 | 337 | func (r deploymentSpecResolver) Replicas() int32 { 338 | return r.d.Replicas 339 | } 340 | 341 | func (r deploymentSpecResolver) RevisionHistoryLimit() int32 { 342 | return r.d.RevisionHistoryLimit 343 | } 344 | 345 | func (r deploymentSpecResolver) Selector() *labelSelectorResolver { 346 | if r.d.Selector != nil { 347 | return &labelSelectorResolver{r.ctx, *r.d.Selector} 348 | } 349 | 350 | return nil 351 | } 352 | 353 | func (r deploymentSpecResolver) Template() podTemplateSpecResolver { 354 | return podTemplateSpecResolver{r.ctx, r.d.Template} 355 | } 356 | 357 | func (r deploymentSpecResolver) Strategy() *deploymentStrategyResolver { 358 | return &deploymentStrategyResolver{r.ctx, r.d.Strategy} 359 | } 360 | 361 | // Resolve child ReplicaSets 362 | func (r *deploymentResolver) ReplicaSets() []*replicaSetResolver { 363 | if r.d.ReplicaSets == nil { 364 | r.d.ReplicaSets = getReplicaSets(r.ctx, r.d) 365 | } 366 | 367 | var res []*replicaSetResolver 368 | for _, rs := range *r.d.ReplicaSets { 369 | res = append(res, &replicaSetResolver{r.ctx, rs}) 370 | } 371 | if res == nil { 372 | res = make([]*replicaSetResolver, 0) 373 | } 374 | return res 375 | } 376 | 377 | func (r deploymentStrategyResolver) RollingUpdate() *rollingUpdateDeploymentResolver { 378 | if r.d.RollingUpdate == nil { 379 | return nil 380 | } 381 | return &rollingUpdateDeploymentResolver{r.ctx, *r.d.RollingUpdate} 382 | } 383 | 384 | func (r deploymentStrategyResolver) Type() *string { 385 | val := "RollingUpdate" 386 | if r.d == nil || r.d.Type == nil { 387 | return &val 388 | } 389 | return r.d.Type 390 | } 391 | 392 | func (r rollingUpdateDeploymentResolver) MaxSurgeInt() *int32 { 393 | return r.r.MaxSurgeInt 394 | } 395 | 396 | func (r rollingUpdateDeploymentResolver) MaxUnavailableInt() *int32 { 397 | return r.r.MaxUnavailableInt 398 | } 399 | 400 | func (r rollingUpdateDeploymentResolver) MaxSurgeString() *string { 401 | return r.r.MaxSurgeString 402 | } 403 | 404 | func (r rollingUpdateDeploymentResolver) MaxUnavailableString() *string { 405 | return r.r.MaxUnavailableString 406 | } 407 | -------------------------------------------------------------------------------- /elf_build.sh: -------------------------------------------------------------------------------- 1 | # Build our executable in a docker container to make sure we get a linux/ELF 2 | # binary. Useful for development on a mac. Not sure about windows... 3 | APP_NAME=kubeiql.elf 4 | docker run --rm -e "GOPATH=/usr" -e "CGO_ENABLED=0" -v "$PWD":/usr/src/${APP_NAME} -w /usr/src/${APP_NAME} golang:1.10 go build 5 | -------------------------------------------------------------------------------- /get_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 CA. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | // "fmt" 20 | graphql "github.com/neelance/graphql-go" 21 | "io/ioutil" 22 | "log" 23 | "testing" 24 | ) 25 | 26 | var testschema *graphql.Schema = graphql.MustParseSchema(Schema, &Resolver{}) 27 | 28 | func simpletest(t *testing.T, query string, result string) { 29 | var stest Test 30 | stest.Schema = testschema 31 | stest.Query = query 32 | stest.ExpectedResult = result 33 | RunTest(t, &stest) 34 | } 35 | 36 | func init() { 37 | cache := make(map[string]interface{}) 38 | ctx := context.WithValue(context.Background(), "queryCache", &cache) 39 | setTestContext(&ctx) 40 | for _, fname := range []string{ 41 | "deployment.json", 42 | "replicaset.json", 43 | "daemonset.json", 44 | "statefulset.json", 45 | "service.json", 46 | "pod1.json", 47 | "pod2.json", 48 | "pod3.json"} { 49 | addToTestCache(&cache, "testdata/"+fname) 50 | } 51 | } 52 | 53 | func addToTestCache(cacheref *map[string]interface{}, fname string) { 54 | bytes, err := ioutil.ReadFile(fname) 55 | if err != nil { 56 | log.Fatal(err) 57 | } 58 | data := fromJson(bytes).(JsonObject) 59 | cache := GetCache() 60 | cache.Add(&data) 61 | } 62 | 63 | func TestPods(t *testing.T) { 64 | simpletest( 65 | t, 66 | `{ 67 | allDeployments() { 68 | metadata { 69 | creationTimestamp 70 | generation 71 | labels { name value } 72 | } 73 | spec { 74 | minReadySeconds 75 | paused 76 | progressDeadlineSeconds 77 | replicas 78 | revisionHistoryLimit 79 | selector { 80 | matchLabels { name value } 81 | matchExpressions { 82 | key 83 | operator 84 | values 85 | } 86 | } 87 | strategy { 88 | type 89 | rollingUpdate { 90 | maxSurgeInt 91 | maxSurgeString 92 | maxUnavailableInt 93 | maxUnavailableString 94 | } 95 | } 96 | template { 97 | metadata { 98 | creationTimestamp 99 | labels { name value } 100 | }, 101 | spec { 102 | dnsPolicy 103 | restartPolicy 104 | schedulerName 105 | terminationGracePeriodSeconds 106 | volumes { 107 | name 108 | persistentVolumeClaim { claimName readOnly } 109 | } 110 | } 111 | } 112 | } 113 | replicaSets { 114 | metadata { 115 | name 116 | } 117 | pods { 118 | metadata { 119 | name 120 | namespace 121 | labels { 122 | name 123 | value 124 | } 125 | } 126 | spec { 127 | dnsPolicy 128 | nodeName 129 | restartPolicy 130 | schedulerName 131 | serviceAccountName 132 | terminationGracePeriodSeconds 133 | tolerations { 134 | effect 135 | key 136 | operator 137 | tolerationSeconds 138 | } 139 | volumes { 140 | name 141 | persistentVolumeClaim { claimName readOnly } 142 | secret { defaultMode secretName } 143 | } 144 | } 145 | } 146 | } 147 | }}`, 148 | `{ 149 | "allDeployments": [ 150 | { 151 | "metadata": { 152 | "creationTimestamp": "2018-07-02T14:53:53Z", 153 | "generation": 1, 154 | "labels": [ 155 | {"name": "app", "value": "clunky-sabertooth-joomla"}, 156 | {"name": "chart", "value": "joomla-2.0.2"}, 157 | {"name": "heritage", "value": "Tiller"}, 158 | {"name": "release", "value": "clunky-sabertooth"} 159 | ] 160 | }, 161 | "spec": { 162 | "minReadySeconds": 0, 163 | "paused": false, 164 | "progressDeadlineSeconds": 600, 165 | "replicas": 1, 166 | "revisionHistoryLimit": 10, 167 | "selector": { 168 | "matchExpressions": [], 169 | "matchLabels": [ 170 | {"name": "app", "value": "clunky-sabertooth-joomla"} 171 | ] 172 | }, 173 | "strategy": { 174 | "rollingUpdate": { 175 | "maxSurgeInt": 1, 176 | "maxSurgeString": null, 177 | "maxUnavailableInt": 1, 178 | "maxUnavailableString": null 179 | }, 180 | "type": "RollingUpdate" 181 | }, 182 | "template": { 183 | "metadata": { 184 | "creationTimestamp": null, 185 | "labels": [ 186 | {"name": "app", "value": "clunky-sabertooth-joomla"} 187 | ] 188 | }, 189 | "spec": { 190 | "dnsPolicy": "ClusterFirst", 191 | "restartPolicy": "Always", 192 | "schedulerName": "default-scheduler", 193 | "terminationGracePeriodSeconds": 30, 194 | "volumes": [ 195 | { 196 | "name": "joomla-data", 197 | "persistentVolumeClaim": { 198 | "claimName": "clunky-sabertooth-joomla-joomla", 199 | "readOnly": false 200 | } 201 | }, 202 | { 203 | "name": "apache-data", 204 | "persistentVolumeClaim": { 205 | "claimName": "clunky-sabertooth-joomla-apache", 206 | "readOnly": false 207 | } 208 | } 209 | ] 210 | } 211 | } 212 | }, 213 | "replicaSets": [ 214 | { 215 | "metadata": { 216 | "name": "clunky-sabertooth-joomla-5d4ddc985d" 217 | }, 218 | "pods": [ 219 | { 220 | "metadata": { 221 | "name": "clunky-sabertooth-joomla-5d4ddc985d-fpddz", 222 | "namespace": "default", 223 | "labels": [ 224 | {"name": "app", "value": "clunky-sabertooth-joomla"}, 225 | {"name": "pod-template-hash", "value": "1808875418"} 226 | ] 227 | }, 228 | "spec": { 229 | "dnsPolicy": "ClusterFirst", 230 | "nodeName": "minikube", 231 | "restartPolicy": "Always", 232 | "schedulerName": "default-scheduler", 233 | "serviceAccountName": "default", 234 | "terminationGracePeriodSeconds": 30, 235 | "tolerations": [ 236 | { 237 | "effect": "NoExecute", 238 | "key": "node.kubernetes.io/not-ready", 239 | "operator": "Exists", 240 | "tolerationSeconds": 300 241 | }, 242 | { 243 | "effect": "NoExecute", 244 | "key": "node.kubernetes.io/unreachable", 245 | "operator": "Exists", 246 | "tolerationSeconds": 300 247 | } 248 | ], 249 | "volumes": [ 250 | { 251 | "name": "joomla-data", 252 | "persistentVolumeClaim": { 253 | "claimName": "clunky-sabertooth-joomla-joomla", 254 | "readOnly": false 255 | }, 256 | "secret": null 257 | }, 258 | { 259 | "name": "apache-data", 260 | "persistentVolumeClaim": { 261 | "claimName": "clunky-sabertooth-joomla-apache", 262 | "readOnly": false 263 | }, 264 | "secret": null 265 | }, 266 | { 267 | "name": "default-token-l6lb2", 268 | "persistentVolumeClaim": null, 269 | "secret": { 270 | "defaultMode": 420, 271 | "secretName": "default-token-l6lb2" 272 | } 273 | } 274 | ] 275 | } 276 | } 277 | ] 278 | } 279 | ] 280 | } 281 | ] 282 | }`) 283 | simpletest( 284 | t, 285 | `{ 286 | podByName(namespace: "default", 287 | name: "clunky-sabertooth-joomla-5d4ddc985d-fpddz") { 288 | owner { metadata { name } } 289 | rootOwner { metadata { name } } 290 | } 291 | }`, 292 | `{ 293 | "podByName": { 294 | "owner": { 295 | "metadata": { "name": "clunky-sabertooth-joomla-5d4ddc985d" } 296 | }, 297 | "rootOwner": { 298 | "metadata": { "name": "clunky-sabertooth-joomla" } 299 | } 300 | } 301 | }`) 302 | simpletest( 303 | t, 304 | `{ 305 | allPodsInNamespace(namespace: "default") { 306 | owner { metadata { name } } 307 | rootOwner { metadata { name } } 308 | } 309 | }`, 310 | `{ 311 | "allPodsInNamespace": [ 312 | { 313 | "owner": { 314 | "metadata": { "name": "clunky-sabertooth-joomla-5d4ddc985d" } 315 | }, 316 | "rootOwner": { 317 | "metadata": { "name": "clunky-sabertooth-joomla" } 318 | } 319 | } 320 | ] 321 | }`) 322 | simpletest( 323 | t, 324 | `{ 325 | allReplicaSets() { 326 | owner { metadata { name } } 327 | rootOwner { metadata { name } } 328 | } 329 | }`, 330 | `{ 331 | "allReplicaSets": [ 332 | { 333 | "owner": { 334 | "metadata": { "name": "clunky-sabertooth-joomla" } 335 | }, 336 | "rootOwner": { 337 | "metadata": { "name": "clunky-sabertooth-joomla" } 338 | } 339 | } 340 | ] 341 | }`) 342 | simpletest( 343 | t, 344 | `{ 345 | allDaemonSets() { 346 | owner { metadata { name namespace } } 347 | rootOwner { metadata { name namespace } } 348 | pods { metadata { name labels { name value } } } 349 | } 350 | }`, 351 | `{ 352 | "allDaemonSets": [ 353 | { 354 | "owner": { 355 | "metadata": { 356 | "name": "calico-node", 357 | "namespace": "kube-system" 358 | } 359 | }, 360 | "rootOwner": { 361 | "metadata": { 362 | "name": "calico-node", 363 | "namespace": "kube-system" 364 | } 365 | }, 366 | "pods": [ 367 | { 368 | "metadata": { 369 | "name": "calico-node-ddxfj", 370 | "labels": [ 371 | {"name": "controller-revision-hash", 372 | "value": "3909226423"}, 373 | {"name": "k8s-app", "value": "calico-node"}, 374 | {"name": "pod-template-generation", "value": "1"} 375 | ] 376 | } 377 | } 378 | ] 379 | } 380 | ] 381 | }`) 382 | simpletest( 383 | t, 384 | `{ 385 | allServices() { 386 | owner { metadata { name namespace } } 387 | rootOwner { metadata { name namespace } } 388 | selected { metadata { name labels { name value } } } 389 | } 390 | }`, 391 | `{ 392 | "allServices": [ 393 | { 394 | "owner": { 395 | "metadata": { 396 | "name": "mongo", 397 | "namespace": "flonjella" 398 | } 399 | }, 400 | "rootOwner": { 401 | "metadata": { 402 | "name": "mongo", 403 | "namespace": "flonjella" 404 | } 405 | }, 406 | "selected": [ 407 | { 408 | "metadata": { 409 | "name": "mongo-0", 410 | "labels": [ 411 | {"name": "app", "value": "mongo"}, 412 | {"name": "controller-revision-hash", 413 | "value": "mongo-fdd786d"}, 414 | {"name": "name", "value": "mongo"}, 415 | {"name": "statefulset.kubernetes.io/pod-name", 416 | "value": "mongo-0"} 417 | ] 418 | } 419 | } 420 | ] 421 | } 422 | ] 423 | }`) 424 | simpletest( 425 | t, 426 | `{ 427 | allServicesInNamespace(namespace: "flonjella") { 428 | owner { metadata { name namespace } } 429 | rootOwner { metadata { name namespace } } 430 | selected { metadata { name labels { name value } } } 431 | } 432 | }`, 433 | `{ 434 | "allServicesInNamespace": [ 435 | { 436 | "owner": { 437 | "metadata": { 438 | "name": "mongo", 439 | "namespace": "flonjella" 440 | } 441 | }, 442 | "rootOwner": { 443 | "metadata": { 444 | "name": "mongo", 445 | "namespace": "flonjella" 446 | } 447 | }, 448 | "selected": [ 449 | { 450 | "metadata": { 451 | "name": "mongo-0", 452 | "labels": [ 453 | {"name": "app", "value": "mongo"}, 454 | {"name": "controller-revision-hash", 455 | "value": "mongo-fdd786d"}, 456 | {"name": "name", "value": "mongo"}, 457 | {"name": "statefulset.kubernetes.io/pod-name", 458 | "value": "mongo-0"} 459 | ] 460 | } 461 | } 462 | ] 463 | } 464 | ] 465 | }`) 466 | simpletest( 467 | t, 468 | `{ 469 | serviceByName(namespace: "flonjella", name: "mongo") { 470 | owner { metadata { name namespace } } 471 | rootOwner { metadata { name namespace } } 472 | selected { metadata { name labels { name value } } } 473 | } 474 | }`, 475 | `{ 476 | "serviceByName": { 477 | "owner": { 478 | "metadata": { 479 | "name": "mongo", 480 | "namespace": "flonjella" 481 | } 482 | }, 483 | "rootOwner": { 484 | "metadata": { 485 | "name": "mongo", 486 | "namespace": "flonjella" 487 | } 488 | }, 489 | "selected": [ 490 | { 491 | "metadata": { 492 | "name": "mongo-0", 493 | "labels": [ 494 | {"name": "app", "value": "mongo"}, 495 | {"name": "controller-revision-hash", 496 | "value": "mongo-fdd786d"}, 497 | {"name": "name", "value": "mongo"}, 498 | {"name": "statefulset.kubernetes.io/pod-name", 499 | "value": "mongo-0"} 500 | ] 501 | } 502 | } 503 | ] 504 | } 505 | }`) 506 | } 507 | -------------------------------------------------------------------------------- /gobuild.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | go get -u github.com/kardianos/govendor 4 | $GOPATH/bin/govendor sync 5 | #CGO_ENABLED=0 go build -a -v -ldflags '-s' 6 | go build -gcflags '-N -l' 7 | # # Run unit tests and generate code coverage reports -- an html one 8 | # # for local viewing and a cobertura one for jenkins builds. 9 | #go get -u github.com/t-yuki/gocover-cobertura 10 | # go test -coverprofile coverage.txt 11 | # result=$? 12 | # go tool cover -html=coverage.txt -o coverage.html 13 | # $GOPATH/bin/gocover-cobertura < coverage.txt > coverage.xml 14 | exit $result 15 | 16 | -------------------------------------------------------------------------------- /k8sdeploy.yaml: -------------------------------------------------------------------------------- 1 | # Generated 2018-10-31T22:17:29.025Z by Yipee.io 2 | # Application: kubeiql 3 | # Last Modified: 2018-10-31T22:17:29.025Z 4 | 5 | # 6 | # Kubernetes object definitions for deploying kubeiql in a k8s cluster 7 | # 8 | 9 | # ServiceAccount that will be given cluster-level access permissions 10 | apiVersion: v1 11 | kind: ServiceAccount 12 | metadata: 13 | name: kubeiql-service-account 14 | 15 | --- 16 | # A role allowing kubeiql to watch all system objects 17 | apiVersion: rbac.authorization.k8s.io/v1 18 | kind: ClusterRole 19 | metadata: 20 | name: kubeiql-actor 21 | labels: 22 | aggregate-to-admin: 'true' 23 | aggregate-to-edit: 'true' 24 | rules: 25 | - apiGroups: 26 | - '*' 27 | resources: 28 | - '*' 29 | verbs: 30 | - watch 31 | 32 | --- 33 | # Bind the kubeiql access role to the kubeiql service account 34 | apiVersion: rbac.authorization.k8s.io/v1beta1 35 | kind: ClusterRoleBinding 36 | metadata: 37 | name: kubeiql-actions 38 | roleRef: 39 | apiGroup: rbac.authorization.k8s.io 40 | kind: ClusterRole 41 | name: kubeiql-actor 42 | subjects: 43 | - kind: ServiceAccount 44 | name: kubeiql-service-account 45 | namespace: default 46 | 47 | --- 48 | # Endpoint for accessing kubeiql. 49 | # This configuration works for deploying in local minikube. 50 | # Change type to LoadBalancer (or set up an ingress) for deployment 51 | # in a production environment 52 | apiVersion: v1 53 | kind: Service 54 | metadata: 55 | name: kubeiql 56 | spec: 57 | selector: 58 | yipee.io/kubeiql: generated 59 | ports: 60 | - port: 8128 61 | targetPort: 8128 62 | name: kubeiql-8128 63 | protocol: TCP 64 | nodePort: 32128 65 | type: NodePort 66 | 67 | --- 68 | # Here's the actual kubeiql app. If you want to run a locally built 69 | # image (instead of the one we've pushed to dockerhub), just change 70 | # the image name here accordingly... 71 | apiVersion: extensions/v1beta1 72 | kind: Deployment 73 | metadata: 74 | name: kubeiql 75 | annotations: 76 | yipee.io.lastModelUpdate: '2018-10-31T22:17:28.959Z' 77 | yipee.io.modelURL: http://192.168.99.100:32080/editor 78 | spec: 79 | selector: 80 | matchLabels: 81 | name: kubeiql 82 | component: kubeiql 83 | yipee.io/kubeiql: generated 84 | rollbackTo: 85 | revision: 0 86 | template: 87 | spec: 88 | imagePullSecrets: [] 89 | containers: 90 | - name: kubeiql 91 | env: 92 | - name: API_HOST 93 | value: https://kubernetes.default.svc 94 | - name: API_SECRET_PATH 95 | value: /var/run/secrets/kubernetes.io/serviceaccount 96 | ports: 97 | - containerPort: 8128 98 | protocol: TCP 99 | imagePullPolicy: Always 100 | image: yipeeio/kubeiql 101 | restartPolicy: Always 102 | serviceAccountName: kubeiql-service-account 103 | metadata: 104 | labels: 105 | name: kubeiql 106 | component: kubeiql 107 | yipee.io/kubeiql: generated 108 | strategy: 109 | type: RollingUpdate 110 | rollingUpdate: {} 111 | replicas: 1 112 | -------------------------------------------------------------------------------- /kubeaccess.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 CA. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "encoding/json" 20 | "fmt" 21 | ) 22 | 23 | // Functions for retrieving Kubernetes information from a cluster 24 | 25 | // Get a single resource instance from a namespace 26 | func getK8sResource(ctx context.Context, kind, namespace, name string) resource { 27 | return lookUpResource(ctx, kind, namespace, name) 28 | } 29 | 30 | func getRawK8sResource( 31 | ctx context.Context, kind, namespace, name string) JsonObject { 32 | return lookUpMap(ctx, kind, namespace, name) 33 | } 34 | 35 | func fromJson(val []byte) interface{} { 36 | var result interface{} 37 | 38 | if err := json.Unmarshal(val, &result); err != nil { 39 | panic(err) 40 | } 41 | 42 | return result 43 | } 44 | 45 | var testContext *context.Context = nil 46 | 47 | func setTestContext(ctx *context.Context) { 48 | testContext = ctx 49 | } 50 | 51 | func getTestContext() *context.Context { 52 | return testContext 53 | } 54 | 55 | func getCache(inctx context.Context) *JsonObject { 56 | ctx := &inctx 57 | if isTest() { 58 | ctx = getTestContext() 59 | } 60 | return (*ctx).Value("queryCache").(*JsonObject) 61 | } 62 | 63 | func isTest() bool { 64 | return testContext != nil 65 | } 66 | 67 | func lookUpMap( 68 | ctx context.Context, 69 | kind, namespace, name string) JsonObject { 70 | key := cacheKey(kind, namespace, name) 71 | var cachedVal interface{} 72 | if !isWatchedKind(kind) { 73 | panic(fmt.Sprintf("Add watcher for kind '%s'", kind)) 74 | } 75 | cachedVal = GetCache().Lookup(key) 76 | if cachedVal == nil { 77 | return nil 78 | } 79 | return cachedVal.(JsonObject) 80 | } 81 | 82 | func lookUpResource(ctx context.Context, kind, namespace, name string) resource { 83 | mapval := lookUpMap(ctx, kind, namespace, name) 84 | 85 | if mapval == nil { 86 | return nil 87 | } 88 | 89 | return mapToResource(ctx, mapval) 90 | } 91 | 92 | func getCachedResourceList( 93 | ctx context.Context, 94 | cacheKey string, 95 | test func(JsonObject) bool) []resource { 96 | 97 | var cachedJsonObjs []JsonObject 98 | var results []resource 99 | 100 | if objs := GetCache().Lookup(cacheKey); objs != nil { 101 | cachedJsonObjs = objs.([]JsonObject) 102 | } 103 | for _, res := range cachedJsonObjs { 104 | val := mapToResource(ctx, res) 105 | if test(res) { 106 | results = append(results, val) 107 | } 108 | } 109 | if results == nil { 110 | results = make([]resource, 0) 111 | } 112 | return results 113 | } 114 | 115 | // Get all resource instances of a specific kind 116 | func getAllK8sObjsOfKind( 117 | ctx context.Context, 118 | kind string, 119 | test func(JsonObject) bool) []resource { 120 | 121 | if !isWatchedKind(kind) { 122 | panic(fmt.Sprintf("Add watcher for kind '%s'", kind)) 123 | } 124 | return getCachedResourceList(ctx, kind, test) 125 | } 126 | 127 | // Get all resource instances of a specific kind in a specific namespace 128 | func getAllK8sObjsOfKindInNamespace( 129 | ctx context.Context, 130 | kind, ns string, 131 | test func(JsonObject) bool) []resource { 132 | key := nsCacheKey(kind, ns) 133 | if !isWatchedKind(kind) { 134 | panic(fmt.Sprintf("Add watcher for kind '%s'", kind)) 135 | } 136 | return getCachedResourceList(ctx, key, test) 137 | } 138 | -------------------------------------------------------------------------------- /kubeiql.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 CA. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | ) 20 | 21 | // The schema below defines the objects and relationships for Kubernetes. 22 | // It is not yet complete. 23 | 24 | var Schema = ` 25 | schema { 26 | query: Query 27 | mutation: Mutation 28 | } 29 | # The query type, represents all of the entry points into our object graph 30 | type Query { 31 | # look up pods 32 | allPods(): [Pod] 33 | allPodsInNamespace(namespace: String!): [Pod] 34 | podByName(namespace: String!, name: String!): Pod 35 | # look up deployments 36 | allDeployments(): [Deployment] 37 | allDeploymentsInNamespace(namespace: String!): [Deployment] 38 | deploymentByName(namespace: String!, name: String!): Deployment 39 | # look up replica sets 40 | allReplicaSets(): [ReplicaSet] 41 | allReplicaSetsInNamespace(namespace: String!): [ReplicaSet] 42 | replicaSetByName(namespace: String!, name: String!): ReplicaSet 43 | # look up daemon sets 44 | allDaemonSets(): [DaemonSet] 45 | allDaemonSetsInNamespace(namespace: String!): [DaemonSet] 46 | daemonSetByName(namespace: String!, name: String!): DaemonSet 47 | # look up stateful sets 48 | allStatefulSets(): [StatefulSet] 49 | allStatefulSetsInNamespace(namespace: String!): [StatefulSet] 50 | statefulSetByName(namespace: String!, name: String!): StatefulSet 51 | # look up services 52 | allServices(): [Service] 53 | allServicesInNamespace(namespace: String!): [Service] 54 | serviceByName(namespace: String!, name: String!): Service 55 | } 56 | 57 | # The mutation type, represents all updates we can make to our data 58 | type Mutation { 59 | } 60 | 61 | # Available logging levels 62 | enum LogLevel { 63 | debug 64 | info 65 | warning 66 | error 67 | fatal 68 | panic 69 | } 70 | 71 | # A service 72 | type Service implements Resource { 73 | # The metadata for the service (name, labels, namespace, etc.) 74 | metadata: Metadata! 75 | # The description for the service 76 | spec: ServiceSpec! 77 | # The direct owner of the replicaSet 78 | owner: Resource 79 | # The root owner of the replicaSet 80 | rootOwner: Resource 81 | # Which controllers does this service select? 82 | selected: [Resource!]! 83 | # Runtime status 84 | # status ServiceStatus! XXX 85 | } 86 | 87 | # Description of a service 88 | type ServiceSpec { 89 | # IP Address of the service 90 | clusterIP: String 91 | # External IPs 92 | externalIPs: [String!] 93 | # External reference for kubedns 94 | externalName: String 95 | # How a service should redirect to ports 96 | externalTrafficPolicy: String 97 | # Port used by health checks 98 | healthCheckNodePort: Int 99 | # IP for load balancer 100 | loadBalancerIP: String 101 | # Ranges from which to choose load balancer IPs 102 | loadBalancerSourceRanges: [String!] 103 | # Exposed ports 104 | ports: [ServicePort!] 105 | # Whether to publish dns addresses when peers are not yet ready for 106 | # discovery 107 | publishNotReadyAddresses: Boolean 108 | # Labels to select controllers for association with the service 109 | selector: [Label!]! 110 | # Whether or not to use session affinity ("ClientIP" or "None") 111 | sessionAffinity: String 112 | # Config for session affinity 113 | sessionAffinityConfig: SessionAffinityConfig 114 | # Type of service 115 | type: ServiceType! 116 | } 117 | 118 | # A port managed by a service 119 | type ServicePort { 120 | # port name, if any -- required if multiple ports 121 | name: String 122 | # port allocated to each node on which this service is exposed 123 | nodePort: Int 124 | # port exposed by the service 125 | port: Int! 126 | # IP Protocol - "TCP" or "UDP", default: "TCP" 127 | protocol: String 128 | # Number or name of port on the pods targeted by the service 129 | targetPortString: String 130 | targetPortInt: Int 131 | } 132 | 133 | # Service Types 134 | enum ServiceType { ClientIP NodePort ExternalName LoadBalancer } 135 | 136 | # Session affinity config 137 | type SessionAffinityConfig { 138 | # config for client IP 139 | clientIP: ClientIPConfig! 140 | } 141 | 142 | # Client IP config 143 | type ClientIPConfig { 144 | # timeout between 0 and 86400 (one day) 145 | timeoutSeconds: Int! 146 | } 147 | 148 | # A pod 149 | type Pod implements Resource { 150 | # The metadata for the pod (name, labels, namespace, etc.) 151 | metadata: Metadata! 152 | # behavior specification 153 | spec: PodSpec! 154 | # The direct owner of the pod 155 | owner: Resource 156 | # The root owner of the pod 157 | rootOwner: Resource 158 | # XXX PodStatus 159 | } 160 | 161 | # A replicaSet 162 | type ReplicaSet implements Resource { 163 | # The metadata for the replicaSet (name, labels, namespace, etc.) 164 | metadata: Metadata! 165 | # The direct owner of the replicaSet 166 | owner: Resource 167 | # The root owner of the replicaSet 168 | rootOwner: Resource 169 | # The pods controlled by this replicaSet 170 | pods: [Pod!]! 171 | } 172 | 173 | # A statefulSet 174 | type StatefulSet implements Resource { 175 | # The metadata for the statefulSet (name, labels, namespace, etc.) 176 | metadata: Metadata! 177 | # The direct owner of the statefulSet 178 | owner: Resource 179 | # The root owner of the statefulSet 180 | rootOwner: Resource 181 | # The pods controlled by this statefulSet 182 | pods: [Pod!]! 183 | } 184 | 185 | # A daemonSet 186 | type DaemonSet implements Resource { 187 | # The metadata for the daemonSet (name, labels, namespace, etc.) 188 | metadata: Metadata! 189 | # The direct owner of the daemonSet 190 | owner: Resource 191 | # The root owner of the daemonSet 192 | rootOwner: Resource 193 | # The pods controlled by this daemonSet 194 | pods: [Pod!]! 195 | } 196 | 197 | # A deployment 198 | type Deployment implements Resource { 199 | # The metadata for the deployment (name, labels, namespace, etc.) 200 | metadata: Metadata! 201 | # Description of the deployment 202 | spec: DeploymentSpec! 203 | # The direct owner of the deployment 204 | owner: Resource 205 | # The root owner of the deployment 206 | rootOwner: Resource 207 | # The replicaSets that are children of this deployment 208 | replicaSets: [ReplicaSet!]! 209 | } 210 | 211 | # A deployment specification 212 | type DeploymentSpec { 213 | # Minimum number of seconds for which a newly created pod should be 214 | # ready without any of its container crashing, for it to be considered 215 | # available (defaults to 0) 216 | minReadySeconds: Int! 217 | # Whether or not the deployment is paused 218 | paused: Boolean! 219 | # The maximum time in seconds for a deployment to make progress before 220 | # it is considered to be failed. 221 | progressDeadlineSeconds: Int! 222 | # Number of desired pods (default: 1). 223 | replicas: Int! 224 | # The number of old ReplicaSets to retain to allow rollback (default: 10). 225 | revisionHistoryLimit: Int! 226 | # Label selector for pods. 227 | selector: LabelSelector 228 | # The deployment strategy to use to replace existing pods with new ones. 229 | strategy: DeploymentStrategy! 230 | # Template describing the pods that will be created. 231 | template: PodTemplateSpec! 232 | } 233 | 234 | # metadata 235 | type Metadata { # annotations?? 236 | # When was the decorated object created 237 | creationTimestamp: String 238 | # Prefix for generated names 239 | generateName: String 240 | # Sequence number for state transitions 241 | generation: Int 242 | # Top level labels 243 | labels: [Label!]! 244 | # Generated name 245 | name: String 246 | # Namespace containing the object 247 | namespace: String 248 | # All owners 249 | ownerReferences: [Resource!] 250 | # Version 251 | resourceVersion: String 252 | # How to find this object 253 | selfLink: String 254 | # UUID 255 | uid: String 256 | } 257 | 258 | # PodTemplateSpec 259 | type PodTemplateSpec { 260 | # standard metadata 261 | metadata: Metadata! 262 | # Specification for generated pods 263 | spec: PodSpec! 264 | } 265 | 266 | # PodSpec 267 | type PodSpec { 268 | # Optional duration in seconds the pod may be active on the node 269 | # relative to StartTime before the system will actively try to mark it 270 | # failed and kill associated containers. Value must be a positive integer. 271 | activeDeadlineSeconds: Int 272 | # Scheduling constraints for the pod, if specified 273 | affinity: Affinity 274 | # AutomountServiceAccountToken indicates whether a service account token 275 | # should be automatically mounted. 276 | automountServiceAccountToken: Boolean! 277 | # List of containers belonging to the pod 278 | containers: [Container!]! 279 | # DNS parameters of a pod 280 | dnsConfig: PodDNSConfig 281 | # DNS policy for the pod -- defaults to "ClusterFirst" 282 | dnsPolicy: DNSPolicy 283 | # List of hosts and IPs to inject into pod host file 284 | hostAliases: [HostAlias!] 285 | # Whether or not to use the host ipc namespace -- default: false 286 | hostIPC: Boolean! 287 | # Whether or not to use host networking -- default: false 288 | hostNetwork: Boolean! 289 | # Whether or not to use the host pid namespace -- default: false 290 | hostPID: Boolean! 291 | # Hostname for the pod -- defaults to system-generated value 292 | hostname: String 293 | # List of references to secrets in the same namespace used to pull images 294 | imagePullSecrets: [LocalObjectReference!] 295 | # Initialization containers for the pod 296 | initContainers: [Container!] 297 | # Name of specific host on which to schedule the pod (if any) 298 | nodeName: String 299 | # Priority of the pod 300 | priority: Int 301 | # Class name of priority class for the pod 302 | priorityClassName: String 303 | # Conditions to evaluate for pod readiness 304 | readinessGates: [PodReadinessGate!] 305 | # Restart policy for all containers within the pod -- default: Always 306 | restartPolicy: RestartPolicy! 307 | # Specific scheduler to use for pod 308 | schedulerName: String 309 | # Pod-level security attributes and common container settings 310 | securityContext: PodSecurityContext 311 | # Name of service account to use when running this pod 312 | serviceAccountName: String 313 | # Whether or not to share a single process namespace between all containers 314 | # default: false 315 | shareProcessNamespace: Boolean! 316 | # Desired pod subdomain, if any 317 | subdomain: String 318 | # Duration in seconds the pod needs to terminate gracefully -- default: 30s 319 | terminationGracePeriodSeconds: Int! 320 | # Tolerations for the pod 321 | tolerations: [Toleration!] 322 | # List of volumes that can be mounted by containers in the pod 323 | volumes: [Volume!] 324 | } 325 | 326 | # Persistent storage 327 | type Volume { 328 | # 329 | # INCOMPLETE 330 | # 331 | 332 | # config map used to populate the volume 333 | configMap: ConfigMapVolumeSource 334 | # volume name -- must be a DNS_LABEL and unique within the pod 335 | name: String! 336 | # a pre-existing file or directory on the host machine 337 | hostPath: HostPathVolumeSource 338 | # reference to a persistent volume claim 339 | persistentVolumeClaim: PersistentVolumeClaimVolumeSource 340 | # a secret that should populate the volume 341 | secret: SecretVolumeSource 342 | } 343 | 344 | # Volume sources 345 | type ConfigMapVolumeSource { 346 | # mode bits to use on created files by default -- default value: 0644 347 | defaultMode: Int 348 | # specific config map items to expose -- if unset, expose all 349 | items: [KeyToPath!] 350 | # name of map 351 | name: String! 352 | # Is it okay if the config map does not exist? -- default: false 353 | optional: Boolean! 354 | } 355 | 356 | type HostPathVolumeSource { 357 | # path of the directory on the host 358 | path: String! 359 | # type for host path volume -- defaults to: "" 360 | type: String 361 | } 362 | 363 | type PersistentVolumeClaimVolumeSource { 364 | # name of a PersistentVolumeClaim in the same namespace as the pod 365 | claimName: String! 366 | # Is the claim read only? -- default: false 367 | readOnly: Boolean! 368 | } 369 | 370 | type SecretVolumeSource { 371 | # mode bits to use on created files by default -- default value: 0644 372 | defaultMode: Int 373 | # specific secret items to expose -- if unset, expose all 374 | items: [KeyToPath!] 375 | # Is it okay if the secret does not exist? -- default: false 376 | optional: Boolean! 377 | # name of secret 378 | secretName: String! 379 | } 380 | 381 | # kernel parameters to set 382 | type Sysctl { 383 | # name of a parameter 384 | name: String! 385 | # parameter value 386 | value: String! 387 | } 388 | 389 | # mapping of string key to path within a volume 390 | type KeyToPath { 391 | # the key to project 392 | key: String! 393 | # mode bits to use on the file [0 .. 0777]; if empty, uses volume default 394 | mode: Int 395 | # relative path of the file to map 396 | path: String! 397 | } 398 | 399 | # Reference to a pod condition 400 | type PodReadinessGate { 401 | # a condition in the condition list for the pod 402 | conditionType: String! 403 | } 404 | 405 | # Pod-level security attributes and common container settings 406 | type PodSecurityContext { 407 | # supplemental group that applies to all containers in a pod 408 | fsGroup: Int 409 | # GID for container process entrypoint 410 | runAsGroup: Int 411 | # Must the container run as non-root? 412 | runAsNonRoot: Boolean! 413 | # UID for container process entrypoint 414 | runAsUser: Int 415 | # SELinux context to apply to all containers 416 | seLinuxOptions: SELinuxOptions 417 | # list of groups applied to the first process run in each container in 418 | # addition to the primary group 419 | supplementalGroups: [Int!] 420 | # list of namespaced sysctls user for the pod 421 | sysctls: [Sysctl!] 422 | } 423 | 424 | # Conditions determining whether a node can host a particular pod 425 | type Toleration { 426 | # the taint effect to match 427 | effect: TolerationEffect 428 | # the taint key the toleration references; empty means match all 429 | key: String 430 | # relationship between key and value 431 | operator: TolerationOperator 432 | # the period of time the toleration tolerates the taint; unset means 433 | # forever, zero or negative means immediately evict 434 | tolerationSeconds: Int 435 | # taint value matched by the toleration -- should be empty if operator 436 | # is "Exists" 437 | value: String 438 | } 439 | 440 | # Operators for tolerations 441 | enum TolerationOperator { 442 | Exists Equal 443 | } 444 | 445 | # Toleration effect possible values 446 | enum TolerationEffect { 447 | NoSchedule PreferNoSchedule NoExecute 448 | } 449 | 450 | # SELinux labels to apply to a container 451 | type SELinuxOptions { 452 | # level label 453 | level: String 454 | # role label 455 | role: String 456 | # type label 457 | type: String 458 | #user label 459 | user: String 460 | } 461 | 462 | # LocalObjectReference 463 | type LocalObjectReference { 464 | # name of referenced object 465 | name: String! 466 | } 467 | 468 | # DNS support 469 | type PodDNSConfig { 470 | # list of DNS name server IP addresses 471 | nameservers: [String!] 472 | # DNS resolver options 473 | options: [PodDNSConfigOption!] 474 | # DNS search domains for host-name lookup 475 | searches: [String!] 476 | } 477 | 478 | # Options for DNS config 479 | type PodDNSConfigOption { 480 | name: String! 481 | value: String! 482 | } 483 | 484 | # HostAlias 485 | type HostAlias { 486 | # Hostnames for the IP address 487 | hostnames: [String!]! 488 | # IP address of the host 489 | ip: String! 490 | } 491 | 492 | # Container 493 | type Container { 494 | # XXX not yet 495 | } 496 | 497 | # Affinity 498 | type Affinity { 499 | # affinity with nodes 500 | nodeAffinity: NodeAffinity 501 | # affinity with other pods 502 | podAffinity: PodAffinity 503 | # anti-affinity with other pods 504 | podAntiAffinity: PodAntiAffinity 505 | } 506 | 507 | # NodeAffinity 508 | type NodeAffinity { 509 | # nodes satisfying these conditions will be preferred 510 | preferredDuringSchedulingIgnoredDuringExecution: [PreferredSchedulingTerm!] 511 | # nodes satisfying these conditions will be required 512 | requiredDuringSchedulingIgnoredDuringExecution: NodeSelector 513 | } 514 | 515 | # NodeSelector 516 | type NodeSelector { 517 | # list of terms used to match nodes for deployment (ORed together) 518 | nodeSelectorTerms: [NodeSelectorTerm!]! 519 | } 520 | 521 | # NodeSelectorTerm 522 | type NodeSelectorTerm { 523 | # node requirements based on node labels 524 | matchExpressions: [NodeSelectorRequirement!] 525 | # node requirements based on node fields 526 | matchFields: [NodeSelectorRequirement!] 527 | } 528 | 529 | # Requirement expression matched against node labels 530 | type NodeSelectorRequirement { 531 | # The node key that the selector applies to 532 | key: String! 533 | # The expression operator 534 | operator: NodeOperator! 535 | # The values to match against 536 | values: [String!] 537 | } 538 | 539 | # PodAffinity 540 | type PodAffinity { 541 | # nodes satisfying these conditions will be preferred 542 | preferredDuringSchedulingIgnoredDuringExecution: [WeightedPodAffinityTerm!] 543 | # nodes satisfying these conditions will be required 544 | requiredDuringSchedulingIgnoredDuringExecution: PodAffinityTerm 545 | } 546 | 547 | # PodAntiAffinity 548 | type PodAntiAffinity { 549 | # nodes satisfying these conditions will be preferred 550 | preferredDuringSchedulingIgnoredDuringExecution: [WeightedPodAffinityTerm!] 551 | # nodes satisfying these conditions will be required 552 | requiredDuringSchedulingIgnoredDuringExecution: PodAffinityTerm 553 | } 554 | 555 | # WeightedPodAffinityTerm 556 | type WeightedPodAffinityTerm { 557 | # term associated with a weight 558 | podAffinityTerm: PodAffinityTerm! 559 | # weight for the term 560 | weight: Int! 561 | } 562 | 563 | # PodAffinityTerm 564 | type PodAffinityTerm { 565 | # selector to match other pod labels 566 | labelSelector: LabelSelector 567 | # which namespaces to match against -- defaults to the pod namespace 568 | namespaces: [String!] 569 | # whether the pod should be co-located or not co-located with pods 570 | # matching the selector 571 | topologyKey: String! 572 | } 573 | 574 | # PreferredSchedulingTerm 575 | type PreferredSchedulingTerm { 576 | # node preference 577 | preference: NodeSelectorTerm! 578 | # weight associated with the term 579 | weight: Int! 580 | } 581 | 582 | # Node operator values 583 | enum NodeOperator { 584 | In NotIn Exists DoesNotExist Gt Lt 585 | } 586 | 587 | # RestartPolicy values 588 | enum RestartPolicy { 589 | Always OnFailure Never 590 | } 591 | 592 | # DNSPolicy values 593 | enum DNSPolicy { 594 | ClusterFirstWithHostNet ClusterFirst Default None 595 | } 596 | 597 | # LabelSelector for matching pods 598 | type LabelSelector { 599 | # constraint expressions for labels 600 | matchExpressions: [LabelSelectorRequirement!] 601 | # key/value matches 602 | matchLabels: [Label!] 603 | } 604 | 605 | # Constraint expression for labels 606 | type LabelSelectorRequirement { 607 | # The label key that the selector applies to 608 | key: String! 609 | # The expression operator 610 | operator: LabelOperator! 611 | # The values to match against 612 | values: [String!]! 613 | } 614 | 615 | # Constraint operators for labels 616 | enum LabelOperator { 617 | In NotIn Exists DoesNotExist 618 | } 619 | 620 | # deployment strategy 621 | type DeploymentStrategy { 622 | # Rolling update config parameters 623 | rollingUpdate: RollingUpdateDeployment 624 | # Type of deployment 625 | type: DeploymentStrategyType 626 | } 627 | 628 | # Types of deployment strategy 629 | enum DeploymentStrategyType { 630 | Recreate RollingUpdate 631 | } 632 | 633 | # The following section is a mess due to the questionable decision by 634 | # the Kubernetes team to make certain fields contain either ints or 635 | # strings (WHY?????) 636 | 637 | # rolling update parameters 638 | type RollingUpdateDeployment { 639 | # The maximum number of pods that can be scheduled above the desired 640 | # number of pods. 641 | maxSurgeInt: Int 642 | maxSurgeString: String 643 | # The maximum number of pods that can be unavailable during the update. 644 | maxUnavailableInt: Int 645 | maxUnavailableString: String 646 | } 647 | 648 | # A label 649 | type Label { 650 | # label name 651 | name: String! 652 | # label value 653 | value: String! 654 | } 655 | 656 | # Any Kubernetes resource 657 | interface Resource { 658 | # type of resource 659 | kind: String! 660 | # resource metadata 661 | metadata: Metadata! 662 | # resource direct owner 663 | owner: Resource 664 | # resource root owner 665 | rootOwner: Resource 666 | } 667 | ` 668 | 669 | const PodKind = "Pod" 670 | const ReplicaSetKind = "ReplicaSet" 671 | const StatefulSetKind = "StatefulSet" 672 | const DaemonSetKind = "DaemonSet" 673 | const DeploymentKind = "Deployment" 674 | const ServiceKind = "Service" 675 | 676 | // The root of all queries and mutations. All defined queries and mutations 677 | // start as methods on Resolver 678 | type Resolver struct { 679 | } 680 | 681 | // Objects in json are unmarshalled into map[string]interface{} 682 | type JsonObject = map[string]interface{} 683 | type JsonArray = []interface{} 684 | 685 | // Pod lookups 686 | func (r *Resolver) AllPods(ctx context.Context) *[]*podResolver { 687 | pset := getAllK8sObjsOfKind( 688 | ctx, 689 | PodKind, 690 | func(jobj JsonObject) bool { return true }) 691 | 692 | results := make([]*podResolver, len(pset)) 693 | 694 | for idx, p := range pset { 695 | results[idx] = p.(*podResolver) 696 | } 697 | 698 | return &results 699 | } 700 | 701 | func (r *Resolver) AllPodsInNamespace( 702 | ctx context.Context, 703 | args *struct { 704 | Namespace string 705 | }) *[]*podResolver { 706 | pset := getAllK8sObjsOfKindInNamespace( 707 | ctx, 708 | PodKind, 709 | args.Namespace, 710 | func(jobj JsonObject) bool { return true }) 711 | 712 | results := make([]*podResolver, len(pset)) 713 | 714 | for idx, p := range pset { 715 | results[idx] = p.(*podResolver) 716 | } 717 | 718 | return &results 719 | } 720 | 721 | func (r *Resolver) PodByName( 722 | ctx context.Context, 723 | args *struct { 724 | Namespace string 725 | Name string 726 | }) *podResolver { 727 | res := getK8sResource(ctx, PodKind, args.Namespace, args.Name) 728 | if res == nil { 729 | return nil 730 | } 731 | return res.(*podResolver) 732 | } 733 | 734 | // Deployment lookups 735 | func (r *Resolver) AllDeployments(ctx context.Context) *[]*deploymentResolver { 736 | dset := getAllK8sObjsOfKind( 737 | ctx, 738 | DeploymentKind, 739 | func(jobj JsonObject) bool { return true }) 740 | 741 | results := make([]*deploymentResolver, len(dset)) 742 | 743 | for idx, d := range dset { 744 | results[idx] = d.(*deploymentResolver) 745 | } 746 | 747 | return &results 748 | } 749 | 750 | func (r *Resolver) AllDeploymentsInNamespace( 751 | ctx context.Context, 752 | args *struct { 753 | Namespace string 754 | }) *[]*deploymentResolver { 755 | dset := getAllK8sObjsOfKindInNamespace( 756 | ctx, 757 | DeploymentKind, 758 | args.Namespace, 759 | func(jobj JsonObject) bool { return true }) 760 | 761 | results := make([]*deploymentResolver, len(dset)) 762 | 763 | for idx, p := range dset { 764 | results[idx] = p.(*deploymentResolver) 765 | } 766 | 767 | return &results 768 | } 769 | 770 | func (r *Resolver) DeploymentByName( 771 | ctx context.Context, 772 | args *struct { 773 | Namespace string 774 | Name string 775 | }) *deploymentResolver { 776 | res := getK8sResource(ctx, DeploymentKind, args.Namespace, args.Name) 777 | if res == nil { 778 | return nil 779 | } 780 | return res.(*deploymentResolver) 781 | } 782 | 783 | // ReplicaSet lookups 784 | func (r *Resolver) AllReplicaSets(ctx context.Context) *[]*replicaSetResolver { 785 | rset := getAllK8sObjsOfKind( 786 | ctx, 787 | ReplicaSetKind, 788 | func(jobj JsonObject) bool { return true }) 789 | 790 | results := make([]*replicaSetResolver, len(rset)) 791 | 792 | for idx, r := range rset { 793 | results[idx] = r.(*replicaSetResolver) 794 | } 795 | 796 | return &results 797 | } 798 | 799 | func (r *Resolver) AllReplicaSetsInNamespace( 800 | ctx context.Context, 801 | args *struct { 802 | Namespace string 803 | }) *[]*replicaSetResolver { 804 | rset := getAllK8sObjsOfKindInNamespace( 805 | ctx, 806 | ReplicaSetKind, 807 | args.Namespace, 808 | func(jobj JsonObject) bool { return true }) 809 | 810 | results := make([]*replicaSetResolver, len(rset)) 811 | 812 | for idx, p := range rset { 813 | results[idx] = p.(*replicaSetResolver) 814 | } 815 | 816 | return &results 817 | } 818 | 819 | func (r *Resolver) ReplicaSetByName( 820 | ctx context.Context, 821 | args *struct { 822 | Namespace string 823 | Name string 824 | }) *replicaSetResolver { 825 | res := getK8sResource(ctx, ReplicaSetKind, args.Namespace, args.Name) 826 | if res == nil { 827 | return nil 828 | } 829 | return res.(*replicaSetResolver) 830 | } 831 | 832 | // StatefulSet lookups 833 | func (r *Resolver) AllStatefulSets(ctx context.Context) *[]*statefulSetResolver { 834 | sset := getAllK8sObjsOfKind( 835 | ctx, 836 | StatefulSetKind, 837 | func(jobj JsonObject) bool { return true }) 838 | 839 | results := make([]*statefulSetResolver, len(sset)) 840 | 841 | for idx, s := range sset { 842 | results[idx] = s.(*statefulSetResolver) 843 | } 844 | 845 | return &results 846 | } 847 | 848 | func (r *Resolver) AllStatefulSetsInNamespace( 849 | ctx context.Context, 850 | args *struct { 851 | Namespace string 852 | }) *[]*statefulSetResolver { 853 | sset := getAllK8sObjsOfKindInNamespace( 854 | ctx, 855 | StatefulSetKind, 856 | args.Namespace, 857 | func(jobj JsonObject) bool { return true }) 858 | 859 | results := make([]*statefulSetResolver, len(sset)) 860 | 861 | for idx, p := range sset { 862 | results[idx] = p.(*statefulSetResolver) 863 | } 864 | 865 | return &results 866 | } 867 | 868 | func (r *Resolver) StatefulSetByName( 869 | ctx context.Context, 870 | args *struct { 871 | Namespace string 872 | Name string 873 | }) *statefulSetResolver { 874 | res := getK8sResource(ctx, StatefulSetKind, args.Namespace, args.Name) 875 | if res == nil { 876 | return nil 877 | } 878 | return res.(*statefulSetResolver) 879 | } 880 | 881 | // Service lookups 882 | func (r *Resolver) AllServices(ctx context.Context) *[]*serviceResolver { 883 | sset := getAllK8sObjsOfKind( 884 | ctx, 885 | ServiceKind, 886 | func(jobj JsonObject) bool { return true }) 887 | 888 | results := make([]*serviceResolver, len(sset)) 889 | 890 | for idx, s := range sset { 891 | results[idx] = s.(*serviceResolver) 892 | } 893 | 894 | return &results 895 | } 896 | 897 | func (r *Resolver) AllServicesInNamespace( 898 | ctx context.Context, 899 | args *struct { 900 | Namespace string 901 | }) *[]*serviceResolver { 902 | sset := getAllK8sObjsOfKindInNamespace( 903 | ctx, 904 | ServiceKind, 905 | args.Namespace, 906 | func(jobj JsonObject) bool { return true }) 907 | 908 | results := make([]*serviceResolver, len(sset)) 909 | 910 | for idx, p := range sset { 911 | results[idx] = p.(*serviceResolver) 912 | } 913 | 914 | return &results 915 | } 916 | 917 | func (r *Resolver) ServiceByName( 918 | ctx context.Context, 919 | args *struct { 920 | Namespace string 921 | Name string 922 | }) *serviceResolver { 923 | res := getK8sResource(ctx, ServiceKind, args.Namespace, args.Name) 924 | if res == nil { 925 | return nil 926 | } 927 | return res.(*serviceResolver) 928 | } 929 | 930 | // DaemonSet lookups 931 | func (r *Resolver) AllDaemonSets(ctx context.Context) *[]*daemonSetResolver { 932 | dset := getAllK8sObjsOfKind( 933 | ctx, 934 | DaemonSetKind, 935 | func(jobj JsonObject) bool { return true }) 936 | 937 | results := make([]*daemonSetResolver, len(dset)) 938 | 939 | for idx, d := range dset { 940 | results[idx] = d.(*daemonSetResolver) 941 | } 942 | 943 | return &results 944 | } 945 | 946 | func (r *Resolver) AllDaemonSetsInNamespace( 947 | ctx context.Context, 948 | args *struct { 949 | Namespace string 950 | }) *[]*daemonSetResolver { 951 | dset := getAllK8sObjsOfKindInNamespace( 952 | ctx, 953 | DaemonSetKind, 954 | args.Namespace, 955 | func(jobj JsonObject) bool { return true }) 956 | 957 | results := make([]*daemonSetResolver, len(dset)) 958 | 959 | for idx, p := range dset { 960 | results[idx] = p.(*daemonSetResolver) 961 | } 962 | 963 | return &results 964 | } 965 | 966 | func (r *Resolver) DaemonSetByName( 967 | ctx context.Context, 968 | args *struct { 969 | Namespace string 970 | Name string 971 | }) *daemonSetResolver { 972 | res := getK8sResource(ctx, DaemonSetKind, args.Namespace, args.Name) 973 | if res == nil { 974 | return nil 975 | } 976 | return res.(*daemonSetResolver) 977 | } 978 | -------------------------------------------------------------------------------- /label.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 CA. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | ) 20 | 21 | // Single label value within a Kubernetes object 22 | type label struct { 23 | Name string 24 | Value string 25 | } 26 | 27 | type labelResolver struct { 28 | ctx context.Context 29 | l *label 30 | } 31 | 32 | // Translate unmarshalled json into a set of labels 33 | func mapToLabels(lMap JsonObject) *[]label { 34 | var labels []label 35 | 36 | for k, v := range lMap { 37 | labels = append(labels, label{k, v.(string)}) 38 | } 39 | 40 | return &labels 41 | } 42 | 43 | func (r labelResolver) Name() string { 44 | return r.l.Name 45 | } 46 | 47 | func (r labelResolver) Value() string { 48 | return r.l.Value 49 | } 50 | -------------------------------------------------------------------------------- /metadata.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 CA. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | // "fmt" 20 | "sort" 21 | ) 22 | 23 | // Kubernetes metadata 24 | type metadata struct { 25 | CreationTimestamp *string 26 | GenerateName *string 27 | Generation *int32 28 | Labels *[]label 29 | Name *string 30 | Namespace *string 31 | OwnerReferences *[]resource 32 | ResourceVersion *string 33 | SelfLink *string 34 | Uid *string 35 | } 36 | 37 | type metadataResolver struct { 38 | ctx context.Context 39 | m metadata 40 | } 41 | 42 | // Translate unmarshalled json into a metadata object 43 | func mapToMetadata( 44 | ctx context.Context, ns string, jsonObj JsonObject) metadata { 45 | var m metadata 46 | var orefs []resource 47 | if ct, ok := jsonObj["creationTimestamp"].(string); ok { 48 | m.CreationTimestamp = &ct 49 | } else { 50 | m.CreationTimestamp = nil 51 | } 52 | if gn, ok := jsonObj["generateName"].(string); ok { 53 | m.GenerateName = &gn 54 | } else { 55 | m.GenerateName = nil 56 | } 57 | if genVal := jsonObj["generation"]; genVal != nil { 58 | if num, ok := genVal.(float64); ok { 59 | numval := int32(num) 60 | m.Generation = &numval 61 | } else { 62 | m.Generation = (genVal.(*int32)) 63 | } 64 | } 65 | jg := jgetter(jsonObj) 66 | m.Labels = mapToLabels(mapItem(jsonObj, "labels")) 67 | m.Name = jg.stringRefItemOr("name", nil) 68 | m.Namespace = jg.stringRefItemOr("namespace", nil) 69 | m.ResourceVersion = jg.stringRefItemOr("resourceVersion", nil) 70 | m.SelfLink = jg.stringRefItemOr("selfLink", nil) 71 | m.Uid = jg.stringRefItemOr("uid", nil) 72 | 73 | // Similar to getOwner 74 | if orArray := jsonObj["ownerReferences"]; orArray != nil { 75 | for _, oref := range orArray.(JsonArray) { 76 | ormap := oref.(JsonObject) 77 | orefs = append( 78 | orefs, 79 | getK8sResource( 80 | ctx, 81 | ormap["kind"].(string), 82 | ns, 83 | ormap["name"].(string))) 84 | } 85 | } 86 | 87 | m.OwnerReferences = &orefs 88 | return m 89 | } 90 | 91 | // Metadata methods 92 | func (r metadataResolver) CreationTimestamp() *string { 93 | return r.m.CreationTimestamp 94 | } 95 | 96 | func (r metadataResolver) GenerateName() *string { 97 | return r.m.GenerateName 98 | } 99 | 100 | func (r metadataResolver) Generation() *int32 { 101 | return r.m.Generation 102 | } 103 | 104 | func (r metadataResolver) Labels() []*labelResolver { 105 | var labelResolvers []*labelResolver 106 | for _, label := range *r.m.Labels { 107 | lab := label 108 | labelResolvers = append(labelResolvers, &labelResolver{r.ctx, &lab}) 109 | } 110 | sort.Slice( 111 | labelResolvers, 112 | func(i, j int) bool { 113 | return labelResolvers[i].Name() < labelResolvers[j].Name() 114 | }) 115 | return labelResolvers 116 | } 117 | 118 | func (r metadataResolver) Name() *string { 119 | return r.m.Name 120 | } 121 | 122 | func (r metadataResolver) Namespace() *string { 123 | return r.m.Namespace 124 | } 125 | 126 | func (r metadataResolver) OwnerReferences() *[]*resourceResolver { 127 | var ownerResolvers []*resourceResolver 128 | for _, owner := range *r.m.OwnerReferences { 129 | own := owner 130 | ownerResolvers = append(ownerResolvers, &resourceResolver{r.ctx, own}) 131 | } 132 | return &ownerResolvers 133 | } 134 | 135 | func (r metadataResolver) ResourceVersion() *string { 136 | return r.m.ResourceVersion 137 | } 138 | 139 | func (r metadataResolver) SelfLink() *string { 140 | return r.m.SelfLink 141 | } 142 | 143 | func (r metadataResolver) Uid() *string { 144 | return r.m.Uid 145 | } 146 | -------------------------------------------------------------------------------- /owner.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 CA. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | // "fmt" 20 | ) 21 | 22 | // Tracks ownership relationships between Kubernetes objects. If an object 23 | // has no owner, we treat it as its own owner 24 | type ownerRef struct { 25 | ctx context.Context 26 | ref JsonObject 27 | cachedOwner resource // cached info for on-demand lookup 28 | } 29 | 30 | // resource method implementations 31 | func (r *ownerRef) Kind() string { 32 | if r.cachedOwner == nil { 33 | r.cachedOwner = getOwner(r.ctx, r.ref) 34 | } 35 | return r.cachedOwner.Kind() 36 | } 37 | 38 | func (r *ownerRef) Metadata() metadataResolver { 39 | if r.cachedOwner == nil { 40 | r.cachedOwner = getOwner(r.ctx, r.ref) 41 | } 42 | return r.cachedOwner.Metadata() 43 | } 44 | 45 | func (r *ownerRef) Owner() *resourceResolver { 46 | if r.cachedOwner == nil { 47 | r.cachedOwner = getOwner(r.ctx, r.ref) 48 | } 49 | return r.cachedOwner.Owner() 50 | } 51 | 52 | func (r *ownerRef) RootOwner() *resourceResolver { 53 | if r.cachedOwner == nil { 54 | r.cachedOwner = getOwner(r.ctx, r.ref) 55 | } 56 | return r.cachedOwner.RootOwner() 57 | } 58 | 59 | // Fetch owners by getting ownerReferences and doing lookups based 60 | // on their contents 61 | func getRawOwner( 62 | ctx context.Context, val JsonObject) JsonObject { 63 | if orefs := getMetadataField(val, "ownerReferences"); orefs != nil { 64 | oArray := orefs.(JsonArray) 65 | if len(oArray) > 0 { 66 | owner := oArray[0].(JsonObject) 67 | if res := getRawK8sResource( 68 | ctx, 69 | owner["kind"].(string), 70 | getNamespace(val), 71 | owner["name"].(string)); res != nil { 72 | return res 73 | } 74 | } 75 | } 76 | 77 | return val 78 | } 79 | 80 | func getOwner(ctx context.Context, val JsonObject) resource { 81 | return mapToResource(ctx, getRawOwner(ctx, val)) 82 | } 83 | 84 | func getRootOwner(ctx context.Context, val JsonObject) resource { 85 | result := getRawOwner(ctx, val) 86 | 87 | if getUid(result) == getUid(val) { 88 | return mapToResource(ctx, result) 89 | } 90 | 91 | return getRootOwner(ctx, getRawOwner(ctx, result)) 92 | } 93 | 94 | func hasMatchingOwner(jsonObj JsonObject, name, kind string) bool { 95 | if orefs := getMetadataField(jsonObj, "ownerReferences"); orefs != nil { 96 | oArray := orefs.(JsonArray) 97 | for _, oref := range oArray { 98 | owner := oref.(JsonObject) 99 | if owner["name"] == name && owner["kind"] == kind { 100 | return true 101 | } 102 | } 103 | } 104 | 105 | return false 106 | } 107 | -------------------------------------------------------------------------------- /replicaset.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 CA. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | // "fmt" 20 | "sort" 21 | "strings" 22 | ) 23 | 24 | // ReplicaSets manage replicated pods 25 | type replicaSet struct { 26 | Metadata metadata 27 | Owner resource 28 | RootOwner resource 29 | Pods *[]pod 30 | } 31 | 32 | type replicaSetResolver struct { 33 | ctx context.Context 34 | r replicaSet 35 | } 36 | 37 | // Translate unmarshalled json into a deployment object 38 | func mapToReplicaSet( 39 | ctx context.Context, 40 | jsonObj JsonObject) replicaSet { 41 | placeholder := &ownerRef{ctx, jsonObj, nil} 42 | owner := placeholder 43 | rootOwner := placeholder 44 | meta := 45 | mapToMetadata(ctx, getNamespace(jsonObj), mapItem(jsonObj, "metadata")) 46 | return replicaSet{meta, owner, rootOwner, nil} 47 | } 48 | 49 | // ReplicaSets have pods as children 50 | func getReplicaSetPods(ctx context.Context, r replicaSet) *[]pod { 51 | rsName := *r.Metadata.Name 52 | rsNamePrefix := rsName + "-" 53 | rsNamespace := *r.Metadata.Namespace 54 | 55 | pset := getAllK8sObjsOfKindInNamespace( 56 | ctx, 57 | PodKind, 58 | rsNamespace, 59 | func(jobj JsonObject) bool { 60 | return (strings.HasPrefix(getName(jobj), rsNamePrefix) && 61 | hasMatchingOwner(jobj, rsName, ReplicaSetKind)) 62 | }) 63 | 64 | results := make([]pod, len(pset)) 65 | 66 | for idx, p := range pset { 67 | pr := p.(*podResolver) 68 | results[idx] = pr.p 69 | } 70 | 71 | sort.Slice( 72 | results, 73 | func(i, j int) bool { 74 | return *results[i].Metadata.Name < *results[j].Metadata.Name 75 | }) 76 | 77 | return &results 78 | } 79 | 80 | // Resource method implementations 81 | func (r *replicaSetResolver) Kind() string { 82 | return ReplicaSetKind 83 | } 84 | 85 | func (r *replicaSetResolver) Metadata() metadataResolver { 86 | return metadataResolver{r.ctx, r.r.Metadata} 87 | } 88 | 89 | func (r *replicaSetResolver) Owner() *resourceResolver { 90 | if oref, ok := r.r.Owner.(*ownerRef); ok { 91 | r.r.Owner = getOwner(oref.ctx, oref.ref) 92 | } 93 | return &resourceResolver{r.ctx, r.r.Owner} 94 | } 95 | 96 | func (r *replicaSetResolver) RootOwner() *resourceResolver { 97 | if oref, ok := r.r.Owner.(*ownerRef); ok { 98 | r.r.Owner = getOwner(oref.ctx, oref.ref) 99 | } 100 | return &resourceResolver{r.ctx, r.r.RootOwner} 101 | } 102 | 103 | // Resolve child Pods 104 | func (r *replicaSetResolver) Pods() []*podResolver { 105 | if r.r.Pods == nil { 106 | r.r.Pods = getReplicaSetPods(r.ctx, r.r) 107 | } 108 | 109 | var res []*podResolver 110 | for _, p := range *r.r.Pods { 111 | res = append(res, &podResolver{r.ctx, p}) 112 | } 113 | if res == nil { 114 | res = make([]*podResolver, 0) 115 | } 116 | return res 117 | } 118 | -------------------------------------------------------------------------------- /resource.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 CA. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | ) 21 | 22 | // Represents all "active" components: (Pods, Deployments, DaemonSets, 23 | // StatefulSets, ReplicaSets 24 | 25 | type resource interface { 26 | Kind() string 27 | Metadata() metadataResolver 28 | Owner() *resourceResolver 29 | RootOwner() *resourceResolver 30 | } 31 | 32 | type resourceResolver struct { 33 | ctx context.Context 34 | r resource 35 | } 36 | 37 | // Translate a map containing unmarshalled json into a resource instance. 38 | func mapToResource( 39 | ctx context.Context, 40 | rMap JsonObject) resource { 41 | kind := getKind(rMap) 42 | 43 | switch kind { 44 | case DeploymentKind: 45 | mtd := mapToDeployment(ctx, rMap) 46 | return &deploymentResolver{ctx, mtd} 47 | case ReplicaSetKind: 48 | return &replicaSetResolver{ctx, mapToReplicaSet(ctx, rMap)} 49 | case DaemonSetKind: 50 | return &daemonSetResolver{ctx, mapToDaemonSet(ctx, rMap)} 51 | case StatefulSetKind: 52 | return &statefulSetResolver{ctx, mapToStatefulSet(ctx, rMap)} 53 | case PodKind: 54 | return &podResolver{ctx, mapToPod(ctx, rMap)} 55 | case ServiceKind: 56 | return &serviceResolver{ctx, mapToService(ctx, rMap)} 57 | } 58 | 59 | fmt.Printf("BAD KIND: %v\n", kind) 60 | return nil 61 | } 62 | 63 | // Turn an instance of a resource into one of its implementers 64 | func (r *resourceResolver) ToPod() (*podResolver, bool) { 65 | c, ok := r.r.(*podResolver) 66 | return c, ok 67 | } 68 | 69 | func (r *resourceResolver) ToReplicaSet() (*replicaSetResolver, bool) { 70 | c, ok := r.r.(*replicaSetResolver) 71 | return c, ok 72 | } 73 | 74 | func (r *resourceResolver) ToDaemonSet() (*daemonSetResolver, bool) { 75 | c, ok := r.r.(*daemonSetResolver) 76 | return c, ok 77 | } 78 | 79 | func (r *resourceResolver) ToStatefulSet() (*statefulSetResolver, bool) { 80 | c, ok := r.r.(*statefulSetResolver) 81 | return c, ok 82 | } 83 | 84 | func (r *resourceResolver) ToDeployment() (*deploymentResolver, bool) { 85 | c, ok := r.r.(*deploymentResolver) 86 | return c, ok 87 | } 88 | 89 | func (r *resourceResolver) ToService() (*serviceResolver, bool) { 90 | c, ok := r.r.(*serviceResolver) 91 | return c, ok 92 | } 93 | 94 | // Implementations of the methods common to all resources 95 | func (r *resourceResolver) Kind() string { 96 | return r.r.Kind() 97 | } 98 | 99 | func (r *resourceResolver) Metadata() metadataResolver { 100 | return r.r.Metadata() 101 | } 102 | 103 | func (r *resourceResolver) Owner() *resourceResolver { 104 | return r.r.Owner() 105 | } 106 | 107 | func (r *resourceResolver) RootOwner() *resourceResolver { 108 | return r.r.RootOwner() 109 | } 110 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 CA. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "github.com/gorilla/mux" 21 | graphql "github.com/neelance/graphql-go" 22 | "github.com/neelance/graphql-go/relay" 23 | "log" 24 | "net/http" 25 | "os" 26 | ) 27 | 28 | var KubectlPath = getFromEnv("KUBECTL_PATH", "/usr/local/bin/kubectl") 29 | var schema *graphql.Schema 30 | 31 | var ApiHost = getFromEnv("API_HOST", "http://localhost:8080") 32 | var ApiSecretPath = getFromEnv("API_SECRET_PATH", "") 33 | 34 | func getFromEnv(key, defval string) string { 35 | val, ok := os.LookupEnv(key) 36 | if !ok { 37 | return defval 38 | } else { 39 | return val 40 | } 41 | } 42 | 43 | func init() { 44 | var err error 45 | schema, err = graphql.ParseSchema(Schema, &Resolver{}) 46 | fmt.Println(schema) 47 | if err != nil { 48 | panic(err) 49 | } 50 | initCache() 51 | initWatchers() 52 | } 53 | 54 | func main() { 55 | r := mux.NewRouter() 56 | handler := &relay.Handler{Schema: schema} 57 | r.HandleFunc( 58 | "/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 59 | w.Write(page) 60 | })) 61 | r.Handle("/query", 62 | http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 63 | cache := make(JsonObject) 64 | handler.ServeHTTP(w, 65 | r.WithContext( 66 | context.WithValue( 67 | r.Context(), 68 | "queryCache", 69 | &cache))) 70 | })).Methods("POST") 71 | r.HandleFunc("/query", 72 | http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 73 | w.Header().Add("Access-Control-Allow-Headers", "Content-Type") 74 | })).Methods("OPTIONS") 75 | log.Fatal(http.ListenAndServe(":8128", r)) 76 | } 77 | 78 | var page = []byte(` 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 |
Loading...
90 | 113 | 114 | 115 | `) 116 | -------------------------------------------------------------------------------- /service.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 CA. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | // "fmt" 20 | ) 21 | 22 | // Services expose functionality to the outside world 23 | type service struct { 24 | Metadata metadata 25 | Spec serviceSpec 26 | Selected []*resource 27 | } 28 | 29 | type serviceSpec struct { 30 | ClusterIP *string 31 | ExternalIPs *[]string 32 | ExternalName *string 33 | ExternalTrafficPolicy *string 34 | HealthCheckNodePort *int32 35 | LoadBalancerIP *string 36 | LoadBalancerSourceRanges *[]string 37 | Ports *[]servicePort 38 | PublishNotReadyAddresses *bool 39 | Selector []label 40 | SessionAffinity *string 41 | SessionAffinityConfig *sessionAffinityConfig 42 | Type string 43 | } 44 | 45 | type sessionAffinityConfig struct { 46 | ClientIP clientIPConfig 47 | } 48 | 49 | type clientIPConfig struct { 50 | TimeoutSeconds int32 51 | } 52 | 53 | type servicePort struct { 54 | Name *string 55 | NodePort *int32 56 | Port int32 57 | Protocol *string 58 | TargetPortString *string 59 | TargetPortInt *int32 60 | } 61 | 62 | type serviceResolver struct { 63 | ctx context.Context 64 | s service 65 | } 66 | 67 | type serviceSpecResolver struct { 68 | ctx context.Context 69 | s serviceSpec 70 | } 71 | 72 | type sessionAffinityConfigResolver struct { 73 | ctx context.Context 74 | s sessionAffinityConfig 75 | } 76 | 77 | type clientIPConfigResolver struct { 78 | ctx context.Context 79 | s clientIPConfig 80 | } 81 | 82 | type servicePortResolver struct { 83 | ctx context.Context 84 | s servicePort 85 | } 86 | 87 | // Translate unmarshalled json into a deployment object 88 | func mapToService( 89 | ctx context.Context, 90 | jsonObj JsonObject) service { 91 | ns := getNamespace(jsonObj) 92 | meta := mapToMetadata(ctx, ns, mapItem(jsonObj, "metadata")) 93 | return service{ 94 | meta, 95 | extractServiceSpec(ctx, jsonObj), 96 | extractSelected(ctx, ns, jsonObj)} 97 | } 98 | 99 | func extractSelected(ctx context.Context, ns string, jsonObj JsonObject) []*resource { 100 | return []*resource{} 101 | } 102 | 103 | func extractServiceSpec(ctx context.Context, jsonObj JsonObject) serviceSpec { 104 | return serviceSpec{ 105 | nil, 106 | nil, 107 | nil, 108 | nil, 109 | nil, 110 | nil, 111 | nil, 112 | nil, 113 | nil, 114 | []label{}, 115 | nil, 116 | nil, 117 | "ClientIP"} 118 | } 119 | 120 | // Service implementations 121 | func (r *serviceResolver) Kind() string { 122 | return ServiceKind 123 | } 124 | 125 | func (r *serviceResolver) Metadata() metadataResolver { 126 | return metadataResolver{r.ctx, r.s.Metadata} 127 | } 128 | 129 | func (r *serviceResolver) Spec() serviceSpecResolver { 130 | return serviceSpecResolver{r.ctx, r.s.Spec} 131 | } 132 | 133 | func (r *serviceResolver) Owner() *resourceResolver { 134 | return &resourceResolver{r.ctx, &serviceResolver{r.ctx, r.s}} 135 | } 136 | 137 | func (r *serviceResolver) RootOwner() *resourceResolver { 138 | return &resourceResolver{r.ctx, &serviceResolver{r.ctx, r.s}} 139 | } 140 | 141 | func (r *serviceResolver) Selected() []*resourceResolver { 142 | return getSelectedResources(r) 143 | } 144 | 145 | func getSelectedResources(r *serviceResolver) []*resourceResolver { 146 | ns := (*r).s.Metadata.Namespace 147 | ls := (*r).s.Spec.Selector 148 | objs := getAllK8sObjsOfKindInNamespace(r.ctx, PodKind, *ns, 149 | func(jo JsonObject) bool { 150 | labels := getMatchLabels(jo) 151 | for _, label := range ls { 152 | seenMatch := false 153 | for k, v := range labels { 154 | if k == label.Name { 155 | seenMatch = true 156 | if v != label.Value { 157 | return false 158 | } 159 | } 160 | } 161 | if !seenMatch { 162 | return false 163 | } 164 | } 165 | return true 166 | }) 167 | results := make([]*resourceResolver, len(objs)) 168 | for idx, val := range objs { 169 | results[idx] = &resourceResolver{r.ctx, val} 170 | } 171 | return results 172 | } 173 | 174 | func getMatchLabels(jo JsonObject) JsonObject { 175 | kind := getKind(jo) 176 | 177 | switch kind { 178 | case PodKind: 179 | return getMetadataField(jo, "labels").(JsonObject) 180 | } 181 | 182 | return JsonObject{} 183 | } 184 | 185 | // Service Spec implementations 186 | func (r serviceSpecResolver) ClusterIP() *string { 187 | return r.s.ClusterIP 188 | } 189 | 190 | func (r serviceSpecResolver) ExternalIPs() *[]string { 191 | return r.s.ExternalIPs 192 | } 193 | 194 | func (r serviceSpecResolver) ExternalName() *string { 195 | return r.s.ExternalName 196 | } 197 | 198 | func (r serviceSpecResolver) ExternalTrafficPolicy() *string { 199 | return r.s.ExternalTrafficPolicy 200 | } 201 | 202 | func (r serviceSpecResolver) HealthCheckNodePort() *int32 { 203 | return r.s.HealthCheckNodePort 204 | } 205 | 206 | func (r serviceSpecResolver) LoadBalancerIP() *string { 207 | return r.s.LoadBalancerIP 208 | } 209 | 210 | func (r serviceSpecResolver) LoadBalancerSourceRanges() *[]string { 211 | return r.s.LoadBalancerSourceRanges 212 | } 213 | 214 | func (r serviceSpecResolver) Ports() *[]servicePortResolver { 215 | s := r.s.Ports 216 | if s == nil || len(*s) == 0 { 217 | res := make([]servicePortResolver, 0) 218 | return &res 219 | } 220 | resolvers := make([]servicePortResolver, len(*s)) 221 | for idx, val := range *s { 222 | resolvers[idx] = servicePortResolver{r.ctx, val} 223 | } 224 | return &resolvers 225 | } 226 | 227 | func (r serviceSpecResolver) PublishNotReadyAddresses() *bool { 228 | return r.s.PublishNotReadyAddresses 229 | } 230 | 231 | func (r serviceSpecResolver) Selector() []labelResolver { 232 | s := r.s.Selector 233 | if len(s) == 0 { 234 | res := make([]labelResolver, 0) 235 | return res 236 | } 237 | resolvers := make([]labelResolver, len(s)) 238 | for idx, val := range s { 239 | l := val 240 | resolvers[idx] = labelResolver{r.ctx, &l} 241 | } 242 | return resolvers 243 | } 244 | 245 | func (r serviceSpecResolver) SessionAffinity() *string { 246 | return r.s.SessionAffinity 247 | } 248 | 249 | func (r serviceSpecResolver) SessionAffinityConfig() *sessionAffinityConfigResolver { 250 | if r.s.SessionAffinityConfig == nil { 251 | return nil 252 | } 253 | return &sessionAffinityConfigResolver{r.ctx, *r.s.SessionAffinityConfig} 254 | } 255 | 256 | func (r serviceSpecResolver) Type() string { 257 | return r.s.Type 258 | } 259 | 260 | // Service port implementations 261 | func (r servicePortResolver) Name() *string { 262 | return r.s.Name 263 | } 264 | 265 | func (r servicePortResolver) NodePort() *int32 { 266 | return r.s.NodePort 267 | } 268 | 269 | func (r servicePortResolver) Port() int32 { 270 | return r.s.Port 271 | } 272 | 273 | func (r servicePortResolver) Protocol() *string { 274 | return r.s.Protocol 275 | } 276 | 277 | func (r servicePortResolver) TargetPortString() *string { 278 | return r.s.TargetPortString 279 | } 280 | 281 | func (r servicePortResolver) TargetPortInt() *int32 { 282 | return r.s.TargetPortInt 283 | } 284 | 285 | // SessionAffinityConfig implementations 286 | func (r *sessionAffinityConfigResolver) ClientIP() clientIPConfigResolver { 287 | return clientIPConfigResolver{r.ctx, r.s.ClientIP} 288 | } 289 | 290 | // ClientIPConfig implementations 291 | func (r clientIPConfigResolver) TimeoutSeconds() int32 { 292 | return r.s.TimeoutSeconds 293 | } 294 | -------------------------------------------------------------------------------- /statefulset.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 CA. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | // "fmt" 20 | "strings" 21 | ) 22 | 23 | // StatefulSets manage pods that have dependencies 24 | type statefulSet struct { 25 | Metadata metadata 26 | Owner resource 27 | RootOwner resource 28 | Pods *[]pod 29 | } 30 | 31 | type statefulSetResolver struct { 32 | ctx context.Context 33 | s statefulSet 34 | } 35 | 36 | // Translate unmarshalled json into a deployment object 37 | func mapToStatefulSet( 38 | ctx context.Context, 39 | jsonObj JsonObject) statefulSet { 40 | placeholder := &ownerRef{ctx, jsonObj, nil} 41 | owner := placeholder 42 | rootOwner := placeholder 43 | meta := 44 | mapToMetadata(ctx, getNamespace(jsonObj), mapItem(jsonObj, "metadata")) 45 | return statefulSet{meta, owner, rootOwner, nil} 46 | } 47 | 48 | // StatefulSets have pods as children 49 | func getStatefulSetPods(ctx context.Context, s statefulSet) *[]pod { 50 | ssName := *s.Metadata.Name 51 | ssNamePrefix := ssName + "-" 52 | ssNamespace := *s.Metadata.Namespace 53 | 54 | pset := getAllK8sObjsOfKindInNamespace( 55 | ctx, 56 | PodKind, 57 | ssNamespace, 58 | func(jobj JsonObject) bool { 59 | return (strings.HasPrefix(getName(jobj), ssNamePrefix) && 60 | hasMatchingOwner(jobj, ssName, StatefulSetKind)) 61 | }) 62 | 63 | results := make([]pod, len(pset)) 64 | 65 | for idx, p := range pset { 66 | pr := p.(*podResolver) 67 | results[idx] = pr.p 68 | } 69 | 70 | return &results 71 | } 72 | 73 | // Resource method implementations 74 | func (r *statefulSetResolver) Kind() string { 75 | return StatefulSetKind 76 | } 77 | 78 | func (r *statefulSetResolver) Metadata() metadataResolver { 79 | return metadataResolver{r.ctx, r.s.Metadata} 80 | } 81 | 82 | func (r *statefulSetResolver) Owner() *resourceResolver { 83 | if oref, ok := r.s.Owner.(*ownerRef); ok { 84 | r.s.Owner = getOwner(oref.ctx, oref.ref) 85 | } 86 | return &resourceResolver{r.ctx, r.s.Owner} 87 | } 88 | 89 | func (r *statefulSetResolver) RootOwner() *resourceResolver { 90 | if oref, ok := r.s.Owner.(*ownerRef); ok { 91 | r.s.Owner = getOwner(oref.ctx, oref.ref) 92 | } 93 | return &resourceResolver{r.ctx, r.s.RootOwner} 94 | } 95 | 96 | // Resolve child Pods 97 | func (r *statefulSetResolver) Pods() []*podResolver { 98 | if r.s.Pods == nil { 99 | r.s.Pods = getStatefulSetPods(r.ctx, r.s) 100 | } 101 | 102 | var res []*podResolver 103 | for _, p := range *r.s.Pods { 104 | res = append(res, &podResolver{r.ctx, p}) 105 | } 106 | if res == nil { 107 | res = make([]*podResolver, 0) 108 | } 109 | return res 110 | } 111 | -------------------------------------------------------------------------------- /testdata/daemonset.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "extensions/v1beta1", 3 | "kind": "DaemonSet", 4 | "metadata": { 5 | "creationTimestamp": "2018-07-26T00:58:25Z", 6 | "generation": 1, 7 | "labels": { 8 | "k8s-app": "calico-node" 9 | }, 10 | "name": "calico-node", 11 | "namespace": "kube-system", 12 | "resourceVersion": "47029", 13 | "selfLink": "/apis/extensions/v1beta1/namespaces/kube-system/daemonsets/calico-node", 14 | "uid": "004e17fc-906f-11e8-bb3f-c29cc5a41b2a" 15 | }, 16 | "spec": { 17 | "revisionHistoryLimit": 10, 18 | "selector": { 19 | "matchLabels": { 20 | "k8s-app": "calico-node" 21 | } 22 | }, 23 | "template": { 24 | "metadata": { 25 | "annotations": { 26 | "scheduler.alpha.kubernetes.io/critical-pod": "" 27 | }, 28 | "creationTimestamp": null, 29 | "labels": { 30 | "k8s-app": "calico-node" 31 | } 32 | }, 33 | "spec": { 34 | "affinity": { 35 | "nodeAffinity": { 36 | "requiredDuringSchedulingIgnoredDuringExecution": { 37 | "nodeSelectorTerms": [ 38 | { 39 | "matchExpressions": [ 40 | { 41 | "key": "ibmcloud.io/systemd-calico-node", 42 | "operator": "DoesNotExist" 43 | } 44 | ] 45 | } 46 | ] 47 | } 48 | } 49 | }, 50 | "containers": [ 51 | { 52 | "env": [ 53 | { 54 | "name": "ETCD_ENDPOINTS", 55 | "valueFrom": { 56 | "configMapKeyRef": { 57 | "key": "etcd_endpoints", 58 | "name": "calico-config" 59 | } 60 | } 61 | }, 62 | { 63 | "name": "CALICO_NETWORKING_BACKEND", 64 | "valueFrom": { 65 | "configMapKeyRef": { 66 | "key": "calico_backend", 67 | "name": "calico-config" 68 | } 69 | } 70 | }, 71 | { 72 | "name": "CLUSTER_TYPE", 73 | "value": "k8s,bgp" 74 | }, 75 | { 76 | "name": "CALICO_DISABLE_FILE_LOGGING", 77 | "value": "true" 78 | }, 79 | { 80 | "name": "FELIX_DEFAULTENDPOINTTOHOSTACTION", 81 | "value": "ACCEPT" 82 | }, 83 | { 84 | "name": "FELIX_IPTABLESREFRESHINTERVAL", 85 | "value": "60" 86 | }, 87 | { 88 | "name": "FELIX_PROMETHEUSMETRICSENABLED", 89 | "value": "true" 90 | }, 91 | { 92 | "name": "FELIX_PROMETHEUSMETRICSPORT", 93 | "value": "9091" 94 | }, 95 | { 96 | "name": "IP", 97 | "value": "autodetect" 98 | }, 99 | { 100 | "name": "IP_AUTODETECTION_METHOD", 101 | "value": "interface=(^bond0$|^eth0$|^ens6$)" 102 | }, 103 | { 104 | "name": "CALICO_IPV4POOL_CIDR", 105 | "value": "172.30.0.0/16" 106 | }, 107 | { 108 | "name": "CALICO_IPV4POOL_IPIP", 109 | "value": "cross-subnet" 110 | }, 111 | { 112 | "name": "CALICO_K8S_NODE_REF", 113 | "valueFrom": { 114 | "fieldRef": { 115 | "apiVersion": "v1", 116 | "fieldPath": "spec.nodeName" 117 | } 118 | } 119 | }, 120 | { 121 | "name": "FELIX_IPV6SUPPORT", 122 | "value": "false" 123 | }, 124 | { 125 | "name": "FELIX_LOGSEVERITYSCREEN", 126 | "value": "info" 127 | }, 128 | { 129 | "name": "FELIX_IPINIPMTU", 130 | "value": "1480" 131 | }, 132 | { 133 | "name": "ETCD_CA_CERT_FILE", 134 | "valueFrom": { 135 | "configMapKeyRef": { 136 | "key": "etcd_ca", 137 | "name": "calico-config" 138 | } 139 | } 140 | }, 141 | { 142 | "name": "ETCD_KEY_FILE", 143 | "valueFrom": { 144 | "configMapKeyRef": { 145 | "key": "etcd_key", 146 | "name": "calico-config" 147 | } 148 | } 149 | }, 150 | { 151 | "name": "ETCD_CERT_FILE", 152 | "valueFrom": { 153 | "configMapKeyRef": { 154 | "key": "etcd_cert", 155 | "name": "calico-config" 156 | } 157 | } 158 | }, 159 | { 160 | "name": "FELIX_HEALTHENABLED", 161 | "value": "true" 162 | } 163 | ], 164 | "image": "registry.ng.bluemix.net/armada-master/node:v2.6.5", 165 | "imagePullPolicy": "IfNotPresent", 166 | "livenessProbe": { 167 | "failureThreshold": 6, 168 | "httpGet": { 169 | "path": "/liveness", 170 | "port": 9099, 171 | "scheme": "HTTP" 172 | }, 173 | "initialDelaySeconds": 10, 174 | "periodSeconds": 10, 175 | "successThreshold": 1, 176 | "timeoutSeconds": 1 177 | }, 178 | "name": "calico-node", 179 | "readinessProbe": { 180 | "failureThreshold": 3, 181 | "httpGet": { 182 | "path": "/readiness", 183 | "port": 9099, 184 | "scheme": "HTTP" 185 | }, 186 | "periodSeconds": 10, 187 | "successThreshold": 1, 188 | "timeoutSeconds": 1 189 | }, 190 | "resources": { 191 | "requests": { 192 | "cpu": "250m" 193 | } 194 | }, 195 | "securityContext": { 196 | "privileged": true 197 | }, 198 | "terminationMessagePath": "/dev/termination-log", 199 | "terminationMessagePolicy": "File", 200 | "volumeMounts": [ 201 | { 202 | "mountPath": "/lib/modules", 203 | "name": "lib-modules", 204 | "readOnly": true 205 | }, 206 | { 207 | "mountPath": "/var/run/calico", 208 | "name": "var-run-calico" 209 | }, 210 | { 211 | "mountPath": "/calico-secrets", 212 | "name": "etcd-certs" 213 | } 214 | ] 215 | }, 216 | { 217 | "command": [ 218 | "/install-cni.sh" 219 | ], 220 | "env": [ 221 | { 222 | "name": "ETCD_ENDPOINTS", 223 | "valueFrom": { 224 | "configMapKeyRef": { 225 | "key": "etcd_endpoints", 226 | "name": "calico-config" 227 | } 228 | } 229 | }, 230 | { 231 | "name": "CNI_NETWORK_CONFIG", 232 | "valueFrom": { 233 | "configMapKeyRef": { 234 | "key": "cni_network_config", 235 | "name": "calico-config" 236 | } 237 | } 238 | } 239 | ], 240 | "image": "registry.ng.bluemix.net/armada-master/cni:v1.11.2", 241 | "imagePullPolicy": "IfNotPresent", 242 | "name": "install-cni", 243 | "resources": {}, 244 | "terminationMessagePath": "/dev/termination-log", 245 | "terminationMessagePolicy": "File", 246 | "volumeMounts": [ 247 | { 248 | "mountPath": "/host/opt/cni/bin", 249 | "name": "cni-bin-dir" 250 | }, 251 | { 252 | "mountPath": "/host/etc/cni/net.d", 253 | "name": "cni-net-dir" 254 | }, 255 | { 256 | "mountPath": "/calico-secrets", 257 | "name": "etcd-certs" 258 | } 259 | ] 260 | } 261 | ], 262 | "dnsPolicy": "ClusterFirst", 263 | "hostNetwork": true, 264 | "restartPolicy": "Always", 265 | "schedulerName": "default-scheduler", 266 | "securityContext": {}, 267 | "serviceAccount": "calico-node", 268 | "serviceAccountName": "calico-node", 269 | "terminationGracePeriodSeconds": 0, 270 | "tolerations": [ 271 | { 272 | "operator": "Exists" 273 | } 274 | ], 275 | "volumes": [ 276 | { 277 | "hostPath": { 278 | "path": "/lib/modules", 279 | "type": "" 280 | }, 281 | "name": "lib-modules" 282 | }, 283 | { 284 | "hostPath": { 285 | "path": "/var/run/calico", 286 | "type": "" 287 | }, 288 | "name": "var-run-calico" 289 | }, 290 | { 291 | "hostPath": { 292 | "path": "/opt/cni/bin", 293 | "type": "" 294 | }, 295 | "name": "cni-bin-dir" 296 | }, 297 | { 298 | "hostPath": { 299 | "path": "/etc/cni/net.d", 300 | "type": "" 301 | }, 302 | "name": "cni-net-dir" 303 | }, 304 | { 305 | "name": "etcd-certs", 306 | "secret": { 307 | "defaultMode": 420, 308 | "secretName": "calico-etcd-secrets" 309 | } 310 | } 311 | ] 312 | } 313 | }, 314 | "templateGeneration": 1, 315 | "updateStrategy": { 316 | "rollingUpdate": { 317 | "maxUnavailable": 5 318 | }, 319 | "type": "RollingUpdate" 320 | } 321 | }, 322 | "status": { 323 | "currentNumberScheduled": 1, 324 | "desiredNumberScheduled": 1, 325 | "numberAvailable": 1, 326 | "numberMisscheduled": 0, 327 | "numberReady": 1, 328 | "observedGeneration": 1, 329 | "updatedNumberScheduled": 1 330 | } 331 | } 332 | -------------------------------------------------------------------------------- /testdata/deployment.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "apps/v1", 3 | "kind": "Deployment", 4 | "metadata": { 5 | "annotations": { 6 | "deployment.kubernetes.io/revision": "1" 7 | }, 8 | "creationTimestamp": "2018-07-02T14:53:53Z", 9 | "generation": 1, 10 | "labels": { 11 | "app": "clunky-sabertooth-joomla", 12 | "chart": "joomla-2.0.2", 13 | "heritage": "Tiller", 14 | "release": "clunky-sabertooth" 15 | }, 16 | "name": "clunky-sabertooth-joomla", 17 | "namespace": "default", 18 | "resourceVersion": "109757", 19 | "selfLink": "/apis/apps/v1/namespaces/default/deployments/clunky-sabertooth-joomla", 20 | "uid": "bcc99362-7e07-11e8-9fd6-0800279d9e51" 21 | }, 22 | "spec": { 23 | "progressDeadlineSeconds": 600, 24 | "replicas": 1, 25 | "revisionHistoryLimit": 10, 26 | "selector": { 27 | "matchLabels": { 28 | "app": "clunky-sabertooth-joomla" 29 | } 30 | }, 31 | "strategy": { 32 | "rollingUpdate": { 33 | "maxSurge": 1, 34 | "maxUnavailable": 1 35 | }, 36 | "type": "RollingUpdate" 37 | }, 38 | "template": { 39 | "metadata": { 40 | "creationTimestamp": null, 41 | "labels": { 42 | "app": "clunky-sabertooth-joomla" 43 | } 44 | }, 45 | "spec": { 46 | "containers": [ 47 | { 48 | "env": [ 49 | { 50 | "name": "ALLOW_EMPTY_PASSWORD", 51 | "value": "yes" 52 | }, 53 | { 54 | "name": "MARIADB_HOST", 55 | "value": "clunky-sabertooth-mariadb" 56 | }, 57 | { 58 | "name": "MARIADB_PORT_NUMBER", 59 | "value": "3306" 60 | }, 61 | { 62 | "name": "JOOMLA_DATABASE_NAME", 63 | "value": "bitnami_joomla" 64 | }, 65 | { 66 | "name": "JOOMLA_DATABASE_USER", 67 | "value": "bn_joomla" 68 | }, 69 | { 70 | "name": "JOOMLA_DATABASE_PASSWORD", 71 | "valueFrom": { 72 | "secretKeyRef": { 73 | "key": "mariadb-password", 74 | "name": "clunky-sabertooth-mariadb" 75 | } 76 | } 77 | }, 78 | { 79 | "name": "JOOMLA_USERNAME", 80 | "value": "user" 81 | }, 82 | { 83 | "name": "JOOMLA_PASSWORD", 84 | "valueFrom": { 85 | "secretKeyRef": { 86 | "key": "joomla-password", 87 | "name": "clunky-sabertooth-joomla" 88 | } 89 | } 90 | }, 91 | { 92 | "name": "JOOMLA_EMAIL", 93 | "value": "user@example.com" 94 | } 95 | ], 96 | "image": "docker.io/bitnami/joomla:3.8.10", 97 | "imagePullPolicy": "IfNotPresent", 98 | "name": "clunky-sabertooth-joomla", 99 | "ports": [ 100 | { 101 | "containerPort": 80, 102 | "name": "http", 103 | "protocol": "TCP" 104 | }, 105 | { 106 | "containerPort": 443, 107 | "name": "https", 108 | "protocol": "TCP" 109 | } 110 | ], 111 | "readinessProbe": { 112 | "failureThreshold": 6, 113 | "httpGet": { 114 | "path": "/index.php", 115 | "port": "http", 116 | "scheme": "HTTP" 117 | }, 118 | "initialDelaySeconds": 30, 119 | "periodSeconds": 10, 120 | "successThreshold": 1, 121 | "timeoutSeconds": 5 122 | }, 123 | "resources": { 124 | "requests": { 125 | "cpu": "300m", 126 | "memory": "512Mi" 127 | } 128 | }, 129 | "terminationMessagePath": "/dev/termination-log", 130 | "terminationMessagePolicy": "File", 131 | "volumeMounts": [ 132 | { 133 | "mountPath": "/bitnami/joomla", 134 | "name": "joomla-data" 135 | }, 136 | { 137 | "mountPath": "/bitnami/apache", 138 | "name": "apache-data" 139 | } 140 | ] 141 | } 142 | ], 143 | "dnsPolicy": "ClusterFirst", 144 | "restartPolicy": "Always", 145 | "schedulerName": "default-scheduler", 146 | "securityContext": null, 147 | "terminationGracePeriodSeconds": 30, 148 | "volumes": [ 149 | { 150 | "name": "joomla-data", 151 | "persistentVolumeClaim": { 152 | "claimName": "clunky-sabertooth-joomla-joomla" 153 | } 154 | }, 155 | { 156 | "name": "apache-data", 157 | "persistentVolumeClaim": { 158 | "claimName": "clunky-sabertooth-joomla-apache" 159 | } 160 | } 161 | ] 162 | } 163 | } 164 | }, 165 | "status": { 166 | "availableReplicas": 1, 167 | "conditions": [ 168 | { 169 | "lastTransitionTime": "2018-07-02T14:53:53Z", 170 | "lastUpdateTime": "2018-07-02T14:53:53Z", 171 | "message": "Deployment has minimum availability.", 172 | "reason": "MinimumReplicasAvailable", 173 | "status": "True", 174 | "type": "Available" 175 | }, 176 | { 177 | "lastTransitionTime": "2018-07-02T14:53:53Z", 178 | "lastUpdateTime": "2018-07-02T14:55:31Z", 179 | "message": "ReplicaSet \"clunky-sabertooth-joomla-5d4ddc985d\" has successfully progressed.", 180 | "reason": "NewReplicaSetAvailable", 181 | "status": "True", 182 | "type": "Progressing" 183 | } 184 | ], 185 | "observedGeneration": 1, 186 | "readyReplicas": 1, 187 | "replicas": 1, 188 | "updatedReplicas": 1 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /testdata/pod1.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "v1", 3 | "kind": "Pod", 4 | "metadata": { 5 | "creationTimestamp": "2018-07-02T14:53:53Z", 6 | "generateName": "clunky-sabertooth-joomla-5d4ddc985d-", 7 | "labels": { 8 | "app": "clunky-sabertooth-joomla", 9 | "pod-template-hash": "1808875418" 10 | }, 11 | "name": "clunky-sabertooth-joomla-5d4ddc985d-fpddz", 12 | "namespace": "default", 13 | "ownerReferences": [ 14 | { 15 | "apiVersion": "extensions/v1beta1", 16 | "blockOwnerDeletion": true, 17 | "controller": true, 18 | "kind": "ReplicaSet", 19 | "name": "clunky-sabertooth-joomla-5d4ddc985d", 20 | "uid": "bcccd488-7e07-11e8-9fd6-0800279d9e51" 21 | } 22 | ], 23 | "resourceVersion": "109754", 24 | "selfLink": "/api/v1/namespaces/default/pods/clunky-sabertooth-joomla-5d4ddc985d-fpddz", 25 | "uid": "bccfe4b9-7e07-11e8-9fd6-0800279d9e51" 26 | }, 27 | "spec": { 28 | "containers": [ 29 | { 30 | "env": [ 31 | { 32 | "name": "ALLOW_EMPTY_PASSWORD", 33 | "value": "yes" 34 | }, 35 | { 36 | "name": "MARIADB_HOST", 37 | "value": "clunky-sabertooth-mariadb" 38 | }, 39 | { 40 | "name": "MARIADB_PORT_NUMBER", 41 | "value": "3306" 42 | }, 43 | { 44 | "name": "JOOMLA_DATABASE_NAME", 45 | "value": "bitnami_joomla" 46 | }, 47 | { 48 | "name": "JOOMLA_DATABASE_USER", 49 | "value": "bn_joomla" 50 | }, 51 | { 52 | "name": "JOOMLA_DATABASE_PASSWORD", 53 | "valueFrom": { 54 | "secretKeyRef": { 55 | "key": "mariadb-password", 56 | "name": "clunky-sabertooth-mariadb" 57 | } 58 | } 59 | }, 60 | { 61 | "name": "JOOMLA_USERNAME", 62 | "value": "user" 63 | }, 64 | { 65 | "name": "JOOMLA_PASSWORD", 66 | "valueFrom": { 67 | "secretKeyRef": { 68 | "key": "joomla-password", 69 | "name": "clunky-sabertooth-joomla" 70 | } 71 | } 72 | }, 73 | { 74 | "name": "JOOMLA_EMAIL", 75 | "value": "user@example.com" 76 | } 77 | ], 78 | "image": "docker.io/bitnami/joomla:3.8.10", 79 | "imagePullPolicy": "IfNotPresent", 80 | "name": "clunky-sabertooth-joomla", 81 | "ports": [ 82 | { 83 | "containerPort": 80, 84 | "name": "http", 85 | "protocol": "TCP" 86 | }, 87 | { 88 | "containerPort": 443, 89 | "name": "https", 90 | "protocol": "TCP" 91 | } 92 | ], 93 | "readinessProbe": { 94 | "failureThreshold": 6, 95 | "httpGet": { 96 | "path": "/index.php", 97 | "port": "http", 98 | "scheme": "HTTP" 99 | }, 100 | "initialDelaySeconds": 30, 101 | "periodSeconds": 10, 102 | "successThreshold": 1, 103 | "timeoutSeconds": 5 104 | }, 105 | "resources": { 106 | "requests": { 107 | "cpu": "300m", 108 | "memory": "512Mi" 109 | } 110 | }, 111 | "terminationMessagePath": "/dev/termination-log", 112 | "terminationMessagePolicy": "File", 113 | "volumeMounts": [ 114 | { 115 | "mountPath": "/bitnami/joomla", 116 | "name": "joomla-data" 117 | }, 118 | { 119 | "mountPath": "/bitnami/apache", 120 | "name": "apache-data" 121 | }, 122 | { 123 | "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", 124 | "name": "default-token-l6lb2", 125 | "readOnly": true 126 | } 127 | ] 128 | } 129 | ], 130 | "dnsPolicy": "ClusterFirst", 131 | "nodeName": "minikube", 132 | "restartPolicy": "Always", 133 | "schedulerName": "default-scheduler", 134 | "securityContext": null, 135 | "serviceAccount": "default", 136 | "serviceAccountName": "default", 137 | "terminationGracePeriodSeconds": 30, 138 | "tolerations": [ 139 | { 140 | "effect": "NoExecute", 141 | "key": "node.kubernetes.io/not-ready", 142 | "operator": "Exists", 143 | "tolerationSeconds": 300 144 | }, 145 | { 146 | "effect": "NoExecute", 147 | "key": "node.kubernetes.io/unreachable", 148 | "operator": "Exists", 149 | "tolerationSeconds": 300 150 | } 151 | ], 152 | "volumes": [ 153 | { 154 | "name": "joomla-data", 155 | "persistentVolumeClaim": { 156 | "claimName": "clunky-sabertooth-joomla-joomla" 157 | } 158 | }, 159 | { 160 | "name": "apache-data", 161 | "persistentVolumeClaim": { 162 | "claimName": "clunky-sabertooth-joomla-apache" 163 | } 164 | }, 165 | { 166 | "name": "default-token-l6lb2", 167 | "secret": { 168 | "defaultMode": 420, 169 | "secretName": "default-token-l6lb2" 170 | } 171 | } 172 | ] 173 | }, 174 | "status": { 175 | "conditions": [ 176 | { 177 | "lastProbeTime": null, 178 | "lastTransitionTime": "2018-07-02T14:53:54Z", 179 | "status": "True", 180 | "type": "Initialized" 181 | }, 182 | { 183 | "lastProbeTime": null, 184 | "lastTransitionTime": "2018-07-02T14:55:31Z", 185 | "status": "True", 186 | "type": "Ready" 187 | }, 188 | { 189 | "lastProbeTime": null, 190 | "lastTransitionTime": "2018-07-02T14:53:54Z", 191 | "status": "True", 192 | "type": "PodScheduled" 193 | } 194 | ], 195 | "containerStatuses": [ 196 | { 197 | "containerID": "docker://4c933bb3d3018a3557e0b721c6c7b5b9a2b9c1670e616e54cbf5fb9883fd7e71", 198 | "image": "bitnami/joomla:3.8.10", 199 | "imageID": "docker-pullable://bitnami/joomla@sha256:da57ce10897126c95d56c802ef069ec89c175922113366038103de5d063dd376", 200 | "lastState": null, 201 | "name": "clunky-sabertooth-joomla", 202 | "ready": true, 203 | "restartCount": 0, 204 | "state": { 205 | "running": { 206 | "startedAt": "2018-07-02T14:54:24Z" 207 | } 208 | } 209 | } 210 | ], 211 | "hostIP": "10.0.2.15", 212 | "phase": "Running", 213 | "podIP": "172.17.0.8", 214 | "qosClass": "Burstable", 215 | "startTime": "2018-07-02T14:53:54Z" 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /testdata/pod2.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "v1", 3 | "kind": "Pod", 4 | "metadata": { 5 | "annotations": { 6 | "kubernetes.io/psp": "ibm-privileged-psp", 7 | "scheduler.alpha.kubernetes.io/critical-pod": "" 8 | }, 9 | "creationTimestamp": "2018-07-26T00:58:25Z", 10 | "generateName": "calico-node-", 11 | "labels": { 12 | "controller-revision-hash": "3909226423", 13 | "k8s-app": "calico-node", 14 | "pod-template-generation": "1" 15 | }, 16 | "name": "calico-node-ddxfj", 17 | "namespace": "kube-system", 18 | "ownerReferences": [ 19 | { 20 | "apiVersion": "extensions/v1beta1", 21 | "blockOwnerDeletion": true, 22 | "controller": true, 23 | "kind": "DaemonSet", 24 | "name": "calico-node", 25 | "uid": "004e17fc-906f-11e8-bb3f-c29cc5a41b2a" 26 | } 27 | ], 28 | "resourceVersion": "47028", 29 | "selfLink": "/api/v1/namespaces/kube-system/pods/calico-node-ddxfj", 30 | "uid": "00531b05-906f-11e8-bb3f-c29cc5a41b2a" 31 | }, 32 | "spec": { 33 | "affinity": { 34 | "nodeAffinity": { 35 | "requiredDuringSchedulingIgnoredDuringExecution": { 36 | "nodeSelectorTerms": [ 37 | { 38 | "matchExpressions": [ 39 | { 40 | "key": "ibmcloud.io/systemd-calico-node", 41 | "operator": "DoesNotExist" 42 | } 43 | ] 44 | } 45 | ] 46 | } 47 | } 48 | }, 49 | "containers": [ 50 | { 51 | "env": [ 52 | { 53 | "name": "ETCD_ENDPOINTS", 54 | "valueFrom": { 55 | "configMapKeyRef": { 56 | "key": "etcd_endpoints", 57 | "name": "calico-config" 58 | } 59 | } 60 | }, 61 | { 62 | "name": "CALICO_NETWORKING_BACKEND", 63 | "valueFrom": { 64 | "configMapKeyRef": { 65 | "key": "calico_backend", 66 | "name": "calico-config" 67 | } 68 | } 69 | }, 70 | { 71 | "name": "CLUSTER_TYPE", 72 | "value": "k8s,bgp" 73 | }, 74 | { 75 | "name": "CALICO_DISABLE_FILE_LOGGING", 76 | "value": "true" 77 | }, 78 | { 79 | "name": "FELIX_DEFAULTENDPOINTTOHOSTACTION", 80 | "value": "ACCEPT" 81 | }, 82 | { 83 | "name": "FELIX_IPTABLESREFRESHINTERVAL", 84 | "value": "60" 85 | }, 86 | { 87 | "name": "FELIX_PROMETHEUSMETRICSENABLED", 88 | "value": "true" 89 | }, 90 | { 91 | "name": "FELIX_PROMETHEUSMETRICSPORT", 92 | "value": "9091" 93 | }, 94 | { 95 | "name": "IP", 96 | "value": "autodetect" 97 | }, 98 | { 99 | "name": "IP_AUTODETECTION_METHOD", 100 | "value": "interface=(^bond0$|^eth0$|^ens6$)" 101 | }, 102 | { 103 | "name": "CALICO_IPV4POOL_CIDR", 104 | "value": "172.30.0.0/16" 105 | }, 106 | { 107 | "name": "CALICO_IPV4POOL_IPIP", 108 | "value": "cross-subnet" 109 | }, 110 | { 111 | "name": "CALICO_K8S_NODE_REF", 112 | "valueFrom": { 113 | "fieldRef": { 114 | "apiVersion": "v1", 115 | "fieldPath": "spec.nodeName" 116 | } 117 | } 118 | }, 119 | { 120 | "name": "FELIX_IPV6SUPPORT", 121 | "value": "false" 122 | }, 123 | { 124 | "name": "FELIX_LOGSEVERITYSCREEN", 125 | "value": "info" 126 | }, 127 | { 128 | "name": "FELIX_IPINIPMTU", 129 | "value": "1480" 130 | }, 131 | { 132 | "name": "ETCD_CA_CERT_FILE", 133 | "valueFrom": { 134 | "configMapKeyRef": { 135 | "key": "etcd_ca", 136 | "name": "calico-config" 137 | } 138 | } 139 | }, 140 | { 141 | "name": "ETCD_KEY_FILE", 142 | "valueFrom": { 143 | "configMapKeyRef": { 144 | "key": "etcd_key", 145 | "name": "calico-config" 146 | } 147 | } 148 | }, 149 | { 150 | "name": "ETCD_CERT_FILE", 151 | "valueFrom": { 152 | "configMapKeyRef": { 153 | "key": "etcd_cert", 154 | "name": "calico-config" 155 | } 156 | } 157 | }, 158 | { 159 | "name": "FELIX_HEALTHENABLED", 160 | "value": "true" 161 | } 162 | ], 163 | "image": "registry.ng.bluemix.net/armada-master/node:v2.6.5", 164 | "imagePullPolicy": "IfNotPresent", 165 | "livenessProbe": { 166 | "failureThreshold": 6, 167 | "httpGet": { 168 | "path": "/liveness", 169 | "port": 9099, 170 | "scheme": "HTTP" 171 | }, 172 | "initialDelaySeconds": 10, 173 | "periodSeconds": 10, 174 | "successThreshold": 1, 175 | "timeoutSeconds": 1 176 | }, 177 | "name": "calico-node", 178 | "readinessProbe": { 179 | "failureThreshold": 3, 180 | "httpGet": { 181 | "path": "/readiness", 182 | "port": 9099, 183 | "scheme": "HTTP" 184 | }, 185 | "periodSeconds": 10, 186 | "successThreshold": 1, 187 | "timeoutSeconds": 1 188 | }, 189 | "resources": { 190 | "requests": { 191 | "cpu": "250m" 192 | } 193 | }, 194 | "securityContext": { 195 | "privileged": true 196 | }, 197 | "terminationMessagePath": "/dev/termination-log", 198 | "terminationMessagePolicy": "File", 199 | "volumeMounts": [ 200 | { 201 | "mountPath": "/lib/modules", 202 | "name": "lib-modules", 203 | "readOnly": true 204 | }, 205 | { 206 | "mountPath": "/var/run/calico", 207 | "name": "var-run-calico" 208 | }, 209 | { 210 | "mountPath": "/calico-secrets", 211 | "name": "etcd-certs" 212 | }, 213 | { 214 | "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", 215 | "name": "calico-node-token-rczbq", 216 | "readOnly": true 217 | } 218 | ] 219 | }, 220 | { 221 | "command": [ 222 | "/install-cni.sh" 223 | ], 224 | "env": [ 225 | { 226 | "name": "ETCD_ENDPOINTS", 227 | "valueFrom": { 228 | "configMapKeyRef": { 229 | "key": "etcd_endpoints", 230 | "name": "calico-config" 231 | } 232 | } 233 | }, 234 | { 235 | "name": "CNI_NETWORK_CONFIG", 236 | "valueFrom": { 237 | "configMapKeyRef": { 238 | "key": "cni_network_config", 239 | "name": "calico-config" 240 | } 241 | } 242 | } 243 | ], 244 | "image": "registry.ng.bluemix.net/armada-master/cni:v1.11.2", 245 | "imagePullPolicy": "IfNotPresent", 246 | "name": "install-cni", 247 | "resources": {}, 248 | "terminationMessagePath": "/dev/termination-log", 249 | "terminationMessagePolicy": "File", 250 | "volumeMounts": [ 251 | { 252 | "mountPath": "/host/opt/cni/bin", 253 | "name": "cni-bin-dir" 254 | }, 255 | { 256 | "mountPath": "/host/etc/cni/net.d", 257 | "name": "cni-net-dir" 258 | }, 259 | { 260 | "mountPath": "/calico-secrets", 261 | "name": "etcd-certs" 262 | }, 263 | { 264 | "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", 265 | "name": "calico-node-token-rczbq", 266 | "readOnly": true 267 | } 268 | ] 269 | } 270 | ], 271 | "dnsPolicy": "ClusterFirst", 272 | "hostNetwork": true, 273 | "imagePullSecrets": [ 274 | { 275 | "name": "bluemix-default-secret" 276 | } 277 | ], 278 | "nodeName": "10.76.193.41", 279 | "restartPolicy": "Always", 280 | "schedulerName": "default-scheduler", 281 | "securityContext": {}, 282 | "serviceAccount": "calico-node", 283 | "serviceAccountName": "calico-node", 284 | "terminationGracePeriodSeconds": 0, 285 | "tolerations": [ 286 | { 287 | "operator": "Exists" 288 | }, 289 | { 290 | "effect": "NoExecute", 291 | "key": "node.kubernetes.io/not-ready", 292 | "operator": "Exists" 293 | }, 294 | { 295 | "effect": "NoExecute", 296 | "key": "node.kubernetes.io/unreachable", 297 | "operator": "Exists" 298 | }, 299 | { 300 | "effect": "NoSchedule", 301 | "key": "node.kubernetes.io/disk-pressure", 302 | "operator": "Exists" 303 | }, 304 | { 305 | "effect": "NoSchedule", 306 | "key": "node.kubernetes.io/memory-pressure", 307 | "operator": "Exists" 308 | } 309 | ], 310 | "volumes": [ 311 | { 312 | "hostPath": { 313 | "path": "/lib/modules", 314 | "type": "" 315 | }, 316 | "name": "lib-modules" 317 | }, 318 | { 319 | "hostPath": { 320 | "path": "/var/run/calico", 321 | "type": "" 322 | }, 323 | "name": "var-run-calico" 324 | }, 325 | { 326 | "hostPath": { 327 | "path": "/opt/cni/bin", 328 | "type": "" 329 | }, 330 | "name": "cni-bin-dir" 331 | }, 332 | { 333 | "hostPath": { 334 | "path": "/etc/cni/net.d", 335 | "type": "" 336 | }, 337 | "name": "cni-net-dir" 338 | }, 339 | { 340 | "name": "etcd-certs", 341 | "secret": { 342 | "defaultMode": 420, 343 | "secretName": "calico-etcd-secrets" 344 | } 345 | }, 346 | { 347 | "name": "calico-node-token-rczbq", 348 | "secret": { 349 | "defaultMode": 420, 350 | "secretName": "calico-node-token-rczbq" 351 | } 352 | } 353 | ] 354 | }, 355 | "status": { 356 | "conditions": [ 357 | { 358 | "lastProbeTime": null, 359 | "lastTransitionTime": "2018-07-26T00:58:25Z", 360 | "status": "True", 361 | "type": "Initialized" 362 | }, 363 | { 364 | "lastProbeTime": null, 365 | "lastTransitionTime": "2018-07-26T00:58:45Z", 366 | "status": "True", 367 | "type": "Ready" 368 | }, 369 | { 370 | "lastProbeTime": null, 371 | "lastTransitionTime": "2018-07-26T00:58:38Z", 372 | "status": "True", 373 | "type": "PodScheduled" 374 | } 375 | ], 376 | "containerStatuses": [ 377 | { 378 | "containerID": "docker://a609bcf2011800564e451b40ab915c21a645774e996acd054aaba6e69299be14", 379 | "image": "registry.ng.bluemix.net/armada-master/node:v2.6.5", 380 | "imageID": "docker-pullable://registry.ng.bluemix.net/armada-master/node@sha256:d79a25b3ef899332f9de73536634c9a3ce3fa12b3e77e504cff1e2d2a8d536e6", 381 | "lastState": {}, 382 | "name": "calico-node", 383 | "ready": true, 384 | "restartCount": 0, 385 | "state": { 386 | "running": { 387 | "startedAt": "2018-07-26T00:58:33Z" 388 | } 389 | } 390 | }, 391 | { 392 | "containerID": "docker://679df731797b5f3538ac083829da595bd0ffe38217104e676058ccb35586de52", 393 | "image": "registry.ng.bluemix.net/armada-master/cni:v1.11.2", 394 | "imageID": "docker-pullable://registry.ng.bluemix.net/armada-master/cni@sha256:df0606795dc43a5f9f0823db26661a722107b93cfc88c1e20ba80bec45634023", 395 | "lastState": {}, 396 | "name": "install-cni", 397 | "ready": true, 398 | "restartCount": 0, 399 | "state": { 400 | "running": { 401 | "startedAt": "2018-07-26T00:58:37Z" 402 | } 403 | } 404 | } 405 | ], 406 | "hostIP": "10.76.193.41", 407 | "phase": "Running", 408 | "podIP": "10.76.193.41", 409 | "qosClass": "Burstable", 410 | "startTime": "2018-07-26T00:58:25Z" 411 | } 412 | } 413 | -------------------------------------------------------------------------------- /testdata/pod3.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "v1", 3 | "kind": "Pod", 4 | "metadata": { 5 | "annotations": { 6 | "kubernetes.io/psp": "ibm-privileged-psp" 7 | }, 8 | "creationTimestamp": "2018-08-20T15:32:11Z", 9 | "generateName": "mongo-", 10 | "labels": { 11 | "app": "mongo", 12 | "controller-revision-hash": "mongo-fdd786d", 13 | "name": "mongo", 14 | "statefulset.kubernetes.io/pod-name": "mongo-0" 15 | }, 16 | "name": "mongo-0", 17 | "namespace": "flonjella", 18 | "ownerReferences": [ 19 | { 20 | "apiVersion": "apps/v1beta1", 21 | "blockOwnerDeletion": true, 22 | "controller": true, 23 | "kind": "StatefulSet", 24 | "name": "mongo", 25 | "uid": "34e2046e-a48e-11e8-bed1-42ec94e14baf" 26 | } 27 | ], 28 | "resourceVersion": "229453", 29 | "selfLink": "/api/v1/namespaces/flonjella/pods/mongo-0", 30 | "uid": "34e49788-a48e-11e8-bed1-42ec94e14baf" 31 | }, 32 | "spec": { 33 | "containers": [ 34 | { 35 | "command": [ 36 | "mongod", 37 | "--replSet", 38 | "rs0" 39 | ], 40 | "image": "mongo:3.4.1", 41 | "imagePullPolicy": "IfNotPresent", 42 | "name": "mongodb", 43 | "ports": [ 44 | { 45 | "containerPort": 27017, 46 | "name": "web", 47 | "protocol": "TCP" 48 | } 49 | ], 50 | "resources": null, 51 | "terminationMessagePath": "/dev/termination-log", 52 | "terminationMessagePolicy": "File", 53 | "volumeMounts": [ 54 | { 55 | "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", 56 | "name": "default-token-885vl", 57 | "readOnly": true 58 | } 59 | ] 60 | }, 61 | { 62 | "command": [ 63 | "bash", 64 | "/config/init.sh" 65 | ], 66 | "image": "mongo:3.4.1", 67 | "imagePullPolicy": "IfNotPresent", 68 | "name": "init-mongo", 69 | "resources": null, 70 | "terminationMessagePath": "/dev/termination-log", 71 | "terminationMessagePolicy": "File", 72 | "volumeMounts": [ 73 | { 74 | "mountPath": "/config", 75 | "name": "config" 76 | }, 77 | { 78 | "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", 79 | "name": "default-token-885vl", 80 | "readOnly": true 81 | } 82 | ] 83 | } 84 | ], 85 | "dnsPolicy": "ClusterFirst", 86 | "hostname": "mongo-0", 87 | "nodeName": "10.76.193.41", 88 | "restartPolicy": "Always", 89 | "schedulerName": "default-scheduler", 90 | "securityContext": null, 91 | "serviceAccount": "default", 92 | "serviceAccountName": "default", 93 | "subdomain": "mongo", 94 | "terminationGracePeriodSeconds": 30, 95 | "tolerations": [ 96 | { 97 | "effect": "NoExecute", 98 | "key": "node.kubernetes.io/not-ready", 99 | "operator": "Exists", 100 | "tolerationSeconds": 300 101 | }, 102 | { 103 | "effect": "NoExecute", 104 | "key": "node.kubernetes.io/unreachable", 105 | "operator": "Exists", 106 | "tolerationSeconds": 300 107 | } 108 | ], 109 | "volumes": [ 110 | { 111 | "configMap": { 112 | "defaultMode": 420, 113 | "name": "mongo-init" 114 | }, 115 | "name": "config" 116 | }, 117 | { 118 | "name": "default-token-885vl", 119 | "secret": { 120 | "defaultMode": 420, 121 | "secretName": "default-token-885vl" 122 | } 123 | } 124 | ] 125 | }, 126 | "status": { 127 | "conditions": [ 128 | { 129 | "lastProbeTime": null, 130 | "lastTransitionTime": "2018-08-20T15:32:11Z", 131 | "status": "True", 132 | "type": "Initialized" 133 | }, 134 | { 135 | "lastProbeTime": null, 136 | "lastTransitionTime": "2018-08-20T15:32:11Z", 137 | "message": "containers with unready status: [mongodb init-mongo]", 138 | "reason": "ContainersNotReady", 139 | "status": "False", 140 | "type": "Ready" 141 | }, 142 | { 143 | "lastProbeTime": null, 144 | "lastTransitionTime": "2018-08-20T15:32:11Z", 145 | "status": "True", 146 | "type": "PodScheduled" 147 | } 148 | ], 149 | "containerStatuses": [ 150 | { 151 | "image": "mongo:3.4.1", 152 | "imageID": "", 153 | "lastState": null, 154 | "name": "init-mongo", 155 | "ready": false, 156 | "restartCount": 0, 157 | "state": { 158 | "waiting": { 159 | "reason": "ContainerCreating" 160 | } 161 | } 162 | }, 163 | { 164 | "image": "mongo:3.4.1", 165 | "imageID": "", 166 | "lastState": null, 167 | "name": "mongodb", 168 | "ready": false, 169 | "restartCount": 0, 170 | "state": { 171 | "waiting": { 172 | "reason": "ContainerCreating" 173 | } 174 | } 175 | } 176 | ], 177 | "hostIP": "10.76.193.41", 178 | "phase": "Pending", 179 | "qosClass": "BestEffort", 180 | "startTime": "2018-08-20T15:32:11Z" 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /testdata/replicaset.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "apps/v1", 3 | "kind": "ReplicaSet", 4 | "metadata": { 5 | "annotations": { 6 | "deployment.kubernetes.io/desired-replicas": "1", 7 | "deployment.kubernetes.io/max-replicas": "2", 8 | "deployment.kubernetes.io/revision": "1" 9 | }, 10 | "creationTimestamp": "2018-07-02T14:53:53Z", 11 | "generation": 1, 12 | "labels": { 13 | "app": "clunky-sabertooth-joomla", 14 | "pod-template-hash": "1808875418" 15 | }, 16 | "name": "clunky-sabertooth-joomla-5d4ddc985d", 17 | "namespace": "default", 18 | "ownerReferences": [ 19 | { 20 | "apiVersion": "extensions/v1beta1", 21 | "blockOwnerDeletion": true, 22 | "controller": true, 23 | "kind": "Deployment", 24 | "name": "clunky-sabertooth-joomla", 25 | "uid": "bcc99362-7e07-11e8-9fd6-0800279d9e51" 26 | } 27 | ], 28 | "resourceVersion": "109756", 29 | "selfLink": "/apis/apps/v1/namespaces/default/replicasets/clunky-sabertooth-joomla-5d4ddc985d", 30 | "uid": "bcccd488-7e07-11e8-9fd6-0800279d9e51" 31 | }, 32 | "spec": { 33 | "replicas": 1, 34 | "selector": { 35 | "matchLabels": { 36 | "app": "clunky-sabertooth-joomla", 37 | "pod-template-hash": "1808875418" 38 | } 39 | }, 40 | "template": { 41 | "metadata": { 42 | "creationTimestamp": null, 43 | "labels": { 44 | "app": "clunky-sabertooth-joomla", 45 | "pod-template-hash": "1808875418" 46 | } 47 | }, 48 | "spec": { 49 | "containers": [ 50 | { 51 | "env": [ 52 | { 53 | "name": "ALLOW_EMPTY_PASSWORD", 54 | "value": "yes" 55 | }, 56 | { 57 | "name": "MARIADB_HOST", 58 | "value": "clunky-sabertooth-mariadb" 59 | }, 60 | { 61 | "name": "MARIADB_PORT_NUMBER", 62 | "value": "3306" 63 | }, 64 | { 65 | "name": "JOOMLA_DATABASE_NAME", 66 | "value": "bitnami_joomla" 67 | }, 68 | { 69 | "name": "JOOMLA_DATABASE_USER", 70 | "value": "bn_joomla" 71 | }, 72 | { 73 | "name": "JOOMLA_DATABASE_PASSWORD", 74 | "valueFrom": { 75 | "secretKeyRef": { 76 | "key": "mariadb-password", 77 | "name": "clunky-sabertooth-mariadb" 78 | } 79 | } 80 | }, 81 | { 82 | "name": "JOOMLA_USERNAME", 83 | "value": "user" 84 | }, 85 | { 86 | "name": "JOOMLA_PASSWORD", 87 | "valueFrom": { 88 | "secretKeyRef": { 89 | "key": "joomla-password", 90 | "name": "clunky-sabertooth-joomla" 91 | } 92 | } 93 | }, 94 | { 95 | "name": "JOOMLA_EMAIL", 96 | "value": "user@example.com" 97 | } 98 | ], 99 | "image": "docker.io/bitnami/joomla:3.8.10", 100 | "imagePullPolicy": "IfNotPresent", 101 | "name": "clunky-sabertooth-joomla", 102 | "ports": [ 103 | { 104 | "containerPort": 80, 105 | "name": "http", 106 | "protocol": "TCP" 107 | }, 108 | { 109 | "containerPort": 443, 110 | "name": "https", 111 | "protocol": "TCP" 112 | } 113 | ], 114 | "readinessProbe": { 115 | "failureThreshold": 6, 116 | "httpGet": { 117 | "path": "/index.php", 118 | "port": "http", 119 | "scheme": "HTTP" 120 | }, 121 | "initialDelaySeconds": 30, 122 | "periodSeconds": 10, 123 | "successThreshold": 1, 124 | "timeoutSeconds": 5 125 | }, 126 | "resources": { 127 | "requests": { 128 | "cpu": "300m", 129 | "memory": "512Mi" 130 | } 131 | }, 132 | "terminationMessagePath": "/dev/termination-log", 133 | "terminationMessagePolicy": "File", 134 | "volumeMounts": [ 135 | { 136 | "mountPath": "/bitnami/joomla", 137 | "name": "joomla-data" 138 | }, 139 | { 140 | "mountPath": "/bitnami/apache", 141 | "name": "apache-data" 142 | } 143 | ] 144 | } 145 | ], 146 | "dnsPolicy": "ClusterFirst", 147 | "restartPolicy": "Always", 148 | "schedulerName": "default-scheduler", 149 | "securityContext": null, 150 | "terminationGracePeriodSeconds": 30, 151 | "volumes": [ 152 | { 153 | "name": "joomla-data", 154 | "persistentVolumeClaim": { 155 | "claimName": "clunky-sabertooth-joomla-joomla" 156 | } 157 | }, 158 | { 159 | "name": "apache-data", 160 | "persistentVolumeClaim": { 161 | "claimName": "clunky-sabertooth-joomla-apache" 162 | } 163 | } 164 | ] 165 | } 166 | } 167 | }, 168 | "status": { 169 | "availableReplicas": 1, 170 | "fullyLabeledReplicas": 1, 171 | "observedGeneration": 1, 172 | "readyReplicas": 1, 173 | "replicas": 1 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /testdata/service.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "v1", 3 | "kind": "Service", 4 | "metadata": { 5 | "annotations": { 6 | "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"mongo\",\"name\":\"mongo\"},\"name\":\"mongo\",\"namespace\":\"flonjella\"},\"spec\":{\"ports\":[{\"name\":\"web\",\"port\":27017}],\"selector\":{\"app\":\"mongo\",\"name\":\"mongo\"}}}\n" 7 | }, 8 | "creationTimestamp": "2018-08-20T15:32:10Z", 9 | "labels": { 10 | "app": "mongo", 11 | "name": "mongo" 12 | }, 13 | "name": "mongo", 14 | "namespace": "flonjella", 15 | "resourceVersion": "229444", 16 | "selfLink": "/api/v1/namespaces/flonjella/services/mongo", 17 | "uid": "34918d6c-a48e-11e8-bed1-42ec94e14baf" 18 | }, 19 | "spec": { 20 | "clusterIP": "172.21.241.195", 21 | "ports": [ 22 | { 23 | "name": "web", 24 | "port": 27017, 25 | "protocol": "TCP", 26 | "targetPort": 27017 27 | } 28 | ], 29 | "selector": { 30 | "app": "mongo", 31 | "name": "mongo" 32 | }, 33 | "sessionAffinity": "None", 34 | "type": "ClusterIP" 35 | }, 36 | "status": { 37 | "loadBalancer": null 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /testdata/statefulset.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "apps/v1", 3 | "kind": "StatefulSet", 4 | "metadata": { 5 | "annotations": { 6 | "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"apps/v1beta1\",\"kind\":\"StatefulSet\",\"metadata\":{\"annotations\":{},\"name\":\"mongo\",\"namespace\":\"flonjella\"},\"spec\":{\"replicas\":3,\"selector\":{\"matchLabels\":{\"app\":\"mongo\",\"name\":\"mongo\"}},\"serviceName\":\"mongo\",\"template\":{\"metadata\":{\"labels\":{\"app\":\"mongo\",\"name\":\"mongo\"}},\"spec\":{\"containers\":[{\"command\":[\"mongod\",\"--replSet\",\"rs0\"],\"image\":\"mongo:3.4.1\",\"name\":\"mongodb\",\"ports\":[{\"containerPort\":27017,\"name\":\"web\"}]},{\"command\":[\"bash\",\"/config/init.sh\"],\"image\":\"mongo:3.4.1\",\"name\":\"init-mongo\",\"volumeMounts\":[{\"mountPath\":\"/config\",\"name\":\"config\"}]}],\"volumes\":[{\"configMap\":{\"name\":\"mongo-init\"},\"name\":\"config\"}]}}}}\n" 7 | }, 8 | "creationTimestamp": "2018-08-20T15:32:11Z", 9 | "generation": 1, 10 | "labels": { 11 | "app": "mongo", 12 | "name": "mongo" 13 | }, 14 | "name": "mongo", 15 | "namespace": "flonjella", 16 | "resourceVersion": "229449", 17 | "selfLink": "/apis/apps/v1/namespaces/flonjella/statefulsets/mongo", 18 | "uid": "34e2046e-a48e-11e8-bed1-42ec94e14baf" 19 | }, 20 | "spec": { 21 | "podManagementPolicy": "OrderedReady", 22 | "replicas": 3, 23 | "revisionHistoryLimit": 10, 24 | "selector": { 25 | "matchLabels": { 26 | "app": "mongo", 27 | "name": "mongo" 28 | } 29 | }, 30 | "serviceName": "mongo", 31 | "template": { 32 | "metadata": { 33 | "creationTimestamp": null, 34 | "labels": { 35 | "app": "mongo", 36 | "name": "mongo" 37 | } 38 | }, 39 | "spec": { 40 | "containers": [ 41 | { 42 | "command": [ 43 | "mongod", 44 | "--replSet", 45 | "rs0" 46 | ], 47 | "image": "mongo:3.4.1", 48 | "imagePullPolicy": "IfNotPresent", 49 | "name": "mongodb", 50 | "ports": [ 51 | { 52 | "containerPort": 27017, 53 | "name": "web", 54 | "protocol": "TCP" 55 | } 56 | ], 57 | "resources": null, 58 | "terminationMessagePath": "/dev/termination-log", 59 | "terminationMessagePolicy": "File" 60 | }, 61 | { 62 | "command": [ 63 | "bash", 64 | "/config/init.sh" 65 | ], 66 | "image": "mongo:3.4.1", 67 | "imagePullPolicy": "IfNotPresent", 68 | "name": "init-mongo", 69 | "resources": null, 70 | "terminationMessagePath": "/dev/termination-log", 71 | "terminationMessagePolicy": "File", 72 | "volumeMounts": [ 73 | { 74 | "mountPath": "/config", 75 | "name": "config" 76 | } 77 | ] 78 | } 79 | ], 80 | "dnsPolicy": "ClusterFirst", 81 | "restartPolicy": "Always", 82 | "schedulerName": "default-scheduler", 83 | "securityContext": null, 84 | "terminationGracePeriodSeconds": 30, 85 | "volumes": [ 86 | { 87 | "configMap": { 88 | "defaultMode": 420, 89 | "name": "mongo-init" 90 | }, 91 | "name": "config" 92 | } 93 | ] 94 | } 95 | }, 96 | "updateStrategy": { 97 | "type": "OnDelete" 98 | } 99 | }, 100 | "status": { 101 | "collisionCount": 0, 102 | "currentReplicas": 1, 103 | "currentRevision": "mongo-fdd786d", 104 | "observedGeneration": 1, 105 | "replicas": 1, 106 | "updateRevision": "mongo-fdd786d" 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /testing.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 CA. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "encoding/json" 21 | "github.com/davecgh/go-spew/spew" 22 | graphql "github.com/neelance/graphql-go" 23 | "strings" 24 | "testing" 25 | ) 26 | 27 | type Test struct { 28 | Schema *graphql.Schema 29 | Query string 30 | OperationName string 31 | Variables map[string]interface{} 32 | ExpectedResult string 33 | } 34 | 35 | func RunTest(t *testing.T, test *Test) { 36 | result := test.Schema.Exec(context.Background(), test.Query, test.OperationName, test.Variables) 37 | if len(result.Errors) != 0 { 38 | t.Fatal(result.Errors[0]) 39 | } 40 | 41 | var data interface{} 42 | dd := json.NewDecoder(bytes.NewReader([]byte(result.Data))) 43 | dd.UseNumber() 44 | if err := dd.Decode(&data); err != nil { 45 | t.Fatalf("invalid JSON return value: %s", err) 46 | } 47 | got, _ := json.Marshal(data) 48 | var v interface{} 49 | d := json.NewDecoder(strings.NewReader(test.ExpectedResult)) 50 | d.UseNumber() 51 | if err := d.Decode(&v); err != nil { 52 | t.Fatalf("invalid JSON for ExpectedResult: %s", err) 53 | } 54 | want, _ := json.Marshal(v) 55 | 56 | if !bytes.Equal(want, got) { 57 | spew.Dump(data, v) 58 | t.Logf("want: %s", want) 59 | t.Logf("got: %s", got) 60 | t.Fail() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 CA. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | ) 21 | 22 | // Utility methods for getting data out of nested maps 23 | 24 | func mapItem(obj JsonObject, item string) JsonObject { 25 | if itemobj, ok := obj[item].(JsonObject); ok { 26 | return itemobj 27 | } 28 | return nil 29 | } 30 | 31 | func mapItemRef(obj JsonObject, item string) *JsonObject { 32 | if mitem, ok := obj[item].(JsonObject); ok { 33 | return &mitem 34 | } 35 | 36 | return nil 37 | } 38 | 39 | func getKind(resourceMap JsonObject) string { 40 | kind := resourceMap["kind"] 41 | if kindstr, ok := kind.(string); ok { 42 | return kindstr 43 | } 44 | 45 | panic(errors.New( 46 | fmt.Sprintf("Invalid Kubernetes resource: %v", resourceMap))) 47 | } 48 | 49 | func getNamespace(resourceMap JsonObject) string { 50 | namespace := getMetadataField(resourceMap, "namespace") 51 | if nsstr, ok := namespace.(string); ok { 52 | return nsstr 53 | } 54 | 55 | panic(errors.New( 56 | fmt.Sprintf("Invalid Kubernetes resource: %v", resourceMap))) 57 | } 58 | 59 | func getName(resourceMap JsonObject) string { 60 | name := getMetadataField(resourceMap, "name") 61 | if nsstr, ok := name.(string); ok { 62 | return nsstr 63 | } 64 | 65 | panic(errors.New( 66 | fmt.Sprintf("Invalid Kubernetes resource: %v", resourceMap))) 67 | } 68 | 69 | func getUid(resourceMap JsonObject) string { 70 | uid := getMetadataField(resourceMap, "uid") 71 | if uidstr, ok := uid.(string); ok { 72 | return uidstr 73 | } 74 | 75 | panic(errors.New( 76 | fmt.Sprintf("Invalid Kubernetes resource: %v", resourceMap))) 77 | } 78 | 79 | func getMetadataField( 80 | resourceMap JsonObject, 81 | field string) interface{} { 82 | if meta, ok := resourceMap["metadata"]; ok { 83 | if mmap, ok := meta.(JsonObject); ok { 84 | if val, ok := mmap[field]; ok { 85 | return val 86 | } 87 | } 88 | } 89 | return nil 90 | } 91 | 92 | func toStringArray(sa JsonArray) []string { 93 | strs := make([]string, len(sa)) 94 | for idx, val := range sa { 95 | strs[idx] = val.(string) 96 | } 97 | return strs 98 | } 99 | 100 | func toStringArrayRef(sa *JsonArray) *[]string { 101 | if sa == nil { 102 | return nil 103 | } 104 | strs := make([]string, len(*sa)) 105 | for idx, val := range *sa { 106 | strs[idx] = val.(string) 107 | } 108 | return &strs 109 | } 110 | 111 | func toIntArrayRef(ia *JsonArray) *[]int32 { 112 | if ia == nil { 113 | return nil 114 | } 115 | ints := make([]int32, len(*ia)) 116 | for idx, val := range *ia { 117 | ints[idx] = toGQLInt(val) 118 | } 119 | return &ints 120 | } 121 | 122 | func toGQLInt(val interface{}) int32 { 123 | if num, ok := val.(float64); ok { 124 | return int32(num) 125 | } else { 126 | return val.(int32) 127 | } 128 | } 129 | 130 | type jsonGetter struct { 131 | jsonObj JsonObject 132 | } 133 | 134 | func (jg jsonGetter) boolItem(field string) bool { 135 | return jg.jsonObj[field].(bool) 136 | } 137 | 138 | func (jg jsonGetter) intItem(field string) int32 { 139 | return toGQLInt(jg.jsonObj[field]) 140 | } 141 | 142 | func (jg jsonGetter) stringItem(field string) string { 143 | return jg.jsonObj[field].(string) 144 | } 145 | 146 | func (jg jsonGetter) arrayItem(field string) JsonArray { 147 | return jg.jsonObj[field].(JsonArray) 148 | } 149 | 150 | func (jg jsonGetter) objItem(field string) JsonObject { 151 | return jg.jsonObj[field].(JsonObject) 152 | } 153 | 154 | func (jg jsonGetter) boolItemOr(field string, defVal bool) bool { 155 | if val := jg.jsonObj[field]; val != nil { 156 | return val.(bool) 157 | } 158 | return defVal 159 | } 160 | 161 | func (jg jsonGetter) intRefItemOr(field string, defVal *int32) *int32 { 162 | if val := jg.jsonObj[field]; val != nil { 163 | intval := toGQLInt(val) 164 | return &intval 165 | } 166 | return defVal 167 | } 168 | 169 | func (jg jsonGetter) intItemOr(field string, defVal int32) int32 { 170 | if val := jg.jsonObj[field]; val != nil { 171 | if num, ok := val.(float64); ok { 172 | return int32(num) 173 | } else { 174 | return val.(int32) 175 | } 176 | } 177 | return defVal 178 | } 179 | 180 | func (jg jsonGetter) stringRefItemOr(field string, defVal *string) *string { 181 | if val := jg.jsonObj[field]; val != nil { 182 | strVal := val.(string) 183 | return &strVal 184 | } 185 | return defVal 186 | } 187 | 188 | func (jg jsonGetter) stringItemOr(field string, defVal string) string { 189 | if val := jg.jsonObj[field]; val != nil { 190 | return val.(string) 191 | } 192 | return defVal 193 | } 194 | 195 | func (jg jsonGetter) arrayItemOr(field string, defVal *JsonArray) *JsonArray { 196 | if val, ok := jg.jsonObj[field].(JsonArray); ok { 197 | arrVal := JsonArray(val) 198 | return &arrVal 199 | } 200 | return defVal 201 | } 202 | 203 | func (jg jsonGetter) objItemOr(field string, defVal *JsonObject) *JsonObject { 204 | if val, ok := jg.jsonObj[field].(JsonObject); ok { 205 | objVal := JsonObject(val) 206 | return &objVal 207 | } 208 | return defVal 209 | } 210 | 211 | func jgetter(jsonObj interface{}) jsonGetter { 212 | if mval, ok := jsonObj.(JsonObject); ok { 213 | return jsonGetter{mval} 214 | } 215 | panic(errors.New( 216 | fmt.Sprintf("Invalid Kubernetes object: %v", jsonObj))) 217 | } 218 | -------------------------------------------------------------------------------- /vendor/vendor.json: -------------------------------------------------------------------------------- 1 | { 2 | "comment": "", 3 | "ignore": "test", 4 | "package": [ 5 | { 6 | "checksumSHA1": "H0V4mMgMFrJLhJdIT4KHT9di4HI=", 7 | "path": "github.com/Sirupsen/logrus", 8 | "revision": "b9def5b3c33f812a4f503538d1ee21eeb6787d6d", 9 | "revisionTime": "2017-02-07T00:44:51Z" 10 | }, 11 | { 12 | "checksumSHA1": "dvabztWVQX8f6oMLRyv4dLH+TGY=", 13 | "path": "github.com/davecgh/go-spew/spew", 14 | "revision": "346938d642f2ec3594ed81d874461961cd0faa76", 15 | "revisionTime": "2016-10-29T20:57:26Z" 16 | }, 17 | { 18 | "checksumSHA1": "g/V4qrXjUGG9B+e3hB+4NAYJ5Gs=", 19 | "path": "github.com/gorilla/context", 20 | "revision": "08b5f424b9271eedf6f9f0ce86cb9396ed337a42", 21 | "revisionTime": "2016-08-17T18:46:32Z" 22 | }, 23 | { 24 | "checksumSHA1": "zmCk+lgIeiOf0Ng9aFP9aFy8ksE=", 25 | "path": "github.com/gorilla/mux", 26 | "revision": "599cba5e7b6137d46ddf58fb1765f5d928e69604", 27 | "revisionTime": "2017-02-28T22:43:54Z" 28 | }, 29 | { 30 | "checksumSHA1": "15NDILTqSGtUYEv1UdZkPVbeCpA=", 31 | "path": "github.com/jackc/pgx", 32 | "revision": "d49a78dd734e07ab34de47d5be1c3d1bfe77094d", 33 | "revisionTime": "2017-08-03T19:17:28Z" 34 | }, 35 | { 36 | "checksumSHA1": "rsDmBQ/jGFUN2t6G4c3ohkKGKdQ=", 37 | "path": "github.com/jackc/pgx/chunkreader", 38 | "revision": "d49a78dd734e07ab34de47d5be1c3d1bfe77094d", 39 | "revisionTime": "2017-08-03T19:17:28Z" 40 | }, 41 | { 42 | "checksumSHA1": "wArf/u0cmHsLafG2tOcjVUUXQBw=", 43 | "path": "github.com/jackc/pgx/internal/sanitize", 44 | "revision": "d49a78dd734e07ab34de47d5be1c3d1bfe77094d", 45 | "revisionTime": "2017-08-03T19:17:28Z" 46 | }, 47 | { 48 | "checksumSHA1": "OCtOmvS8BLHQAyQXJ/Ub8QxKz0s=", 49 | "path": "github.com/jackc/pgx/pgio", 50 | "revision": "d49a78dd734e07ab34de47d5be1c3d1bfe77094d", 51 | "revisionTime": "2017-08-03T19:17:28Z" 52 | }, 53 | { 54 | "checksumSHA1": "qu2S8snSUEOLCHAZZO/7dVD5WoA=", 55 | "path": "github.com/jackc/pgx/pgproto3", 56 | "revision": "d49a78dd734e07ab34de47d5be1c3d1bfe77094d", 57 | "revisionTime": "2017-08-03T19:17:28Z" 58 | }, 59 | { 60 | "checksumSHA1": "X1mEx0bbPZ+HVj6q5Uf6PXa/urY=", 61 | "path": "github.com/jackc/pgx/pgtype", 62 | "revision": "d49a78dd734e07ab34de47d5be1c3d1bfe77094d", 63 | "revisionTime": "2017-08-03T19:17:28Z" 64 | }, 65 | { 66 | "checksumSHA1": "FXB8JdMDLDKjFAifsUvVbv5jY8I=", 67 | "path": "github.com/jackc/pgx/stdlib", 68 | "revision": "d49a78dd734e07ab34de47d5be1c3d1bfe77094d", 69 | "revisionTime": "2017-08-03T19:17:28Z" 70 | }, 71 | { 72 | "checksumSHA1": "zcEtH7VUcRFCeEMKmVwtj0/k9sI=", 73 | "path": "github.com/lib/pq", 74 | "revision": "0477eb88c5ca4009cb281f13c90633375b6a9987", 75 | "revisionTime": "2017-02-06T20:06:38Z" 76 | }, 77 | { 78 | "checksumSHA1": "KzsJ/GkPEyvxG8eBcKrHRFoPhzU=", 79 | "path": "github.com/lib/pq/oid", 80 | "revision": "0477eb88c5ca4009cb281f13c90633375b6a9987", 81 | "revisionTime": "2017-02-06T20:06:38Z" 82 | }, 83 | { 84 | "checksumSHA1": "Xe767JGU0kFzRBki569Z+wWNpyE=", 85 | "path": "github.com/neelance/graphql-go", 86 | "revision": "52080e1f0951c75dd4addba57db27fc4de1d0019", 87 | "revisionTime": "2017-07-10T07:49:50Z" 88 | }, 89 | { 90 | "checksumSHA1": "2xo5wWbyp/TuujcXVKIEPVXzrU0=", 91 | "path": "github.com/neelance/graphql-go/errors", 92 | "revision": "52080e1f0951c75dd4addba57db27fc4de1d0019", 93 | "revisionTime": "2017-07-10T07:49:50Z" 94 | }, 95 | { 96 | "checksumSHA1": "G5EgrmrBH4Ff0Vh9qbZTmhD/JgI=", 97 | "path": "github.com/neelance/graphql-go/internal/common", 98 | "revision": "52080e1f0951c75dd4addba57db27fc4de1d0019", 99 | "revisionTime": "2017-07-10T07:49:50Z" 100 | }, 101 | { 102 | "checksumSHA1": "BCgntIJgm65/5izmReUwwFbhL6k=", 103 | "path": "github.com/neelance/graphql-go/internal/exec", 104 | "revision": "52080e1f0951c75dd4addba57db27fc4de1d0019", 105 | "revisionTime": "2017-07-10T07:49:50Z" 106 | }, 107 | { 108 | "checksumSHA1": "2jnkfnl371KrzlvVXTBZpEkVieg=", 109 | "path": "github.com/neelance/graphql-go/internal/exec/packer", 110 | "revision": "52080e1f0951c75dd4addba57db27fc4de1d0019", 111 | "revisionTime": "2017-07-10T07:49:50Z" 112 | }, 113 | { 114 | "checksumSHA1": "NzIEa3L5Bac+MgP7NJTgyegsNyA=", 115 | "path": "github.com/neelance/graphql-go/internal/exec/resolvable", 116 | "revision": "52080e1f0951c75dd4addba57db27fc4de1d0019", 117 | "revisionTime": "2017-07-10T07:49:50Z" 118 | }, 119 | { 120 | "checksumSHA1": "PpRbkvmCxc+dSizVv65I0xvGlQo=", 121 | "path": "github.com/neelance/graphql-go/internal/exec/selected", 122 | "revision": "52080e1f0951c75dd4addba57db27fc4de1d0019", 123 | "revisionTime": "2017-07-10T07:49:50Z" 124 | }, 125 | { 126 | "checksumSHA1": "jfcNYm+kb5DPigLhLvGO5QoXGnM=", 127 | "path": "github.com/neelance/graphql-go/internal/lexer", 128 | "revision": "5b2978fcb1baf5d104a51d2638e2204abac0f5fd", 129 | "revisionTime": "2017-02-06T02:30:34Z" 130 | }, 131 | { 132 | "checksumSHA1": "9gS7LUWp7G78ZYvbGtOHlqCp448=", 133 | "path": "github.com/neelance/graphql-go/internal/query", 134 | "revision": "52080e1f0951c75dd4addba57db27fc4de1d0019", 135 | "revisionTime": "2017-07-10T07:49:50Z" 136 | }, 137 | { 138 | "checksumSHA1": "UQDy8HWW0f3Ra/BIamumRNEOXNo=", 139 | "path": "github.com/neelance/graphql-go/internal/schema", 140 | "revision": "52080e1f0951c75dd4addba57db27fc4de1d0019", 141 | "revisionTime": "2017-07-10T07:49:50Z" 142 | }, 143 | { 144 | "checksumSHA1": "lm4eMItTymOnxqEjocli8DafSSY=", 145 | "path": "github.com/neelance/graphql-go/internal/validation", 146 | "revision": "52080e1f0951c75dd4addba57db27fc4de1d0019", 147 | "revisionTime": "2017-07-10T07:49:50Z" 148 | }, 149 | { 150 | "checksumSHA1": "jpGTrWqoBIeW5d+ROIgWWDr9fzM=", 151 | "path": "github.com/neelance/graphql-go/introspection", 152 | "revision": "52080e1f0951c75dd4addba57db27fc4de1d0019", 153 | "revisionTime": "2017-07-10T07:49:50Z" 154 | }, 155 | { 156 | "checksumSHA1": "/Z0UFL0llSFn7HkOSzJlr05waz0=", 157 | "path": "github.com/neelance/graphql-go/log", 158 | "revision": "52080e1f0951c75dd4addba57db27fc4de1d0019", 159 | "revisionTime": "2017-07-10T07:49:50Z" 160 | }, 161 | { 162 | "checksumSHA1": "BjWt9L3iK6mnS6rxnJ9C1l9cJjA=", 163 | "path": "github.com/neelance/graphql-go/relay", 164 | "revision": "52080e1f0951c75dd4addba57db27fc4de1d0019", 165 | "revisionTime": "2017-07-10T07:49:50Z" 166 | }, 167 | { 168 | "checksumSHA1": "rpQOH1goO/+oS+1xFcwvGAsRY7I=", 169 | "path": "github.com/neelance/graphql-go/trace", 170 | "revision": "52080e1f0951c75dd4addba57db27fc4de1d0019", 171 | "revisionTime": "2017-07-10T07:49:50Z" 172 | }, 173 | { 174 | "checksumSHA1": "lzPj2yJz4Dy6ev0O0L4PNVzplAw=", 175 | "path": "github.com/opentracing/opentracing-go", 176 | "revision": "6edb48674bd9467b8e91fda004f2bd7202d60ce4", 177 | "revisionTime": "2017-02-06T22:16:52Z" 178 | }, 179 | { 180 | "checksumSHA1": "+q64ZAwI2qF5goM1mLjav88NOYQ=", 181 | "path": "github.com/opentracing/opentracing-go/ext", 182 | "revision": "6edb48674bd9467b8e91fda004f2bd7202d60ce4", 183 | "revisionTime": "2017-02-06T22:16:52Z" 184 | }, 185 | { 186 | "checksumSHA1": "+rbKrafLHDnrQFgeWawo9tfZhV4=", 187 | "path": "github.com/opentracing/opentracing-go/log", 188 | "revision": "6edb48674bd9467b8e91fda004f2bd7202d60ce4", 189 | "revisionTime": "2017-02-06T22:16:52Z" 190 | }, 191 | { 192 | "checksumSHA1": "rJab1YdNhQooDiBWNnt7TLWPyBU=", 193 | "path": "github.com/pkg/errors", 194 | "revision": "c605e284fe17294bda444b34710735b29d1a9d90", 195 | "revisionTime": "2017-05-05T04:36:39Z" 196 | }, 197 | { 198 | "checksumSHA1": "Y+HGqEkYM15ir+J93MEaHdyFy0c=", 199 | "path": "golang.org/x/net/context", 200 | "revision": "236b8f043b920452504e263bc21d354427127473", 201 | "revisionTime": "2017-02-06T03:21:01Z" 202 | }, 203 | { 204 | "checksumSHA1": "Xhsm+TevJogC8U4sG6FO+czBMps=", 205 | "path": "golang.org/x/sys/unix", 206 | "revision": "7a6e5648d140666db5d920909e082ca00a87ba2c", 207 | "revisionTime": "2017-02-01T04:15:14Z" 208 | } 209 | ], 210 | "rootPath": "kubeiql" 211 | } 212 | -------------------------------------------------------------------------------- /volume.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 CA. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | // "fmt" 20 | ) 21 | 22 | // Volumes hold the actual data in Kubernetes 23 | 24 | type volume struct { 25 | // XXX incomplete 26 | ConfigMap *configMapVolumeSource 27 | Name string 28 | HostPath *hostPathVolumeSource 29 | PersistentVolumeClaim *persistentVolumeClaimVolumeSource 30 | Secret *secretVolumeSource 31 | } 32 | 33 | type configMapVolumeSource struct { 34 | DefaultMode *int32 35 | Items *[]keyToPath 36 | Name string 37 | Optional bool 38 | } 39 | 40 | type hostPathVolumeSource struct { 41 | Path string 42 | Type *string 43 | } 44 | 45 | type persistentVolumeClaimVolumeSource struct { 46 | ClaimName string 47 | ReadOnly bool 48 | } 49 | 50 | type secretVolumeSource struct { 51 | DefaultMode *int32 52 | Items *[]keyToPath 53 | Optional bool 54 | SecretName string 55 | } 56 | 57 | type keyToPath struct { 58 | Key string 59 | Mode *int32 60 | Path string 61 | } 62 | 63 | type volumeResolver struct { 64 | ctx context.Context 65 | v volume 66 | } 67 | 68 | type configMapVolumeSourceResolver struct { 69 | ctx context.Context 70 | c configMapVolumeSource 71 | } 72 | 73 | type hostPathVolumeSourceResolver struct { 74 | ctx context.Context 75 | h hostPathVolumeSource 76 | } 77 | 78 | type persistentVolumeClaimVolumeSourceResolver struct { 79 | ctx context.Context 80 | p persistentVolumeClaimVolumeSource 81 | } 82 | 83 | type secretVolumeSourceResolver struct { 84 | ctx context.Context 85 | s secretVolumeSource 86 | } 87 | 88 | type keyToPathResolver struct { 89 | ctx context.Context 90 | k keyToPath 91 | } 92 | 93 | func (r volumeResolver) ConfigMap() *configMapVolumeSourceResolver { 94 | if r.v.ConfigMap == nil { 95 | return nil 96 | } 97 | return &configMapVolumeSourceResolver{r.ctx, *r.v.ConfigMap} 98 | } 99 | 100 | func (r volumeResolver) Name() string { 101 | return r.v.Name 102 | } 103 | 104 | func (r volumeResolver) HostPath() *hostPathVolumeSourceResolver { 105 | if r.v.HostPath == nil { 106 | return nil 107 | } 108 | return &hostPathVolumeSourceResolver{r.ctx, *r.v.HostPath} 109 | } 110 | 111 | func (r volumeResolver) PersistentVolumeClaim() *persistentVolumeClaimVolumeSourceResolver { 112 | if r.v.PersistentVolumeClaim == nil { 113 | return nil 114 | } 115 | return &persistentVolumeClaimVolumeSourceResolver{r.ctx, *r.v.PersistentVolumeClaim} 116 | } 117 | 118 | func (r volumeResolver) Secret() *secretVolumeSourceResolver { 119 | if r.v.Secret == nil { 120 | return nil 121 | } 122 | return &secretVolumeSourceResolver{r.ctx, *r.v.Secret} 123 | } 124 | 125 | // configMapVolumeSourceResolver implementations 126 | func (r *configMapVolumeSourceResolver) DefaultMode() *int32 { 127 | return r.c.DefaultMode 128 | } 129 | 130 | func (r *configMapVolumeSourceResolver) Items() *[]keyToPathResolver { 131 | c := r.c.Items 132 | if c == nil || len(*c) == 0 { 133 | res := make([]keyToPathResolver, 0) 134 | return &res 135 | } 136 | resolvers := make([]keyToPathResolver, len(*c)) 137 | for idx, val := range *c { 138 | resolvers[idx] = keyToPathResolver{r.ctx, val} 139 | } 140 | return &resolvers 141 | } 142 | 143 | func (r *configMapVolumeSourceResolver) Name() string { 144 | return r.c.Name 145 | } 146 | 147 | func (r *configMapVolumeSourceResolver) Optional() bool { 148 | return r.c.Optional 149 | } 150 | 151 | // keyToPath implementations 152 | func (r keyToPathResolver) Key() string { 153 | return r.k.Key 154 | } 155 | 156 | func (r keyToPathResolver) Mode() *int32 { 157 | return r.k.Mode 158 | } 159 | 160 | func (r keyToPathResolver) Path() string { 161 | return r.k.Path 162 | } 163 | 164 | // hostPathVolumeSource implementations 165 | func (r *hostPathVolumeSourceResolver) Path() string { 166 | return r.h.Path 167 | } 168 | 169 | func (r *hostPathVolumeSourceResolver) Type() *string { 170 | return r.h.Type 171 | } 172 | 173 | // persistentVolumeClaimVolumeSource implementations 174 | func (r *persistentVolumeClaimVolumeSourceResolver) ClaimName() string { 175 | return r.p.ClaimName 176 | } 177 | 178 | func (r *persistentVolumeClaimVolumeSourceResolver) ReadOnly() bool { 179 | return r.p.ReadOnly 180 | } 181 | 182 | // secretVolumeSource implementations 183 | func (r *secretVolumeSourceResolver) DefaultMode() *int32 { 184 | return r.s.DefaultMode 185 | } 186 | 187 | func (r *secretVolumeSourceResolver) Items() *[]keyToPathResolver { 188 | s := r.s.Items 189 | if s == nil || len(*s) == 0 { 190 | res := make([]keyToPathResolver, 0) 191 | return &res 192 | } 193 | resolvers := make([]keyToPathResolver, len(*s)) 194 | for idx, val := range *s { 195 | resolvers[idx] = keyToPathResolver{r.ctx, val} 196 | } 197 | return &resolvers 198 | } 199 | 200 | func (r *secretVolumeSourceResolver) Optional() bool { 201 | return r.s.Optional 202 | } 203 | 204 | func (r *secretVolumeSourceResolver) SecretName() string { 205 | return r.s.SecretName 206 | } 207 | 208 | // Translate unmarshalled json into a deployment object 209 | func mapToVolume(ctx context.Context, _ JsonObject) volume { 210 | return volume{} 211 | } 212 | -------------------------------------------------------------------------------- /watchers.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 CA. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "bufio" 19 | "crypto/tls" 20 | "crypto/x509" 21 | "encoding/json" 22 | "fmt" 23 | "io/ioutil" 24 | "log" 25 | "net/http" 26 | ) 27 | 28 | type Notification struct { 29 | Type string 30 | Object JsonObject 31 | } 32 | 33 | type HttpConfig struct { 34 | token string 35 | tr *http.Transport 36 | } 37 | 38 | var watchUrlByKind = map[string]string{ 39 | "Pod": "/api/v1/watch/pods?watch=true", 40 | "Deployment": "/apis/apps/v1/watch/deployments?watch=true", 41 | "ReplicaSet": "/apis/apps/v1/watch/replicasets?watch=true", 42 | "StatefulSet": "/apis/apps/v1/watch/statefulsets?watch=true", 43 | "DaemonSet": "/apis/apps/v1/watch/daemonsets?watch=true", 44 | "Service": "/api/v1/watch/services?watch=true", 45 | } 46 | 47 | func isWatchedKind(kind string) bool { 48 | _, ok := watchUrlByKind[kind] 49 | return ok 50 | } 51 | 52 | func buildWatchUrl(kind string) string { 53 | if watchurl, ok := watchUrlByKind[kind]; ok { 54 | return ApiHost + watchurl 55 | } 56 | panic(fmt.Sprintf("no watcher url for kind: '%s'", kind)) 57 | } 58 | 59 | func readSecret(name string) []byte { 60 | if ApiSecretPath == "" { 61 | return nil 62 | } 63 | b, err := ioutil.ReadFile(ApiSecretPath + "/" + name) 64 | if err != nil { 65 | panic(fmt.Sprintf("watcher error reading secret %s: %s\n", 66 | name, err.Error())) 67 | } 68 | return b 69 | } 70 | 71 | func getTlsConfig() *tls.Config { 72 | cert := readSecret("ca.crt") 73 | if cert != nil { 74 | roots := x509.NewCertPool() 75 | roots.AppendCertsFromPEM(cert) 76 | tlsConfig := &tls.Config{} 77 | tlsConfig.RootCAs = roots 78 | 79 | return tlsConfig 80 | } 81 | return nil 82 | } 83 | 84 | var httpConfig HttpConfig 85 | 86 | func initHttpConfig() { 87 | tr := &http.Transport{} 88 | if tlsConfig := getTlsConfig(); tlsConfig != nil { 89 | tr.TLSClientConfig = tlsConfig 90 | } 91 | token := "" 92 | if tok := readSecret("token"); tok != nil { 93 | token = string(tok) 94 | } 95 | httpConfig = HttpConfig{token, tr} 96 | } 97 | 98 | func makeWatchRequest(kind string) *http.Request { 99 | req, err := http.NewRequest("GET", buildWatchUrl(kind), nil) 100 | if err != nil { 101 | panic(fmt.Sprintf("watcher http.NewRequest error for kind %s: %s\n", 102 | kind, err.Error())) 103 | } 104 | if httpConfig.token != "" { 105 | req.Header.Add("Authorization", "Bearer "+httpConfig.token) 106 | } 107 | return req 108 | } 109 | 110 | func runWatcher(kind string, endchan chan string) { 111 | req := makeWatchRequest(kind) 112 | k8sClient := &http.Client{Transport: httpConfig.tr} 113 | resp, err := k8sClient.Do(req) 114 | if err != nil { 115 | panic(fmt.Sprintf("http GET error for watch of kind %s: %s", 116 | kind, err.Error())) 117 | } 118 | defer resp.Body.Close() 119 | reader := bufio.NewReader(resp.Body) 120 | for { 121 | line, err := reader.ReadBytes('\n') 122 | if err != nil { 123 | log.Printf("read error on %s watcher stream: %s\n", kind, err.Error()) 124 | break 125 | } 126 | var notif Notification 127 | if err := json.Unmarshal(line, ¬if); err != nil { 128 | log.Printf("JSON unmarshal error on %s watcher input: %s\n", 129 | kind, err.Error()) 130 | break 131 | } 132 | 133 | if notif.Type == "ADDED" || notif.Type == "MODIFIED" { 134 | GetCache().Add(¬if.Object) 135 | } else if notif.Type == "DELETED" { 136 | GetCache().Remove(¬if.Object) 137 | } 138 | } 139 | endchan <- kind 140 | } 141 | 142 | func restartWatchers(endchan chan string) { 143 | // if a watcher terminates, restart it 144 | for { 145 | restartName := <-endchan 146 | log.Printf("restarting terminated watcher: %s\n", restartName) 147 | go runWatcher(restartName, endchan) 148 | } 149 | } 150 | 151 | func initWatchers() { 152 | initHttpConfig() 153 | endchan := make(chan string) 154 | for key, _ := range watchUrlByKind { 155 | go runWatcher(key, endchan) 156 | } 157 | 158 | go restartWatchers(endchan) 159 | } 160 | --------------------------------------------------------------------------------