├── .editorconfig ├── .gitattributes ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Gopkg.lock ├── Gopkg.toml ├── Makefile ├── README.md ├── cli └── completion.go ├── context ├── context.go └── kubectl │ └── context.go ├── kubensx.go └── spec ├── expected.log ├── kubeconfig.envsubst.yml └── run.sh /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | 9 | [*.go] 10 | indent_style = tab 11 | indent_size = 4 12 | 13 | [Makefile] 14 | indent_style = tab 15 | indent_size = 4 16 | 17 | [*.md] 18 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .tmp 3 | release 4 | vendor 5 | /kubensx 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.9 4 | - tip 5 | install: make fetch 6 | script: make test 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [0.2.0](https://github.com/shyiko/kubensx/compare/0.1.1...0.2.0) - 2018-04-29 8 | 9 | ### Added 10 | 11 | - Support for users that `cannot list namespaces at the cluster scope` (per Access Control policy). 12 | - Ability to combine `--user`(`-u`) / `--cluster`(`-c`) / `--namespace`(`--ns`,`-n`) 13 | (e.g. `kubensx current -cn` should print `/`). 14 | - `kubensx use` option filtering ("type to filter"). 15 | 16 | ## [0.1.1](https://github.com/shyiko/kubensx/compare/0.1.0...0.1.1) - 2018-01-05 17 | 18 | ### Fixed 19 | - Interactive selection (from the list containing a single option that doesn't match "current" one). 20 | - Azure/GCP/OIDC/OpenStack auth ([#1](https://github.com/shyiko/kubensx/pull/1)). 21 | 22 | ## 0.1.0 - 2018-01-01 23 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | name = "cloud.google.com/go" 6 | packages = ["compute/metadata"] 7 | revision = "050b16d2314d5fc3d4c9a51e4cd5c7468e77f162" 8 | version = "v0.17.0" 9 | 10 | [[projects]] 11 | name = "github.com/Azure/go-autorest" 12 | packages = [ 13 | "autorest", 14 | "autorest/adal", 15 | "autorest/azure", 16 | "autorest/date" 17 | ] 18 | revision = "809ed2ef5c4c9a60c3c2f3aa9cc11f3a7c2ce59d" 19 | version = "v9.6.0" 20 | 21 | [[projects]] 22 | name = "github.com/PuerkitoBio/purell" 23 | packages = ["."] 24 | revision = "0bcb03f4b4d0a9428594752bd2a3b9aa0a9d4bd4" 25 | version = "v1.1.0" 26 | 27 | [[projects]] 28 | branch = "master" 29 | name = "github.com/PuerkitoBio/urlesc" 30 | packages = ["."] 31 | revision = "de5bf2ad457846296e2031421a34e2568e304e35" 32 | 33 | [[projects]] 34 | name = "github.com/Sirupsen/logrus" 35 | packages = ["."] 36 | revision = "d682213848ed68c0a260ca37d6dd5ace8423f5ba" 37 | version = "v1.0.4" 38 | 39 | [[projects]] 40 | name = "github.com/dgrijalva/jwt-go" 41 | packages = ["."] 42 | revision = "dbeaa9332f19a944acb5736b4456cfcc02140e29" 43 | version = "v3.1.0" 44 | 45 | [[projects]] 46 | name = "github.com/emicklei/go-restful" 47 | packages = [ 48 | ".", 49 | "log" 50 | ] 51 | revision = "5741799b275a3c4a5a9623a993576d7545cf7b5c" 52 | version = "v2.4.0" 53 | 54 | [[projects]] 55 | name = "github.com/fatih/color" 56 | packages = ["."] 57 | revision = "570b54cabe6b8eb0bc2dfce68d964677d63b5260" 58 | version = "v1.5.0" 59 | 60 | [[projects]] 61 | name = "github.com/ghodss/yaml" 62 | packages = ["."] 63 | revision = "0ca9ea5df5451ffdf184b4428c902747c2c11cd7" 64 | version = "v1.0.0" 65 | 66 | [[projects]] 67 | branch = "master" 68 | name = "github.com/go-openapi/jsonpointer" 69 | packages = ["."] 70 | revision = "779f45308c19820f1a69e9a4cd965f496e0da10f" 71 | 72 | [[projects]] 73 | branch = "master" 74 | name = "github.com/go-openapi/jsonreference" 75 | packages = ["."] 76 | revision = "36d33bfe519efae5632669801b180bf1a245da3b" 77 | 78 | [[projects]] 79 | branch = "master" 80 | name = "github.com/go-openapi/spec" 81 | packages = ["."] 82 | revision = "fa03337d7da5735229ee8f5e9d5d0b996014b7f8" 83 | 84 | [[projects]] 85 | branch = "master" 86 | name = "github.com/go-openapi/swag" 87 | packages = ["."] 88 | revision = "cf0bdb963811675a4d7e74901cefc7411a1df939" 89 | 90 | [[projects]] 91 | name = "github.com/gogo/protobuf" 92 | packages = [ 93 | "proto", 94 | "sortkeys" 95 | ] 96 | revision = "342cbe0a04158f6dcb03ca0079991a51a4248c02" 97 | version = "v0.5" 98 | 99 | [[projects]] 100 | branch = "master" 101 | name = "github.com/golang/glog" 102 | packages = ["."] 103 | revision = "23def4e6c14b4da8ac2ed8007337bc5eb5007998" 104 | 105 | [[projects]] 106 | branch = "master" 107 | name = "github.com/golang/protobuf" 108 | packages = [ 109 | "proto", 110 | "ptypes", 111 | "ptypes/any", 112 | "ptypes/duration", 113 | "ptypes/timestamp" 114 | ] 115 | revision = "1e59b77b52bf8e4b449a57e6f79f21226d571845" 116 | 117 | [[projects]] 118 | branch = "master" 119 | name = "github.com/google/btree" 120 | packages = ["."] 121 | revision = "316fb6d3f031ae8f4d457c6c5186b9e3ded70435" 122 | 123 | [[projects]] 124 | branch = "master" 125 | name = "github.com/google/gofuzz" 126 | packages = ["."] 127 | revision = "24818f796faf91cd76ec7bddd72458fbced7a6c1" 128 | 129 | [[projects]] 130 | name = "github.com/googleapis/gnostic" 131 | packages = [ 132 | "OpenAPIv2", 133 | "compiler", 134 | "extensions" 135 | ] 136 | revision = "ee43cbb60db7bd22502942cccbc39059117352ab" 137 | version = "v0.1.0" 138 | 139 | [[projects]] 140 | branch = "master" 141 | name = "github.com/gophercloud/gophercloud" 142 | packages = [ 143 | ".", 144 | "openstack", 145 | "openstack/identity/v2/tenants", 146 | "openstack/identity/v2/tokens", 147 | "openstack/identity/v3/tokens", 148 | "openstack/utils", 149 | "pagination" 150 | ] 151 | revision = "fe4853e064e386919340e25c447d0fed6cd8c6ea" 152 | 153 | [[projects]] 154 | branch = "master" 155 | name = "github.com/gregjones/httpcache" 156 | packages = [ 157 | ".", 158 | "diskcache" 159 | ] 160 | revision = "2bcd89a1743fd4b373f7370ce8ddc14dfbd18229" 161 | 162 | [[projects]] 163 | branch = "master" 164 | name = "github.com/hashicorp/errwrap" 165 | packages = ["."] 166 | revision = "7554cd9344cec97297fa6649b055a8c98c2a1e55" 167 | 168 | [[projects]] 169 | branch = "master" 170 | name = "github.com/hashicorp/go-multierror" 171 | packages = ["."] 172 | revision = "b7773ae218740a7be65057fc60b366a49b538a44" 173 | 174 | [[projects]] 175 | branch = "master" 176 | name = "github.com/howeyc/gopass" 177 | packages = ["."] 178 | revision = "bf9dde6d0d2c004a008c27aaee91170c786f6db8" 179 | 180 | [[projects]] 181 | name = "github.com/imdario/mergo" 182 | packages = ["."] 183 | revision = "7fe0c75c13abdee74b09fcacef5ea1c6bba6a874" 184 | version = "0.2.4" 185 | 186 | [[projects]] 187 | name = "github.com/inconshreveable/mousetrap" 188 | packages = ["."] 189 | revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75" 190 | version = "v1.0" 191 | 192 | [[projects]] 193 | name = "github.com/json-iterator/go" 194 | packages = ["."] 195 | revision = "f7279a603edee96fe7764d3de9c6ff8cf9970994" 196 | version = "1.0.4" 197 | 198 | [[projects]] 199 | branch = "master" 200 | name = "github.com/juju/ratelimit" 201 | packages = ["."] 202 | revision = "59fac5042749a5afb9af70e813da1dd5474f0167" 203 | 204 | [[projects]] 205 | branch = "master" 206 | name = "github.com/mailru/easyjson" 207 | packages = [ 208 | "buffer", 209 | "jlexer", 210 | "jwriter" 211 | ] 212 | revision = "32fa128f234d041f196a9f3e0fea5ac9772c08e1" 213 | 214 | [[projects]] 215 | name = "github.com/mattn/go-colorable" 216 | packages = ["."] 217 | revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072" 218 | version = "v0.0.9" 219 | 220 | [[projects]] 221 | name = "github.com/mattn/go-isatty" 222 | packages = ["."] 223 | revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39" 224 | version = "v0.0.3" 225 | 226 | [[projects]] 227 | branch = "master" 228 | name = "github.com/mgutz/ansi" 229 | packages = ["."] 230 | revision = "9520e82c474b0a04dd04f8a40959027271bab992" 231 | 232 | [[projects]] 233 | branch = "master" 234 | name = "github.com/petar/GoLLRB" 235 | packages = ["llrb"] 236 | revision = "53be0d36a84c2a886ca057d34b6aa4468df9ccb4" 237 | 238 | [[projects]] 239 | name = "github.com/peterbourgon/diskv" 240 | packages = ["."] 241 | revision = "5f041e8faa004a95c88a202771f4cc3e991971e6" 242 | version = "v2.0.1" 243 | 244 | [[projects]] 245 | name = "github.com/posener/complete" 246 | packages = [ 247 | ".", 248 | "cmd", 249 | "cmd/install", 250 | "match" 251 | ] 252 | revision = "dc2bc5a81accba8782bebea28628224643a8286a" 253 | version = "v1.1" 254 | 255 | [[projects]] 256 | name = "github.com/renstrom/fuzzysearch" 257 | packages = ["fuzzy"] 258 | revision = "2d205ac6ec17a839a94bdbfd16d2fa6c6dada2e0" 259 | 260 | [[projects]] 261 | name = "github.com/spf13/cobra" 262 | packages = ["."] 263 | revision = "b26b538f693051ac6518e65672de3144ce3fbedc" 264 | 265 | [[projects]] 266 | name = "github.com/spf13/pflag" 267 | packages = ["."] 268 | revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66" 269 | version = "v1.0.0" 270 | 271 | [[projects]] 272 | branch = "master" 273 | name = "golang.org/x/crypto" 274 | packages = ["ssh/terminal"] 275 | revision = "d585fd2cc9195196078f516b69daff6744ef5e84" 276 | 277 | [[projects]] 278 | branch = "master" 279 | name = "golang.org/x/net" 280 | packages = [ 281 | "context", 282 | "context/ctxhttp", 283 | "http2", 284 | "http2/hpack", 285 | "idna", 286 | "lex/httplex" 287 | ] 288 | revision = "d866cfc389cec985d6fda2859936a575a55a3ab6" 289 | 290 | [[projects]] 291 | branch = "master" 292 | name = "golang.org/x/oauth2" 293 | packages = [ 294 | ".", 295 | "google", 296 | "internal", 297 | "jws", 298 | "jwt" 299 | ] 300 | revision = "30785a2c434e431ef7c507b54617d6a951d5f2b4" 301 | 302 | [[projects]] 303 | branch = "master" 304 | name = "golang.org/x/sys" 305 | packages = [ 306 | "unix", 307 | "windows" 308 | ] 309 | revision = "83801418e1b59fb1880e363299581ee543af32ca" 310 | 311 | [[projects]] 312 | branch = "master" 313 | name = "golang.org/x/text" 314 | packages = [ 315 | "collate", 316 | "collate/build", 317 | "internal/colltab", 318 | "internal/gen", 319 | "internal/tag", 320 | "internal/triegen", 321 | "internal/ucd", 322 | "language", 323 | "secure/bidirule", 324 | "transform", 325 | "unicode/bidi", 326 | "unicode/cldr", 327 | "unicode/norm", 328 | "unicode/rangetable", 329 | "width" 330 | ] 331 | revision = "eb22672bea55af56d225d4e35405f4d2e9f062a0" 332 | 333 | [[projects]] 334 | name = "google.golang.org/appengine" 335 | packages = [ 336 | ".", 337 | "internal", 338 | "internal/app_identity", 339 | "internal/base", 340 | "internal/datastore", 341 | "internal/log", 342 | "internal/modules", 343 | "internal/remote_api", 344 | "internal/urlfetch", 345 | "urlfetch" 346 | ] 347 | revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a" 348 | version = "v1.0.0" 349 | 350 | [[projects]] 351 | name = "gopkg.in/AlecAivazis/survey.v1" 352 | packages = [ 353 | ".", 354 | "core", 355 | "terminal" 356 | ] 357 | revision = "a14316b11132a62dec3bb47ff8aecf1741ecba1d" 358 | source = "https://github.com/shyiko/survey.git" 359 | 360 | [[projects]] 361 | name = "gopkg.in/inf.v0" 362 | packages = ["."] 363 | revision = "3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4" 364 | version = "v0.9.0" 365 | 366 | [[projects]] 367 | branch = "v2" 368 | name = "gopkg.in/yaml.v2" 369 | packages = ["."] 370 | revision = "287cf08546ab5e7e37d55a84f7ed3fd1db036de5" 371 | 372 | [[projects]] 373 | branch = "master" 374 | name = "k8s.io/api" 375 | packages = [ 376 | "admissionregistration/v1alpha1", 377 | "admissionregistration/v1beta1", 378 | "apps/v1", 379 | "apps/v1beta1", 380 | "apps/v1beta2", 381 | "authentication/v1", 382 | "authentication/v1beta1", 383 | "authorization/v1", 384 | "authorization/v1beta1", 385 | "autoscaling/v1", 386 | "autoscaling/v2beta1", 387 | "batch/v1", 388 | "batch/v1beta1", 389 | "batch/v2alpha1", 390 | "certificates/v1beta1", 391 | "core/v1", 392 | "events/v1beta1", 393 | "extensions/v1beta1", 394 | "networking/v1", 395 | "policy/v1beta1", 396 | "rbac/v1", 397 | "rbac/v1alpha1", 398 | "rbac/v1beta1", 399 | "scheduling/v1alpha1", 400 | "settings/v1alpha1", 401 | "storage/v1", 402 | "storage/v1alpha1", 403 | "storage/v1beta1" 404 | ] 405 | revision = "d19b0bad6fd13374f9b4568c3fdfe5af633ecbff" 406 | 407 | [[projects]] 408 | branch = "master" 409 | name = "k8s.io/apimachinery" 410 | packages = [ 411 | "pkg/api/errors", 412 | "pkg/api/meta", 413 | "pkg/api/resource", 414 | "pkg/apis/meta/v1", 415 | "pkg/apis/meta/v1/unstructured", 416 | "pkg/apis/meta/v1alpha1", 417 | "pkg/conversion", 418 | "pkg/conversion/queryparams", 419 | "pkg/fields", 420 | "pkg/labels", 421 | "pkg/runtime", 422 | "pkg/runtime/schema", 423 | "pkg/runtime/serializer", 424 | "pkg/runtime/serializer/json", 425 | "pkg/runtime/serializer/protobuf", 426 | "pkg/runtime/serializer/recognizer", 427 | "pkg/runtime/serializer/streaming", 428 | "pkg/runtime/serializer/versioning", 429 | "pkg/selection", 430 | "pkg/types", 431 | "pkg/util/clock", 432 | "pkg/util/errors", 433 | "pkg/util/framer", 434 | "pkg/util/intstr", 435 | "pkg/util/json", 436 | "pkg/util/net", 437 | "pkg/util/runtime", 438 | "pkg/util/sets", 439 | "pkg/util/validation", 440 | "pkg/util/validation/field", 441 | "pkg/util/wait", 442 | "pkg/util/yaml", 443 | "pkg/version", 444 | "pkg/watch", 445 | "third_party/forked/golang/reflect" 446 | ] 447 | revision = "bc1325710437b54535daa5d6877ca62df7bd76db" 448 | 449 | [[projects]] 450 | name = "k8s.io/client-go" 451 | packages = [ 452 | "discovery", 453 | "kubernetes", 454 | "kubernetes/scheme", 455 | "kubernetes/typed/admissionregistration/v1alpha1", 456 | "kubernetes/typed/admissionregistration/v1beta1", 457 | "kubernetes/typed/apps/v1", 458 | "kubernetes/typed/apps/v1beta1", 459 | "kubernetes/typed/apps/v1beta2", 460 | "kubernetes/typed/authentication/v1", 461 | "kubernetes/typed/authentication/v1beta1", 462 | "kubernetes/typed/authorization/v1", 463 | "kubernetes/typed/authorization/v1beta1", 464 | "kubernetes/typed/autoscaling/v1", 465 | "kubernetes/typed/autoscaling/v2beta1", 466 | "kubernetes/typed/batch/v1", 467 | "kubernetes/typed/batch/v1beta1", 468 | "kubernetes/typed/batch/v2alpha1", 469 | "kubernetes/typed/certificates/v1beta1", 470 | "kubernetes/typed/core/v1", 471 | "kubernetes/typed/events/v1beta1", 472 | "kubernetes/typed/extensions/v1beta1", 473 | "kubernetes/typed/networking/v1", 474 | "kubernetes/typed/policy/v1beta1", 475 | "kubernetes/typed/rbac/v1", 476 | "kubernetes/typed/rbac/v1alpha1", 477 | "kubernetes/typed/rbac/v1beta1", 478 | "kubernetes/typed/scheduling/v1alpha1", 479 | "kubernetes/typed/settings/v1alpha1", 480 | "kubernetes/typed/storage/v1", 481 | "kubernetes/typed/storage/v1alpha1", 482 | "kubernetes/typed/storage/v1beta1", 483 | "pkg/version", 484 | "plugin/pkg/client/auth", 485 | "plugin/pkg/client/auth/azure", 486 | "plugin/pkg/client/auth/gcp", 487 | "plugin/pkg/client/auth/oidc", 488 | "plugin/pkg/client/auth/openstack", 489 | "rest", 490 | "rest/watch", 491 | "third_party/forked/golang/template", 492 | "tools/auth", 493 | "tools/clientcmd", 494 | "tools/clientcmd/api", 495 | "tools/clientcmd/api/latest", 496 | "tools/clientcmd/api/v1", 497 | "tools/metrics", 498 | "tools/reference", 499 | "transport", 500 | "util/cert", 501 | "util/flowcontrol", 502 | "util/homedir", 503 | "util/integer", 504 | "util/jsonpath" 505 | ] 506 | revision = "78700dec6369ba22221b72770783300f143df150" 507 | version = "v6.0.0" 508 | 509 | [[projects]] 510 | branch = "master" 511 | name = "k8s.io/kube-openapi" 512 | packages = ["pkg/common"] 513 | revision = "b16ebc07f5cad97831f961e4b5a9cc1caed33b7e" 514 | 515 | [solve-meta] 516 | analyzer-name = "dep" 517 | analyzer-version = 1 518 | inputs-digest = "a2c0b5c4468bca10bfcc7562b6057fe2a8faf4e2433f96f8dbaa149e3f440281" 519 | solver-name = "gps-cdcl" 520 | solver-version = 1 521 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | [[constraint]] 2 | name = "github.com/Sirupsen/logrus" 3 | version = "1.0.4" 4 | 5 | [[constraint]] 6 | name = "github.com/spf13/pflag" 7 | version = "1.0.0" 8 | 9 | [[constraint]] 10 | name = "k8s.io/client-go" 11 | version = "6.0.0" 12 | 13 | [[constraint]] 14 | name = "github.com/spf13/cobra" 15 | revision = "b26b538f693051ac6518e65672de3144ce3fbedc" 16 | 17 | [[constraint]] 18 | name = "github.com/renstrom/fuzzysearch" 19 | revision = "2d205ac6ec17a839a94bdbfd16d2fa6c6dada2e0" 20 | 21 | [[constraint]] 22 | name = "gopkg.in/AlecAivazis/survey.v1" 23 | source = "https://github.com/shyiko/survey.git" 24 | revision = "a14316b11132a62dec3bb47ff8aecf1741ecba1d" 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash -o pipefail 2 | VERSION := $(shell git describe --tags --abbrev=0) 3 | fetch: 4 | go get \ 5 | github.com/mitchellh/gox \ 6 | github.com/golang/dep/cmd/dep \ 7 | github.com/modocache/gover \ 8 | github.com/aktau/github-release && \ 9 | dep ensure 10 | 11 | clean: 12 | rm -f ./kubensx 13 | rm -rf ./build 14 | 15 | fmt: 16 | gofmt -l -s -w `find . -type f -name '*.go' -not -path "./vendor/*"` 17 | 18 | test: 19 | go vet `go list ./... | grep -v /vendor/` 20 | SRC=`find . -type f -name '*.go' -not -path "./vendor/*" -not -path "./.tmp/*"` && \ 21 | gofmt -l -s $$SRC | read && gofmt -l -s -d $$SRC && exit 1 || true 22 | go test -v `go list ./... | grep -v /vendor/` | grep -v "=== RUN" 23 | 24 | test-coverage: 25 | go list ./... | grep -v /vendor/ | xargs -L1 -I{} sh -c 'go test -coverprofile `basename {}`.coverprofile {}' && \ 26 | gover && \ 27 | go tool cover -html=gover.coverprofile -o coverage.html && \ 28 | rm *.coverprofile 29 | 30 | spec-diff: 31 | spec/run.sh > /tmp/kubensx-spec-log 2>&1 && diff /tmp/kubensx-spec-log spec/expected.log | cat -A 32 | 33 | build: 34 | go build -ldflags "-X main.version=${VERSION}" 35 | 36 | build-release: 37 | gox -verbose \ 38 | -ldflags "-X main.version=${VERSION}" \ 39 | -osarch="windows/amd64 linux/amd64 darwin/amd64" \ 40 | -output="release/{{.Dir}}-${VERSION}-{{.OS}}-{{.Arch}}" . 41 | 42 | sign-release: 43 | for file in $$(ls release/kubensx-${VERSION}-*); do gpg --detach-sig --sign -a $$file; done 44 | 45 | publish: clean build-release sign-release 46 | test -n "$(GITHUB_TOKEN)" # $$GITHUB_TOKEN must be set 47 | github-release release --user shyiko --repo kubensx --tag ${VERSION} \ 48 | --name "${VERSION}" --description "${VERSION}" && \ 49 | github-release upload --user shyiko --repo kubensx --tag ${VERSION} \ 50 | --name "kubensx-${VERSION}-windows-amd64.exe" --file release/kubensx-${VERSION}-windows-amd64.exe; \ 51 | github-release upload --user shyiko --repo kubensx --tag ${VERSION} \ 52 | --name "kubensx-${VERSION}-windows-amd64.exe.asc" --file release/kubensx-${VERSION}-windows-amd64.exe.asc; \ 53 | for qualifier in darwin-amd64 linux-amd64 ; do \ 54 | github-release upload --user shyiko --repo kubensx --tag ${VERSION} \ 55 | --name "kubensx-${VERSION}-$$qualifier" --file release/kubensx-${VERSION}-$$qualifier; \ 56 | github-release upload --user shyiko --repo kubensx --tag ${VERSION} \ 57 | --name "kubensx-${VERSION}-$$qualifier.asc" --file release/kubensx-${VERSION}-$$qualifier.asc; \ 58 | done 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kubensx ![Latest Version](https://img.shields.io/badge/latest-0.2.0-blue.svg) [![Build Status](https://travis-ci.org/shyiko/kubensx.svg?branch=master)](https://travis-ci.org/shyiko/kubensx) 2 | 3 | Simpler Cluster/User/Namespace switching for Kubernetes 4 | (featuring interactive mode and wildcard/fuzzy matching (among other things)). 5 | 6 | [![asciicast](https://asciinema.org/a/wtn1L6Tq4wavQcKbIn45lDiLe.png)](https://asciinema.org/a/wtn1L6Tq4wavQcKbIn45lDiLe) 7 | 8 | In short, instead of 9 | ```sh 10 | kubectl config get-clusters 11 | kubectl config view -o=go-template --template=$'{{range $u := .users}}{{$u.name}}\n{{end}}' 12 | kubectl get --cluster= --user= namespaces 13 | kubectl config set-context --cluster= --user= --namespace= 14 | kubectl config use-context 15 | ``` 16 | context can be changed with `kubensx use user:cluster/namespace` or simply `kubensx use` (interactive). 17 | 18 | ## Installation 19 | 20 | #### macOS / Linux 21 | 22 | ```sh 23 | curl -sSL https://github.com/shyiko/kubensx/releases/download/0.2.0/kubensx-0.2.0-$( 24 | bash -c '[[ $OSTYPE == darwin* ]] && echo darwin || echo linux' 25 | )-amd64 -o kubensx && chmod a+x kubensx && sudo mv kubensx /usr/local/bin/ 26 | ``` 27 | 28 | Verify PGP signature (optional but recommended): 29 | 30 | ``` 31 | curl -sSL https://github.com/shyiko/kubensx/releases/download/0.2.0/kubensx-0.2.0-$( 32 | bash -c '[[ $OSTYPE == darwin* ]] && echo darwin || echo linux' 33 | )-amd64.asc -o kubensx.asc 34 | curl -sS https://keybase.io/shyiko/pgp_keys.asc | gpg --import 35 | gpg --verify kubensx.asc /usr/local/bin/kubensx 36 | ``` 37 | 38 | > macOS: `gpg` can be installed with `brew install gnupg` 39 | 40 | #### Windows 41 | 42 | Download executable from the [Releases](https://github.com/shyiko/kubensx/releases) page. 43 | 44 | ## Usage 45 | 46 | ```sh 47 | # change :/ (interactive) 48 | $ kubensx use 49 | # change only (interactive) 50 | $ kubensx use -n 51 | 52 | # switch to :/ 53 | $ kubensx use minikube:minikube/default 54 | # switch to a different within current ( stays the same) 55 | $ kubensx use kube-public 56 | 57 | # context matching is wildcard-ish by default, which means you don't have to type the whole thing 58 | # if there are two or more options available - you'll be asked to select one 59 | $ kubensx use west/def 60 | Switched to account@possibly-gmail.com:us-west1/default 61 | # prefer fuzzy? 62 | $ kubensx use -z us1/dfl 63 | Switched to account@possibly-gmail.com:us-west1/default 64 | 65 | # switch to previous context 66 | $ kubensx use - 67 | 68 | # print current context 69 | $ kubensx current 70 | minikube:minikube/default 71 | 72 | # list s 73 | $ kubensx ls -u 74 | # list s 75 | $ kubensx ls -c 76 | # list s (inside current ) 77 | $ kubensx ls -n 78 | ``` 79 | 80 | > (for more information see `kubensx --help`) 81 | 82 | #### User <-> Cluster assoc[iation] 83 | 84 | By default, any `` can be used with any ``. 85 | If you want to restrict (assoc[iate]) certain user(s) to some of the clusters use 86 | ```sh 87 | $ kubensx assoc 88 | ``` 89 | 90 | For example: if you have a "minikube" user which you only use in the context of local "minikube" cluster, 91 | you may want to `kubensx assoc minikube:minikube` (`:`) so that "minikube" 92 | wouldn't be shown among the users for any cluster other than "minikube" (when `kubesec use`ing). 93 | 94 | #### Access Control 95 | 96 | If a user is not allowed to list namespaces, you can either provide a list of namespaces known to that user with `ns-list` 97 | 98 | ```sh 99 | $ kubensx ns-list 100 | # make default, kube-system and kube-public namespaces known to current user in us-west1 cluster 101 | $ kubensx ns-list us-west1/{default,kube-system,kube-public} 102 | $ kubensx use west/def 103 | Switched to account@possibly-gmail.com:us-west1/default 104 | ``` 105 | or use --force(-f) to suppress namespace validation (namespace will have to be provided --exact|ly) 106 | 107 | ```sh 108 | $ kubensx use west/default --force 109 | Switched to account@possibly-gmail.com:us-west1/default 110 | ``` 111 | 112 | #### Tab completion 113 | 114 | ```sh 115 | # bash 116 | $ source <(kubensx completion bash) 117 | 118 | # zsh 119 | $ source <(kubensx completion zsh) 120 | ``` 121 | 122 | ## Development 123 | 124 | > PREREQUISITE: [go1.9+](https://golang.org/dl/). 125 | 126 | ```sh 127 | git clone https://github.com/shyiko/kubensx $GOPATH/src/github.com/shyiko/kubensx 128 | cd $GOPATH/src/github.com/shyiko/kubensx 129 | make fetch 130 | 131 | go run kubensx.go 132 | ``` 133 | 134 | ## Legal 135 | 136 | All code, unless specified otherwise, is licensed under the [MIT](https://opensource.org/licenses/MIT) license. 137 | Copyright (c) 2018 Stanley Shyiko. 138 | -------------------------------------------------------------------------------- /cli/completion.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/posener/complete" 7 | nsx "github.com/shyiko/kubensx/context" 8 | "io" 9 | "os" 10 | "path/filepath" 11 | ) 12 | 13 | type Completion struct { 14 | ctx func() nsx.Context 15 | } 16 | 17 | func (c *Completion) GenBashCompletion(w io.Writer) error { 18 | bin, err := os.Executable() 19 | if err != nil { 20 | return err 21 | } 22 | fmt.Fprintf(w, "complete -C %s %s\n", bin, filepath.Base(bin)) 23 | return nil 24 | } 25 | 26 | func (c *Completion) GenZshCompletion(w io.Writer) error { 27 | bin, err := os.Executable() 28 | if err != nil { 29 | return err 30 | } 31 | fmt.Fprintf(w, "autoload +X compinit && compinit\nautoload +X bashcompinit && bashcompinit\ncomplete -C %s %s\n", 32 | bin, filepath.Base(bin)) 33 | return nil 34 | } 35 | 36 | // complete.PredictSet(...) alternative 37 | /* 38 | type oneOf []string 39 | 40 | func (p oneOf) Predict(args complete.Args) []string { 41 | for _, opt := range p { 42 | if args.LastCompleted == opt { 43 | return nil 44 | } 45 | } 46 | return p 47 | } 48 | */ 49 | 50 | func (c *Completion) Execute() (bool, error) { 51 | bin, err := os.Executable() 52 | if err != nil { 53 | return false, err 54 | } 55 | run := complete.Command{ 56 | Sub: complete.Commands{ 57 | "assoc": complete.Command{ 58 | Flags: complete.Flags{ 59 | "--delete": complete.PredictNothing, 60 | "-d": complete.PredictNothing, 61 | "--delete-all": complete.PredictNothing, 62 | "--dry-run": complete.PredictNothing, 63 | "-x": complete.PredictNothing, 64 | "--exact": complete.PredictNothing, 65 | "-e": complete.PredictNothing, 66 | "--fuzzy": complete.PredictNothing, 67 | "-z": complete.PredictNothing, 68 | "--list": complete.PredictNothing, 69 | "-l": complete.PredictNothing, 70 | }, 71 | // todo: 72 | // Args: oneOf(c.ctx().Users()), 73 | }, 74 | "completion": complete.Command{ 75 | Sub: complete.Commands{ 76 | "bash": complete.Command{}, 77 | "zsh": complete.Command{}, 78 | }, 79 | }, 80 | "current": complete.Command{ 81 | Flags: complete.Flags{ 82 | "--cluster": complete.PredictNothing, 83 | "-c": complete.PredictNothing, 84 | "--namespace": complete.PredictNothing, 85 | "--ns": complete.PredictNothing, 86 | "-n": complete.PredictNothing, 87 | "--user": complete.PredictNothing, 88 | "-u": complete.PredictNothing, 89 | }, 90 | }, 91 | "ls": complete.Command{ 92 | Flags: complete.Flags{ 93 | "--users": complete.PredictNothing, 94 | "-u": complete.PredictNothing, 95 | "--clusters": complete.PredictNothing, 96 | "-c": complete.PredictNothing, 97 | "--namespaces": complete.PredictNothing, 98 | "-n": complete.PredictNothing, 99 | }, 100 | }, 101 | "ns-list": complete.Command{ 102 | Flags: complete.Flags{ 103 | "--delete": complete.PredictNothing, 104 | "-d": complete.PredictNothing, 105 | "--delete-all": complete.PredictNothing, 106 | "--dry-run": complete.PredictNothing, 107 | "-x": complete.PredictNothing, 108 | "--exact": complete.PredictNothing, 109 | "-e": complete.PredictNothing, 110 | "--fuzzy": complete.PredictNothing, 111 | "-z": complete.PredictNothing, 112 | "--list": complete.PredictNothing, 113 | "-l": complete.PredictNothing, 114 | }, 115 | // todo: 116 | // Args: oneOf(c.ctx().Users()), 117 | }, 118 | "use": complete.Command{ 119 | Flags: complete.Flags{ 120 | "--cluster": complete.PredictNothing, 121 | "-c": complete.PredictNothing, 122 | "--dry-run": complete.PredictNothing, 123 | "-x": complete.PredictNothing, 124 | "--exact": complete.PredictNothing, 125 | "-e": complete.PredictNothing, 126 | "--force": complete.PredictNothing, 127 | "-f": complete.PredictNothing, 128 | "--fuzzy": complete.PredictNothing, 129 | "-z": complete.PredictNothing, 130 | "--ignore-assoc": complete.PredictNothing, 131 | "--ignore-ns-list": complete.PredictNothing, 132 | "--namespace": complete.PredictNothing, 133 | "--ns": complete.PredictNothing, 134 | "-n": complete.PredictNothing, 135 | "--user": complete.PredictNothing, 136 | "-u": complete.PredictNothing, 137 | }, 138 | // todo: 139 | // Args: oneOf(c.ctx().Users()), 140 | }, 141 | "help": complete.Command{ 142 | Sub: complete.Commands{ 143 | "assoc": complete.Command{}, 144 | "completion": complete.Command{ 145 | Sub: complete.Commands{ 146 | "bash": complete.Command{}, 147 | "zsh": complete.Command{}, 148 | }, 149 | }, 150 | "current": complete.Command{}, 151 | "ls": complete.Command{}, 152 | "ns-list": complete.Command{}, 153 | "use": complete.Command{}, 154 | }, 155 | }, 156 | }, 157 | Flags: complete.Flags{ 158 | "--version": complete.PredictNothing, 159 | }, 160 | GlobalFlags: complete.Flags{ 161 | "--debug": complete.PredictNothing, 162 | "--kubeconfig": complete.PredictFiles("*"), 163 | "--no-color": complete.PredictNothing, 164 | "--help": complete.PredictNothing, 165 | "-h": complete.PredictNothing, 166 | }, 167 | } 168 | run.Sub["a"] = run.Sub["assoc"] 169 | run.Sub["c"] = run.Sub["current"] 170 | run.Sub["l"] = run.Sub["ls"] 171 | run.Sub["n"] = run.Sub["ns-list"] 172 | run.Sub["u"] = run.Sub["use"] 173 | completion := complete.New(filepath.Base(bin), run) 174 | if os.Getenv("COMP_LINE") != "" { 175 | flag.Parse() 176 | completion.Complete() 177 | return true, nil 178 | } 179 | return false, nil 180 | } 181 | 182 | func NewCompletion(ctx func() nsx.Context) *Completion { 183 | return &Completion{ctx} 184 | } 185 | -------------------------------------------------------------------------------- /context/context.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | type Context interface { 4 | SetUser(value string) 5 | User() string 6 | UserPrevious() string 7 | Users() []string 8 | SetCluster(value string) 9 | Cluster() string 10 | ClusterPrevious() string 11 | Clusters() []string 12 | SetNamespace(value string) 13 | Namespace() string 14 | NamespacePrevious() string 15 | Namespaces() ([]string, error) 16 | NamespaceView() ([]string, error) 17 | 18 | Associate(user string, cluster string) bool 19 | UsersByCluster() map[string][]string // cluster -> []user 20 | ClustersByUser() map[string][]string // user -> []cluster 21 | Dissociate(user string, cluster string) bool 22 | 23 | ExplicitNamespaces() []FQNS 24 | SetExplicitNamespace(user string, cluster string, namespace string) bool 25 | DeleteExplicitNamespace(user string, cluster string, namespace string) bool 26 | 27 | Commit() error 28 | } 29 | 30 | type FQNS struct { 31 | User string 32 | Cluster string 33 | NS string 34 | } 35 | -------------------------------------------------------------------------------- /context/kubectl/context.go: -------------------------------------------------------------------------------- 1 | package kubectl 2 | 3 | import ( 4 | log "github.com/Sirupsen/logrus" 5 | nsx "github.com/shyiko/kubensx/context" 6 | "k8s.io/apimachinery/pkg/api/errors" 7 | k8smetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | k8s "k8s.io/client-go/kubernetes" 9 | _ "k8s.io/client-go/plugin/pkg/client/auth" 10 | k8sclientcmd "k8s.io/client-go/tools/clientcmd" 11 | k8sclientcmdapi "k8s.io/client-go/tools/clientcmd/api" 12 | "sort" 13 | "strings" 14 | ) 15 | 16 | const ( 17 | assocPrefix = "kubensx-assoc:" 18 | assocSeparator = ":" 19 | nsPrefix = "kubensx-ns:" 20 | nsSeparator = "/" 21 | contextCurrent = "kubensx-current" 22 | contextPrev = "kubensx-prev" 23 | ) 24 | 25 | type context struct { 26 | pre *k8sclientcmdapi.Context 27 | acs k8sclientcmd.ConfigAccess 28 | cfg *k8sclientcmdapi.Config 29 | nss func(user string, cluster string) ([]string, error) 30 | currentContextMutated bool 31 | } 32 | 33 | type contextRef struct { 34 | key string 35 | ctx *k8sclientcmdapi.Context 36 | } 37 | 38 | func (ctx *context) SetUser(value string) { 39 | ctx.mutateCurrentNSX(func(ctx *k8sclientcmdapi.Context) { 40 | ctx.AuthInfo = value 41 | }) 42 | } 43 | 44 | func (ctx *context) User() string { 45 | return currentNSX(ctx).AuthInfo 46 | } 47 | 48 | func (ctx *context) UserPrevious() string { 49 | return previousNSX(ctx).AuthInfo 50 | } 51 | 52 | func (ctx *context) Users() []string { 53 | keys := make([]string, 0, len(ctx.cfg.AuthInfos)) 54 | for key := range ctx.cfg.AuthInfos { 55 | keys = append(keys, key) 56 | } 57 | return keys 58 | } 59 | 60 | func (ctx *context) SetCluster(value string) { 61 | ctx.mutateCurrentNSX(func(ctx *k8sclientcmdapi.Context) { 62 | ctx.Cluster = value 63 | }) 64 | } 65 | 66 | func (ctx *context) Cluster() string { 67 | return currentNSX(ctx).Cluster 68 | } 69 | 70 | func (ctx *context) ClusterPrevious() string { 71 | return previousNSX(ctx).Cluster 72 | } 73 | 74 | func (ctx *context) Clusters() []string { 75 | keys := make([]string, 0, len(ctx.cfg.Clusters)) 76 | for key := range ctx.cfg.Clusters { 77 | keys = append(keys, key) 78 | } 79 | return keys 80 | } 81 | 82 | func (ctx *context) SetNamespace(value string) { 83 | ctx.mutateCurrentNSX(func(ctx *k8sclientcmdapi.Context) { 84 | ctx.Namespace = value 85 | }) 86 | } 87 | 88 | func (ctx *context) Namespace() string { 89 | return currentNSX(ctx).Namespace 90 | } 91 | 92 | func (ctx *context) NamespacePrevious() string { 93 | return previousNSX(ctx).Namespace 94 | } 95 | 96 | func (ctx *context) Namespaces() ([]string, error) { 97 | r, err := ctx.nss(ctx.User(), ctx.Cluster()) 98 | if statusError, ok := err.(*errors.StatusError); ok && statusError.ErrStatus.Code == 403 { 99 | return r, nil 100 | } 101 | return r, err 102 | } 103 | 104 | func (ctx *context) NamespaceView() ([]string, error) { 105 | var r []string 106 | user := ctx.User() 107 | cluster := ctx.Cluster() 108 | for _, ref := range ctx.ExplicitNamespaces() { 109 | if ref.User == user && ref.Cluster == cluster { 110 | r = append(r, ref.NS) 111 | } 112 | } 113 | if len(r) > 0 { 114 | return r, nil 115 | } 116 | return ctx.Namespaces() 117 | } 118 | 119 | func (ctx *context) Associate(user string, cluster string) bool { 120 | key := assocKey(user, cluster) 121 | if ctx.cfg.Contexts[key] != nil { 122 | return false 123 | } 124 | ctx.cfg.Contexts[key] = &k8sclientcmdapi.Context{AuthInfo: user, Cluster: cluster} 125 | return true 126 | } 127 | 128 | func assocKey(user string, cluster string) string { 129 | return assocPrefix + user + assocSeparator + cluster 130 | } 131 | 132 | func (ctx *context) UsersByCluster() map[string][]string { 133 | m := make(map[string][]string) 134 | ctx.forEachAssoc(func(user string, cluster string) { 135 | m[cluster] = append(m[cluster], user) 136 | }) 137 | return m 138 | } 139 | 140 | func (ctx *context) ClustersByUser() map[string][]string { 141 | m := make(map[string][]string) 142 | ctx.forEachAssoc(func(user string, cluster string) { 143 | m[user] = append(m[user], cluster) 144 | }) 145 | return m 146 | } 147 | 148 | func (ctx *context) forEachAssoc(cb func(string, string)) { 149 | for key := range ctx.cfg.Contexts { 150 | if strings.HasPrefix(key, assocPrefix) { 151 | pair := strings.TrimPrefix(key, assocPrefix) 152 | idx := strings.LastIndex(pair, assocSeparator) 153 | if idx != -1 { 154 | user, cluster := pair[:idx], pair[idx+1:] 155 | if ctx.cfg.AuthInfos[user] != nil && ctx.cfg.Clusters[cluster] != nil { 156 | cb(user, cluster) 157 | } 158 | } 159 | } 160 | } 161 | } 162 | 163 | func (ctx *context) Dissociate(user string, cluster string) bool { 164 | key := assocKey(user, cluster) 165 | if ctx.cfg.Contexts[key] == nil { 166 | return false 167 | } 168 | delete(ctx.cfg.Contexts, key) 169 | return true 170 | } 171 | 172 | func (ctx *context) ExplicitNamespaces() []nsx.FQNS { 173 | var r []nsx.FQNS 174 | for key := range ctx.cfg.Contexts { 175 | if strings.HasPrefix(key, nsPrefix) { 176 | pair := strings.TrimPrefix(key, nsPrefix) 177 | idx := strings.LastIndex(pair, nsSeparator) 178 | if idx != -1 { 179 | split := strings.SplitN(pair[:idx], assocSeparator, 2) 180 | if len(split) == 2 { 181 | r = append(r, nsx.FQNS{User: split[0], Cluster: split[1], NS: pair[idx+1:]}) 182 | } 183 | } 184 | } 185 | } 186 | return r 187 | } 188 | 189 | func (ctx *context) SetExplicitNamespace(user string, cluster string, namespace string) bool { 190 | key := nsKey(user, cluster, namespace) 191 | if ctx.cfg.Contexts[key] != nil { 192 | return false 193 | } 194 | ctx.cfg.Contexts[key] = &k8sclientcmdapi.Context{AuthInfo: user, Cluster: cluster, Namespace: namespace} 195 | return true 196 | } 197 | 198 | func (ctx *context) DeleteExplicitNamespace(user string, cluster string, namespace string) bool { 199 | key := nsKey(user, cluster, namespace) 200 | if ctx.cfg.Contexts[key] == nil { 201 | return false 202 | } 203 | delete(ctx.cfg.Contexts, key) 204 | return true 205 | } 206 | 207 | func nsKey(user string, cluster string, namespace string) string { 208 | return nsPrefix + user + assocSeparator + cluster + nsSeparator + namespace 209 | } 210 | 211 | func (ctx *context) Commit() error { 212 | if ctx.currentContextMutated { 213 | if ctx.pre != nil { 214 | ctx.cfg.Contexts[contextPrev] = ctx.pre 215 | log.Debugf(`Set "%s" to "%s:%s/%s"`, contextPrev, ctx.pre.AuthInfo, ctx.pre.Cluster, ctx.pre.Namespace) 216 | } 217 | curr := ctx.cfg.Contexts[ctx.cfg.CurrentContext] 218 | log.Debugf(`Set "%s" to "%s:%s/%s"`, contextCurrent, curr.AuthInfo, curr.Cluster, curr.Namespace) 219 | } 220 | ctx.purgeInvalid() 221 | k8sclientcmd.ModifyConfig(ctx.acs, *ctx.cfg, false) 222 | return nil 223 | } 224 | 225 | func (ctx *context) purgeInvalid() { 226 | var keys []string 227 | for key := range ctx.cfg.Contexts { 228 | keys = append(keys, key) 229 | } 230 | sort.Strings(keys) 231 | for _, key := range keys { 232 | if strings.HasPrefix(key, assocPrefix) { 233 | pair := strings.TrimPrefix(key, assocPrefix) 234 | path := strings.SplitN(pair, assocSeparator, 2) 235 | if len(path) == 2 && ctx.cfg.AuthInfos[path[0]] != nil && ctx.cfg.Clusters[path[1]] != nil { 236 | log.Debugf(`Found assoc[iation] "%s"`, key) 237 | continue 238 | } 239 | log.Debugf(`Deleted assoc[iation] "%s"`, key) 240 | delete(ctx.cfg.Contexts, key) 241 | } else if strings.HasPrefix(key, nsPrefix) { 242 | triple := strings.TrimPrefix(key, nsPrefix) 243 | idx := strings.LastIndex(triple, nsSeparator) 244 | if idx != -1 { 245 | path := strings.SplitN(triple[:idx], assocSeparator, 2) 246 | if len(path) == 2 && ctx.cfg.AuthInfos[path[0]] != nil && ctx.cfg.Clusters[path[1]] != nil { 247 | log.Debugf(`Found explicit ns "%s"`, key) 248 | continue 249 | } 250 | } 251 | log.Debugf(`Deleted explicit ns "%s"`, key) 252 | delete(ctx.cfg.Contexts, key) 253 | } 254 | } 255 | } 256 | 257 | func (ctx *context) mutateCurrentNSX(cb func(ctx *k8sclientcmdapi.Context)) { 258 | ctx.currentContextMutated = true 259 | ref := currentNSXRef(ctx) 260 | if ref.key == contextCurrent { 261 | cb(ref.ctx) 262 | return 263 | } 264 | k8sctx := &k8sclientcmdapi.Context{ 265 | AuthInfo: ref.ctx.AuthInfo, 266 | Cluster: ref.ctx.Cluster, 267 | Namespace: ref.ctx.Namespace, 268 | } 269 | ctx.cfg.Contexts[contextCurrent] = k8sctx 270 | ctx.cfg.CurrentContext = contextCurrent 271 | cb(k8sctx) 272 | } 273 | 274 | func currentNSX(ctx *context) *k8sclientcmdapi.Context { 275 | return currentNSXRef(ctx).ctx 276 | } 277 | 278 | func currentNSXRef(ctx *context) *contextRef { 279 | currentContext := ctx.cfg.Contexts[ctx.cfg.CurrentContext] 280 | if currentContext == nil { 281 | currentContext = ctx.cfg.Contexts[contextCurrent] 282 | if currentContext == nil { 283 | currentContext = &k8sclientcmdapi.Context{} 284 | ctx.cfg.Contexts[contextCurrent] = currentContext 285 | } 286 | ctx.cfg.CurrentContext = contextCurrent 287 | } 288 | return &contextRef{ctx.cfg.CurrentContext, currentContext} 289 | } 290 | 291 | func previousNSX(ctx *context) *k8sclientcmdapi.Context { 292 | r := ctx.cfg.Contexts[contextPrev] 293 | if r == nil { 294 | r = currentNSX(ctx) 295 | } 296 | return r 297 | } 298 | 299 | func newContext(nss func(cfg k8sclientcmdapi.Config) func(user string, cluster string) ([]string, error)) (nsx.Context, error) { 300 | clientConfig := k8sclientcmd.NewNonInteractiveDeferredLoadingClientConfig( 301 | k8sclientcmd.NewDefaultClientConfigLoadingRules(), 302 | &k8sclientcmd.ConfigOverrides{}, 303 | ) 304 | cfg, err := clientConfig.RawConfig() 305 | if err != nil { 306 | return nil, err 307 | } 308 | ctx := cfg.Contexts[cfg.CurrentContext] 309 | if ctx != nil { 310 | ctx = ctx.DeepCopy() 311 | } 312 | return &context{pre: ctx, acs: clientConfig.ConfigAccess(), cfg: &cfg, nss: nss(cfg)}, nil 313 | } 314 | 315 | func NewContext() (nsx.Context, error) { 316 | // this method will have to be rewritten if ctx.Namespaces() is ever executed more than once over the course 317 | // of single command execution 318 | return newContext(func(cfg k8sclientcmdapi.Config) func(user string, cluster string) ([]string, error) { 319 | return func(user string, cluster string) ([]string, error) { 320 | def := k8sclientcmd.NewDefaultClientConfigLoadingRules() 321 | override := &k8sclientcmd.ConfigOverrides{ 322 | Context: k8sclientcmdapi.Context{AuthInfo: user, Cluster: cluster}, 323 | } 324 | log.Debugf(`Initializing client with "%s:%s"`, override.Context.AuthInfo, override.Context.Cluster) 325 | clientConfig, err := k8sclientcmd.NewNonInteractiveDeferredLoadingClientConfig(def, override).ClientConfig() 326 | if err != nil { 327 | return nil, err 328 | } 329 | client, err := k8s.NewForConfig(clientConfig) 330 | if err != nil { 331 | return nil, err 332 | } 333 | nss, err := client.CoreV1().Namespaces().List(k8smetav1.ListOptions{}) 334 | if err != nil { 335 | return nil, err 336 | } 337 | acc := make([]string, 0, len(nss.Items)) 338 | for _, ns := range nss.Items { 339 | acc = append(acc, ns.Name) 340 | } 341 | return acc, nil 342 | } 343 | }) 344 | } 345 | 346 | func NewContextStub(nss func(user string, cluster string) ([]string, error)) (nsx.Context, error) { 347 | return newContext(func(cfg k8sclientcmdapi.Config) func(user string, cluster string) ([]string, error) { return nss }) 348 | } 349 | -------------------------------------------------------------------------------- /kubensx.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | log "github.com/Sirupsen/logrus" 8 | "github.com/fatih/color" 9 | "github.com/renstrom/fuzzysearch/fuzzy" 10 | "github.com/shyiko/kubensx/cli" 11 | nsx "github.com/shyiko/kubensx/context" 12 | nsxkubectl "github.com/shyiko/kubensx/context/kubectl" 13 | "github.com/spf13/cobra" 14 | "github.com/spf13/pflag" 15 | "gopkg.in/AlecAivazis/survey.v1" 16 | surveycore "gopkg.in/AlecAivazis/survey.v1/core" 17 | surveyterminal "gopkg.in/AlecAivazis/survey.v1/terminal" 18 | "os" 19 | "regexp" 20 | "sort" 21 | "strings" 22 | ) 23 | 24 | var version string 25 | 26 | func init() { 27 | log.SetFormatter(&simpleFormatter{}) 28 | log.SetLevel(log.InfoLevel) 29 | } 30 | 31 | type simpleFormatter struct{} 32 | 33 | func (f *simpleFormatter) Format(entry *log.Entry) ([]byte, error) { 34 | b := &bytes.Buffer{} 35 | fmt.Fprintf(b, "%s ", entry.Message) 36 | for k, v := range entry.Data { 37 | fmt.Fprintf(b, "%s=%+v ", k, v) 38 | } 39 | b.Truncate(b.Len() - 1) 40 | b.WriteByte('\n') 41 | return b.Bytes(), nil 42 | } 43 | 44 | func init() { 45 | surveyterminal.DiscardUnsupportedEscapeSequences = true 46 | // remove "? " prefix 47 | survey.SelectQuestionTemplate = strings.Replace(survey.SelectQuestionTemplate, 48 | `{{- color "green+hb"}}{{ QuestionIcon }} {{color "reset"}}`, "", 1) 49 | survey.SelectQuestionTemplate = strings.Replace(survey.SelectQuestionTemplate, 50 | `{{.Answer}}`, `{{or .Answer "\"\""}}`, 1) 51 | survey.SelectQuestionTemplate = strings.Replace(survey.SelectQuestionTemplate, 52 | `{{- $choice}}`, `{{- or $choice "\"\""}}`, 1) 53 | survey.SelectQuestionTemplate = strings.Replace(survey.SelectQuestionTemplate, 54 | `{{- " "}}{{- color "cyan"}}[Use arrows to move, type to filter{{- if and .Help (not .ShowHelp)}}, {{ HelpInputRune }} for more help{{end}}]{{color "reset"}}`, ``, 1) 55 | // remove "? " prefix 56 | survey.MultiSelectQuestionTemplate = strings.Replace(survey.MultiSelectQuestionTemplate, 57 | `{{- color "green+hb"}}{{ QuestionIcon }} {{color "reset"}}`, "", 1) 58 | survey.MultiSelectQuestionTemplate = strings.Replace(survey.MultiSelectQuestionTemplate, 59 | `{{- if .ShowAnswer}}`, 60 | `{{- if not .ShowAnswer}}{{color "cyan"}} (use space to (multi)select, enter to confirm){{color "reset"}}{{end}}`+ 61 | `{{- if .ShowAnswer}}`, 1) 62 | // " " -> " " before option 63 | survey.MultiSelectQuestionTemplate = strings.Replace(survey.MultiSelectQuestionTemplate, 64 | `{{- " "}}{{$option}}`, "{{- $option}}", 1) 65 | survey.InputQuestionTemplate = strings.Replace(survey.InputQuestionTemplate, 66 | `{{- color "green+hb"}}{{ QuestionIcon }} {{color "reset"}}`, "", 1) 67 | survey.InputQuestionTemplate = strings.Replace(survey.InputQuestionTemplate, 68 | `[{{ HelpInputRune }} for help]`, "({{ .Help }})", 1) 69 | survey.InputQuestionTemplate = strings.Replace(survey.InputQuestionTemplate, 70 | `{{.Answer}}`, `{{or .Answer "\"\""}}`, 1) 71 | surveycore.MarkedOptionIcon = "+" 72 | surveycore.UnmarkedOptionIcon = " " 73 | } 74 | 75 | var newContext = nsxkubectl.NewContext 76 | 77 | /* 78 | var newContext = func () (nsx.Context, error) { 79 | return nsxkubectl.NewContextStub(func(user string, cluster string) ([]string, error) { 80 | if user == "minikube" && cluster == "minikube" { 81 | return []string{"default", "kube-public", "kube-system"}, nil 82 | } 83 | if user == "example@possibly-gmail.com" && cluster != "minikube" { 84 | return []string{"default", "kube-public", "kube-system", "dev", "testing", "staging"}, nil 85 | } 86 | return nil, fmt.Errorf("Unauthorized (%s:%s)", user, cluster) 87 | }) 88 | } 89 | */ 90 | 91 | func lazyContext() func() nsx.Context { 92 | var ctx nsx.Context 93 | return func() nsx.Context { 94 | if ctx == nil { 95 | var err error 96 | if ctx, err = newContext(); err != nil { 97 | log.Fatal(err) 98 | } 99 | } 100 | return ctx 101 | } 102 | } 103 | 104 | var validNS = regexp.MustCompile(`^[a-z0-9-.]+$`) 105 | var whitespace = regexp.MustCompile("\\s+") 106 | 107 | func main() { 108 | completion := cli.NewCompletion(lazyContext()) 109 | completed, err := completion.Execute() 110 | if err != nil { 111 | log.Debug(err) 112 | os.Exit(3) 113 | } 114 | if completed { 115 | os.Exit(0) 116 | } 117 | rootCmd := &cobra.Command{ 118 | Use: "kubensx", 119 | Long: "Simpler Cluster/User/Namespace switching for Kubernetes (https://github.com/shyiko/kubensx).", 120 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 121 | if debug, _ := cmd.Flags().GetBool("debug"); debug { 122 | log.SetLevel(log.DebugLevel) 123 | } 124 | if kubeconfig, _ := cmd.Flags().GetString("kubeconfig"); kubeconfig != "" { 125 | os.Setenv("KUBECONFIG", kubeconfig) 126 | } 127 | if noColor, _ := cmd.Flags().GetBool("no-color"); noColor { 128 | surveycore.DisableColor = true 129 | color.NoColor = true 130 | } 131 | }, 132 | RunE: func(cmd *cobra.Command, args []string) error { 133 | if showVersion, _ := cmd.Flags().GetBool("version"); showVersion { 134 | fmt.Println(version) 135 | return nil 136 | } 137 | return pflag.ErrHelp 138 | }, 139 | } 140 | assocCmd := &cobra.Command{ 141 | Use: "assoc [pattern]", 142 | Aliases: []string{"a"}, 143 | Short: "Assoc[iate] user with one or more clusters", 144 | RunE: func(cmd *cobra.Command, args []string) error { 145 | ctx, err := newContext() 146 | if err != nil { 147 | log.Fatal(err) 148 | } 149 | dissociate, _ := cmd.Flags().GetBool("delete") 150 | if dissociate && len(args) == 0 { 151 | return errors.New("pattern (:) required") 152 | } 153 | dissociateAll, _ := cmd.Flags().GetBool("delete-all") 154 | if dissociateAll && len(args) != 0 { 155 | return errors.New("--delete-all and pattern cannot be used together") 156 | } 157 | if list, _ := cmd.Flags().GetBool("list"); list { 158 | if dissociate || dissociateAll { 159 | return errors.New("--list and --delete/--delete-all cannot be used together") 160 | } 161 | clustersByUser := ctx.ClustersByUser() 162 | var users []string 163 | for user := range clustersByUser { 164 | users = append(users, user) 165 | } 166 | for _, user := range sortInPlace(users) { 167 | for _, cluster := range sortInPlace(clustersByUser[user]) { 168 | fmt.Printf("%s:%s\n", user, cluster) 169 | } 170 | } 171 | return nil 172 | } 173 | dryRun, _ := cmd.Flags().GetBool("dry-run") 174 | if len(args) == 0 && dryRun && !dissociateAll { 175 | return pflag.ErrHelp 176 | } 177 | if len(args) == 0 && !dryRun && !dissociateAll { 178 | mustContainAtLeastOneCluster(ctx) 179 | mustContainAtLeastOneUser(ctx) 180 | user := prompt("user:", sortInPlace(ctx.Users()), ctx.User(), true) 181 | defclusters := ctx.ClustersByUser()[user] 182 | clusters := promptMultiSelect("cluster:", ctx.Clusters(), defclusters) 183 | nextdef: 184 | for _, defcluster := range defclusters { 185 | for _, cluster := range clusters { 186 | if defcluster == cluster { 187 | continue nextdef 188 | } 189 | } 190 | ctx.Dissociate(user, defcluster) 191 | fmt.Printf("- %s:%s\n", user, defcluster) 192 | } 193 | for _, cluster := range clusters { 194 | if ctx.Associate(user, cluster) { 195 | fmt.Printf("+ %s:%s\n", user, cluster) 196 | } 197 | } 198 | } else { 199 | userMatcher := matchAll 200 | clusterMatcher := matchAll 201 | if len(args) != 0 { 202 | pattern := args[0] 203 | pattern, patternMatcher := newPatternMatcher(cmd, pattern) 204 | chunks := regexp.MustCompile(":").Split(pattern, 2) 205 | if chunks[0] == "" { 206 | log.Fatal(" cannot be empty") 207 | } 208 | userMatcher = bindMatcher(patternMatcher, chunks[0], ctx.User()) 209 | if len(chunks) == 2 { 210 | // must be user:cluster (not just user) 211 | if chunks[1] == "" { 212 | log.Fatal(" cannot be empty") 213 | } 214 | clusterMatcher = bindMatcher(patternMatcher, chunks[1], ctx.Cluster()) 215 | } 216 | } 217 | clusters := ctx.Clusters() 218 | assoc := ctx.ClustersByUser() 219 | clustersByUser := func(user string) []string { return clusters } 220 | if dissociate || dissociateAll { 221 | clustersByUser = func(user string) []string { 222 | return assoc[user] 223 | } 224 | } 225 | for _, user := range sortInPlace(userMatcher(ctx.Users())) { 226 | for _, cluster := range sortInPlace(clusterMatcher(clustersByUser(user))) { 227 | if dissociate || dissociateAll { 228 | if ctx.Dissociate(user, cluster) { 229 | fmt.Printf("- %s:%s\n", user, cluster) 230 | } 231 | } else { 232 | if ctx.Associate(user, cluster) { 233 | fmt.Printf("+ %s:%s\n", user, cluster) 234 | } 235 | } 236 | } 237 | } 238 | } 239 | if !dryRun { 240 | ctx.Commit() 241 | } 242 | return nil 243 | }, 244 | Example: " # assoc[iate] (interactive)\n" + 245 | " kubensx assoc\n" + 246 | " # assoc[iate] minikube user with minikube cluster\n" + 247 | " kubensx assoc minikube:minikube\n" + 248 | " \n" + 249 | " # list assoc[iations]\n" + 250 | " kubensx assoc -l\n" + 251 | " \n" + 252 | " # list : pairs that would be assoc[iated] should\n" + 253 | " # `kubensx assoc :` be executed\n" + 254 | " kubensx assoc --dry-run minikube\n" + 255 | " kubensx assoc --dry-run '*:minikube'", 256 | } 257 | assocCmd.Flags().BoolP("delete", "d", false, "Delete assoc[iation](s)") 258 | assocCmd.Flags().Bool("delete-all", false, "Delete all assoc[iations]") 259 | assocCmd.Flags().BoolP("dry-run", "x", false, "Do not modify the config (just show what's going happen)") 260 | assocCmd.Flags().BoolP("exact", "e", false, "Match exactly (instead of default (wildcard) matching)") 261 | assocCmd.Flags().BoolP("fuzzy", "z", false, "Match fuzzily (instead of default (wildcard) matching)") 262 | assocCmd.Flags().BoolP("list", "l", false, "List assoc[iations] (:|s)") 263 | rootCmd.AddCommand(assocCmd) 264 | assocNsCmd := &cobra.Command{ 265 | Use: "ns-list [pattern...]", 266 | Aliases: []string{"n"}, 267 | Short: "Control list of namespaces", 268 | Long: "Control list of namespaces\n\n" + 269 | "Use ns-list when:" + 270 | "\n - You wish to able to select a namespace from a list of options (e.g. \"kubensx use\")\nbut Access Control configuration prohibits user from listing namespaces; " + 271 | "\n - You wish to reduce number of namespaces available for selection.", 272 | RunE: func(cmd *cobra.Command, args []string) error { 273 | ctx, err := newContext() 274 | if err != nil { 275 | log.Fatal(err) 276 | } 277 | dissociate, _ := cmd.Flags().GetBool("delete") 278 | if dissociate && len(args) == 0 { 279 | return errors.New("pattern (:/) required") 280 | } 281 | dissociateAll, _ := cmd.Flags().GetBool("delete-all") 282 | if dissociateAll && len(args) != 0 { 283 | return errors.New("--delete-all and pattern cannot be used together") 284 | } 285 | if list, _ := cmd.Flags().GetBool("list"); list { 286 | if dissociate || dissociateAll { 287 | return errors.New("--list and --delete/--delete-all cannot be used together") 288 | } 289 | for _, fqns := range sortFQNSSliceInPlace(ctx.ExplicitNamespaces()) { 290 | fmt.Printf("%s:%s/%s\n", fqns.User, fqns.Cluster, fqns.NS) 291 | } 292 | return nil 293 | } 294 | ignoreAssoc, _ := cmd.Flags().GetBool("ignore-assoc") 295 | if len(args) == 0 && !dissociateAll { 296 | mustContainAtLeastOneUser(ctx) 297 | mustContainAtLeastOneCluster(ctx) 298 | user := prompt("user:", sortInPlace(ctx.Users()), ctx.User(), true) 299 | clusters := ctx.ClustersByUser()[user] 300 | if ignoreAssoc || len(clusters) == 0 { 301 | clusters = ctx.Clusters() 302 | } 303 | cluster := prompt("cluster:", sortInPlace(clusters), ctx.Cluster(), true) 304 | var nss []string 305 | for _, r := range ctx.ExplicitNamespaces() { 306 | if r.User == user && r.Cluster == cluster { 307 | nss = append(nss, r.NS) 308 | } 309 | } 310 | sort.Strings(nss) 311 | input := promptInput("namespace(s):", strings.Join(nss, " "), "space-separated") 312 | var unss []string 313 | for _, m := range whitespace.Split(input, -1) { 314 | if m != "" { 315 | if err := validateNS(m); err != nil { 316 | log.Fatalf(err.Error()) 317 | } 318 | unss = append(unss, m) 319 | } 320 | } 321 | nextNS: 322 | for _, ns := range nss { 323 | for _, uns := range unss { 324 | if ns == uns { 325 | continue nextNS 326 | } 327 | } 328 | ctx.DeleteExplicitNamespace(user, cluster, ns) 329 | fmt.Printf("- %s:%s/%s\n", user, cluster, ns) 330 | } 331 | nextUNS: 332 | for _, uns := range unss { 333 | for _, ns := range nss { 334 | if ns == uns { 335 | continue nextUNS 336 | } 337 | } 338 | ctx.SetExplicitNamespace(user, cluster, uns) 339 | fmt.Printf("+ %s:%s/%s\n", user, cluster, uns) 340 | } 341 | } else { 342 | var fqnss []nsx.FQNS 343 | if len(args) != 0 { 344 | for _, arg := range args { 345 | slashIndex := strings.LastIndex(arg, "/") 346 | if slashIndex == -1 { 347 | log.Fatalf(`Expected :/ or / (instead got "%s")`, arg) 348 | } 349 | namespace := arg[slashIndex+1:] 350 | if err := validateNS(namespace); err != nil { 351 | log.Fatal(err.Error()) 352 | } 353 | pattern, patternMatcher := newPatternMatcher(cmd, arg[0:slashIndex]) 354 | chunks := regexp.MustCompile(":").Split(pattern, 2) 355 | if len(chunks) == 1 { 356 | chunks = append([]string{"*"}, chunks...) 357 | } 358 | if chunks[0] == "" { 359 | log.Fatalf(` cannot be empty ("%s")`, arg) 360 | } 361 | if chunks[1] == "" { 362 | log.Fatalf(` cannot be empty ("%s")`, arg) 363 | } 364 | userMatcher := bindMatcher(patternMatcher, chunks[0], ctx.User()) 365 | clusterMatcher := bindMatcher(patternMatcher, chunks[1], ctx.Cluster()) 366 | clusters := ctx.Clusters() 367 | assoc := ctx.ClustersByUser() 368 | clustersByUser := func(user string) []string { 369 | r := assoc[user] 370 | if ignoreAssoc || len(r) == 0 { 371 | return clusters 372 | } 373 | return r 374 | } 375 | namespaceMatcher := bindMatcher(patternMatcher, namespace, ctx.Namespace()) 376 | for _, ns := range namespaceMatcher([]string{namespace}) { 377 | for _, user := range userMatcher(ctx.Users()) { 378 | for _, cluster := range clusterMatcher(clustersByUser(user)) { 379 | fqnss = append(fqnss, nsx.FQNS{User: user, Cluster: cluster, NS: ns}) 380 | } 381 | } 382 | } 383 | } 384 | } else { 385 | fqnss = ctx.ExplicitNamespaces() 386 | } 387 | for _, fqns := range sortFQNSSliceInPlace(fqnss) { 388 | if dissociate || dissociateAll { 389 | if ctx.DeleteExplicitNamespace(fqns.User, fqns.Cluster, fqns.NS) { 390 | fmt.Printf("- %s:%s/%s\n", fqns.User, fqns.Cluster, fqns.NS) 391 | } 392 | } else { 393 | if ctx.SetExplicitNamespace(fqns.User, fqns.Cluster, fqns.NS) { 394 | fmt.Printf("+ %s:%s/%s\n", fqns.User, fqns.Cluster, fqns.NS) 395 | } 396 | } 397 | } 398 | } 399 | dryRun, _ := cmd.Flags().GetBool("dry-run") 400 | if !dryRun { 401 | ctx.Commit() 402 | } 403 | return nil 404 | }, 405 | Example: " # (interactive)\n" + 406 | " kubensx ns-list\n" + 407 | " # make staging namespace known for qa in us-west1 cluster\n" + 408 | " kubensx ns-list qa:us-west1/staging\n" + 409 | " \n" + 410 | " # list assoc[iations]\n" + 411 | " kubensx ns-list -l\n" + 412 | " \n" + 413 | " # list :/ triples that would be assoc[iated] should\n" + 414 | " # `kubensx ns-list :/` be executed\n" + 415 | " kubensx ns-list --dry-run minikube/staging\n" + 416 | " kubensx ns-list --dry-run '*:minikube/staging'", 417 | } 418 | assocNsCmd.Flags().BoolP("delete", "d", false, "Delete assoc[iation](s)") 419 | assocNsCmd.Flags().Bool("delete-all", false, "Delete all assoc[iations]") 420 | assocNsCmd.Flags().BoolP("dry-run", "x", false, "Do not modify the config (just show what's going happen)") 421 | assocNsCmd.Flags().BoolP("exact", "e", false, "Match exactly (instead of default (wildcard) matching)") 422 | assocNsCmd.Flags().BoolP("fuzzy", "z", false, "Match fuzzily (instead of default (wildcard) matching)") 423 | assocNsCmd.Flags().Bool("ignore-assoc", false, "Ignore user:cluster assoc[iations] (if any)") 424 | assocNsCmd.Flags().BoolP("list", "l", false, "List assoc[iations] (:/|s)") 425 | rootCmd.AddCommand(assocNsCmd) 426 | completionCmd := &cobra.Command{ 427 | Use: "completion", 428 | Short: "Command-line completion", 429 | } 430 | completionCmd.AddCommand( 431 | &cobra.Command{ 432 | Use: "bash", 433 | Short: "Generate Bash completion", 434 | RunE: func(cmd *cobra.Command, args []string) error { 435 | if len(args) != 0 { 436 | return pflag.ErrHelp 437 | } 438 | if err := completion.GenBashCompletion(os.Stdout); err != nil { 439 | log.Error(err) 440 | } 441 | return nil 442 | }, 443 | Example: " source <(kubensx completion bash)", 444 | }, 445 | &cobra.Command{ 446 | Use: "zsh", 447 | Short: "Generate Z shell completion", 448 | RunE: func(cmd *cobra.Command, args []string) error { 449 | if len(args) != 0 { 450 | return pflag.ErrHelp 451 | } 452 | if err := completion.GenZshCompletion(os.Stdout); err != nil { 453 | log.Error(err) 454 | } 455 | return nil 456 | }, 457 | Example: " source <(kubensx completion zsh)", 458 | }, 459 | ) 460 | rootCmd.AddCommand(completionCmd) 461 | currentCmd := &cobra.Command{ 462 | Use: "current", 463 | Aliases: []string{"c"}, 464 | Short: "Show current context (user:cluster/namespace)", 465 | RunE: func(cmd *cobra.Command, args []string) error { 466 | ctx, err := newContext() 467 | if err != nil { 468 | log.Fatal(err) 469 | } 470 | u, _ := cmd.Flags().GetBool("user") 471 | c, _ := cmd.Flags().GetBool("cluster") 472 | n, _ := cmd.Flags().GetBool("namespace") 473 | if !n { 474 | n, _ = cmd.Flags().GetBool("ns") 475 | } 476 | if u && !c && n { 477 | return errors.New("--cluster(-c) cannot be omitted when both --user(-u) and --namespace(--ns,-n) are present") 478 | } 479 | switch { 480 | case u == c && c == n: 481 | fmt.Println(formatContext(ctx)) 482 | case u && c && !n: 483 | fmt.Printf("%s:%s\n", ctx.User(), ctx.Cluster()) 484 | case !u && c && n: 485 | fmt.Printf("%s/%s\n", ctx.Cluster(), ctx.Namespace()) 486 | case u: 487 | fmt.Println(ctx.User()) 488 | case c: 489 | fmt.Println(ctx.Cluster()) 490 | case n: 491 | fmt.Println(ctx.Namespace()) 492 | } 493 | return nil 494 | }, 495 | } 496 | currentCmd.Flags().BoolP("cluster", "c", false, "Output cluster only (can be combined with --user(-u) and --namespace(--ns,-n))") 497 | currentCmd.Flags().BoolP("namespace", "n", false, "Output namespace only (can be combined with --cluster(-c))") 498 | currentCmd.Flags().Bool("ns", false, "Alias for --namespace") 499 | currentCmd.Flags().BoolP("user", "u", false, "Output user only (can be combined with --cluster(-c))") 500 | rootCmd.AddCommand(currentCmd) 501 | lsCmd := &cobra.Command{ 502 | Use: "ls", 503 | Aliases: []string{"l"}, 504 | Short: "List users/clusters/namespaces", 505 | RunE: func(cmd *cobra.Command, args []string) error { 506 | u, _ := cmd.Flags().GetBool("users") 507 | c, _ := cmd.Flags().GetBool("clusters") 508 | n, _ := cmd.Flags().GetBool("namespaces") 509 | ignoreExplicitNS, _ := cmd.Flags().GetBool("ignore-ns-list") 510 | if !u && !c && !n { 511 | return pflag.ErrHelp 512 | } 513 | if u && c || u && n || c && n { 514 | return errors.New("--users(-u)/--clusters(-c)/--namespaces(-n) cannot be used together") 515 | } 516 | ctx, err := newContext() 517 | if err != nil { 518 | log.Fatal(err) 519 | } 520 | switch { 521 | case u: 522 | printWithSelectionHighlighted(ctx.Users(), ctx.User()) 523 | case c: 524 | printWithSelectionHighlighted(ctx.Clusters(), ctx.Cluster()) 525 | case n: 526 | printWithSelectionHighlighted(requireNamespaces(ctx, !ignoreExplicitNS), ctx.Namespace()) 527 | } 528 | return nil 529 | }, 530 | } 531 | lsCmd.Flags().BoolP("clusters", "c", false, "List clusters") 532 | lsCmd.Flags().BoolP("namespaces", "n", false, "List namespaces") 533 | lsCmd.Flags().BoolP("users", "u", false, "List users") 534 | lsCmd.Flags().Bool("ignore-ns-list", false, "Ignore explicit user:cluster/namespace(s) (if any)") 535 | rootCmd.AddCommand(lsCmd) 536 | useCmd := &cobra.Command{ 537 | Use: "use [user:cluster/namespace]", 538 | Aliases: []string{"u"}, 539 | Short: "Change context", 540 | RunE: func(cmd *cobra.Command, args []string) error { 541 | ctx, err := newContext() 542 | if err != nil { 543 | log.Fatal(err) 544 | } 545 | u, _ := cmd.Flags().GetBool("user") 546 | c, _ := cmd.Flags().GetBool("cluster") 547 | n, _ := cmd.Flags().GetBool("namespace") 548 | if !n { 549 | n, _ = cmd.Flags().GetBool("ns") 550 | } 551 | if !u && !c && !n { 552 | u, c, n = true, true, true 553 | } 554 | dryRun, _ := cmd.Flags().GetBool("dry-run") 555 | ignoreAssoc, _ := cmd.Flags().GetBool("ignore-assoc") 556 | ignoreExplicitNS, _ := cmd.Flags().GetBool("ignore-ns-list") 557 | force, _ := cmd.Flags().GetBool("force") 558 | if len(args) == 0 { 559 | mustContainAtLeastOneCluster(ctx) 560 | mustContainAtLeastOneUser(ctx) 561 | ctx.SetCluster(prompt("cluster:", sortInPlace(ctx.Clusters()), ctx.Cluster(), c)) 562 | users, user := sortInPlace(ctx.Users()), ctx.User() 563 | if !ignoreAssoc { 564 | assoc := ctx.UsersByCluster()[ctx.Cluster()] 565 | if len(assoc) != 0 { 566 | users = sortInPlace(assoc) 567 | if index(users, user) == -1 { 568 | user = users[0] 569 | } 570 | } 571 | } 572 | ctx.SetUser(prompt("user:", users, user, u)) 573 | nss := requireNamespaces(ctx, !ignoreExplicitNS) 574 | if len(nss) == 0 { 575 | fmt.Println("\nIt appears that the user you have selected is not allowed to list namespaces.\n" + 576 | "If you wish to avoid manual entry next time you `kubensx use` - see `kubensx ns-list --help`.\n") 577 | ns := promptInput("namespace:", "", "") 578 | if err := validateNS(ns); err != nil { 579 | log.Fatalf(err.Error()) 580 | } 581 | ctx.SetNamespace(ns) 582 | } else { 583 | ctx.SetNamespace(prompt("namespace:", sortInPlace(nss), ctx.Namespace(), n)) 584 | } 585 | } else if args[0] == "-" { 586 | ctx.SetCluster(ctx.ClusterPrevious()) 587 | ctx.SetUser(ctx.UserPrevious()) 588 | ctx.SetNamespace(ctx.NamespacePrevious()) 589 | } else { 590 | pattern := args[0] 591 | pattern, patternMatcher := newPatternMatcher(cmd, pattern) 592 | var user, cluster, namespace string 593 | var uexp, nexp bool // user/cluster/namespace explicit 594 | if !(u && c && n) { 595 | if u && c || u && n || c && n { 596 | return errors.New("--user(-u)/--cluster(-c)/--namespace(--ns,-n) cannot be used together") 597 | } 598 | user, cluster, namespace = ctx.User(), ctx.Cluster(), ctx.Namespace() 599 | switch { 600 | case u: 601 | uexp = true 602 | user = pattern 603 | case c: 604 | cluster = pattern 605 | case n: 606 | nexp = true 607 | namespace = pattern 608 | } 609 | } else { 610 | chunks := regexp.MustCompile("[:/]").Split(pattern, 3) 611 | switch len(chunks) { 612 | case 3: // user:cluster/namespace 613 | user, cluster, namespace = chunks[0], chunks[1], chunks[2] 614 | uexp, nexp = true, true 615 | case 2: // user:cluster or cluster/namespace 616 | if pattern[len(chunks[0])] == ':' { 617 | user, cluster, namespace = chunks[0], chunks[1], ctx.Namespace() 618 | uexp = true 619 | } else { 620 | user, cluster, namespace = ctx.User(), chunks[0], chunks[1] 621 | nexp = true 622 | } 623 | case 1: // namespace 624 | user, cluster, namespace = ctx.User(), ctx.Cluster(), chunks[0] 625 | nexp = true 626 | } 627 | } 628 | log.Debugf(`Searching for "%s(%v):%s/%s(%v)"`, user, uexp, cluster, namespace, nexp) 629 | // user and cluster can be empty per 630 | // https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/ 631 | // (same goes for namespace) 632 | // have said that said, "" (empty) option should not be available for selection (through "prompt") 633 | clusterMatcher := stableMatcher(bindMatcher(patternMatcher, cluster, ctx.Cluster())) 634 | if cluster == "" { 635 | clusterMatcher = allowEmpty(clusterMatcher) 636 | } 637 | userMatcher := stableMatcher(bindMatcher(patternMatcher, user, ctx.User())) 638 | if user == "" { 639 | userMatcher = allowEmpty(userMatcher) 640 | } 641 | namespaceMatcher := stableMatcher(bindMatcher(patternMatcher, namespace, ctx.Namespace())) 642 | boundNSS := func() []string { 643 | if force { 644 | return []string{namespace} 645 | } 646 | r := requireNamespaces(ctx, !ignoreExplicitNS) 647 | if len(r) == 0 { 648 | log.Fatalf("It appears that \"%s\" is not allowed to list namespaces in \"%s\" cluster.\n"+ 649 | "Either use --force(-f) (in which case namespace must be specified --exact|ly) or "+ 650 | "provide an explicit list of namespaces via `kubensx ns-list`.", ctx.User(), ctx.Cluster()) 651 | } 652 | return r 653 | } 654 | if namespace == "" { 655 | namespaceMatcher = allowEmpty(namespaceMatcher) 656 | boundNSS = func() []string { return []string{""} } 657 | } 658 | if !nexp { 659 | namespaceMatcher = fallbackToAllAvailable(namespaceMatcher) 660 | } 661 | assoc := ctx.UsersByCluster() 662 | usersByCluster := func(cluster string) []string { 663 | if ignoreAssoc { 664 | return ctx.Users() 665 | } 666 | users := assoc[cluster] 667 | if len(users) == 0 { 668 | users = ctx.Users() 669 | } 670 | return users 671 | } 672 | if !uexp { 673 | userMatcher = fallbackToAllAvailable(userMatcher) 674 | } 675 | if dryRun { 676 | for _, cluster := range clusterMatcher(ctx.Clusters()) { 677 | ctx.SetCluster(cluster) 678 | for _, user := range userMatcher(usersByCluster(cluster)) { 679 | ctx.SetUser(user) 680 | for _, namespace := range namespaceMatcher(boundNSS()) { 681 | ctx.SetNamespace(namespace) 682 | fmt.Println(formatContext(ctx)) 683 | } 684 | } 685 | } 686 | return nil 687 | } 688 | mustContainAtLeastOneCluster(ctx) 689 | mustContainAtLeastOneUser(ctx) 690 | promptPattern := func(msg string, opts []string, def string, pattern string, matcher matcher) string { 691 | matches := matcher(opts) 692 | switch len(matches) { 693 | case 0: 694 | log.Fatalf(`"%s" does not match any of the %ss (expected one of (%s))`, 695 | pattern, msg, strings.Join(opts, ", ")) 696 | case 1: 697 | return matches[0] 698 | } 699 | var opt = def 700 | if index(matches, def) == -1 { 701 | opt = matches[0] 702 | } 703 | match := prompt(msg+":", matches, opt, true) 704 | erasePreviousLine() 705 | return match 706 | } 707 | ctx.SetCluster(promptPattern("cluster", ctx.Clusters(), ctx.Cluster(), cluster, clusterMatcher)) 708 | ctx.SetUser(promptPattern("user", usersByCluster(ctx.Cluster()), ctx.User(), user, userMatcher)) 709 | ctx.SetNamespace(promptPattern("namespace", boundNSS(), ctx.Namespace(), namespace, namespaceMatcher)) 710 | } 711 | if !dryRun { 712 | ctx.Commit() 713 | } 714 | fmt.Println("Switched to " + formatContext(ctx)) 715 | return nil 716 | }, 717 | } 718 | useCmd.Flags().BoolP("cluster", "c", false, "Change cluster only") 719 | useCmd.Flags().BoolP("dry-run", "x", false, "List matches (without changing the context)") 720 | useCmd.Flags().BoolP("exact", "e", false, "Match exactly (by default wildcard matching is used)") 721 | useCmd.Flags().BoolP("fuzzy", "z", false, "Match fuzzily (by default wildcard matching is used)") 722 | useCmd.Flags().Bool("ignore-assoc", false, "Ignore user:cluster assoc[iations] (if any)") 723 | useCmd.Flags().Bool("ignore-ns-list", false, "Ignore explicit user:cluster/namespace(s) (if any)") 724 | useCmd.Flags().BoolP("namespace", "n", false, "Change namespace only") 725 | useCmd.Flags().Bool("ns", false, "Alias for --namespace") 726 | useCmd.Flags().BoolP("user", "u", false, "Change user only") 727 | useCmd.Flags().BoolP("force", "f", false, "Skip namespace validation (NOTE: namespace must be provided --exact|ly)"+ 728 | "\n(useful when user is not allowed to list namespaces; see also \"kubensx ns-list --help\")") 729 | rootCmd.AddCommand(useCmd) 730 | walk(rootCmd, func(cmd *cobra.Command) { 731 | cmd.Flags().BoolP("help", "h", false, "Print usage") 732 | cmd.Flags().MarkHidden("help") 733 | }) 734 | rootCmd.PersistentFlags().Bool("debug", false, "Turn on debug output") 735 | rootCmd.PersistentFlags().String("kubeconfig", "", "Path to the config file (e.g. ~/.kube/config)") 736 | rootCmd.PersistentFlags().Bool("no-color", false, "Disable color output") 737 | rootCmd.Flags().Bool("version", false, "Print version information") 738 | if err := rootCmd.Execute(); err != nil { 739 | log.Debug(err) 740 | os.Exit(-1) 741 | } 742 | } 743 | 744 | func validateNS(ns string) error { 745 | if !validNS.MatchString(ns) { 746 | return fmt.Errorf(`"%s" is not a valid namespace`, ns) 747 | } 748 | return nil 749 | } 750 | 751 | func sortFQNSSliceInPlace(s []nsx.FQNS) []nsx.FQNS { 752 | sort.Slice(s, func(i, j int) bool { 753 | switch strings.Compare(s[i].User, s[j].User) { 754 | case -1: 755 | return true 756 | case 1: 757 | return false 758 | } 759 | switch strings.Compare(s[i].Cluster, s[j].Cluster) { 760 | case -1: 761 | return true 762 | case 1: 763 | return false 764 | } 765 | switch strings.Compare(s[i].NS, s[j].NS) { 766 | case -1: 767 | return true 768 | case 1: 769 | return false 770 | } 771 | return false 772 | }) 773 | return s 774 | } 775 | 776 | func mustContainAtLeastOneCluster(ctx nsx.Context) { 777 | if len(ctx.Clusters()) == 0 { 778 | log.Fatal("No clusters have been found.\n" + 779 | "See `kubectl config set-cluster --help` on how to add one.") 780 | } 781 | } 782 | 783 | func mustContainAtLeastOneUser(ctx nsx.Context) { 784 | if len(ctx.Users()) == 0 { 785 | log.Fatal("No users have been found.\n" + 786 | "See `kubectl config set-credentials --help` on how to add one.") 787 | } 788 | } 789 | 790 | func requireNamespaces(ctx nsx.Context, explicit bool) []string { 791 | var r []string 792 | var err error 793 | if explicit { 794 | r, err = ctx.NamespaceView() 795 | } else { 796 | r, err = ctx.Namespaces() 797 | } 798 | if err != nil { 799 | log.Fatal(err) 800 | } 801 | return r 802 | } 803 | 804 | func formatContext(ctx nsx.Context) string { 805 | return fmt.Sprintf("%s:%s/%s", ctx.User(), ctx.Cluster(), ctx.Namespace()) 806 | } 807 | 808 | type partialMatcher = func(pattern string, arr []string) []string 809 | type matcher = func(arr []string) []string 810 | 811 | func bindMatcher(m partialMatcher, pattern string, def string) matcher { 812 | switch pattern { 813 | case ".": 814 | return func(arr []string) []string { return []string{def} } 815 | case "*": 816 | return matchAll 817 | default: 818 | return func(arr []string) []string { 819 | return m(pattern, arr) 820 | } 821 | } 822 | } 823 | 824 | func fallbackToAllAvailable(m matcher) matcher { 825 | return func(arr []string) []string { 826 | r := m(arr) 827 | if len(r) == 0 { 828 | r = arr 829 | } 830 | return r 831 | } 832 | } 833 | 834 | func allowEmpty(m matcher) matcher { 835 | return func(arr []string) []string { 836 | return m(append([]string{""}, arr...)) 837 | } 838 | } 839 | 840 | func stableMatcher(m matcher) matcher { 841 | return func(arr []string) []string { 842 | return sortInPlace(m(arr)) 843 | } 844 | } 845 | 846 | func matchAll(arr []string) []string { 847 | return arr 848 | } 849 | 850 | func matchExact(v string, arr []string) []string { 851 | for _, a := range arr { 852 | if a == v { 853 | return []string{a} 854 | } 855 | } 856 | return nil 857 | } 858 | 859 | func matchWildcard(v string, arr []string) []string { 860 | var r []string 861 | split := strings.Split(v, "*") 862 | nextstr: 863 | for _, str := range arr { 864 | if str == v { 865 | r = []string{v} 866 | break 867 | } 868 | for _, sub := range split { 869 | if sub != "" && !strings.Contains(str, sub) { 870 | continue nextstr 871 | } 872 | } 873 | r = append(r, str) 874 | } 875 | return r 876 | } 877 | 878 | func matchFuzzy(v string, arr []string) []string { 879 | if index(arr, v) != -1 { 880 | return []string{v} 881 | } 882 | return fuzzy.FindFold(v, arr) 883 | } 884 | 885 | func newPatternMatcher(cmd *cobra.Command, pattern string) (string, partialMatcher) { 886 | if exact, _ := cmd.Flags().GetBool("exact"); exact || strings.HasPrefix(pattern, "=") { 887 | return strings.TrimPrefix(pattern, "="), matchExact 888 | } 889 | if tilda, _ := cmd.Flags().GetBool("fuzzy"); tilda || strings.HasPrefix(pattern, "~") { 890 | return strings.TrimPrefix(pattern, "~"), matchFuzzy 891 | } 892 | return pattern, matchWildcard 893 | } 894 | 895 | func prompt(text string, opts []string, selection string, askUserToSelect bool) string { 896 | if askUserToSelect && len(opts) > 1 { 897 | return promptSelect(text, opts, selection) 898 | } else { 899 | if len(opts) == 1 { 900 | selection = opts[0] 901 | } 902 | return printSelect(text, selection) 903 | } 904 | } 905 | 906 | func printSelect(text string, value string) string { 907 | opt := value 908 | if opt == "" { 909 | opt = `""` 910 | } 911 | fmt.Println(text + " " + color.CyanString(opt)) 912 | return value 913 | } 914 | 915 | func promptSelect(text string, opts []string, def string) string { 916 | value := def 917 | if err := survey.AskOne( 918 | &survey.Select{ 919 | Message: text, 920 | Options: opts, 921 | Default: def, 922 | FilterResetDefault: true, 923 | }, 924 | &value, 925 | nil, 926 | ); err != nil { 927 | log.Fatal(err) 928 | } 929 | return value 930 | } 931 | 932 | func promptMultiSelect(text string, opts []string, def []string) []string { 933 | value := def 934 | if err := survey.AskOne( 935 | &survey.MultiSelect{ 936 | Message: text, 937 | Options: opts, 938 | Default: def, 939 | }, 940 | &value, 941 | nil, 942 | ); err != nil { 943 | log.Fatal(err) 944 | } 945 | return value 946 | } 947 | 948 | func promptInput(text string, def string, help string) string { 949 | value := def 950 | if err := survey.AskOne( 951 | &survey.Input{ 952 | Message: text, 953 | Default: def, 954 | EditDefault: true, 955 | Help: help, 956 | }, 957 | &value, 958 | nil, 959 | ); err != nil { 960 | log.Fatal(err) 961 | } 962 | return value 963 | } 964 | 965 | func erasePreviousLine() { 966 | surveyterminal.CursorPreviousLine(1) 967 | surveyterminal.EraseLine(surveyterminal.ERASE_LINE_ALL) 968 | } 969 | 970 | func printWithSelectionHighlighted(arr []string, selection string) { 971 | for _, namespace := range sortInPlace(arr) { 972 | if namespace == selection { 973 | namespace = color.CyanString(namespace) 974 | } 975 | fmt.Println(namespace) 976 | } 977 | } 978 | 979 | func index(arr []string, val string) int { 980 | for i, v := range arr { 981 | if v == val { 982 | return i 983 | } 984 | } 985 | return -1 986 | } 987 | 988 | func sortInPlace(arr []string) []string { 989 | sort.Strings(arr) 990 | return arr 991 | } 992 | 993 | func walk(cmd *cobra.Command, cb func(*cobra.Command)) { 994 | cb(cmd) 995 | for _, c := range cmd.Commands() { 996 | walk(c, cb) 997 | } 998 | } 999 | -------------------------------------------------------------------------------- /spec/expected.log: -------------------------------------------------------------------------------- 1 | + ./kubensx --debug ls -u 2 | example-us@possibly-gmail.com 3 | example@possibly-gmail.com 4 | minikube 5 | + ./kubensx --debug ls -c 6 | minikube 7 | us 8 | us-east1 9 | us-west1 10 | + ./kubensx --debug use minikube:minikube/default 11 | Searching for "minikube(true):minikube/default(true)" 12 | Initializing client with "minikube:minikube" 13 | Set "kubensx-prev" to "minikube:minikube/" 14 | Set "kubensx-current" to "minikube:minikube/default" 15 | Deleted assoc[iation] "kubensx-assoc:example-us@possibly-gmail.com:cluster-that-no-longer-exist" 16 | Found assoc[iation] "kubensx-assoc:example-us@possibly-gmail.com:us-east1" 17 | Found assoc[iation] "kubensx-assoc:example-us@possibly-gmail.com:us-west1" 18 | Deleted assoc[iation] "kubensx-assoc:user-that-no-longer-exists:us-east1" 19 | Switched to minikube:minikube/default 20 | + ./kubensx --debug use -x - 21 | Switched to minikube:minikube/ 22 | + yes 23 | + ./kubensx use --debug --no-color 24 | cluster: 25 | ❯ minikube 26 | us 27 | us-east1 28 | us-west1 29 | [?25lcluster: 30 | ❯ minikube 31 | us 32 | us-east1 33 | us-west1 34 | [?25hcluster: minikube 35 | user: 36 | example-us@possibly-gmail.com 37 | example@possibly-gmail.com 38 | ❯ minikube 39 | [?25luser: 40 | example-us@possibly-gmail.com 41 | example@possibly-gmail.com 42 | ❯ minikube 43 | [?25huser: minikube 44 | Initializing client with "minikube:minikube" 45 | namespace: 46 | ❯ default 47 | kube-public 48 | kube-system 49 | [?25lnamespace: 50 | ❯ default 51 | kube-public 52 | kube-system 53 | [?25hnamespace: default 54 | Set "kubensx-prev" to "minikube:minikube/default" 55 | Set "kubensx-current" to "minikube:minikube/default" 56 | Found assoc[iation] "kubensx-assoc:example-us@possibly-gmail.com:us-east1" 57 | Found assoc[iation] "kubensx-assoc:example-us@possibly-gmail.com:us-west1" 58 | Switched to minikube:minikube/default 59 | + ./kubensx --debug current 60 | minikube:minikube/default 61 | + ./kubensx --debug current -u 62 | minikube 63 | + ./kubensx --debug current -c 64 | minikube 65 | + ./kubensx --debug current -n 66 | default 67 | + ./kubensx --debug use -x ':/*' 68 | Searching for "(true):/*(true)" 69 | Initializing client with ":" 70 | :/default 71 | :/kube-public 72 | :/kube-system 73 | + ./kubensx --debug use -x ':*/' 74 | Searching for "(true):*/(true)" 75 | :minikube/ 76 | :us/ 77 | :us-east1/ 78 | :us-west1/ 79 | + ./kubensx --debug use -x '*:/' 80 | Searching for "*(true):/(true)" 81 | example-us@possibly-gmail.com:/ 82 | example@possibly-gmail.com:/ 83 | minikube:/ 84 | + ./kubensx --debug use -x : 85 | Searching for "(true):/default(false)" 86 | Initializing client with ":" 87 | :/default 88 | + ./kubensx --debug use -x minikube:minikube 89 | Searching for "minikube(true):minikube/default(false)" 90 | Initializing client with "minikube:minikube" 91 | minikube:minikube/default 92 | + ./kubensx --debug use -x kube:kube 93 | Searching for "kube(true):kube/default(false)" 94 | Initializing client with "minikube:minikube" 95 | minikube:minikube/default 96 | + ./kubensx --debug use -x / 97 | Searching for "minikube(false):/(true)" 98 | minikube:/ 99 | + ./kubensx --debug use -x /default 100 | Searching for "minikube(false):/default(true)" 101 | Initializing client with "minikube:" 102 | minikube:/default 103 | + ./kubensx --debug use -x /def 104 | Searching for "minikube(false):/def(true)" 105 | Initializing client with "minikube:" 106 | minikube:/default 107 | + ./kubensx --debug use -x '' 108 | Searching for "minikube(false):minikube/(true)" 109 | minikube:minikube/ 110 | + ./kubensx --debug use -x default 111 | Searching for "minikube(false):minikube/default(true)" 112 | Initializing client with "minikube:minikube" 113 | minikube:minikube/default 114 | + ./kubensx --debug use -x def 115 | Searching for "minikube(false):minikube/def(true)" 116 | Initializing client with "minikube:minikube" 117 | minikube:minikube/default 118 | + ./kubensx --debug use -xu kube 119 | Searching for "kube(true):minikube/default(false)" 120 | Initializing client with "minikube:minikube" 121 | minikube:minikube/default 122 | + ./kubensx --debug use -xc 'us*' 123 | Searching for "minikube(false):us*/default(false)" 124 | Initializing client with "minikube:us" 125 | minikube:us/default 126 | Initializing client with "example-us@possibly-gmail.com:us-east1" 127 | example-us@possibly-gmail.com:us-east1/default 128 | Initializing client with "example-us@possibly-gmail.com:us-west1" 129 | example-us@possibly-gmail.com:us-west1/default 130 | + ./kubensx --debug use -x :us1/ 131 | Searching for "(true):us1/(true)" 132 | + ./kubensx --debug use -x :us-/ 133 | Searching for "(true):us-/(true)" 134 | :us-east1/ 135 | :us-west1/ 136 | + ./kubensx --debug use -x :us/ 137 | Searching for "(true):us/(true)" 138 | :us/ 139 | + ./kubensx --debug use -x ':us*/' 140 | Searching for "(true):us*/(true)" 141 | :us/ 142 | :us-east1/ 143 | :us-west1/ 144 | + ./kubensx --debug use -xe :us1/ 145 | Searching for "(true):us1/(true)" 146 | + ./kubensx --debug use -xe :us-/ 147 | Searching for "(true):us-/(true)" 148 | + ./kubensx --debug use -xe :us/ 149 | Searching for "(true):us/(true)" 150 | :us/ 151 | + ./kubensx --debug use -xz :us1/ 152 | Searching for "(true):us1/(true)" 153 | :us-east1/ 154 | :us-west1/ 155 | + ./kubensx --debug use -xz :us-/ 156 | Searching for "(true):us-/(true)" 157 | :us-east1/ 158 | :us-west1/ 159 | + ./kubensx --debug use -xz :us/ 160 | Searching for "(true):us/(true)" 161 | :us/ 162 | + ./kubensx --debug assoc minikube:minikube 163 | + minikube:minikube 164 | Found assoc[iation] "kubensx-assoc:example-us@possibly-gmail.com:us-east1" 165 | Found assoc[iation] "kubensx-assoc:example-us@possibly-gmail.com:us-west1" 166 | Found assoc[iation] "kubensx-assoc:minikube:minikube" 167 | + ./kubensx --debug assoc -l 168 | example-us@possibly-gmail.com:us-east1 169 | example-us@possibly-gmail.com:us-west1 170 | minikube:minikube 171 | + ./kubensx --debug assoc -x minikube 172 | + minikube:us 173 | + minikube:us-east1 174 | + minikube:us-west1 175 | + ./kubensx --debug assoc -x kube 176 | + minikube:us 177 | + minikube:us-east1 178 | + minikube:us-west1 179 | + ./kubensx --debug assoc -x minikube:minikube 180 | + ./kubensx --debug assoc -x kube:kube 181 | + ./kubensx --debug assoc -x 'kube:*' 182 | + minikube:us 183 | + minikube:us-east1 184 | + minikube:us-west1 185 | + ./kubensx --debug assoc -x '*:kube' 186 | + example-us@possibly-gmail.com:minikube 187 | + example@possibly-gmail.com:minikube 188 | + ./kubensx --debug assoc -x : 189 | cannot be empty 190 | + ./kubensx --debug assoc -x '*:' 191 | cannot be empty 192 | + ./kubensx --debug assoc -x ':*' 193 | cannot be empty 194 | + ./kubensx --debug use -x '*:kube' 195 | Searching for "*(true):kube/default(false)" 196 | Initializing client with "minikube:minikube" 197 | minikube:minikube/default 198 | + ./kubensx --debug use -x --ignore-assoc '*:kube' 199 | Searching for "*(true):kube/default(false)" 200 | Initializing client with "example-us@possibly-gmail.com:minikube" 201 | example-us@possibly-gmail.com:minikube/default 202 | Initializing client with "example@possibly-gmail.com:minikube" 203 | example@possibly-gmail.com:minikube/default 204 | Initializing client with "minikube:minikube" 205 | minikube:minikube/default 206 | + echo done 207 | done 208 | -------------------------------------------------------------------------------- /spec/kubeconfig.envsubst.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | clusters: 3 | - cluster: 4 | certificate-authority: $HOME/.minikube/ca.crt 5 | server: https://$MINIKUBE_IP:8443 6 | name: minikube 7 | - cluster: 8 | certificate-authority: $HOME/.minikube/ca.crt 9 | server: https://$MINIKUBE_IP:8443 10 | name: us-west1 11 | - cluster: 12 | certificate-authority: $HOME/.minikube/ca.crt 13 | server: https://$MINIKUBE_IP:8443 14 | name: us-east1 15 | - cluster: 16 | certificate-authority: $HOME/.minikube/ca.crt 17 | server: https://$MINIKUBE_IP:8443 18 | name: us 19 | contexts: 20 | - context: 21 | cluster: us-west1 22 | user: example-us@possibly-gmail.com 23 | name: "kubensx-assoc:example-us@possibly-gmail.com:us-west1" 24 | - context: 25 | cluster: us-east1 26 | user: example-us@possibly-gmail.com 27 | name: "kubensx-assoc:example-us@possibly-gmail.com:us-east1" 28 | - context: 29 | cluster: cluster-that-no-longer-exists 30 | user: example-us@possibly-gmail.com 31 | name: "kubensx-assoc:example-us@possibly-gmail.com:cluster-that-no-longer-exist" 32 | - context: 33 | cluster: us-east1 34 | user: user-that-no-longer-exists 35 | name: "kubensx-assoc:user-that-no-longer-exists:us-east1" 36 | - context: 37 | cluster: minikube 38 | user: minikube 39 | name: minikube 40 | current-context: minikube 41 | kind: Config 42 | preferences: {} 43 | users: 44 | - name: minikube 45 | user: 46 | client-certificate: $HOME/.minikube/client.crt 47 | client-key: $HOME/.minikube/client.key 48 | - name: example@possibly-gmail.com 49 | user: 50 | client-certificate: $HOME/.minikube/client.crt 51 | client-key: $HOME/.minikube/client.key 52 | - name: example-us@possibly-gmail.com 53 | user: 54 | client-certificate: $HOME/.minikube/client.crt 55 | client-key: $HOME/.minikube/client.key 56 | -------------------------------------------------------------------------------- /spec/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | export KUBECONFIG=/tmp/kubensx-spec-kubeconfig 5 | cat $(dirname "$0")/kubeconfig.envsubst.yml | MINIKUBE_IP=$(minikube ip) envsubst > $KUBECONFIG 6 | 7 | go build 8 | 9 | set -x 10 | 11 | ./kubensx --debug ls -u 12 | ./kubensx --debug ls -c 13 | 14 | ./kubensx --debug use minikube:minikube/default 15 | ./kubensx --debug use -x - 16 | # escaped symbols are expected (vendor/gopkg.in/AlecAivazis/survey.v1/terminal/cursor.go) 17 | # but not the coloring 18 | yes | ./kubensx use --debug --no-color 19 | 20 | ./kubensx --debug current 21 | ./kubensx --debug current -u 22 | ./kubensx --debug current -c 23 | ./kubensx --debug current -n 24 | 25 | # user:cluster/namespace 26 | ./kubensx --debug use -x ':/*' 27 | ./kubensx --debug use -x ':*/' 28 | ./kubensx --debug use -x '*:/' 29 | # user:cluster 30 | ./kubensx --debug use -x : 31 | ./kubensx --debug use -x minikube:minikube 32 | ./kubensx --debug use -x kube:kube 33 | # cluster/namespace 34 | ./kubensx --debug use -x / 35 | ./kubensx --debug use -x /default 36 | ./kubensx --debug use -x /def 37 | # namespace 38 | ./kubensx --debug use -x '' 39 | ./kubensx --debug use -x default 40 | ./kubensx --debug use -x def 41 | 42 | ./kubensx --debug use -xu kube 43 | # should yield 3 matches (us, us-west1 & us-east1) 44 | ./kubensx --debug use -xc 'us*' 45 | 46 | # should yield 0 matches 47 | ./kubensx --debug use -x :us1/ 48 | # should yield 2 matches (us-west1 & us-east1) 49 | ./kubensx --debug use -x :us-/ 50 | # should yield 1 match (us, but not us-west1 & us-east1) 51 | ./kubensx --debug use -x :us/ 52 | # should yield 3 matches (us, us-west1 & us-east1) 53 | ./kubensx --debug use -x ':us*/' 54 | 55 | # should yield 0 matches 56 | ./kubensx --debug use -xe :us1/ 57 | # should yield 0 matches 58 | ./kubensx --debug use -xe :us-/ 59 | # should yield 1 match (us, but not us-west1 & us-east1) 60 | ./kubensx --debug use -xe :us/ 61 | 62 | # should yield 2 matches 63 | ./kubensx --debug use -xz :us1/ 64 | # should yield 2 matches 65 | ./kubensx --debug use -xz :us-/ 66 | # should yield 1 match (us, but not us-west1 & us-east1) 67 | ./kubensx --debug use -xz :us/ 68 | 69 | ./kubensx --debug assoc minikube:minikube 70 | ./kubensx --debug assoc -l 71 | # user 72 | ./kubensx --debug assoc -x minikube 73 | ./kubensx --debug assoc -x kube 74 | # user:cluster 75 | ./kubensx --debug assoc -x minikube:minikube 76 | ./kubensx --debug assoc -x kube:kube 77 | ./kubensx --debug assoc -x 'kube:*' 78 | ./kubensx --debug assoc -x '*:kube' 79 | # empty (not allowed) 80 | ./kubensx --debug assoc -x : && exit 1 81 | ./kubensx --debug assoc -x '*:' && exit 1 82 | ./kubensx --debug assoc -x ':*' && exit 1 83 | 84 | # at this point minikube user is assoc[iated] with minikube cluster 85 | # and so only one record should be printed 86 | ./kubensx --debug use -x '*:kube' 87 | ./kubensx --debug use -x --ignore-assoc '*:kube' 88 | 89 | echo done 90 | --------------------------------------------------------------------------------