├── .gitignore ├── CONTRIBUTING.md ├── Dockerfile ├── Gopkg.lock ├── Gopkg.toml ├── LICENSE ├── Makefile ├── README.md ├── cmd └── quack │ └── main.go ├── configure ├── deploy ├── clusterrole-quack-apiserver.yaml ├── configmap-audit.yaml ├── crb-authentication-reader.yaml ├── crb-delegator.yaml ├── crb-quack-apiserver.yaml ├── daemonset.yaml ├── mutatingwebhookconfiguration.yaml ├── namespace.yaml ├── rb.yaml ├── role.yaml ├── service.yaml └── serviceaccount.yaml ├── logo.svg └── pkg └── quack ├── quack.go └── quack_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | 16 | # Vendored dependencies 17 | vendor/ 18 | 19 | # binary 20 | quack 21 | 22 | # env file 23 | .env 24 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | To develop on this project, please fork the repo and clone into your `$GOPATH`. 3 | 4 | Dependencies are **not** checked in so please download those separately. 5 | Download the dependencies using [`dep`](https://github.com/golang/dep). 6 | 7 | ```bash 8 | cd $GOPATH/src/github.com # Create this directory if it doesn't exist 9 | git clone git@github.com:/quack pusher/quack 10 | dep ensure # Installs dependencies to vendor folder. 11 | ``` 12 | 13 | ## Pull Requests and Issues 14 | We track bugs and issues using Github . 15 | 16 | If you find a bug, please open an Issue. 17 | 18 | If you want to fix a bug, please fork, fix the bug and open a PR back to this repo. 19 | Please mention the open bug issue number within your PR if applicable. 20 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.12 AS builder 2 | WORKDIR /go/src/github.com/pusher/quack 3 | RUN curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 4 | 5 | COPY . . 6 | RUN dep ensure --vendor-only 7 | RUN CGO_ENABLED=0 GOOS=linux go build -o /bin/quack github.com/pusher/quack/cmd/quack 8 | 9 | FROM alpine:3.10 10 | COPY --from=builder /bin/quack /bin/quack 11 | 12 | ENTRYPOINT ["/bin/quack"] 13 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | branch = "default" 6 | name = "bitbucket.org/ww/goautoneg" 7 | packages = ["."] 8 | revision = "75cd24fc2f2c2a2088577d12123ddee5f54e0675" 9 | 10 | [[projects]] 11 | name = "github.com/NYTimes/gziphandler" 12 | packages = ["."] 13 | revision = "2600fb119af974220d3916a5916d6e31176aac1b" 14 | version = "v1.0.1" 15 | 16 | [[projects]] 17 | name = "github.com/PuerkitoBio/purell" 18 | packages = ["."] 19 | revision = "0bcb03f4b4d0a9428594752bd2a3b9aa0a9d4bd4" 20 | version = "v1.1.0" 21 | 22 | [[projects]] 23 | branch = "master" 24 | name = "github.com/PuerkitoBio/urlesc" 25 | packages = ["."] 26 | revision = "de5bf2ad457846296e2031421a34e2568e304e35" 27 | 28 | [[projects]] 29 | branch = "master" 30 | name = "github.com/beorn7/perks" 31 | packages = ["quantile"] 32 | revision = "4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9" 33 | 34 | [[projects]] 35 | name = "github.com/coreos/etcd" 36 | packages = [ 37 | "auth/authpb", 38 | "client", 39 | "clientv3", 40 | "etcdserver/api/v3rpc/rpctypes", 41 | "etcdserver/etcdserverpb", 42 | "mvcc/mvccpb", 43 | "pkg/pathutil", 44 | "pkg/srv", 45 | "pkg/tlsutil", 46 | "pkg/transport", 47 | "pkg/types", 48 | "version" 49 | ] 50 | revision = "28f3f26c0e303392556035b694f75768d449d33d" 51 | version = "v3.3.1" 52 | 53 | [[projects]] 54 | name = "github.com/coreos/go-semver" 55 | packages = ["semver"] 56 | revision = "8ab6407b697782a06568d4b7f1db25550ec2e4c6" 57 | version = "v0.2.0" 58 | 59 | [[projects]] 60 | name = "github.com/coreos/go-systemd" 61 | packages = ["daemon"] 62 | revision = "40e2722dffead74698ca12a750f64ef313ddce05" 63 | version = "v16" 64 | 65 | [[projects]] 66 | name = "github.com/davecgh/go-spew" 67 | packages = ["spew"] 68 | revision = "346938d642f2ec3594ed81d874461961cd0faa76" 69 | version = "v1.1.0" 70 | 71 | [[projects]] 72 | branch = "master" 73 | name = "github.com/dustin/go-humanize" 74 | packages = ["."] 75 | revision = "bb3d318650d48840a39aa21a027c6630e198e626" 76 | 77 | [[projects]] 78 | name = "github.com/elazarl/go-bindata-assetfs" 79 | packages = ["."] 80 | revision = "30f82fa23fd844bd5bb1e5f216db87fd77b5eb43" 81 | version = "v1.0.0" 82 | 83 | [[projects]] 84 | name = "github.com/emicklei/go-restful" 85 | packages = [ 86 | ".", 87 | "log" 88 | ] 89 | revision = "26b41036311f2da8242db402557a0dbd09dc83da" 90 | version = "v2.6.0" 91 | 92 | [[projects]] 93 | name = "github.com/emicklei/go-restful-swagger12" 94 | packages = ["."] 95 | revision = "dcef7f55730566d41eae5db10e7d6981829720f6" 96 | version = "1.0.1" 97 | 98 | [[projects]] 99 | branch = "master" 100 | name = "github.com/evanphx/json-patch" 101 | packages = ["."] 102 | revision = "944e07253867aacae43c04b2e6a239005443f33a" 103 | 104 | [[projects]] 105 | name = "github.com/ghodss/yaml" 106 | packages = ["."] 107 | revision = "0ca9ea5df5451ffdf184b4428c902747c2c11cd7" 108 | version = "v1.0.0" 109 | 110 | [[projects]] 111 | branch = "master" 112 | name = "github.com/go-openapi/jsonpointer" 113 | packages = ["."] 114 | revision = "779f45308c19820f1a69e9a4cd965f496e0da10f" 115 | 116 | [[projects]] 117 | branch = "master" 118 | name = "github.com/go-openapi/jsonreference" 119 | packages = ["."] 120 | revision = "36d33bfe519efae5632669801b180bf1a245da3b" 121 | 122 | [[projects]] 123 | branch = "master" 124 | name = "github.com/go-openapi/spec" 125 | packages = ["."] 126 | revision = "1de3e0542de65ad8d75452a595886fdd0befb363" 127 | 128 | [[projects]] 129 | branch = "master" 130 | name = "github.com/go-openapi/swag" 131 | packages = ["."] 132 | revision = "0d03ad0b6405ada874d59d97c416b5cf4234e154" 133 | 134 | [[projects]] 135 | name = "github.com/gogo/protobuf" 136 | packages = [ 137 | "gogoproto", 138 | "proto", 139 | "protoc-gen-gogo/descriptor", 140 | "sortkeys" 141 | ] 142 | revision = "1adfc126b41513cc696b209667c8656ea7aac67c" 143 | version = "v1.0.0" 144 | 145 | [[projects]] 146 | branch = "master" 147 | name = "github.com/golang/glog" 148 | packages = ["."] 149 | revision = "23def4e6c14b4da8ac2ed8007337bc5eb5007998" 150 | 151 | [[projects]] 152 | name = "github.com/golang/protobuf" 153 | packages = [ 154 | "proto", 155 | "ptypes", 156 | "ptypes/any", 157 | "ptypes/duration", 158 | "ptypes/timestamp" 159 | ] 160 | revision = "925541529c1fa6821df4e44ce2723319eb2be768" 161 | version = "v1.0.0" 162 | 163 | [[projects]] 164 | branch = "master" 165 | name = "github.com/google/btree" 166 | packages = ["."] 167 | revision = "e89373fe6b4a7413d7acd6da1725b83ef713e6e4" 168 | 169 | [[projects]] 170 | branch = "master" 171 | name = "github.com/google/gofuzz" 172 | packages = ["."] 173 | revision = "24818f796faf91cd76ec7bddd72458fbced7a6c1" 174 | 175 | [[projects]] 176 | name = "github.com/googleapis/gnostic" 177 | packages = [ 178 | "OpenAPIv2", 179 | "compiler", 180 | "extensions" 181 | ] 182 | revision = "ee43cbb60db7bd22502942cccbc39059117352ab" 183 | version = "v0.1.0" 184 | 185 | [[projects]] 186 | branch = "master" 187 | name = "github.com/gregjones/httpcache" 188 | packages = [ 189 | ".", 190 | "diskcache" 191 | ] 192 | revision = "2bcd89a1743fd4b373f7370ce8ddc14dfbd18229" 193 | 194 | [[projects]] 195 | branch = "master" 196 | name = "github.com/hashicorp/golang-lru" 197 | packages = [ 198 | ".", 199 | "simplelru" 200 | ] 201 | revision = "0fb14efe8c47ae851c0034ed7a448854d3d34cf3" 202 | 203 | [[projects]] 204 | branch = "master" 205 | name = "github.com/howeyc/gopass" 206 | packages = ["."] 207 | revision = "bf9dde6d0d2c004a008c27aaee91170c786f6db8" 208 | 209 | [[projects]] 210 | name = "github.com/imdario/mergo" 211 | packages = ["."] 212 | revision = "163f41321a19dd09362d4c63cc2489db2015f1f4" 213 | version = "0.3.2" 214 | 215 | [[projects]] 216 | name = "github.com/inconshreveable/mousetrap" 217 | packages = ["."] 218 | revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75" 219 | version = "v1.0" 220 | 221 | [[projects]] 222 | name = "github.com/json-iterator/go" 223 | packages = ["."] 224 | revision = "3353055b2a1a5ae1b6a8dfde887a524e7088f3a2" 225 | version = "1.1.2" 226 | 227 | [[projects]] 228 | name = "github.com/juju/ratelimit" 229 | packages = ["."] 230 | revision = "59fac5042749a5afb9af70e813da1dd5474f0167" 231 | version = "1.0.1" 232 | 233 | [[projects]] 234 | branch = "master" 235 | name = "github.com/mailru/easyjson" 236 | packages = [ 237 | "buffer", 238 | "jlexer", 239 | "jwriter" 240 | ] 241 | revision = "32fa128f234d041f196a9f3e0fea5ac9772c08e1" 242 | 243 | [[projects]] 244 | branch = "master" 245 | name = "github.com/mattbaird/jsonpatch" 246 | packages = ["."] 247 | revision = "81af80346b1a01caae0cbc27fd3c1ba5b11e189f" 248 | 249 | [[projects]] 250 | name = "github.com/matttproud/golang_protobuf_extensions" 251 | packages = ["pbutil"] 252 | revision = "3247c84500bff8d9fb6d579d800f20b3e091582c" 253 | version = "v1.0.0" 254 | 255 | [[projects]] 256 | name = "github.com/modern-go/concurrent" 257 | packages = ["."] 258 | revision = "938152ca6a933f501bb238954eebd3cbcbf489ff" 259 | version = "1.0.2" 260 | 261 | [[projects]] 262 | name = "github.com/modern-go/reflect2" 263 | packages = ["."] 264 | revision = "1df9eeb2bb81f327b96228865c5687bc2194af3f" 265 | version = "1.0.0" 266 | 267 | [[projects]] 268 | branch = "master" 269 | name = "github.com/mxk/go-flowrate" 270 | packages = ["flowrate"] 271 | revision = "cca7078d478f8520f85629ad7c68962d31ed7682" 272 | 273 | [[projects]] 274 | name = "github.com/openshift/generic-admission-server" 275 | packages = [ 276 | "pkg/apiserver", 277 | "pkg/cmd/server", 278 | "pkg/registry/admissionreview" 279 | ] 280 | revision = "0d37a6c7bb2c96d2363c0fd934d2e7cdb7d90ebc" 281 | version = "v1.9.0" 282 | 283 | [[projects]] 284 | name = "github.com/pborman/uuid" 285 | packages = ["."] 286 | revision = "e790cca94e6cc75c7064b1332e63811d4aae1a53" 287 | version = "v1.1" 288 | 289 | [[projects]] 290 | branch = "master" 291 | name = "github.com/petar/GoLLRB" 292 | packages = ["llrb"] 293 | revision = "53be0d36a84c2a886ca057d34b6aa4468df9ccb4" 294 | 295 | [[projects]] 296 | name = "github.com/peterbourgon/diskv" 297 | packages = ["."] 298 | revision = "5f041e8faa004a95c88a202771f4cc3e991971e6" 299 | version = "v2.0.1" 300 | 301 | [[projects]] 302 | name = "github.com/pmezard/go-difflib" 303 | packages = ["difflib"] 304 | revision = "792786c7400a136282c1664665ae0a8db921c6c2" 305 | version = "v1.0.0" 306 | 307 | [[projects]] 308 | name = "github.com/prometheus/client_golang" 309 | packages = ["prometheus"] 310 | revision = "c5b7fccd204277076155f10851dad72b76a49317" 311 | version = "v0.8.0" 312 | 313 | [[projects]] 314 | branch = "master" 315 | name = "github.com/prometheus/client_model" 316 | packages = ["go"] 317 | revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c" 318 | 319 | [[projects]] 320 | branch = "master" 321 | name = "github.com/prometheus/common" 322 | packages = [ 323 | "expfmt", 324 | "internal/bitbucket.org/ww/goautoneg", 325 | "model" 326 | ] 327 | revision = "6fb6fce6f8b75884b92e1889c150403fc0872c5e" 328 | 329 | [[projects]] 330 | branch = "master" 331 | name = "github.com/prometheus/procfs" 332 | packages = [ 333 | ".", 334 | "internal/util", 335 | "nfs", 336 | "xfs" 337 | ] 338 | revision = "d274e363d5759d1c916232217be421f1cc89c5fe" 339 | 340 | [[projects]] 341 | name = "github.com/spf13/cobra" 342 | packages = ["."] 343 | revision = "7b2c5ac9fc04fc5efafb60700713d4fa609b777b" 344 | version = "v0.0.1" 345 | 346 | [[projects]] 347 | name = "github.com/spf13/pflag" 348 | packages = ["."] 349 | revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66" 350 | version = "v1.0.0" 351 | 352 | [[projects]] 353 | name = "github.com/stretchr/testify" 354 | packages = ["assert"] 355 | revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71" 356 | version = "v1.2.1" 357 | 358 | [[projects]] 359 | name = "github.com/ugorji/go" 360 | packages = ["codec"] 361 | revision = "9831f2c3ac1068a78f50999a30db84270f647af6" 362 | version = "v1.1" 363 | 364 | [[projects]] 365 | branch = "master" 366 | name = "golang.org/x/crypto" 367 | packages = ["ssh/terminal"] 368 | revision = "91a49db82a88618983a78a06c1cbd4e00ab749ab" 369 | 370 | [[projects]] 371 | branch = "master" 372 | name = "golang.org/x/net" 373 | packages = [ 374 | "context", 375 | "html", 376 | "html/atom", 377 | "http2", 378 | "http2/hpack", 379 | "idna", 380 | "internal/timeseries", 381 | "lex/httplex", 382 | "trace", 383 | "websocket" 384 | ] 385 | revision = "cbe0f9307d0156177f9dd5dc85da1a31abc5f2fb" 386 | 387 | [[projects]] 388 | branch = "master" 389 | name = "golang.org/x/sys" 390 | packages = [ 391 | "unix", 392 | "windows" 393 | ] 394 | revision = "f6cff0780e542efa0c8e864dc8fa522808f6a598" 395 | 396 | [[projects]] 397 | name = "golang.org/x/text" 398 | packages = [ 399 | "collate", 400 | "collate/build", 401 | "internal/colltab", 402 | "internal/gen", 403 | "internal/tag", 404 | "internal/triegen", 405 | "internal/ucd", 406 | "language", 407 | "secure/bidirule", 408 | "transform", 409 | "unicode/bidi", 410 | "unicode/cldr", 411 | "unicode/norm", 412 | "unicode/rangetable", 413 | "width" 414 | ] 415 | revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" 416 | version = "v0.3.0" 417 | 418 | [[projects]] 419 | branch = "master" 420 | name = "google.golang.org/genproto" 421 | packages = ["googleapis/rpc/status"] 422 | revision = "2d9486acae19cf9bd0c093d7dc236a323726a9e4" 423 | 424 | [[projects]] 425 | name = "google.golang.org/grpc" 426 | packages = [ 427 | ".", 428 | "balancer", 429 | "balancer/base", 430 | "balancer/roundrobin", 431 | "codes", 432 | "connectivity", 433 | "credentials", 434 | "encoding", 435 | "encoding/proto", 436 | "grpclb/grpc_lb_v1/messages", 437 | "grpclog", 438 | "health/grpc_health_v1", 439 | "internal", 440 | "keepalive", 441 | "metadata", 442 | "naming", 443 | "peer", 444 | "resolver", 445 | "resolver/dns", 446 | "resolver/passthrough", 447 | "stats", 448 | "status", 449 | "tap", 450 | "transport" 451 | ] 452 | revision = "8e4536a86ab602859c20df5ebfd0bd4228d08655" 453 | version = "v1.10.0" 454 | 455 | [[projects]] 456 | name = "gopkg.in/inf.v0" 457 | packages = ["."] 458 | revision = "3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4" 459 | version = "v0.9.0" 460 | 461 | [[projects]] 462 | name = "gopkg.in/natefinch/lumberjack.v2" 463 | packages = ["."] 464 | revision = "a96e63847dc3c67d17befa69c303767e2f84e54f" 465 | version = "v2.1" 466 | 467 | [[projects]] 468 | name = "gopkg.in/yaml.v2" 469 | packages = ["."] 470 | revision = "7f97868eec74b32b0982dd158a51a446d1da7eb5" 471 | version = "v2.1.1" 472 | 473 | [[projects]] 474 | name = "k8s.io/api" 475 | packages = [ 476 | "admission/v1beta1", 477 | "admissionregistration/v1alpha1", 478 | "admissionregistration/v1beta1", 479 | "apps/v1", 480 | "apps/v1beta1", 481 | "apps/v1beta2", 482 | "authentication/v1", 483 | "authentication/v1beta1", 484 | "authorization/v1", 485 | "authorization/v1beta1", 486 | "autoscaling/v1", 487 | "autoscaling/v2beta1", 488 | "batch/v1", 489 | "batch/v1beta1", 490 | "batch/v2alpha1", 491 | "certificates/v1beta1", 492 | "core/v1", 493 | "events/v1beta1", 494 | "extensions/v1beta1", 495 | "networking/v1", 496 | "policy/v1beta1", 497 | "rbac/v1", 498 | "rbac/v1alpha1", 499 | "rbac/v1beta1", 500 | "scheduling/v1alpha1", 501 | "settings/v1alpha1", 502 | "storage/v1", 503 | "storage/v1alpha1", 504 | "storage/v1beta1" 505 | ] 506 | revision = "af4bc157c3a209798fc897f6d4aaaaeb6c2e0d6a" 507 | version = "kubernetes-1.9.0" 508 | 509 | [[projects]] 510 | name = "k8s.io/apimachinery" 511 | packages = [ 512 | "pkg/api/equality", 513 | "pkg/api/errors", 514 | "pkg/api/meta", 515 | "pkg/api/resource", 516 | "pkg/api/validation", 517 | "pkg/api/validation/path", 518 | "pkg/apimachinery", 519 | "pkg/apimachinery/announced", 520 | "pkg/apimachinery/registered", 521 | "pkg/apis/meta/internalversion", 522 | "pkg/apis/meta/v1", 523 | "pkg/apis/meta/v1/unstructured", 524 | "pkg/apis/meta/v1/validation", 525 | "pkg/apis/meta/v1alpha1", 526 | "pkg/conversion", 527 | "pkg/conversion/queryparams", 528 | "pkg/fields", 529 | "pkg/labels", 530 | "pkg/runtime", 531 | "pkg/runtime/schema", 532 | "pkg/runtime/serializer", 533 | "pkg/runtime/serializer/json", 534 | "pkg/runtime/serializer/protobuf", 535 | "pkg/runtime/serializer/recognizer", 536 | "pkg/runtime/serializer/streaming", 537 | "pkg/runtime/serializer/versioning", 538 | "pkg/selection", 539 | "pkg/types", 540 | "pkg/util/cache", 541 | "pkg/util/clock", 542 | "pkg/util/diff", 543 | "pkg/util/errors", 544 | "pkg/util/framer", 545 | "pkg/util/httpstream", 546 | "pkg/util/intstr", 547 | "pkg/util/json", 548 | "pkg/util/mergepatch", 549 | "pkg/util/net", 550 | "pkg/util/proxy", 551 | "pkg/util/rand", 552 | "pkg/util/runtime", 553 | "pkg/util/sets", 554 | "pkg/util/strategicpatch", 555 | "pkg/util/uuid", 556 | "pkg/util/validation", 557 | "pkg/util/validation/field", 558 | "pkg/util/wait", 559 | "pkg/util/waitgroup", 560 | "pkg/util/yaml", 561 | "pkg/version", 562 | "pkg/watch", 563 | "third_party/forked/golang/json", 564 | "third_party/forked/golang/netutil", 565 | "third_party/forked/golang/reflect" 566 | ] 567 | revision = "180eddb345a5be3a157cea1c624700ad5bd27b8f" 568 | version = "kubernetes-1.9.0" 569 | 570 | [[projects]] 571 | branch = "release-1.9" 572 | name = "k8s.io/apiserver" 573 | packages = [ 574 | "pkg/admission", 575 | "pkg/admission/configuration", 576 | "pkg/admission/initializer", 577 | "pkg/admission/metrics", 578 | "pkg/admission/plugin/initialization", 579 | "pkg/admission/plugin/namespace/lifecycle", 580 | "pkg/admission/plugin/webhook/config", 581 | "pkg/admission/plugin/webhook/config/apis/webhookadmission", 582 | "pkg/admission/plugin/webhook/config/apis/webhookadmission/v1alpha1", 583 | "pkg/admission/plugin/webhook/errors", 584 | "pkg/admission/plugin/webhook/mutating", 585 | "pkg/admission/plugin/webhook/namespace", 586 | "pkg/admission/plugin/webhook/request", 587 | "pkg/admission/plugin/webhook/rules", 588 | "pkg/admission/plugin/webhook/validating", 589 | "pkg/admission/plugin/webhook/versioned", 590 | "pkg/apis/apiserver", 591 | "pkg/apis/apiserver/install", 592 | "pkg/apis/apiserver/v1alpha1", 593 | "pkg/apis/audit", 594 | "pkg/apis/audit/install", 595 | "pkg/apis/audit/v1alpha1", 596 | "pkg/apis/audit/v1beta1", 597 | "pkg/apis/audit/validation", 598 | "pkg/audit", 599 | "pkg/audit/policy", 600 | "pkg/authentication/authenticator", 601 | "pkg/authentication/authenticatorfactory", 602 | "pkg/authentication/group", 603 | "pkg/authentication/request/anonymous", 604 | "pkg/authentication/request/bearertoken", 605 | "pkg/authentication/request/headerrequest", 606 | "pkg/authentication/request/union", 607 | "pkg/authentication/request/websocket", 608 | "pkg/authentication/request/x509", 609 | "pkg/authentication/serviceaccount", 610 | "pkg/authentication/token/tokenfile", 611 | "pkg/authentication/user", 612 | "pkg/authorization/authorizer", 613 | "pkg/authorization/authorizerfactory", 614 | "pkg/authorization/union", 615 | "pkg/endpoints", 616 | "pkg/endpoints/discovery", 617 | "pkg/endpoints/filters", 618 | "pkg/endpoints/handlers", 619 | "pkg/endpoints/handlers/negotiation", 620 | "pkg/endpoints/handlers/responsewriters", 621 | "pkg/endpoints/metrics", 622 | "pkg/endpoints/openapi", 623 | "pkg/endpoints/request", 624 | "pkg/features", 625 | "pkg/registry/generic", 626 | "pkg/registry/generic/registry", 627 | "pkg/registry/rest", 628 | "pkg/server", 629 | "pkg/server/filters", 630 | "pkg/server/healthz", 631 | "pkg/server/httplog", 632 | "pkg/server/mux", 633 | "pkg/server/options", 634 | "pkg/server/routes", 635 | "pkg/server/routes/data/swagger", 636 | "pkg/server/storage", 637 | "pkg/storage", 638 | "pkg/storage/errors", 639 | "pkg/storage/etcd", 640 | "pkg/storage/etcd/metrics", 641 | "pkg/storage/etcd/util", 642 | "pkg/storage/etcd3", 643 | "pkg/storage/etcd3/preflight", 644 | "pkg/storage/names", 645 | "pkg/storage/storagebackend", 646 | "pkg/storage/storagebackend/factory", 647 | "pkg/storage/value", 648 | "pkg/util/feature", 649 | "pkg/util/flag", 650 | "pkg/util/flushwriter", 651 | "pkg/util/logs", 652 | "pkg/util/trace", 653 | "pkg/util/webhook", 654 | "pkg/util/wsstream", 655 | "plugin/pkg/audit/log", 656 | "plugin/pkg/audit/webhook", 657 | "plugin/pkg/authenticator/token/webhook", 658 | "plugin/pkg/authorizer/webhook" 659 | ] 660 | revision = "5343f14b1d39e19add5eb0245eca715d8b37b164" 661 | 662 | [[projects]] 663 | name = "k8s.io/client-go" 664 | packages = [ 665 | "discovery", 666 | "informers", 667 | "informers/admissionregistration", 668 | "informers/admissionregistration/v1alpha1", 669 | "informers/admissionregistration/v1beta1", 670 | "informers/apps", 671 | "informers/apps/v1", 672 | "informers/apps/v1beta1", 673 | "informers/apps/v1beta2", 674 | "informers/autoscaling", 675 | "informers/autoscaling/v1", 676 | "informers/autoscaling/v2beta1", 677 | "informers/batch", 678 | "informers/batch/v1", 679 | "informers/batch/v1beta1", 680 | "informers/batch/v2alpha1", 681 | "informers/certificates", 682 | "informers/certificates/v1beta1", 683 | "informers/core", 684 | "informers/core/v1", 685 | "informers/events", 686 | "informers/events/v1beta1", 687 | "informers/extensions", 688 | "informers/extensions/v1beta1", 689 | "informers/internalinterfaces", 690 | "informers/networking", 691 | "informers/networking/v1", 692 | "informers/policy", 693 | "informers/policy/v1beta1", 694 | "informers/rbac", 695 | "informers/rbac/v1", 696 | "informers/rbac/v1alpha1", 697 | "informers/rbac/v1beta1", 698 | "informers/scheduling", 699 | "informers/scheduling/v1alpha1", 700 | "informers/settings", 701 | "informers/settings/v1alpha1", 702 | "informers/storage", 703 | "informers/storage/v1", 704 | "informers/storage/v1alpha1", 705 | "informers/storage/v1beta1", 706 | "kubernetes", 707 | "kubernetes/scheme", 708 | "kubernetes/typed/admissionregistration/v1alpha1", 709 | "kubernetes/typed/admissionregistration/v1beta1", 710 | "kubernetes/typed/apps/v1", 711 | "kubernetes/typed/apps/v1beta1", 712 | "kubernetes/typed/apps/v1beta2", 713 | "kubernetes/typed/authentication/v1", 714 | "kubernetes/typed/authentication/v1beta1", 715 | "kubernetes/typed/authorization/v1", 716 | "kubernetes/typed/authorization/v1beta1", 717 | "kubernetes/typed/autoscaling/v1", 718 | "kubernetes/typed/autoscaling/v2beta1", 719 | "kubernetes/typed/batch/v1", 720 | "kubernetes/typed/batch/v1beta1", 721 | "kubernetes/typed/batch/v2alpha1", 722 | "kubernetes/typed/certificates/v1beta1", 723 | "kubernetes/typed/core/v1", 724 | "kubernetes/typed/events/v1beta1", 725 | "kubernetes/typed/extensions/v1beta1", 726 | "kubernetes/typed/networking/v1", 727 | "kubernetes/typed/policy/v1beta1", 728 | "kubernetes/typed/rbac/v1", 729 | "kubernetes/typed/rbac/v1alpha1", 730 | "kubernetes/typed/rbac/v1beta1", 731 | "kubernetes/typed/scheduling/v1alpha1", 732 | "kubernetes/typed/settings/v1alpha1", 733 | "kubernetes/typed/storage/v1", 734 | "kubernetes/typed/storage/v1alpha1", 735 | "kubernetes/typed/storage/v1beta1", 736 | "listers/admissionregistration/v1alpha1", 737 | "listers/admissionregistration/v1beta1", 738 | "listers/apps/v1", 739 | "listers/apps/v1beta1", 740 | "listers/apps/v1beta2", 741 | "listers/autoscaling/v1", 742 | "listers/autoscaling/v2beta1", 743 | "listers/batch/v1", 744 | "listers/batch/v1beta1", 745 | "listers/batch/v2alpha1", 746 | "listers/certificates/v1beta1", 747 | "listers/core/v1", 748 | "listers/events/v1beta1", 749 | "listers/extensions/v1beta1", 750 | "listers/networking/v1", 751 | "listers/policy/v1beta1", 752 | "listers/rbac/v1", 753 | "listers/rbac/v1alpha1", 754 | "listers/rbac/v1beta1", 755 | "listers/scheduling/v1alpha1", 756 | "listers/settings/v1alpha1", 757 | "listers/storage/v1", 758 | "listers/storage/v1alpha1", 759 | "listers/storage/v1beta1", 760 | "pkg/version", 761 | "rest", 762 | "rest/watch", 763 | "tools/auth", 764 | "tools/cache", 765 | "tools/clientcmd", 766 | "tools/clientcmd/api", 767 | "tools/clientcmd/api/latest", 768 | "tools/clientcmd/api/v1", 769 | "tools/metrics", 770 | "tools/pager", 771 | "tools/reference", 772 | "transport", 773 | "util/buffer", 774 | "util/cert", 775 | "util/flowcontrol", 776 | "util/homedir", 777 | "util/integer" 778 | ] 779 | revision = "78700dec6369ba22221b72770783300f143df150" 780 | version = "v6.0.0" 781 | 782 | [[projects]] 783 | branch = "master" 784 | name = "k8s.io/kube-openapi" 785 | packages = [ 786 | "pkg/builder", 787 | "pkg/common", 788 | "pkg/handler", 789 | "pkg/util", 790 | "pkg/util/proto" 791 | ] 792 | revision = "50ae88d24ede7b8bad68e23c805b5d3da5c8abaf" 793 | 794 | [solve-meta] 795 | analyzer-name = "dep" 796 | analyzer-version = 1 797 | inputs-digest = "bc2003ffb650f5de05ae3392df5f7de48c3302dcd7f13d1295bc17739b2bfca3" 798 | solver-name = "gps-cdcl" 799 | solver-version = 1 800 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | 2 | # Gopkg.toml example 3 | # 4 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 5 | # for detailed Gopkg.toml documentation. 6 | # 7 | # required = ["github.com/user/thing/cmd/thing"] 8 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 9 | # 10 | # [[constraint]] 11 | # name = "github.com/user/project" 12 | # version = "1.0.0" 13 | # 14 | # [[constraint]] 15 | # name = "github.com/user/project2" 16 | # branch = "dev" 17 | # source = "github.com/myfork/project2" 18 | # 19 | # [[override]] 20 | # name = "github.com/x/y" 21 | # version = "2.4.0" 22 | 23 | 24 | [[constraint]] 25 | name = "github.com/openshift/generic-admission-server" 26 | version = "1.9.0" 27 | 28 | [[constraint]] 29 | name = "k8s.io/api" 30 | version = "kubernetes-1.9.0" 31 | 32 | [[constraint]] 33 | name = "k8s.io/apimachinery" 34 | version = "kubernetes-1.9.0" 35 | 36 | [[constraint]] 37 | name = "k8s.io/client-go" 38 | version = "6.0.0" 39 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include .env 2 | BINARY := quack 3 | VERSION := $(shell git describe --always --dirty --tags 2>/dev/null || echo "undefined") 4 | 5 | RED := \033[31m 6 | GREEN := \033[32m 7 | NC := \033[0m 8 | 9 | IMG ?= quay.io/pusher/quack 10 | 11 | .NOTPARALLEL: 12 | 13 | .PHONY: all 14 | all: distclean test build 15 | 16 | .PHONY: build 17 | build: clean $(BINARY) 18 | 19 | .PHONY: clean 20 | clean: 21 | rm -f $(BINARY) 22 | 23 | .PHONY: distclean 24 | distclean: clean 25 | rm -rf vendor 26 | rm -rf release 27 | 28 | .PHONY: fmt 29 | fmt: 30 | $(GO) fmt ./cmd/... ./pkg/... 31 | 32 | .PHONY: vet 33 | vet: vendor 34 | $(GO) vet ./cmd/... ./pkg/... 35 | 36 | .PHONY: lint 37 | lint: vendor 38 | @ echo -e "$(GREEN)Linting code$(NC)" 39 | $(LINTER) run --disable-all \ 40 | --exclude-use-default=false \ 41 | --enable=govet \ 42 | --enable=ineffassign \ 43 | --enable=deadcode \ 44 | --enable=golint \ 45 | --enable=goconst \ 46 | --enable=gofmt \ 47 | --enable=goimports \ 48 | --deadline=120s \ 49 | --tests ./... 50 | @ echo 51 | 52 | vendor: 53 | @ echo -e "$(GREEN)Pulling dependencies$(NC)" 54 | $(DEP) ensure -v --vendor-only 55 | @ echo 56 | 57 | .PHONY: test 58 | test: vendor 59 | @ echo -e "$(GREEN)Running test suite$(NC)" 60 | $(GO) test ./... 61 | @ echo 62 | 63 | .PHONY: check 64 | check: fmt lint vet test 65 | 66 | .PHONY: build 67 | build: clean $(BINARY) 68 | 69 | $(BINARY): fmt vet 70 | CGO_ENABLED=0 $(GO) build -o $(BINARY) github.com/pusher/quack/cmd/quack 71 | 72 | .PHONY: docker-build 73 | docker-build: check 74 | docker build . -t ${IMG}:${VERSION} 75 | @echo -e "$(GREEN)Built $(IMG):$(VERSION)$(NC)" 76 | 77 | TAGS ?= latest 78 | .PHONY: docker-tag 79 | docker-tag: docker-build 80 | @IFS=","; tags=${TAGS}; for tag in $${tags}; do docker tag ${IMG}:${VERSION} ${IMG}:$${tag}; echo -e "$(GREEN)Tagged $(IMG):$(VERSION) as $${tag}$(NC)"; done 81 | 82 | PUSH_TAGS ?= ${VERSION}, latest 83 | .PHONY: docker-push 84 | docker-push: docker-build docker-tag 85 | @IFS=","; tags=${PUSH_TAGS}; for tag in $${tags}; do docker push ${IMG}:$${tag}; echo -e "$(GREEN)Pushed $(IMG):$${tag}$(NC)"; done 86 | 87 | TAGS ?= latest 88 | .PHONY: docker-clean 89 | docker-clean: 90 | @IFS=","; tags=${TAGS}; for tag in $${tags}; do docker rmi -f ${IMG}:${VERSION} ${IMG}:$${tag}; done 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Quack Logo 2 | 3 | # Quack 4 | 5 | > NOTE: this repository is currently **UNMAINTAINED** and is looking for new owner(s). 6 | > See [#19](/../../issues/19) for more information. 7 | 8 | In-Cluster templating using the Go Template syntax. 9 | 10 | **Note:** This is a proof of concept in the early alpha stage. 11 | We are providing no guarantees. 12 | We also cannot provide support for Kubernetes configuration, 13 | Mutating Webhooks are currently beta and the required configuration is 14 | complex. 15 | Please see the [Installation](#installation) section for further detail. 16 | 17 | ## Table of contents 18 | 19 | - [Introduction](#introduction) 20 | - [Installation](#installation) 21 | - [Deploying to Kubernetes](#deploying-to-kubernetes) 22 | - [Configuration](#configuration) 23 | - [Example Quack Template](#example-quack-template) 24 | - [Custom Delimiters](#custom-delimiters) 25 | - [Quack vs Other Systems](#quack-vs-other-systems) 26 | - [Communication](#communication) 27 | - [Contributing](#contributing) 28 | - [License](#license) 29 | 30 | ## Introduction 31 | 32 | Client Side templating can be error prone. When running multiple Kubernetes 33 | clusters it can be very easy to apply rendered templates to the wrong 34 | cluster. 35 | 36 | Quack solves this problem by rendering the templates as they are applied to the 37 | cluster. 38 | 39 | Quack acts as a Mutating Webhook Admission Controller and intercepts requests to 40 | create and update Kubernetes resources such as Deployments, Ingresses and 41 | ConfigMaps. 42 | 43 | Any resource that contains Go Template syntax will be templated (using values 44 | from Quack's ConfigMap) before it passes through API validation. 45 | 46 | Quack is ideal for templating Cluster-wide environment such as the Cluster Name, 47 | Region and Environment (Staging/Production). 48 | 49 | ## Installation 50 | 51 | ### Deploying to Kubernetes 52 | 53 | Example Kubernetes manifests are in the [deploy](/deploy) folder. 54 | 55 | #### Quack RBAC 56 | 57 | Quack uses RBAC to ensure that requests for templating are authorized. 58 | If you are running RBAC within your Kubernetes cluster you must ensure that 59 | you configure Webhook Authentication in your Kubernetes API server. 60 | [See the docs.](https://github.com/kubernetes/website/blob/49a81e1a69a0d7d0b8f98bf7119583b025517792/docs/admin/extensible-admission-controllers.md#authenticate-apiservers) 61 | 62 | Once you have configured the API server authentication, ensure you update the 63 | [API Server ClusterRoleBinding](/deploy/crb-quack-apiserver.yaml) with the 64 | appropriate subject. 65 | 66 | #### Certificates 67 | 68 | Quack requires a TLS certificate for serving the webhook securely. 69 | This certificate **must** be signed by a CA certificate. 70 | 71 | In Kubernetes 1.9, Webhooks must be called over HTTPS and the serving 72 | certificate's CA bundle must be configured in the 73 | [MutatingWebhookConfiguration](deploy/mutatingwebhookconfiguration.yaml). 74 | This establishes trust between the API server and Quack. 75 | 76 | Base64 encode your CA certificate bundle and substitute it for the `caBundle` 77 | field in the 78 | [MutatingWebhookConfiguration](deploy/mutatingwebhookconfiguration.yaml). 79 | 80 | The example [Daemonset](deploy/daemonset.yaml) expects a secret containing the 81 | certificate in the format as below: 82 | 83 | ```yaml 84 | apiVersion: v1 85 | data: 86 | cert.pem: 87 | key.pem: 88 | kind: Secret 89 | metadata: 90 | name: quack-certs 91 | namespace: quack 92 | type: Opaque 93 | ``` 94 | 95 | ### Configuration 96 | 97 | Quack adopts the standard Kubernetes Generic API server flags (including 98 | Authentication and Authorization flags). 99 | It loads the In Cluster configuration by default but this can be overridden 100 | by the `--kubeconfig` flag. 101 | 102 | Quack takes the following additional flags: 103 | 104 | - `--values-configmap` (Default: `quack-values`): Defines the name of the 105 | ConfigMap to load cluster level template values from. 106 | - `--values-configmap-namespace` (Default: `quack`): Defines the namespace in 107 | which the Values ConfigMap exists. 108 | - `--required-annotation`: Filter objects based on the existence of a named 109 | annotation before templating them. 110 | - `--ignore-path`: Ignore patches for certain paths in when templating files. 111 | May be called multiple times. Paths should be specified as 112 | [RFC6901 JSON Pointers](https://tools.ietf.org/html/rfc6901). 113 | 114 | #### Restricting Quack 115 | 116 | You should configure Quack to only template the resources you need it to 117 | template. 118 | 119 | There are three parts to restricting Quack: 120 | 121 | ##### Namespace 122 | 123 | A Namespace selector in the 124 | [MutatingWebhookConfiguration](deploy/mutatingwebhookconfiguration.yaml) 125 | must be matched for resources within the Namespace to be templated by Quack. 126 | 127 | ```yaml 128 | apiVersion: v1 129 | kind: Namespace 130 | metadata: 131 | name: kube-system 132 | labels: 133 | quack.pusher.com/enabled: "true" 134 | ``` 135 | 136 | ##### Resource rules 137 | 138 | A list of resoucre rules in the 139 | [MutatingWebhookConfiguration](deploy/mutatingwebhookconfiguration.yaml) 140 | must be matched for resources to be templated by Quack. 141 | 142 | Add and remove resources to fit your requirements: 143 | 144 | ```yaml 145 | rules: 146 | - operations: ["CREATE", "UPDATE"] 147 | apiGroups: ["*"] 148 | apiVersions: ["*"] 149 | resources: 150 | - configmaps 151 | - daemonsets 152 | - deployments 153 | - statefulsets 154 | - ingresses 155 | - services 156 | ``` 157 | 158 | ##### Required annotation 159 | 160 | Using the flag `--required-annotation`, you can tell Quack to skip templating 161 | for any resource not annotated appropriately. 162 | 163 | For example, if Quack is configured with 164 | `--required-annotation=quack.pusher.com/template`, only resources (matching the 165 | Resource rules and Namespace Selector) with an annotation as below will be 166 | templated: 167 | 168 | ```yaml 169 | --- 170 | apiVersion: v1 171 | metadata: 172 | annotations: 173 | quack.pusher.com/template: "true" # The value is not checked. 174 | ``` 175 | 176 | ## Example Quack Template 177 | 178 | In this example, we are defining an Ingress object for the Kubernetes Dashboard. 179 | 180 | We suppose that there is an existing Kubernetes cluster known as `alpha`. 181 | We then assume that Quack is configured to watch the Namespace 182 | `kube-system` and that a DNS record for `alpha.example.com` points to the 183 | Ingress Controller on the Kubernetes cluster `alpha`. 184 | 185 | In this case, the Quack installation might have the ConfigMap: 186 | 187 | ```yaml 188 | apiVersion: v1 189 | kind: ConfigMap 190 | metadata: 191 | name: quack-values 192 | namespace: quack 193 | data: 194 | ClusterName: alpha 195 | ``` 196 | 197 | Create the Ingress object as below and apply it to the cluster: 198 | 199 | ```sh 200 | kubectl apply -f ingress.yaml 201 | ``` 202 | 203 | ingress.yaml: 204 | 205 | ```yaml 206 | apiVersion: extensions/v1beta1 207 | kind: Ingress 208 | metadata: 209 | name: kubernetes-dashboard 210 | namespace: kube-system 211 | spec: 212 | tls: 213 | - hosts: 214 | - "{{- .ClusterName -}}.example.com" 215 | rules: 216 | - host: "{{- .ClusterName -}}.example.com" 217 | http: 218 | paths: 219 | - path: /ui 220 | backend: 221 | serviceName: kubernetes-dashboard 222 | servicePort: 443 223 | ``` 224 | 225 | Once applied, fetch the Ingress object to see that it has been templated 226 | appropriately by Quack: 227 | 228 | ```sh 229 | kubectl get -f ingress.yaml -o yaml 230 | apiVersion: extensions/v1beta1 231 | kind: Ingress 232 | metadata: 233 | name: kubernetes-dashboard 234 | namespace: kube-system 235 | spec: 236 | tls: 237 | - hosts: 238 | - "alpha.example.com" 239 | rules: 240 | - host: "alpha.example.com" 241 | http: 242 | paths: 243 | - path: /ui 244 | backend: 245 | serviceName: kubernetes-dashboard 246 | servicePort: 443 247 | ``` 248 | 249 | When creating further Kubernetes clusters, the same template can be applied 250 | directly to each cluster and the resulting Kubernetes resources will be correct 251 | for their cluster's particular environment. 252 | 253 | ### Custom Delimiters 254 | 255 | Each individual Quack template can specify their own delimiters for use against 256 | the template. 257 | 258 | Add annotations `quack.pusher.com/left-delim` and `quack.pusher.com/right-delim` 259 | to your template with the desired left and right template delimiters 260 | respectively. 261 | 262 | ```yaml 263 | --- 264 | apiVersion: v1 265 | metadata: 266 | annotations: 267 | quack.pusher.com/left-delim: "[[" 268 | quack.pusher.com/right-delim: "]]" 269 | ... 270 | spec: 271 | foo: "[[- .FooValue -]]" 272 | ``` 273 | 274 | ## Quack vs Other Systems 275 | 276 | - Quack intercepts the standard flow of `kubectl apply`. This means there are no 277 | extra tools and no additional syntax to learn, bar the Go Templating Syntax. 278 | - Resources are still created using the main Kubernetes API server. This means 279 | that regular syntax validation and Authorization still happen as the object is 280 | being created or updated. 281 | - Cluster specific values are stored in cluster and separate from your templates. 282 | Allows you to keep one copy of Kubernetes manifests and reuse them across all 283 | clusters without any client side changes. 284 | - Protects against mistakenly applying manifests cross clusters. (Templates have 285 | no specific configuration for each cluster, Quack abstracts this.) 286 | 287 | ## Communication 288 | 289 | - Found a bug? Please open an issue. 290 | - Have a feature request. Please open an issue. 291 | - If you want to contribute, please submit a pull request 292 | 293 | ## Contributing 294 | 295 | Please see our [Contributing](CONTRIBUTING.md) guidelines. 296 | 297 | ## License 298 | 299 | This project is licensed under Apache 2.0 and a copy of the license is available [here](LICENSE). 300 | -------------------------------------------------------------------------------- /cmd/quack/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "os" 6 | "runtime" 7 | 8 | "github.com/golang/glog" 9 | "github.com/openshift/generic-admission-server/pkg/apiserver" 10 | "github.com/openshift/generic-admission-server/pkg/cmd/server" 11 | "github.com/pusher/quack/pkg/quack" 12 | "github.com/spf13/pflag" 13 | genericapiserver "k8s.io/apiserver/pkg/server" 14 | "k8s.io/apiserver/pkg/util/logs" 15 | ) 16 | 17 | func main() { 18 | flagset := pflag.NewFlagSet("quack", pflag.ExitOnError) 19 | 20 | // quack.AdmissionHook is the main package 21 | ah := &quack.AdmissionHook{} 22 | 23 | // Set flags to populate admission hook configuration 24 | flagset.StringVarP(&ah.ValuesMapName, "values-configmap", "c", "quack-values", "Defines the name of the ConfigMap to load templating values from") 25 | flagset.StringVarP(&ah.ValuesMapNamespace, "values-configmap-namespace", "n", "quack", "Defines the namespace to load the Values ConfigMap from") 26 | flagset.StringVarP(&ah.RequiredAnnotation, "required-annotation", "a", "", "Require annotation on objects before templating them") 27 | flagset.StringSliceVar(&ah.IgnoredPaths, "ignore-path", []string{}, "Ignore patches that are applied to this path") 28 | 29 | // Run server 30 | runAdmissionServer(flagset, ah) 31 | } 32 | 33 | // Originally from: https://github.com/openshift/generic-admission-server/blob/v1.9.0/pkg/cmd/cmd.go 34 | func runAdmissionServer(flagset *pflag.FlagSet, admissionHooks ...apiserver.AdmissionHook) { 35 | logs.InitLogs() 36 | defer logs.FlushLogs() 37 | 38 | if len(os.Getenv("GOMAXPROCS")) == 0 { 39 | runtime.GOMAXPROCS(runtime.NumCPU()) 40 | } 41 | 42 | stopCh := genericapiserver.SetupSignalHandler() 43 | 44 | cmd := server.NewCommandStartAdmissionServer(os.Stdout, os.Stderr, stopCh, admissionHooks...) 45 | cmd.Short = "Launch Quack Templating Server" 46 | cmd.Long = "Launch Quack Templating Server" 47 | 48 | // Add admission hook flags 49 | cmd.PersistentFlags().AddFlagSet(flagset) 50 | 51 | // Flags for glog 52 | cmd.PersistentFlags().AddGoFlagSet(flag.CommandLine) 53 | // Fix glog printing "Error: logging before flag.Parse" 54 | flag.CommandLine.Parse([]string{}) 55 | 56 | if err := cmd.Execute(); err != nil { 57 | glog.Fatal(err) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /configure: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if (( ${BASH_VERSION:0:1} < 4 )); then 4 | echo "This configure script requires bash 4" 5 | exit 1 6 | fi 7 | 8 | RED='\033[0;31m' 9 | GREEN='\033[0;32m' 10 | BLUE='\033[0;34m' 11 | NC='\033[0m' 12 | 13 | declare -A tools=() 14 | 15 | vercomp () { 16 | if [[ $1 == $2 ]] 17 | then 18 | return 0 19 | fi 20 | local IFS=. 21 | local i ver1=($1) ver2=($2) 22 | # fill empty fields in ver1 with zeros 23 | for ((i=${#ver1[@]}; i<${#ver2[@]}; i++)) 24 | do 25 | ver1[i]=0 26 | done 27 | for ((i=0; i<${#ver1[@]}; i++)) 28 | do 29 | if [[ -z ${ver2[i]} ]] 30 | then 31 | # fill empty fields in ver2 with zeros 32 | ver2[i]=0 33 | fi 34 | if ((10#${ver1[i]} > 10#${ver2[i]})) 35 | then 36 | return 1 37 | fi 38 | if ((10#${ver1[i]} < 10#${ver2[i]})) 39 | then 40 | return 2 41 | fi 42 | done 43 | return 0 44 | } 45 | 46 | check_for() { 47 | echo -n "Checking for $1... " 48 | TOOL_PATH=$(command -v $1) 49 | if ! [ -x "$TOOL_PATH" -a -f "$TOOL_PATH" ]; then 50 | printf "${RED}not found${NC}\n" 51 | cd - > /dev/null 52 | exit 1 53 | else 54 | printf "${GREEN}found${NC}\n" 55 | tools[$1]=$TOOL_PATH 56 | fi 57 | } 58 | 59 | check_go_env() { 60 | echo -n "Checking \$GOPATH... " 61 | if [ -z "$GOPATH" ]; then 62 | printf "${RED}invalid${NC} - GOPATH not set\n" 63 | exit 1 64 | fi 65 | printf "${GREEN}valid${NC} - $GOPATH\n" 66 | } 67 | 68 | check_go_version() { 69 | echo -n "Checking go version... " 70 | GO_VERSION=$(${tools[go]} version | ${tools[awk]} '{where = match($0, /[0-9]\.[0-9]+[\.0-9]*/); if (where != 0) print substr($0, RSTART, RLENGTH)}') 71 | vercomp $GO_VERSION 1.10 72 | case $? in 73 | 0) ;& 74 | 1) 75 | printf "${GREEN}" 76 | echo $GO_VERSION 77 | printf "${NC}" 78 | ;; 79 | 2) 80 | printf "${RED}" 81 | echo "$GO_VERSION < 1.10" 82 | exit 1 83 | ;; 84 | esac 85 | } 86 | 87 | cd ${0%/*} 88 | 89 | check_for make 90 | check_for awk 91 | check_for go 92 | check_for dep 93 | check_for golangci-lint 94 | check_go_env 95 | check_go_version 96 | 97 | cat <<- EOF > .env 98 | MAKE := ${tools[make]} 99 | SHASUM := ${tools[shasum]} 100 | GO := ${tools[go]} 101 | GOVERSION := $GO_VERSION 102 | DEP := ${tools[dep]} 103 | LINTER := ${tools[golangci-lint]} 104 | EOF 105 | 106 | echo "Environment configuration written to .env" 107 | -------------------------------------------------------------------------------- /deploy/clusterrole-quack-apiserver.yaml: -------------------------------------------------------------------------------- 1 | kind: ClusterRole 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | metadata: 4 | name: quack:system:api-server-auth 5 | rules: 6 | - apiGroups: 7 | - "quack.pusher.com" 8 | resources: 9 | - admissionreviews 10 | verbs: 11 | - create 12 | -------------------------------------------------------------------------------- /deploy/configmap-audit.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: quack-config 5 | namespace: quack 6 | data: 7 | audit.yaml: | 8 | # Log all requests at the Metadata level. 9 | apiVersion: audit.k8s.io/v1beta1 10 | kind: Policy 11 | rules: 12 | - level: Metadata 13 | -------------------------------------------------------------------------------- /deploy/crb-authentication-reader.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: quack:system:extension-apiserver-authentication-reader 5 | namespace: kube-system 6 | roleRef: 7 | apiGroup: rbac.authorization.k8s.io 8 | kind: Role 9 | name: extension-apiserver-authentication-reader 10 | subjects: 11 | - kind: ServiceAccount 12 | name: quack 13 | namespace: quack 14 | -------------------------------------------------------------------------------- /deploy/crb-delegator.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: quack:system:auth-delegator 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: system:auth-delegator 9 | subjects: 10 | - kind: ServiceAccount 11 | name: quack 12 | namespace: quack 13 | -------------------------------------------------------------------------------- /deploy/crb-quack-apiserver.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: quack:system:api-server-auth 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: quack:system:api-server-auth 9 | subjects: 10 | - kind: User 11 | name: kube-apiserver 12 | -------------------------------------------------------------------------------- /deploy/daemonset.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: DaemonSet 3 | metadata: 4 | name: quack 5 | namespace: quack 6 | labels: 7 | app: quack 8 | spec: 9 | selector: 10 | matchLabels: 11 | app: quack 12 | updateStrategy: 13 | type: RollingUpdate 14 | template: 15 | metadata: 16 | name: quack 17 | labels: 18 | app: quack 19 | spec: 20 | serviceAccountName: quack 21 | containers: 22 | - name: quack 23 | image: quay.io/pusher/quack:v0.1.0 24 | imagePullPolicy: Always 25 | args: 26 | - --tls-cert-file=/etc/certs/cert.pem 27 | - --tls-private-key-file=/etc/certs/key.pem 28 | - --audit-log-path=- 29 | - --audit-policy-file=/etc/config/audit.yaml 30 | - --v=2 31 | resources: 32 | requests: 33 | cpu: 10m 34 | memory: 20Mi 35 | limits: 36 | cpu: 100m 37 | memory: 100Mi 38 | livenessProbe: 39 | httpGet: 40 | scheme: HTTPS 41 | path: /healthz 42 | port: 443 43 | initialDelaySeconds: 10 44 | readinessProbe: 45 | httpGet: 46 | scheme: HTTPS 47 | path: /healthz 48 | port: 443 49 | initialDelaySeconds: 10 50 | volumeMounts: 51 | - name: certs 52 | mountPath: /etc/certs 53 | readOnly: true 54 | - name: config 55 | mountPath: /etc/config 56 | readOnly: true 57 | volumes: 58 | - name: certs 59 | secret: 60 | secretName: quack-certs 61 | - name: config 62 | configMap: 63 | name: quack-config 64 | tolerations: 65 | - key: node-role.kubernetes.io/master 66 | operator: Exists 67 | effect: NoSchedule 68 | nodeSelector: 69 | "node-role.kubernetes.io/master": "true" 70 | -------------------------------------------------------------------------------- /deploy/mutatingwebhookconfiguration.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: admissionregistration.k8s.io/v1beta1 2 | kind: MutatingWebhookConfiguration 3 | metadata: 4 | name: quack 5 | webhooks: 6 | - name: quack.pusher.com 7 | clientConfig: 8 | service: 9 | name: quack 10 | namespace: quack 11 | path: /apis/quack.pusher.com/v1alpha1/admissionreviews 12 | caBundle: "" 13 | rules: 14 | - operations: ["CREATE", "UPDATE"] 15 | apiGroups: ["*"] 16 | apiVersions: ["*"] 17 | resources: 18 | - configmaps 19 | - daemonsets 20 | - deployments 21 | - statefulsets 22 | - ingresses 23 | - services 24 | failurePolicy: Fail 25 | namespaceSelector: 26 | matchExpressions: 27 | - key: quack.pusher.com/enabled 28 | operator: In 29 | values: 30 | - "true" 31 | -------------------------------------------------------------------------------- /deploy/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: quack 5 | -------------------------------------------------------------------------------- /deploy/rb.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: quack 5 | namespace: quack 6 | roleRef: 7 | apiGroup: rbac.authorization.k8s.io 8 | kind: Role 9 | name: quack 10 | subjects: 11 | - kind: ServiceAccount 12 | name: quack 13 | namespace: quack 14 | -------------------------------------------------------------------------------- /deploy/role.yaml: -------------------------------------------------------------------------------- 1 | kind: Role 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | metadata: 4 | name: quack 5 | namespace: quack 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - configmaps 11 | verbs: 12 | - get 13 | -------------------------------------------------------------------------------- /deploy/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: quack 5 | namespace: quack 6 | labels: 7 | app: quack 8 | spec: 9 | ports: 10 | - name: https-webhook # optional 11 | port: 443 12 | selector: 13 | app: quack 14 | -------------------------------------------------------------------------------- /deploy/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: quack 5 | namespace: quack 6 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pkg/quack/quack.go: -------------------------------------------------------------------------------- 1 | package quack 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "html/template" 8 | "net/http" 9 | "strings" 10 | 11 | mergepatch "github.com/evanphx/json-patch" 12 | "github.com/golang/glog" 13 | "github.com/mattbaird/jsonpatch" 14 | admissionv1beta1 "k8s.io/api/admission/v1beta1" 15 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 16 | "k8s.io/apimachinery/pkg/runtime/schema" 17 | "k8s.io/client-go/kubernetes" 18 | restclient "k8s.io/client-go/rest" 19 | ) 20 | 21 | const ( 22 | lastAppliedConfigPath = "/metadata/annotations/kubectl.kubernetes.io~1last-applied-configuration" 23 | quackAnnotationPrefix = "/metadata/annotations/quack.pusher.com" 24 | leftDelimAnnotation = "quack.pusher.com/left-delim" 25 | rightDelimAnnotation = "quack.pusher.com/right-delim" 26 | ) 27 | 28 | // AdmissionHook implements the OpenShift MutatingAdmissionHook interface. 29 | // https://github.com/openshift/generic-admission-server/blob/v1.9.0/pkg/apiserver/apiserver.go#L45 30 | type AdmissionHook struct { 31 | client *kubernetes.Clientset // Kubernetes client for calling Api 32 | ValuesMapName string // Source of templating values 33 | ValuesMapNamespace string // Namespace the configmap lives in 34 | RequiredAnnotation string // Annotation required before templating 35 | IgnoredPaths []string // Paths to not patch 36 | } 37 | 38 | // Initialize configures the AdmissionHook. 39 | // 40 | // Initializes connection Kubernetes Client 41 | func (ah *AdmissionHook) Initialize(kubeClientConfig *restclient.Config, stopCh <-chan struct{}) error { 42 | // Initialise a Kubernetes client 43 | client, err := kubernetes.NewForConfig(kubeClientConfig) 44 | if err != nil { 45 | return fmt.Errorf("failed to intialise kubernetes clientset: %v", err) 46 | } 47 | ah.client = client 48 | 49 | // Add lastAppliedConfigPath to ignored paths, unless it's already present 50 | if !contains(ah.IgnoredPaths, lastAppliedConfigPath) { 51 | ah.IgnoredPaths = append(ah.IgnoredPaths, lastAppliedConfigPath) 52 | } 53 | 54 | glog.Info("Webhook Initialization Complete.") 55 | return nil 56 | } 57 | 58 | // MutatingResource defines where the Webhook is hosted. 59 | func (ah *AdmissionHook) MutatingResource() (schema.GroupVersionResource, string) { 60 | return schema.GroupVersionResource{ 61 | Group: "quack.pusher.com", 62 | Version: "v1alpha1", 63 | Resource: "admissionreviews", 64 | }, 65 | "AdmissionReview" 66 | } 67 | 68 | // Admit is the actual business logic of the webhook. 69 | // This is the method that processes the request to the admission controller. 70 | // 71 | // Checks the operation is a create or update operation. 72 | // Loads the template values from the configmap. 73 | // Templates the values into the raw object (json) from the admission request. 74 | // Calculates a JSON Patch to append to the admission response. 75 | func (ah *AdmissionHook) Admit(req *admissionv1beta1.AdmissionRequest) *admissionv1beta1.AdmissionResponse { 76 | resp := &admissionv1beta1.AdmissionResponse{} 77 | resp.UID = req.UID 78 | requestName := fmt.Sprintf("%s %s", req.Kind, podID(req.Namespace, req.Name)) 79 | 80 | // Skip operations that aren't create or update 81 | if req.Operation != admissionv1beta1.Create && 82 | req.Operation != admissionv1beta1.Update { 83 | glog.V(2).Infof("Skipping %s request for %s", req.Operation, requestName) 84 | resp.Allowed = true 85 | return resp 86 | } 87 | 88 | // Skip requests that do not have the required annotation 89 | annototationPresent, err := requestHasAnnotation(ah.RequiredAnnotation, req.Object.Raw) 90 | if err != nil { 91 | return errorResponse(resp, "Failed to read annotations: %v", err) 92 | } 93 | if !annototationPresent { 94 | glog.V(2).Infof("Skipping %s request for %s: Required annotation not present.", req.Operation, requestName) 95 | resp.Allowed = true 96 | return resp 97 | } 98 | 99 | glog.V(2).Infof("Processing %s request for %s", req.Operation, requestName) 100 | 101 | // Load template values from configmap 102 | values, err := getValues(ah.client, ah.ValuesMapNamespace, ah.ValuesMapName) 103 | if err != nil { 104 | return errorResponse(resp, "Failed to get template values: %v", err) 105 | } 106 | 107 | delims, err := getDelims(req.Object.Raw) 108 | if err != nil { 109 | return errorResponse(resp, "Invalid delimiters: %v", err) 110 | } 111 | 112 | templateInput, err := getTemplateInput(req.Object.Raw, ah.IgnoredPaths) 113 | if err != nil { 114 | return errorResponse(resp, "Error creating template input: %v", err) 115 | } 116 | // Run Templating 117 | glog.V(6).Infof("Input for %s: %s", requestName, templateInput) 118 | 119 | output, err := renderTemplate(templateInput, values, delims) 120 | if err != nil { 121 | return errorResponse(resp, "Error rendering template: %v", err) 122 | } 123 | glog.V(6).Infof("Output for %s: %s", requestName, output) 124 | 125 | // Create a JSON Patch 126 | // https://tools.ietf.org/html/rfc6902 127 | patchBytes, err := ah.createPatch(req.Object.Raw, output) 128 | if err != nil { 129 | return errorResponse(resp, "Error creating patch: %v", err) 130 | } 131 | 132 | // If the patch is non-zero, append it 133 | if string(patchBytes) != "[]" { 134 | glog.V(2).Infof("Patching %s", requestName) 135 | glog.V(4).Infof("Patch for %s: %s", requestName, string(patchBytes)) 136 | resp.Patch = patchBytes 137 | resp.PatchType = func() *admissionv1beta1.PatchType { 138 | pt := admissionv1beta1.PatchTypeJSONPatch 139 | return &pt 140 | }() 141 | } 142 | 143 | resp.Allowed = true 144 | return resp 145 | } 146 | 147 | func renderTemplate(input []byte, values map[string]string, delims delimiters) ([]byte, error) { 148 | tmpl, err := template.New("object").Delims(delims.left, delims.right).Parse(string(input)) 149 | if err != nil { 150 | return nil, fmt.Errorf("failed to parse template: %v", err) 151 | } 152 | buff := new(bytes.Buffer) 153 | err = tmpl.Execute(buff, values) 154 | if err != nil { 155 | return nil, fmt.Errorf("failed to execute template: %v", err) 156 | } 157 | return buff.Bytes(), nil 158 | } 159 | 160 | func getValues(client *kubernetes.Clientset, namespace string, name string) (map[string]string, error) { 161 | getOpts := metav1.GetOptions{} 162 | cm, err := client.CoreV1().ConfigMaps(namespace).Get(name, getOpts) 163 | if err != nil { 164 | return nil, fmt.Errorf("couldn't get configmap: %v", err) 165 | } 166 | return cm.Data, nil 167 | } 168 | 169 | func (ah *AdmissionHook) createPatch(old []byte, new []byte) ([]byte, error) { 170 | patch, err := jsonpatch.CreatePatch(old, new) 171 | if err != nil { 172 | return nil, fmt.Errorf("error calculating patch: %v", err) 173 | } 174 | 175 | allowedOps := []jsonpatch.JsonPatchOperation{} 176 | for _, op := range patch { 177 | // Don't patch the lastAppliedConfig created by kubectl 178 | if op.Path == lastAppliedConfigPath || 179 | strings.HasPrefix(op.Path, quackAnnotationPrefix) || 180 | contains(ah.IgnoredPaths, op.Path) || 181 | strings.HasPrefix(op.Path, "/status") { 182 | continue 183 | } 184 | allowedOps = append(allowedOps, op) 185 | } 186 | 187 | patchBytes, err := json.Marshal(allowedOps) 188 | if err != nil { 189 | return nil, fmt.Errorf("error marshalling patch: %v", err) 190 | } 191 | return patchBytes, nil 192 | } 193 | 194 | func getTemplateInput(data []byte, ignoredPaths []string) ([]byte, error) { 195 | // Fetch object meta into object 196 | objectMeta, err := getObjectMeta(data) 197 | if err != nil { 198 | return nil, fmt.Errorf("error reading object metadata: %v", err) 199 | } 200 | 201 | // We should not modify the status of objects 202 | hasStatus, err := requestHasStatus(data) 203 | if err != nil { 204 | return nil, fmt.Errorf("error reading object status: %v", err) 205 | } 206 | if hasStatus { 207 | patch := []byte(fmt.Sprintf(`[ 208 | {"op": "remove", "path": "/status"} 209 | ]`)) 210 | data, err = applyPatch(data, patch) 211 | if err != nil { 212 | return nil, fmt.Errorf("error removing status: %v", err) 213 | } 214 | } 215 | 216 | for _, path := range ignoredPaths { 217 | patch := []byte(fmt.Sprintf(`[ 218 | {"op": "remove", "path": "%s"} 219 | ]`, path)) 220 | newData, err := applyPatch(data, patch) 221 | if err != nil && !nonExistentPath(err) { 222 | return nil, fmt.Errorf("error removing %s: %v", path, err) 223 | } else if newData != nil { 224 | data = newData 225 | } 226 | } 227 | 228 | for annotation := range objectMeta.Annotations { 229 | if strings.HasPrefix(annotation, "quack.pusher.com") { 230 | // Remove annotations from input template 231 | escapedAnnotation := strings.Replace(annotation, "/", "~1", -1) 232 | patch := []byte(fmt.Sprintf(`[ 233 | {"op": "remove", "path": "/metadata/annotations/%s"} 234 | ]`, escapedAnnotation)) 235 | data, err = applyPatch(data, patch) 236 | if err != nil { 237 | return nil, fmt.Errorf("error removing annotation %s: %v", annotation, err) 238 | } 239 | } 240 | } 241 | 242 | return data, nil 243 | } 244 | 245 | func requestHasAnnotation(requiredAnnotation string, raw []byte) (bool, error) { 246 | if requiredAnnotation == "" { 247 | return true, nil 248 | } 249 | 250 | // Fetch object meta into object 251 | objectMeta, err := getObjectMeta(raw) 252 | if err != nil { 253 | return false, fmt.Errorf("error reading object metadata: %v", err) 254 | } 255 | 256 | glog.V(6).Infof("Requested Object Annotations: %v", objectMeta.Annotations) 257 | 258 | // Check required annotation exists in struct 259 | if _, ok := objectMeta.Annotations[requiredAnnotation]; ok { 260 | return true, nil 261 | } 262 | return false, nil 263 | } 264 | 265 | func getObjectMeta(raw []byte) (metav1.ObjectMeta, error) { 266 | requestMeta := struct { 267 | metav1.ObjectMeta `json:"metadata"` 268 | }{ 269 | ObjectMeta: metav1.ObjectMeta{}, 270 | } 271 | err := json.Unmarshal(raw, &requestMeta) 272 | if err != nil { 273 | return metav1.ObjectMeta{}, fmt.Errorf("failed to unmarshal input: %v", err) 274 | } 275 | return requestMeta.ObjectMeta, nil 276 | } 277 | 278 | func requestHasStatus(raw []byte) (bool, error) { 279 | requestStatus := struct { 280 | Status map[string]interface{} `json:"status"` 281 | }{} 282 | err := json.Unmarshal(raw, &requestStatus) 283 | if err != nil { 284 | return false, fmt.Errorf("failed to unmarshal input: %v", err) 285 | } 286 | return requestStatus.Status != nil, nil 287 | } 288 | 289 | func applyPatch(data, patchBytes []byte) ([]byte, error) { 290 | patch, err := mergepatch.DecodePatch(patchBytes) 291 | if err != nil { 292 | return nil, fmt.Errorf("unable to decode patch: %v", err) 293 | } 294 | 295 | // Apply patch to remove annotations 296 | patchedData, err := patch.Apply(data) 297 | if err != nil { 298 | return nil, fmt.Errorf("unable to apply patch: %v", err) 299 | } 300 | return patchedData, nil 301 | } 302 | 303 | type delimiters struct { 304 | left string 305 | right string 306 | } 307 | 308 | func getDelims(raw []byte) (delimiters, error) { 309 | // Fetch object meta into object 310 | requestMeta := struct { 311 | metav1.ObjectMeta `json:"metadata"` 312 | }{ 313 | ObjectMeta: metav1.ObjectMeta{}, 314 | } 315 | err := json.Unmarshal(raw, &requestMeta) 316 | if err != nil { 317 | return delimiters{}, fmt.Errorf("failed ot unmarshal input: %v", err) 318 | } 319 | 320 | glog.V(6).Infof("Requested Object Annotations: %v", requestMeta.ObjectMeta.Annotations) 321 | 322 | left, lOk := requestMeta.ObjectMeta.Annotations[leftDelimAnnotation] 323 | right, rOk := requestMeta.ObjectMeta.Annotations[rightDelimAnnotation] 324 | 325 | // If one annotation is set but not the other, this is an error 326 | if lOk != rOk { 327 | return delimiters{}, fmt.Errorf("must set either both %s and %s, or neither", leftDelimAnnotation, rightDelimAnnotation) 328 | } 329 | 330 | // lOk == rOk, if neither set, not an error 331 | if lOk == false { 332 | return delimiters{}, nil 333 | } 334 | 335 | if left == "" || right == "" { 336 | return delimiters{}, fmt.Errorf("delimiters must not be empty") 337 | } 338 | 339 | return delimiters{ 340 | left: left, 341 | right: right, 342 | }, nil 343 | } 344 | 345 | func errorResponse(resp *admissionv1beta1.AdmissionResponse, message string, args ...interface{}) *admissionv1beta1.AdmissionResponse { 346 | glog.Errorf(message, args...) 347 | resp.Allowed = false 348 | resp.Result = &metav1.Status{ 349 | Status: metav1.StatusFailure, Code: http.StatusInternalServerError, Reason: metav1.StatusReasonInternalError, 350 | Message: fmt.Sprintf(message, args...), 351 | } 352 | return resp 353 | } 354 | 355 | func podID(namespace string, name string) string { 356 | if namespace != "" { 357 | return fmt.Sprintf("%s/%s", namespace, name) 358 | } 359 | return name 360 | } 361 | 362 | func contains(list []string, item string) bool { 363 | for _, l := range list { 364 | if l == item { 365 | return true 366 | } 367 | } 368 | return false 369 | } 370 | 371 | func nonExistentPath(err error) bool { 372 | return strings.Contains(err.Error(), "Unable to remove nonexistent key:") 373 | } 374 | -------------------------------------------------------------------------------- /pkg/quack/quack_test.go: -------------------------------------------------------------------------------- 1 | package quack 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | ) 11 | 12 | func TestRenderTemplate(t *testing.T) { 13 | values := map[string]string{ 14 | "A": "alpha", 15 | "B": "beta", 16 | } 17 | input := struct { 18 | Alpha string 19 | Beta string 20 | }{ 21 | "{{- .A -}}", 22 | "{{- .B -}}", 23 | } 24 | 25 | inputBytes, err := json.Marshal(input) 26 | if err != nil { 27 | assert.FailNowf(t, "jsonError", "Failed to marshal test input: %v", err) 28 | } 29 | 30 | fmt.Printf("Template Test Input: %s\n", string(inputBytes)) 31 | 32 | outputBytes, err := renderTemplate(inputBytes, values, delimiters{}) 33 | if err != nil { 34 | assert.FailNowf(t, "methodError", "Failed rendering template: %v", err) 35 | } 36 | 37 | fmt.Printf("Template Test Output: %s\n", string(outputBytes)) 38 | output := struct { 39 | Alpha string 40 | Beta string 41 | }{} 42 | err = json.Unmarshal(outputBytes, &output) 43 | if err != nil { 44 | assert.FailNowf(t, "jsonError", "Failed to unmarshal template output: %v", err) 45 | } 46 | 47 | assert.Equal(t, values["A"], output.Alpha, "Value for A should be substituted for Alpha output") 48 | assert.Equal(t, values["B"], output.Beta, "Value for B should be substituted for Beta output") 49 | } 50 | 51 | func TestRenderTemplateDoesntRemoveQuackAnnotations(t *testing.T) { 52 | values := make(map[string]string) 53 | input := struct { 54 | ObjectMeta metav1.ObjectMeta `json:"metadata"` 55 | }{ 56 | ObjectMeta: metav1.ObjectMeta{ 57 | Annotations: map[string]string{ 58 | "quack.pusher.com/foo": "bar", 59 | }, 60 | }, 61 | } 62 | 63 | inputBytes, err := json.Marshal(input) 64 | if err != nil { 65 | assert.FailNowf(t, "jsonError", "Failed to marshal test input: %v", err) 66 | } 67 | 68 | fmt.Printf("Template Test Input: %s\n", string(inputBytes)) 69 | 70 | outputBytes, err := renderTemplate(inputBytes, values, delimiters{}) 71 | if err != nil { 72 | assert.FailNowf(t, "methodError", "Failed rendering template: %v", err) 73 | } 74 | 75 | fmt.Printf("Template Test Output: %s\n", string(outputBytes)) 76 | output := struct { 77 | ObjectMeta metav1.ObjectMeta `json:"metadata"` 78 | }{} 79 | err = json.Unmarshal(outputBytes, &output) 80 | if err != nil { 81 | assert.FailNowf(t, "jsonError", "Failed to unmarshal template output: %v", err) 82 | } 83 | 84 | assert.Equal(t, input.ObjectMeta.Annotations, output.ObjectMeta.Annotations, "Annotations should not be changed") 85 | } 86 | 87 | func TestRenderTemplateWithDelims(t *testing.T) { 88 | values := map[string]string{ 89 | "A": "alpha", 90 | "B": "beta", 91 | } 92 | input := struct { 93 | Alpha string 94 | Beta string 95 | }{ 96 | "[[- .A -]]", 97 | "[[- .B -]]", 98 | } 99 | 100 | inputBytes, err := json.Marshal(input) 101 | if err != nil { 102 | assert.FailNowf(t, "jsonError", "Failed to marshal test input: %v", err) 103 | } 104 | 105 | fmt.Printf("Template Test Input: %s\n", string(inputBytes)) 106 | 107 | delims := delimiters{ 108 | left: "[[", 109 | right: "]]", 110 | } 111 | 112 | outputBytes, err := renderTemplate(inputBytes, values, delims) 113 | if err != nil { 114 | assert.FailNowf(t, "methodError", "Failed rendering template: %v", err) 115 | } 116 | 117 | fmt.Printf("Template Test Output: %s\n", string(outputBytes)) 118 | output := struct { 119 | Alpha string 120 | Beta string 121 | }{} 122 | err = json.Unmarshal(outputBytes, &output) 123 | if err != nil { 124 | assert.FailNowf(t, "jsonError", "Failed to unmarshal template output: %v", err) 125 | } 126 | 127 | assert.Equal(t, values["A"], output.Alpha, "Value for A should be substituted for Alpha output") 128 | assert.Equal(t, values["B"], output.Beta, "Value for B should be substituted for Beta output") 129 | } 130 | 131 | func TestRequestHasAnnotation(t *testing.T) { 132 | requiredAnnotation := "quack-required" 133 | objectWithRequired := struct { 134 | metav1.ObjectMeta `json:"metadata"` 135 | }{ 136 | ObjectMeta: metav1.ObjectMeta{ 137 | Annotations: map[string]string{ 138 | requiredAnnotation: "true", 139 | }, 140 | }, 141 | } 142 | objectWithoutRequired := struct { 143 | metav1.ObjectMeta `json:"metadata"` 144 | }{ 145 | ObjectMeta: metav1.ObjectMeta{ 146 | Annotations: map[string]string{ 147 | "quack-not-required": "true", 148 | }, 149 | }, 150 | } 151 | 152 | objectWithRequiredRaw, err := json.Marshal(objectWithRequired) 153 | if err != nil { 154 | assert.FailNowf(t, "jsonError", "Failed to marshal 'with required' input: %v", err) 155 | } 156 | objectWithoutRequiredRaw, err := json.Marshal(objectWithoutRequired) 157 | if err != nil { 158 | assert.FailNowf(t, "jsonError", "Failed to marshal 'without required' input: %v", err) 159 | } 160 | 161 | fmt.Printf("Annotation Test Input (with annotation): %s\n", string(objectWithRequiredRaw)) 162 | fmt.Printf("Annotation Test Input (without annotation): %s\n", string(objectWithoutRequiredRaw)) 163 | withRequired, err := requestHasAnnotation(requiredAnnotation, objectWithRequiredRaw) 164 | if err != nil { 165 | assert.FailNowf(t, "methodError", "Error in requestHasAnnotation: %v", err) 166 | } 167 | withoutRequired, err := requestHasAnnotation(requiredAnnotation, objectWithoutRequiredRaw) 168 | if err != nil { 169 | assert.FailNowf(t, "methodError", "Error in requestHasAnnotation %v", err) 170 | } 171 | 172 | noAnnotation, err := requestHasAnnotation("", objectWithRequiredRaw) 173 | if err != nil { 174 | assert.FailNowf(t, "methodError", "Error in requestHasAnnotation %v", err) 175 | } 176 | 177 | assert.True(t, withRequired, "Object with required annotation should return true") 178 | assert.False(t, withoutRequired, "Object without required annotation should return false") 179 | assert.True(t, noAnnotation, "Specifying no required annotation should return true") 180 | } 181 | 182 | func TestGetTemplateInput(t *testing.T) { 183 | type testObject struct { 184 | metav1.ObjectMeta `json:"metadata"` 185 | Foo string `json:"foo"` 186 | Status map[string]string `json:"status"` 187 | } 188 | 189 | object := testObject{ 190 | ObjectMeta: metav1.ObjectMeta{ 191 | Annotations: map[string]string{ 192 | "annotation": "value", 193 | "quack.pusher.com/foo": "bar", 194 | }, 195 | }, 196 | Foo: "bar", 197 | } 198 | objectNoQuackAnnotations := testObject{ 199 | Foo: "bar", 200 | ObjectMeta: metav1.ObjectMeta{ 201 | Annotations: map[string]string{ 202 | "annotation": "value", 203 | }, 204 | }, 205 | } 206 | ignoredPaths := []string{} 207 | 208 | objectRaw, err := json.Marshal(object) 209 | if err != nil { 210 | assert.FailNowf(t, "jsonError", "Failed to marshal input: %v", err) 211 | } 212 | 213 | template, err := getTemplateInput(objectRaw, ignoredPaths) 214 | if err != nil { 215 | assert.FailNowf(t, "methodError", "Error in getTemplateInput: %v", err) 216 | } 217 | 218 | templateObject := testObject{} 219 | err = json.Unmarshal(template, &templateObject) 220 | if err != nil { 221 | assert.FailNowf(t, "jsonError", "Error in unmarshall: %v", err) 222 | } 223 | assert.Equal(t, objectNoQuackAnnotations, templateObject, "Object should have no quack annotations") 224 | } 225 | 226 | func TestGetTemplateInputRemovesIgnoredPaths(t *testing.T) { 227 | type testObject struct { 228 | metav1.ObjectMeta `json:"metadata"` 229 | Foo string `json:"foo"` 230 | Status map[string]string `json:"status"` 231 | } 232 | 233 | object := testObject{ 234 | ObjectMeta: metav1.ObjectMeta{ 235 | Annotations: map[string]string{ 236 | "annotation": "value", 237 | "quack.pusher.com/template": "true", 238 | "other/annotation": "bar", 239 | }, 240 | }, 241 | Foo: "bar", 242 | } 243 | objectNoOtherAnnotation := testObject{ 244 | ObjectMeta: metav1.ObjectMeta{ 245 | Annotations: map[string]string{ 246 | "annotation": "value", 247 | }, 248 | }, 249 | Foo: "bar", 250 | } 251 | ignoredPaths := []string{"/metadata/annotations/other~1annotation"} 252 | 253 | objectRaw, err := json.Marshal(object) 254 | if err != nil { 255 | assert.FailNowf(t, "jsonError", "Failed to marshal input: %v", err) 256 | } 257 | objectNoOtherRaw, err := json.Marshal(objectNoOtherAnnotation) 258 | if err != nil { 259 | assert.FailNowf(t, "jsonError", "Failed to marshal input: %v", err) 260 | } 261 | 262 | template, err := getTemplateInput(objectRaw, ignoredPaths) 263 | if err != nil { 264 | assert.FailNowf(t, "methodError", "Error in getTemplateInput: %v", err) 265 | } 266 | 267 | assert.NotNil(t, template, "template should not be nil") 268 | 269 | templateObject := testObject{} 270 | err = json.Unmarshal(template, &templateObject) 271 | if err != nil { 272 | assert.FailNowf(t, "jsonError", "Error in unmarshall: %v", err) 273 | } 274 | assert.Equal(t, objectNoOtherAnnotation, templateObject, "Object should have no ignored paths") 275 | 276 | template, err = getTemplateInput(objectNoOtherRaw, ignoredPaths) 277 | if err != nil { 278 | assert.FailNowf(t, "methodError", "Error in getTemplateInput: %v", err) 279 | } 280 | 281 | assert.NotNil(t, template, "template should not be nil") 282 | 283 | err = json.Unmarshal(template, &templateObject) 284 | if err != nil { 285 | assert.FailNowf(t, "jsonError", "Error in unmarshall: %v", err) 286 | } 287 | 288 | assert.Equal(t, objectNoOtherAnnotation, templateObject, "Object should have no ignored paths") 289 | } 290 | 291 | func TestGetTemplateInputRemovesStatus(t *testing.T) { 292 | type testObject struct { 293 | metav1.ObjectMeta `json:"metadata"` 294 | Foo string `json:"foo"` 295 | Status map[string]string `json:"status"` 296 | } 297 | 298 | object := testObject{ 299 | Foo: "bar", 300 | Status: map[string]string{ 301 | "condition": "{{ .Condition }}", 302 | }, 303 | } 304 | objectWithoutStatus := testObject{ 305 | Foo: "bar", 306 | } 307 | 308 | objectRaw, err := json.Marshal(object) 309 | if err != nil { 310 | assert.FailNowf(t, "jsonError", "Failed to marshal input: %v", err) 311 | } 312 | ignoredPaths := []string{} 313 | 314 | template, err := getTemplateInput(objectRaw, ignoredPaths) 315 | if err != nil { 316 | assert.FailNowf(t, "methodError", "Error in getTemplateInput: %v", err) 317 | } 318 | 319 | templateObject := testObject{} 320 | err = json.Unmarshal(template, &templateObject) 321 | if err != nil { 322 | assert.FailNowf(t, "jsonError", "Error in unmarshall: %v", err) 323 | } 324 | assert.Equal(t, objectWithoutStatus, templateObject, "Object should have no quack annotations") 325 | } 326 | 327 | func TestGetDelims(t *testing.T) { 328 | objectWithNoAnnotations := struct { 329 | metav1.ObjectMeta `json:"metadata"` 330 | }{} 331 | objectWithSetDelimiters := struct { 332 | metav1.ObjectMeta `json:"metadata"` 333 | }{ 334 | ObjectMeta: metav1.ObjectMeta{ 335 | Annotations: map[string]string{ 336 | leftDelimAnnotation: "[[", 337 | rightDelimAnnotation: "]]", 338 | }, 339 | }, 340 | } 341 | objectWithLeftDelimiter := struct { 342 | metav1.ObjectMeta `json:"metadata"` 343 | }{ 344 | ObjectMeta: metav1.ObjectMeta{ 345 | Annotations: map[string]string{ 346 | leftDelimAnnotation: "[[", 347 | }, 348 | }, 349 | } 350 | objectWithRightDelimiter := struct { 351 | metav1.ObjectMeta `json:"metadata"` 352 | }{ 353 | ObjectMeta: metav1.ObjectMeta{ 354 | Annotations: map[string]string{ 355 | rightDelimAnnotation: "]]", 356 | }, 357 | }, 358 | } 359 | objectWithEmptyDelimiters := struct { 360 | metav1.ObjectMeta `json:"metadata"` 361 | }{ 362 | ObjectMeta: metav1.ObjectMeta{ 363 | Annotations: map[string]string{ 364 | leftDelimAnnotation: "", 365 | rightDelimAnnotation: "]]", 366 | }, 367 | }, 368 | } 369 | 370 | objectWithNoAnnotationsRaw, err := json.Marshal(objectWithNoAnnotations) 371 | if err != nil { 372 | assert.FailNowf(t, "jsonError", "Failed to marshal 'with no annotations' input: %v", err) 373 | } 374 | objectWithSetDelimitersRaw, err := json.Marshal(objectWithSetDelimiters) 375 | if err != nil { 376 | assert.FailNowf(t, "jsonError", "Failed to marshal 'with set delimeters' input: %v", err) 377 | } 378 | objectWithLeftDelimiterRaw, err := json.Marshal(objectWithLeftDelimiter) 379 | if err != nil { 380 | assert.FailNowf(t, "jsonError", "Failed to marshal 'with left delimeter' input: %v", err) 381 | } 382 | objectWithRightDelimiterRaw, err := json.Marshal(objectWithRightDelimiter) 383 | if err != nil { 384 | assert.FailNowf(t, "jsonError", "Failed to marshal 'with right delimeter' input: %v", err) 385 | } 386 | objectWithEmptyDelimitersRaw, err := json.Marshal(objectWithEmptyDelimiters) 387 | if err != nil { 388 | assert.FailNowf(t, "jsonError", "Failed to marshal 'with empty delimeter' input: %v", err) 389 | } 390 | 391 | withNoAnnotations, err := getDelims(objectWithNoAnnotationsRaw) 392 | if err != nil { 393 | assert.FailNowf(t, "methodError", "Error in getDelims: %v", err) 394 | } 395 | withSetDelimters, err := getDelims(objectWithSetDelimitersRaw) 396 | if err != nil { 397 | assert.FailNowf(t, "methodError", "Error in getDelims: %v", err) 398 | } 399 | withLeftDelimeter, leftErr := getDelims(objectWithLeftDelimiterRaw) 400 | withRightDelimeter, rightErr := getDelims(objectWithRightDelimiterRaw) 401 | withEmptyDelimeters, emptyErr := getDelims(objectWithEmptyDelimitersRaw) 402 | 403 | assert.Equal(t, delimiters{}, withNoAnnotations, "Object with no annotations should return empty delimiters") 404 | assert.Equal(t, delimiters{left: "[[", right: "]]"}, withSetDelimters, "Object with set delimiters should return `left: [[, right: ]]`") 405 | assert.Equal(t, delimiters{}, withLeftDelimeter, "Object with empty delimiter should return empty delimiters") 406 | assert.NotNil(t, leftErr, "Object with only left delimiter should return error") 407 | assert.Equal(t, delimiters{}, withRightDelimeter, "Object with empty delimiter should return empty delimiters") 408 | assert.NotNil(t, rightErr, "Object with only right delimiter should return error") 409 | assert.Equal(t, delimiters{}, withEmptyDelimeters, "Object with empty delimiter should return empty delimiters") 410 | assert.NotNil(t, emptyErr, "Object with empty left delimiter should return error") 411 | } 412 | 413 | func TestRequestHasStatus(t *testing.T) { 414 | withStatus := `{ 415 | "status": { 416 | "foo": "bar", 417 | "baz": 3 418 | } 419 | }` 420 | hasStatus, err := requestHasStatus([]byte(withStatus)) 421 | assert.Equal(t, nil, err, "Error should not have occurred") 422 | assert.Equal(t, true, hasStatus, "Expected object with status to return true") 423 | 424 | withoutStatus := `{ 425 | "foo": "bar" 426 | }` 427 | hasStatus, err = requestHasStatus([]byte(withoutStatus)) 428 | assert.Equal(t, nil, err, "Error should not have occurred") 429 | assert.Equal(t, false, hasStatus, "Expected object without status to return false") 430 | } 431 | --------------------------------------------------------------------------------