init drop
commit
da190c204c
|
|
@ -0,0 +1,10 @@
|
||||||
|
.vscode
|
||||||
|
k9s.log
|
||||||
|
.envrc
|
||||||
|
cov.out
|
||||||
|
execs
|
||||||
|
k9s
|
||||||
|
samples
|
||||||
|
dist
|
||||||
|
notes
|
||||||
|
styles
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
project_name: k9s
|
||||||
|
before:
|
||||||
|
hooks:
|
||||||
|
# you may remove this if you don't use vgo
|
||||||
|
- go mod download
|
||||||
|
# you may remove this if you don't need go generate
|
||||||
|
- go generate ./...
|
||||||
|
|
||||||
|
release:
|
||||||
|
prerelease: true
|
||||||
|
|
||||||
|
builds:
|
||||||
|
- env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
goos:
|
||||||
|
- linux
|
||||||
|
- darwin
|
||||||
|
- windows
|
||||||
|
goarch:
|
||||||
|
- 386
|
||||||
|
- amd64
|
||||||
|
ldflags:
|
||||||
|
- -s -w -X github.com/k8sland/k9s/cmd.version={{.Version}} -X github.com/k8sland/k9s/cmd.commit={{.Commit}} -X github.com/k8sland/k9s/cmd.date={{.Date}}
|
||||||
|
|
||||||
|
archive:
|
||||||
|
replacements:
|
||||||
|
darwin: Darwin
|
||||||
|
linux: Linux
|
||||||
|
windows: Windows
|
||||||
|
386: i386
|
||||||
|
amd64: x86_64
|
||||||
|
checksum:
|
||||||
|
name_template: 'checksums.txt'
|
||||||
|
snapshot:
|
||||||
|
name_template: "{{ .Tag }}-next"
|
||||||
|
changelog:
|
||||||
|
sort: asc
|
||||||
|
filters:
|
||||||
|
exclude:
|
||||||
|
- '^docs:'
|
||||||
|
- '^test:'
|
||||||
|
|
||||||
|
# Homebrew
|
||||||
|
brew:
|
||||||
|
name: k9s
|
||||||
|
github:
|
||||||
|
owner: k8sland
|
||||||
|
name: k9s-homebrew-tap
|
||||||
|
commit_author:
|
||||||
|
name: k8sland
|
||||||
|
email: fernand@k8sland.io
|
||||||
|
folder: Formula
|
||||||
|
homepage: https://k9ss.io
|
||||||
|
description: A CLI to interact with your Kubernetes clusters
|
||||||
|
test: |
|
||||||
|
system "k9s version"
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
version: v1.0
|
||||||
|
name: First pipeline example
|
||||||
|
agent:
|
||||||
|
machine:
|
||||||
|
type: e1-standard-2
|
||||||
|
os_image: ubuntu1804
|
||||||
|
|
||||||
|
blocks:
|
||||||
|
- name: "Build"
|
||||||
|
task:
|
||||||
|
env_vars:
|
||||||
|
- name: APP_ENV
|
||||||
|
value: prod
|
||||||
|
jobs:
|
||||||
|
- name: Docker build
|
||||||
|
commands:
|
||||||
|
- checkout
|
||||||
|
- ls -1
|
||||||
|
- echo $APP_ENV
|
||||||
|
- echo "Docker build..."
|
||||||
|
- echo "done"
|
||||||
|
|
||||||
|
- name: "Smoke tests"
|
||||||
|
task:
|
||||||
|
jobs:
|
||||||
|
- name: Smoke
|
||||||
|
commands:
|
||||||
|
- checkout
|
||||||
|
- echo "make smoke"
|
||||||
|
|
||||||
|
- name: "Unit tests"
|
||||||
|
task:
|
||||||
|
jobs:
|
||||||
|
- name: RSpec
|
||||||
|
commands:
|
||||||
|
- checkout
|
||||||
|
- echo "make rspec"
|
||||||
|
|
||||||
|
- name: Lint code
|
||||||
|
commands:
|
||||||
|
- checkout
|
||||||
|
- echo "make lint"
|
||||||
|
|
||||||
|
- name: Check security
|
||||||
|
commands:
|
||||||
|
- checkout
|
||||||
|
- echo "make security"
|
||||||
|
|
||||||
|
- name: "Integration tests"
|
||||||
|
task:
|
||||||
|
jobs:
|
||||||
|
- name: Cucumber
|
||||||
|
commands:
|
||||||
|
- checkout
|
||||||
|
- echo "make cucumber"
|
||||||
|
|
||||||
|
- name: "Push Image"
|
||||||
|
task:
|
||||||
|
jobs:
|
||||||
|
- name: Push
|
||||||
|
commands:
|
||||||
|
- checkout
|
||||||
|
- echo "make docker.push"
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
language: go
|
||||||
|
go_import_path: github.com/k8sland/k9s
|
||||||
|
go:
|
||||||
|
- 1.11.2
|
||||||
|
# - master
|
||||||
|
|
||||||
|
os:
|
||||||
|
- linux
|
||||||
|
- osx
|
||||||
|
|
||||||
|
dist: trusty
|
||||||
|
sudo: false
|
||||||
|
|
||||||
|
install: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
- GO111MODULE=on
|
||||||
|
|
||||||
|
script:
|
||||||
|
- go build
|
||||||
|
- go test ./...
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
Copyright © 2018, Imhotep Software LLC <fernand@k8sland.io>
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
NAME := k9s
|
||||||
|
PACKAGE := github.com/k8sland/$(NAME)
|
||||||
|
VERSION := $(shell git rev-parse --short HEAD)
|
||||||
|
|
||||||
|
default: help
|
||||||
|
|
||||||
|
cover: ## Run test coverage suite
|
||||||
|
@go test ./... --coverprofile=cov.out
|
||||||
|
@go tool cover --html=cov.out
|
||||||
|
|
||||||
|
osx: ## Builds OSX CLI
|
||||||
|
@env GOOS=darwin GOARCH=amd64 go build \
|
||||||
|
-ldflags "-w -X ${PACKAGE}/cmd.Version=${VERSION}" \
|
||||||
|
-a -tags netgo -o execs/${NAME} *.go
|
||||||
|
|
||||||
|
|
||||||
|
help:
|
||||||
|
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[38;5;69m%-30s\033[38;5;168m %s\n", $$1, $$2}'
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
<img src="assets/logo_w.png">
|
||||||
|
|
||||||
|
# K9s - Kubernetes Screens
|
||||||
|
|
||||||
|
A Kubernetes CLI written in GO and curses to interact with your clusters. The initial
|
||||||
|
aim of this project is to make it simpler to navigate and diagnose a cluster.
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[](https://goreportcard.com/report/github.com/k8sland/k9s)
|
||||||
|
[](https://travis-ci.org/k8sland/k9s)
|
||||||
|
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
---
|
||||||
|
## Description
|
||||||
|
|
||||||
|
K9s is a CLI for Kubernetes. It provides a bit more information about your cluster
|
||||||
|
than *kubectl* while allowing to perform primordial Kubernetes commands with
|
||||||
|
ease.
|
||||||
|
|
||||||
|
At the time of this writing, K9s only supports a subset of all available Kubernetes
|
||||||
|
resources. More will be added soon (please PR us to add your favorite resource!)
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
---
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Homebrew (OSX)
|
||||||
|
|
||||||
|
```shell
|
||||||
|
brew tap k8sland/k9s https://github.com/k8sland/k9s-homebrew-tap.git
|
||||||
|
brew install k9s
|
||||||
|
```
|
||||||
|
|
||||||
|
### Binary Releases
|
||||||
|
|
||||||
|
- [Releases](https://github.com/k8sland/k9s/releases)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
---
|
||||||
|
## Features
|
||||||
|
|
||||||
|
> Note: K9s does not have an idiot light. Please be sure to hit the correct command
|
||||||
|
> sequences to avoid pilot errors. `Are you sure?` not in effect here...
|
||||||
|
|
||||||
|
+ K9s uses 2 or 3 letters alias to navigate to a K8s resource
|
||||||
|
+ At any time you can use `?<Enter>` to look up the various commands
|
||||||
|
+ Use `alias<Enter>` to activate a resource under that alias
|
||||||
|
+ Use `Esc` to erase previous keystrokes.
|
||||||
|
+ Use `Q` or `Ctrl-C` to Quit.
|
||||||
|
+ `Ctrl` sequences are used to view, edit, delete, ssh ...
|
||||||
|
+ Use `ctx<Enter>` to see and switch between your clusters
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
---
|
||||||
|
## Video Demo
|
||||||
|
|
||||||
|
+ [K9s Demo](https://youtu.be/k7zseUhaXeU)
|
||||||
|
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
---
|
||||||
|
## Screen Shots
|
||||||
|
|
||||||
|
### Pod View
|
||||||
|
|
||||||
|
<img src="assets/screen_1.png">
|
||||||
|
|
||||||
|
### Log View
|
||||||
|
|
||||||
|
<img src="assets/screen_2.png">
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
---
|
||||||
|
## Known Issues...
|
||||||
|
|
||||||
|
This initial drop is brittle. K9s will most likely blow up if...
|
||||||
|
|
||||||
|
+ Your kube-config file does not live under $HOME/.kube or you use multiple configs
|
||||||
|
+ You don't have enough RBAC fu to manage your cluster
|
||||||
|
+ Your cluster does not run a metrics-server
|
||||||
|
+ You have more than 9 namespaces
|
||||||
|
+ Most likely will bork on older Kubernetes revs. Guessing > 1.9+ is Ok??
|
||||||
|
+ Not sure at this time about the ill effects for large clusters??
|
||||||
|
+ Many others for sure...
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
---
|
||||||
|
## Disclaimer
|
||||||
|
|
||||||
|
This is still work in progress! If there is enough interest in the Kubernetes
|
||||||
|
community, we will enhance per your recommendations/contributions. Also if you
|
||||||
|
dig this effort, please let us know that too!
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
---
|
||||||
|
## ATTA Girls/Boys!
|
||||||
|
|
||||||
|
K9s sits on top of two very cool GO projects that provides the much needed terminal
|
||||||
|
support. So big thanks and shootout to the good folks at tcell+tview for
|
||||||
|
making K9s a reality!!
|
||||||
|
|
||||||
|
+ [tcell](https://github.com/gdamore/tcell)
|
||||||
|
+ [tview](https://github.com/rivo/tview)
|
||||||
|
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
---
|
||||||
|
## Contact Information
|
||||||
|
|
||||||
|
+ **Email**: fernand@k8sland.io
|
||||||
|
+ **Twitter**: [@kitesurfer](https://twitter.com/kitesurfer?lang=en)
|
||||||
|
+ **Github**: [K9s](https://github.com/k8sland/k9s)
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
---
|
||||||
|
<img src="assets/imhotep_logo.png" width="32" height="auto"/> © 2018 Imhotep Software LLC.
|
||||||
|
All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 7.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 410 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 330 KiB |
|
|
@ -0,0 +1,124 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/gdamore/tcell"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/views"
|
||||||
|
"github.com/k8sland/tview"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultRefreshRate = 5 // secs
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
version = "dev"
|
||||||
|
commit = "dev"
|
||||||
|
date = "n/a"
|
||||||
|
srv kubernetes.Interface
|
||||||
|
refreshRate int
|
||||||
|
namespace string
|
||||||
|
|
||||||
|
rootCmd = &cobra.Command{
|
||||||
|
Use: "k9s",
|
||||||
|
Short: "K9s a Kubernetes cluster management CLI",
|
||||||
|
Long: `K9s is a Kubernetes CLI to view and manage your Kubernetes clusters.`,
|
||||||
|
Run: run,
|
||||||
|
}
|
||||||
|
versionCmd = &cobra.Command{
|
||||||
|
Use: "version",
|
||||||
|
Short: "Print K9s version",
|
||||||
|
Long: "Prints K9s version",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
fmt.Printf("Version:%s GitCommit:%s On %s\n", version, commit, date)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(versionCmd)
|
||||||
|
|
||||||
|
rootCmd.Flags().IntVarP(
|
||||||
|
&refreshRate,
|
||||||
|
"refresh", "r",
|
||||||
|
defaultRefreshRate,
|
||||||
|
"Specifies the default refresh rate as an integer (sec)",
|
||||||
|
)
|
||||||
|
|
||||||
|
rootCmd.Flags().StringVarP(
|
||||||
|
&namespace,
|
||||||
|
"namespace", "n",
|
||||||
|
"",
|
||||||
|
"Uses a given namespace versus all-namespaces",
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute root command
|
||||||
|
func Execute() {
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(cmd *cobra.Command, args []string) {
|
||||||
|
initStyles()
|
||||||
|
initKeys()
|
||||||
|
|
||||||
|
if refreshRate < 0 {
|
||||||
|
refreshRate = defaultRefreshRate
|
||||||
|
}
|
||||||
|
app := views.NewApp(version, refreshRate, namespace)
|
||||||
|
app.Init()
|
||||||
|
app.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func initKeys() {
|
||||||
|
tcell.KeyNames[tcell.Key(views.Key0)] = "0"
|
||||||
|
tcell.KeyNames[tcell.Key(views.Key1)] = "1"
|
||||||
|
tcell.KeyNames[tcell.Key(views.Key2)] = "2"
|
||||||
|
tcell.KeyNames[tcell.Key(views.Key3)] = "3"
|
||||||
|
tcell.KeyNames[tcell.Key(views.Key4)] = "4"
|
||||||
|
tcell.KeyNames[tcell.Key(views.Key5)] = "5"
|
||||||
|
tcell.KeyNames[tcell.Key(views.Key6)] = "6"
|
||||||
|
tcell.KeyNames[tcell.Key(views.Key7)] = "7"
|
||||||
|
tcell.KeyNames[tcell.Key(views.Key8)] = "8"
|
||||||
|
tcell.KeyNames[tcell.Key(views.Key9)] = "9"
|
||||||
|
tcell.KeyNames[tcell.Key(views.KeyA)] = "a"
|
||||||
|
tcell.KeyNames[tcell.Key(views.KeyB)] = "b"
|
||||||
|
tcell.KeyNames[tcell.Key(views.KeyC)] = "c"
|
||||||
|
tcell.KeyNames[tcell.Key(views.KeyD)] = "d"
|
||||||
|
tcell.KeyNames[tcell.Key(views.KeyE)] = "e"
|
||||||
|
tcell.KeyNames[tcell.Key(views.KeyF)] = "f"
|
||||||
|
tcell.KeyNames[tcell.Key(views.KeyG)] = "g"
|
||||||
|
tcell.KeyNames[tcell.Key(views.KeyH)] = "h"
|
||||||
|
tcell.KeyNames[tcell.Key(views.KeyI)] = "i"
|
||||||
|
tcell.KeyNames[tcell.Key(views.KeyJ)] = "j"
|
||||||
|
tcell.KeyNames[tcell.Key(views.KeyK)] = "k"
|
||||||
|
tcell.KeyNames[tcell.Key(views.KeyL)] = "l"
|
||||||
|
tcell.KeyNames[tcell.Key(views.KeyM)] = "m"
|
||||||
|
tcell.KeyNames[tcell.Key(views.KeyN)] = "n"
|
||||||
|
tcell.KeyNames[tcell.Key(views.KeyO)] = "o"
|
||||||
|
tcell.KeyNames[tcell.Key(views.KeyP)] = "p"
|
||||||
|
tcell.KeyNames[tcell.Key(views.KeyQ)] = "q"
|
||||||
|
tcell.KeyNames[tcell.Key(views.KeyR)] = "r"
|
||||||
|
tcell.KeyNames[tcell.Key(views.KeyS)] = "s"
|
||||||
|
tcell.KeyNames[tcell.Key(views.KeyT)] = "t"
|
||||||
|
tcell.KeyNames[tcell.Key(views.KeyU)] = "u"
|
||||||
|
tcell.KeyNames[tcell.Key(views.KeyV)] = "v"
|
||||||
|
tcell.KeyNames[tcell.Key(views.KeyX)] = "x"
|
||||||
|
tcell.KeyNames[tcell.Key(views.KeyY)] = "y"
|
||||||
|
tcell.KeyNames[tcell.Key(views.KeyZ)] = "z"
|
||||||
|
}
|
||||||
|
|
||||||
|
func initStyles() {
|
||||||
|
tview.Styles.PrimitiveBackgroundColor = tcell.ColorBlack
|
||||||
|
tview.Styles.ContrastBackgroundColor = tcell.ColorBlack
|
||||||
|
tview.Styles.FocusColor = tcell.ColorLightSkyBlue
|
||||||
|
tview.Styles.BorderColor = tcell.ColorDodgerBlue
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
module github.com/k8sland/k9s
|
||||||
|
|
||||||
|
// replace github.com/k8sland/tview => /Users/fernand/k8sland/src/github.com/k8sland/tview
|
||||||
|
|
||||||
|
require (
|
||||||
|
cloud.google.com/go v0.34.0 // indirect
|
||||||
|
github.com/gdamore/tcell v1.1.0
|
||||||
|
github.com/ghodss/yaml v1.0.0 // indirect
|
||||||
|
github.com/gogo/protobuf v1.1.1 // indirect
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
|
||||||
|
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c // indirect
|
||||||
|
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect
|
||||||
|
github.com/googleapis/gnostic v0.2.0 // indirect
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
|
||||||
|
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
|
||||||
|
github.com/imdario/mergo v0.3.6 // indirect
|
||||||
|
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.5 // indirect
|
||||||
|
github.com/jtolds/gls v4.2.1+incompatible // indirect
|
||||||
|
github.com/k8sland/tview v0.1.0
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.1 // indirect
|
||||||
|
github.com/onsi/gomega v1.4.3 // indirect
|
||||||
|
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
|
||||||
|
github.com/petergtz/pegomock v0.0.0-20181206220228-b113d17a7e81
|
||||||
|
github.com/sirupsen/logrus v1.2.0
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect
|
||||||
|
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect
|
||||||
|
github.com/spf13/cobra v0.0.3
|
||||||
|
github.com/spf13/pflag v1.0.3 // indirect
|
||||||
|
github.com/stretchr/testify v1.2.2
|
||||||
|
golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd // indirect
|
||||||
|
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc // indirect
|
||||||
|
golang.org/x/oauth2 v0.0.0-20181105165119-ca4130e427c7 // indirect
|
||||||
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20181031143558-9b800f95dbbc // indirect
|
||||||
|
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 // indirect
|
||||||
|
google.golang.org/appengine v1.3.0 // indirect
|
||||||
|
gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0 // indirect
|
||||||
|
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.2.1
|
||||||
|
k8s.io/api v0.0.0-20181102122915-de5c567eef5c
|
||||||
|
k8s.io/apiextensions-apiserver v0.0.0-20181206111255-bb0a52a3f19d // indirect
|
||||||
|
k8s.io/apimachinery v0.0.0-20181108045954-261df694e725
|
||||||
|
k8s.io/client-go v9.0.0+incompatible
|
||||||
|
k8s.io/kubernetes v1.13.0
|
||||||
|
k8s.io/metrics v0.0.0-20181121073115-d8618695b08f
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg=
|
||||||
|
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/gdamore/encoding v0.0.0-20151215212835-b23993cbb635 h1:hheUEMzaOie/wKeIc1WPa7CDVuIO5hqQxjS+dwTQEnI=
|
||||||
|
github.com/gdamore/encoding v0.0.0-20151215212835-b23993cbb635/go.mod h1:yrQYJKKDTrHmbYxI7CYi+/hbdiDT2m4Hj+t0ikCjsrQ=
|
||||||
|
github.com/gdamore/tcell v1.1.0 h1:RbQgl7jukmdqROeNcKps7R2YfDCQbWkOd1BwdXrxfr4=
|
||||||
|
github.com/gdamore/tcell v1.1.0/go.mod h1:tqyG50u7+Ctv1w5VX67kLzKcj9YXR/JSBZQq/+mLl1A=
|
||||||
|
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||||
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
|
github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
|
||||||
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
|
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw=
|
||||||
|
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
|
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck=
|
||||||
|
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
|
||||||
|
github.com/googleapis/gnostic v0.2.0 h1:l6N3VoaVzTncYYW+9yOz2LJJammFZGBO13sqgEhpy9g=
|
||||||
|
github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
|
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM=
|
||||||
|
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||||
|
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||||
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
|
github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
|
||||||
|
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||||
|
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||||
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
|
github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE=
|
||||||
|
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||||
|
github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE=
|
||||||
|
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
|
github.com/k8sland/tview v0.1.0 h1:nXX/f55woLQvuXtz+7bMhfY+jMtJIrRRiKSOSH357iE=
|
||||||
|
github.com/k8sland/tview v0.1.0/go.mod h1:5qZeY4qT3maFEq50mfj5xBnFvLNoUtl7NyyW48XxmBs=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/lucasb-eyer/go-colorful v0.0.0-20181028223441-12d3b2882a08 h1:5MnxBC15uMxFv5FY/J/8vzyaBiArCOkMdFT9Jsw78iY=
|
||||||
|
github.com/lucasb-eyer/go-colorful v0.0.0-20181028223441-12d3b2882a08/go.mod h1:NXg0ArsFk0Y01623LgUqoqcouGDB+PwCCQlrwrG6xJ4=
|
||||||
|
github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
|
||||||
|
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||||
|
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
|
||||||
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
|
||||||
|
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
|
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
|
||||||
|
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
|
||||||
|
github.com/petergtz/pegomock v0.0.0-20181206220228-b113d17a7e81 h1:MhSbvsIs4KvpPYr4taOvb6j+r9VNbj/08AfjsKi+Ui0=
|
||||||
|
github.com/petergtz/pegomock v0.0.0-20181206220228-b113d17a7e81/go.mod h1:nuBLWZpVyv/fLo56qTwt/AUau7jgouO1h7bEvZCq82o=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
|
||||||
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
|
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w=
|
||||||
|
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
|
||||||
|
github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
|
||||||
|
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||||
|
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||||
|
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd h1:VtIkGDhk0ph3t+THbvXHfMZ8QHgsBO39Nh52+74pq7w=
|
||||||
|
golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc h1:ZMCWScCvS2fUVFw8LOpxyUUW5qiviqr4Dg5NdjLeiLU=
|
||||||
|
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20181105165119-ca4130e427c7 h1:g9UOdtsRWEwHYUG2bDHMxKrvfSGE5epIX2HkaMHSMBY=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20181105165119-ca4130e427c7/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
|
||||||
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181031143558-9b800f95dbbc h1:SdCq5U4J+PpbSDIl9bM0V1e1Ug1jsnBkAFvTs1htn7U=
|
||||||
|
golang.org/x/sys v0.0.0-20181031143558-9b800f95dbbc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 h1:+DCIGbF/swA92ohVg0//6X2IVY3KZs6p9mix0ziNYJM=
|
||||||
|
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
google.golang.org/appengine v1.3.0 h1:FBSsiFRMz3LBeXIomRnVzrQwSDj4ibvcRexLG0LZGQk=
|
||||||
|
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
|
gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0 h1:FVCohIoYO7IJoDDVpV2pdq7SgrMH6wHnuTyrdrxJNoY=
|
||||||
|
gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0/go.mod h1:OdE7CF6DbADk7lN8LIKRzRJTTZXIjtWgA5THM5lhBAw=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
|
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||||
|
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
k8s.io/api v0.0.0-20181102122915-de5c567eef5c h1:zyOnDtUzVKOq6N8zrpdzocoytjSxznRy0TmfKDahC20=
|
||||||
|
k8s.io/api v0.0.0-20181102122915-de5c567eef5c/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA=
|
||||||
|
k8s.io/apiextensions-apiserver v0.0.0-20181206111255-bb0a52a3f19d h1:Be0abb3DYtNLaSVW4xCGGBSQqtT7myqj1coR6Bu2ZM8=
|
||||||
|
k8s.io/apiextensions-apiserver v0.0.0-20181206111255-bb0a52a3f19d/go.mod h1:IxkesAMoaCRoLrPJdZNZUQp9NfZnzqaVzLhb2VEQzXE=
|
||||||
|
k8s.io/apimachinery v0.0.0-20181108045954-261df694e725 h1:b4fe6FhSyMdpi6WNeCxxr+kKM8Ya4TaKxeXkpWwh594=
|
||||||
|
k8s.io/apimachinery v0.0.0-20181108045954-261df694e725/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0=
|
||||||
|
k8s.io/client-go v9.0.0+incompatible h1:2kqW3X2xQ9SbFvWZjGEHBLlWc1LG9JIJNXWkuqwdZ3A=
|
||||||
|
k8s.io/client-go v9.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s=
|
||||||
|
k8s.io/kubernetes v1.13.0 h1:2psb4AOWOU3rESSjRVkqHRIIXkqLppfeiR6YE0trpt0=
|
||||||
|
k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk=
|
||||||
|
k8s.io/metrics v0.0.0-20181121073115-d8618695b08f h1:HyUoIBzks9xTaSnMJ6kv/SSmwaQQccokuiriu2cV0aA=
|
||||||
|
k8s.io/metrics v0.0.0-20181121073115-d8618695b08f/go.mod h1:a25VAbm3QT3xiVl1jtoF1ueAKQM149UdZ+L93ePfV3M=
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/cmd"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
file, err := os.OpenFile(path.Join("/tmp", "k9s.log"), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
log.SetOutput(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cmd.Execute()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// Creator can create a new resources.
|
||||||
|
Creator interface {
|
||||||
|
NewInstance(interface{}) Columnar
|
||||||
|
}
|
||||||
|
|
||||||
|
// Caller can call Kubernetes verbs on a resource.
|
||||||
|
Caller interface {
|
||||||
|
k8s.Res
|
||||||
|
}
|
||||||
|
|
||||||
|
// APIFn knows how to call K8s api server.
|
||||||
|
APIFn func() k8s.Res
|
||||||
|
|
||||||
|
// InstanceFn instantiates a concrete resource.
|
||||||
|
InstanceFn func(interface{}) Columnar
|
||||||
|
|
||||||
|
// Base resource.
|
||||||
|
Base struct {
|
||||||
|
path string
|
||||||
|
|
||||||
|
caller Caller
|
||||||
|
creator Creator
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Name returns the resource namespaced name.
|
||||||
|
func (b *Base) Name() string {
|
||||||
|
return b.path
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a resource by name
|
||||||
|
func (b *Base) Get(path string) (Columnar, error) {
|
||||||
|
ns, n := namespaced(path)
|
||||||
|
i, err := b.caller.Get(ns, n)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return b.creator.NewInstance(i), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all resources
|
||||||
|
func (b *Base) List(ns string) (Columnars, error) {
|
||||||
|
ii, err := b.caller.List(ns)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := make(Columnars, 0, len(ii))
|
||||||
|
for i := 0; i < len(ii); i++ {
|
||||||
|
cc = append(cc, b.creator.NewInstance(ii[i]))
|
||||||
|
}
|
||||||
|
return cc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a resource by name.
|
||||||
|
func (b *Base) Delete(path string) error {
|
||||||
|
ns, n := namespaced(path)
|
||||||
|
return b.caller.Delete(ns, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Base) namespacedName(m metav1.ObjectMeta) string {
|
||||||
|
return path.Join(m.Namespace, m.Name)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
"k8s.io/api/core/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// ClusterIfc represents a cluster.
|
||||||
|
ClusterIfc interface {
|
||||||
|
Version() (string, error)
|
||||||
|
ClusterName() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// MetricsIfc represents a metrics server.
|
||||||
|
MetricsIfc interface {
|
||||||
|
NodeMetrics() (k8s.Metric, error)
|
||||||
|
PerNodeMetrics([]v1.Node) (map[string]k8s.Metric, error)
|
||||||
|
PodMetrics() (map[string]k8s.Metric, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cluster represents a kubernetes resource.
|
||||||
|
Cluster struct {
|
||||||
|
api ClusterIfc
|
||||||
|
mx MetricsIfc
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewCluster returns a new cluster info resource.
|
||||||
|
func NewCluster() *Cluster {
|
||||||
|
return NewClusterWithArgs(k8s.NewCluster(), k8s.NewMetricsServer())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClusterWithArgs for tests only!
|
||||||
|
func NewClusterWithArgs(ci ClusterIfc, mx MetricsIfc) *Cluster {
|
||||||
|
return &Cluster{api: ci, mx: mx}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version returns the current K8s cluster version.
|
||||||
|
func (c *Cluster) Version() string {
|
||||||
|
info, err := c.api.Version()
|
||||||
|
if err != nil {
|
||||||
|
return "n/a"
|
||||||
|
}
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the cluster name
|
||||||
|
func (c *Cluster) Name() string {
|
||||||
|
return c.api.ClusterName()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metrics gathers node level metrics and compute utilization percentages.
|
||||||
|
func (c *Cluster) Metrics() (k8s.Metric, error) {
|
||||||
|
return c.mx.NodeMetrics()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
package resource_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/resource"
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
m "github.com/petergtz/pegomock"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClusterVersion(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
cIfc, mxIfc := NewMockClusterIfc(), NewMockMetricsIfc()
|
||||||
|
m.When(cIfc.Version()).ThenReturn("1.2.3", nil)
|
||||||
|
|
||||||
|
ci := resource.NewClusterWithArgs(cIfc, mxIfc)
|
||||||
|
assert.Equal(t, "1.2.3", ci.Version())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClusterNoVersion(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
cIfc, mxIfc := NewMockClusterIfc(), NewMockMetricsIfc()
|
||||||
|
m.When(cIfc.Version()).ThenReturn("bad", fmt.Errorf("No data"))
|
||||||
|
|
||||||
|
ci := resource.NewClusterWithArgs(cIfc, mxIfc)
|
||||||
|
assert.Equal(t, "n/a", ci.Version())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClusterName(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
cIfc, mxIfc := NewMockClusterIfc(), NewMockMetricsIfc()
|
||||||
|
m.When(cIfc.ClusterName()).ThenReturn("fred")
|
||||||
|
|
||||||
|
ci := resource.NewClusterWithArgs(cIfc, mxIfc)
|
||||||
|
assert.Equal(t, "fred", ci.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClusterMetrics(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
cIfc, mxIfc := NewMockClusterIfc(), NewMockMetricsIfc()
|
||||||
|
m.When(mxIfc.NodeMetrics()).ThenReturn(testMetric(), nil)
|
||||||
|
|
||||||
|
c := resource.NewClusterWithArgs(cIfc, mxIfc)
|
||||||
|
m, err := c.Metrics()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, testMetric(), m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
func setup(t *testing.T) {
|
||||||
|
m.RegisterMockTestingT(t)
|
||||||
|
m.RegisterMockFailHandler(func(m string, i ...int) {
|
||||||
|
log.Println("Boom!", m, i)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testMetric() k8s.Metric {
|
||||||
|
return k8s.Metric{
|
||||||
|
CPU: "100m",
|
||||||
|
AvailCPU: "1000m",
|
||||||
|
Mem: "256Gi",
|
||||||
|
AvailMem: "512Gi",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
"k8s.io/api/core/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConfigMap tracks a kubernetes resource.
|
||||||
|
type ConfigMap struct {
|
||||||
|
*Base
|
||||||
|
instance *v1.ConfigMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConfigMapList returns a new resource list.
|
||||||
|
func NewConfigMapList(ns string) List {
|
||||||
|
return NewConfigMapListWithArgs(ns, NewConfigMap())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConfigMapListWithArgs returns a new resource list.
|
||||||
|
func NewConfigMapListWithArgs(ns string, res Resource) List {
|
||||||
|
return newList(ns, "cm", res, AllVerbsAccess)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConfigMap instantiates a new ConfigMap.
|
||||||
|
func NewConfigMap() *ConfigMap {
|
||||||
|
return NewConfigMapWithArgs(k8s.NewConfigMap())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConfigMapWithArgs instantiates a new ConfigMap.
|
||||||
|
func NewConfigMapWithArgs(r k8s.Res) *ConfigMap {
|
||||||
|
cm := &ConfigMap{
|
||||||
|
Base: &Base{
|
||||||
|
caller: r,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cm.creator = cm
|
||||||
|
return cm
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInstance builds a new ConfigMap instance from a k8s resource.
|
||||||
|
func (*ConfigMap) NewInstance(i interface{}) Columnar {
|
||||||
|
cm := NewConfigMap()
|
||||||
|
switch i.(type) {
|
||||||
|
case *v1.ConfigMap:
|
||||||
|
cm.instance = i.(*v1.ConfigMap)
|
||||||
|
case v1.ConfigMap:
|
||||||
|
ii := i.(v1.ConfigMap)
|
||||||
|
cm.instance = &ii
|
||||||
|
default:
|
||||||
|
log.Fatalf("Unknown %#v", i)
|
||||||
|
}
|
||||||
|
cm.path = cm.namespacedName(cm.instance.ObjectMeta)
|
||||||
|
return cm
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal resource to yaml.
|
||||||
|
func (r *ConfigMap) Marshal(path string) (string, error) {
|
||||||
|
ns, n := namespaced(path)
|
||||||
|
i, err := r.caller.Get(ns, n)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
cm := i.(*v1.ConfigMap)
|
||||||
|
cm.TypeMeta.APIVersion = "v1"
|
||||||
|
cm.TypeMeta.Kind = "ConfigMap"
|
||||||
|
raw, err := yaml.Marshal(i)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(raw), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header return resource header.
|
||||||
|
func (*ConfigMap) Header(ns string) Row {
|
||||||
|
hh := Row{}
|
||||||
|
if ns == AllNamespaces {
|
||||||
|
hh = append(hh, "NAMESPACE")
|
||||||
|
}
|
||||||
|
return append(hh, "NAME", "DATA", "AGE")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fields retrieves displayable fields.
|
||||||
|
func (r *ConfigMap) Fields(ns string) Row {
|
||||||
|
ff := make(Row, 0, len(r.Header(ns)))
|
||||||
|
i := r.instance
|
||||||
|
if ns == AllNamespaces {
|
||||||
|
ff = append(ff, i.Namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(ff,
|
||||||
|
i.Name,
|
||||||
|
strconv.Itoa(len(i.Data)),
|
||||||
|
toAge(i.ObjectMeta.CreationTimestamp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtFields returns extended fields in relation to headers.
|
||||||
|
func (*ConfigMap) ExtFields() Properties {
|
||||||
|
return Properties{}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,211 @@
|
||||||
|
package resource_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/resource"
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
m "github.com/petergtz/pegomock"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCMHeader(t *testing.T) {
|
||||||
|
assert.Equal(t,
|
||||||
|
resource.Row{"NAME", "DATA", "AGE"},
|
||||||
|
newConfigMap().Header("default"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCMHeaderAllNS(t *testing.T) {
|
||||||
|
assert.Equal(t,
|
||||||
|
resource.Row{"NAMESPACE", "NAME", "DATA", "AGE"},
|
||||||
|
newConfigMap().Header(resource.AllNamespaces),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCMFieldsAllNS(t *testing.T) {
|
||||||
|
r := newConfigMap().Fields(resource.AllNamespaces)
|
||||||
|
assert.Equal(t, "blee", r[0])
|
||||||
|
assert.Equal(t, "fred", r[1])
|
||||||
|
assert.Equal(t, "2", r[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCMFields(t *testing.T) {
|
||||||
|
r := newConfigMap().Fields("blee")
|
||||||
|
assert.Equal(t, "fred", r[0])
|
||||||
|
assert.Equal(t, "2", r[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCMGet(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.Get("blee", "fred")).ThenReturn(k8sCM(), nil)
|
||||||
|
|
||||||
|
cm := resource.NewConfigMapWithArgs(ca)
|
||||||
|
ma, err := cm.Get("blee/fred")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
ca.VerifyWasCalledOnce().Get("blee", "fred")
|
||||||
|
assert.Equal(t, cm.NewInstance(k8sCM()), ma)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCMList(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.List("blee")).ThenReturn(k8s.Collection{*k8sCM()}, nil)
|
||||||
|
|
||||||
|
cm := resource.NewConfigMapWithArgs(ca)
|
||||||
|
ma, err := cm.List("blee")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
ca.VerifyWasCalledOnce().List("blee")
|
||||||
|
assert.Equal(t, resource.Columnars{cm.NewInstance(k8sCM())}, ma)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCMDelete(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.Delete("blee", "fred")).ThenReturn(nil)
|
||||||
|
|
||||||
|
cm := resource.NewConfigMapWithArgs(ca)
|
||||||
|
assert.Nil(t, cm.Delete("blee/fred"))
|
||||||
|
ca.VerifyWasCalledOnce().Delete("blee", "fred")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCMMarshal(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.Get("blee", "fred")).ThenReturn(k8sCM(), nil)
|
||||||
|
|
||||||
|
cm := resource.NewConfigMapWithArgs(ca)
|
||||||
|
ma, err := cm.Marshal("blee/fred")
|
||||||
|
|
||||||
|
ca.VerifyWasCalledOnce().Get("blee", "fred")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, cmYaml(), ma)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCMListSort(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
l := resource.NewConfigMapListWithArgs("blee", resource.NewConfigMapWithArgs(ca))
|
||||||
|
kk := []string{"c", "b", "a"}
|
||||||
|
l.SortFn()(kk)
|
||||||
|
assert.Equal(t, []string{"a", "b", "c"}, kk)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCMListHasName(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
l := resource.NewConfigMapListWithArgs("blee", resource.NewConfigMapWithArgs(ca))
|
||||||
|
assert.Equal(t, "cm", l.GetName())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCMListHasNamespace(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
l := resource.NewConfigMapListWithArgs("blee", resource.NewConfigMapWithArgs(ca))
|
||||||
|
assert.Equal(t, "blee", l.GetNamespace())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCMListHasResource(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
l := resource.NewConfigMapListWithArgs("blee", resource.NewConfigMapWithArgs(ca))
|
||||||
|
assert.NotNil(t, l.Resource())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCMListDescribe(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.Get("blee", "fred")).ThenReturn(k8sCM(), nil)
|
||||||
|
|
||||||
|
l := resource.NewConfigMapListWithArgs("blee", resource.NewConfigMapWithArgs(ca))
|
||||||
|
props, err := l.Describe("blee/fred")
|
||||||
|
|
||||||
|
ca.VerifyWasCalledOnce().Get("blee", "fred")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, 0, len(props))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCMListData(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.List("blee")).ThenReturn(k8s.Collection{*k8sCM()}, nil)
|
||||||
|
|
||||||
|
l := resource.NewConfigMapListWithArgs("blee", resource.NewConfigMapWithArgs(ca))
|
||||||
|
// Make sure we can get deltas!
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
err := l.Reconcile()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ca.VerifyWasCalled(m.Times(2)).List("blee")
|
||||||
|
|
||||||
|
td := l.Data()
|
||||||
|
assert.Equal(t, 1, len(td.Rows))
|
||||||
|
|
||||||
|
assert.Equal(t, "blee", l.GetNamespace())
|
||||||
|
assert.False(t, l.HasXRay())
|
||||||
|
row := td.Rows["blee/fred"]
|
||||||
|
assert.Equal(t, 3, len(row.Deltas))
|
||||||
|
for _, d := range row.Deltas {
|
||||||
|
assert.Equal(t, "", d)
|
||||||
|
}
|
||||||
|
assert.Equal(t, resource.Row{"fred", "2"}, row.Fields[:2])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
func newConfigMap() resource.Columnar {
|
||||||
|
return resource.NewConfigMap().NewInstance(k8sCM())
|
||||||
|
}
|
||||||
|
|
||||||
|
func k8sCM() *v1.ConfigMap {
|
||||||
|
return &v1.ConfigMap{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "fred",
|
||||||
|
Namespace: "blee",
|
||||||
|
CreationTimestamp: metav1.Time{testTime()},
|
||||||
|
},
|
||||||
|
Data: map[string]string{"blee": "blee", "duh": "duh"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmYaml() string {
|
||||||
|
return `typemeta:
|
||||||
|
kind: ConfigMap
|
||||||
|
apiversion: v1
|
||||||
|
objectmeta:
|
||||||
|
name: fred
|
||||||
|
generatename: ""
|
||||||
|
namespace: blee
|
||||||
|
selflink: ""
|
||||||
|
uid: ""
|
||||||
|
resourceversion: ""
|
||||||
|
generation: 0
|
||||||
|
creationtimestamp: "2018-12-14T10:36:43.326972-07:00"
|
||||||
|
deletiontimestamp: null
|
||||||
|
deletiongraceperiodseconds: null
|
||||||
|
labels: {}
|
||||||
|
annotations: {}
|
||||||
|
ownerreferences: []
|
||||||
|
initializers: null
|
||||||
|
finalizers: []
|
||||||
|
clustername: ""
|
||||||
|
data:
|
||||||
|
blee: blee
|
||||||
|
duh: duh
|
||||||
|
binarydata: {}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SwitchableRes represents a resource that can be switched.
|
||||||
|
type SwitchableRes interface {
|
||||||
|
k8s.ContextRes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context tracks a kubernetes resource.
|
||||||
|
type Context struct {
|
||||||
|
*Base
|
||||||
|
instance *k8s.NamedContext
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewContextList returns a new resource list.
|
||||||
|
func NewContextList(ns string) List {
|
||||||
|
return NewContextListWithArgs(ns, NewContext())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewContextListWithArgs returns a new resource list.
|
||||||
|
func NewContextListWithArgs(ns string, res Resource) List {
|
||||||
|
return newList(NotNamespaced, "ctx", res, SwitchAccess|DeleteAccess)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewContext instantiates a new Context.
|
||||||
|
func NewContext() *Context {
|
||||||
|
return NewContextWithArgs(k8s.NewContext().(SwitchableRes))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewContextWithArgs instantiates a new Context.
|
||||||
|
func NewContextWithArgs(r SwitchableRes) *Context {
|
||||||
|
ctx := &Context{
|
||||||
|
Base: &Base{
|
||||||
|
caller: r,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ctx.creator = ctx
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInstance builds a new Context instance from a k8s resource.
|
||||||
|
func (r *Context) NewInstance(i interface{}) Columnar {
|
||||||
|
c := NewContext()
|
||||||
|
switch i.(type) {
|
||||||
|
case *k8s.NamedContext:
|
||||||
|
c.instance = i.(*k8s.NamedContext)
|
||||||
|
case k8s.NamedContext:
|
||||||
|
ii := i.(k8s.NamedContext)
|
||||||
|
c.instance = &ii
|
||||||
|
default:
|
||||||
|
log.Fatalf("unknown context type %#v", i)
|
||||||
|
}
|
||||||
|
c.path = c.instance.Name
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch out current context.
|
||||||
|
func (r *Context) Switch(c string) error {
|
||||||
|
return r.caller.(k8s.ContextRes).Switch(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal the resource to yaml.
|
||||||
|
func (r *Context) Marshal(path string) (string, error) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header return resource header.
|
||||||
|
func (*Context) Header(string) Row {
|
||||||
|
return append(Row{}, "NAME", "CLUSTER", "AUTHINFO", "NAMESPACE")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fields retrieves displayable fields.
|
||||||
|
func (r *Context) Fields(ns string) Row {
|
||||||
|
ff := make(Row, 0, len(r.Header(ns)))
|
||||||
|
i := r.instance
|
||||||
|
|
||||||
|
name := i.Name
|
||||||
|
if i.CurrentCluster() == name {
|
||||||
|
name += "*"
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(ff,
|
||||||
|
name,
|
||||||
|
i.Context.Cluster,
|
||||||
|
i.Context.AuthInfo,
|
||||||
|
i.Context.Namespace,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtFields returns extended fields in relation to headers.
|
||||||
|
func (*Context) ExtFields() Properties {
|
||||||
|
return Properties{}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
package resource_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/resource"
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
m "github.com/petergtz/pegomock"
|
||||||
|
pegomock "github.com/petergtz/pegomock"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"k8s.io/client-go/tools/clientcmd/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCTXHeader(t *testing.T) {
|
||||||
|
assert.Equal(t,
|
||||||
|
resource.Row{"NAME", "CLUSTER", "AUTHINFO", "NAMESPACE"},
|
||||||
|
newContext().Header(""),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCTXFieldsAllNS(t *testing.T) {
|
||||||
|
r := newContext().Fields(resource.AllNamespaces)
|
||||||
|
assert.Equal(t, "test", r[0])
|
||||||
|
assert.Equal(t, "blee", r[1])
|
||||||
|
assert.Equal(t, "secret", r[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCTXSwitch(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockSwitchableRes()
|
||||||
|
m.When(ca.Switch("fred")).ThenReturn(nil)
|
||||||
|
|
||||||
|
ctx := resource.NewContextWithArgs(ca)
|
||||||
|
err := ctx.Switch("fred")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
ca.VerifyWasCalledOnce().Switch("fred")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCTXList(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockSwitchableRes()
|
||||||
|
m.When(ca.List("blee")).ThenReturn(k8s.Collection{*k8sNamedCTX()}, nil)
|
||||||
|
|
||||||
|
ctx := resource.NewContextWithArgs(ca)
|
||||||
|
cc, err := ctx.List("blee")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, resource.Columnars{ctx.NewInstance(k8sNamedCTX())}, cc)
|
||||||
|
ca.VerifyWasCalledOnce().List("blee")
|
||||||
|
}
|
||||||
|
func TestCTXDelete(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockSwitchableRes()
|
||||||
|
m.When(ca.Delete("", "fred")).ThenReturn(nil)
|
||||||
|
|
||||||
|
cm := resource.NewContextWithArgs(ca)
|
||||||
|
assert.Nil(t, cm.Delete("fred"))
|
||||||
|
ca.VerifyWasCalledOnce().Delete("", "fred")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCTXListSort(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockSwitchableRes()
|
||||||
|
l := resource.NewContextListWithArgs("blee", resource.NewContextWithArgs(ca))
|
||||||
|
kk := []string{"c", "b", "a"}
|
||||||
|
l.SortFn()(kk)
|
||||||
|
assert.Equal(t, []string{"a", "b", "c"}, kk)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCTXListHasName(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockSwitchableRes()
|
||||||
|
m.When(ca.List("blee")).ThenReturn(k8s.Collection{*k8sNamedCTX()}, nil)
|
||||||
|
|
||||||
|
l := resource.NewContextListWithArgs("blee", resource.NewContextWithArgs(ca))
|
||||||
|
assert.Equal(t, "ctx", l.GetName())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCTXListHasNamespace(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockSwitchableRes()
|
||||||
|
l := resource.NewContextListWithArgs("blee", resource.NewContextWithArgs(ca))
|
||||||
|
assert.Equal(t, resource.NotNamespaced, l.GetNamespace())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCTXListHasResource(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockSwitchableRes()
|
||||||
|
l := resource.NewContextListWithArgs("blee", resource.NewContextWithArgs(ca))
|
||||||
|
assert.NotNil(t, l.Resource())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCTXListDescribe(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockSwitchableRes()
|
||||||
|
m.When(ca.Get("blee", "fred")).ThenReturn(k8sNamedCTX(), nil)
|
||||||
|
|
||||||
|
l := resource.NewContextListWithArgs("blee", resource.NewContextWithArgs(ca))
|
||||||
|
props, err := l.Describe("blee/fred")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, 0, len(props))
|
||||||
|
ca.VerifyWasCalledOnce().Get("blee", "fred")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCTXListData(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockSwitchableRes()
|
||||||
|
m.When(ca.List(resource.NotNamespaced)).ThenReturn(k8s.Collection{*k8sNamedCTX()}, nil)
|
||||||
|
|
||||||
|
l := resource.NewContextListWithArgs("blee", resource.NewContextWithArgs(ca))
|
||||||
|
// Make sure we can get deltas!
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
assert.Nil(t, l.Reconcile())
|
||||||
|
}
|
||||||
|
ca.VerifyWasCalled(pegomock.Times(2)).List(resource.NotNamespaced)
|
||||||
|
|
||||||
|
td := l.Data()
|
||||||
|
assert.Equal(t, 1, len(td.Rows))
|
||||||
|
assert.False(t, l.HasXRay())
|
||||||
|
row := td.Rows["test"]
|
||||||
|
assert.Equal(t, 4, len(row.Deltas))
|
||||||
|
for _, d := range row.Deltas {
|
||||||
|
assert.Equal(t, "", d)
|
||||||
|
}
|
||||||
|
assert.Equal(t, resource.Row{"test", "blee", "secret", ""}, row.Fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
func newContext() resource.Columnar {
|
||||||
|
return resource.NewContext().NewInstance(k8sNamedCTX())
|
||||||
|
}
|
||||||
|
|
||||||
|
func k8sCTX() *api.Context {
|
||||||
|
return &api.Context{
|
||||||
|
LocationOfOrigin: "fred",
|
||||||
|
Cluster: "blee",
|
||||||
|
AuthInfo: "secret",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func k8sNamedCTX() *k8s.NamedContext {
|
||||||
|
ctx := k8s.NamedContext{
|
||||||
|
Name: "test",
|
||||||
|
Context: &api.Context{
|
||||||
|
LocationOfOrigin: "fred",
|
||||||
|
Cluster: "blee",
|
||||||
|
AuthInfo: "secret",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return &ctx
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"k8s.io/api/rbac/v1"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ClusterRole tracks a kubernetes resource.
|
||||||
|
type ClusterRole struct {
|
||||||
|
*Base
|
||||||
|
instance *v1.ClusterRole
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClusterRoleList returns a new resource list.
|
||||||
|
func NewClusterRoleList(ns string) List {
|
||||||
|
return NewClusterRoleListWithArgs(ns, NewClusterRole())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClusterRoleListWithArgs returns a new resource list.
|
||||||
|
func NewClusterRoleListWithArgs(ns string, res Resource) List {
|
||||||
|
return newList(NotNamespaced, "clusterrole", res, CRUDAccess)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClusterRole instantiates a new ClusterRole.
|
||||||
|
func NewClusterRole() *ClusterRole {
|
||||||
|
return NewClusterRoleWithArgs(k8s.NewClusterRole())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClusterRoleWithArgs instantiates a new Context.
|
||||||
|
func NewClusterRoleWithArgs(r k8s.Res) *ClusterRole {
|
||||||
|
ctx := &ClusterRole{
|
||||||
|
Base: &Base{
|
||||||
|
caller: r,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ctx.creator = ctx
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInstance builds a new Context instance from a k8s resource.
|
||||||
|
func (r *ClusterRole) NewInstance(i interface{}) Columnar {
|
||||||
|
c := NewClusterRole()
|
||||||
|
switch i.(type) {
|
||||||
|
case *v1.ClusterRole:
|
||||||
|
c.instance = i.(*v1.ClusterRole)
|
||||||
|
case v1.ClusterRole:
|
||||||
|
ii := i.(v1.ClusterRole)
|
||||||
|
c.instance = &ii
|
||||||
|
default:
|
||||||
|
log.Fatalf("unknown context type %#v", i)
|
||||||
|
}
|
||||||
|
c.path = c.instance.Name
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal resource to yaml.
|
||||||
|
func (r *ClusterRole) Marshal(path string) (string, error) {
|
||||||
|
ns, n := namespaced(path)
|
||||||
|
i, err := r.caller.Get(ns, n)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
cr := i.(*v1.ClusterRole)
|
||||||
|
cr.TypeMeta.APIVersion = "rbac.authorization.k8s.io/v1"
|
||||||
|
cr.TypeMeta.Kind = "ClusterRole"
|
||||||
|
raw, err := yaml.Marshal(i)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(raw), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header return resource header.
|
||||||
|
func (*ClusterRole) Header(ns string) Row {
|
||||||
|
return append(Row{}, "NAME", "AGE")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fields retrieves displayable fields.
|
||||||
|
func (r *ClusterRole) Fields(ns string) Row {
|
||||||
|
ff := make(Row, 0, len(r.Header(ns)))
|
||||||
|
i := r.instance
|
||||||
|
|
||||||
|
return append(ff,
|
||||||
|
Pad(i.Name, 70),
|
||||||
|
toAge(i.ObjectMeta.CreationTimestamp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtFields returns extended fields in relation to headers.
|
||||||
|
func (*ClusterRole) ExtFields() Properties {
|
||||||
|
return Properties{}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
"k8s.io/api/rbac/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ClusterRoleBinding tracks a kubernetes resource.
|
||||||
|
type ClusterRoleBinding struct {
|
||||||
|
*Base
|
||||||
|
instance *v1.ClusterRoleBinding
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClusterRoleBindingList returns a new resource list.
|
||||||
|
func NewClusterRoleBindingList(ns string) List {
|
||||||
|
return NewClusterRoleBindingListWithArgs(ns, NewClusterRoleBinding())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClusterRoleBindingListWithArgs returns a new resource list.
|
||||||
|
func NewClusterRoleBindingListWithArgs(ns string, res Resource) List {
|
||||||
|
return newList(NotNamespaced, "ctx", res, SwitchAccess|DeleteAccess)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClusterRoleBinding instantiates a new ClusterRoleBinding.
|
||||||
|
func NewClusterRoleBinding() *ClusterRoleBinding {
|
||||||
|
return NewClusterRoleBindingWithArgs(k8s.NewClusterRoleBinding())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClusterRoleBindingWithArgs instantiates a new Context.
|
||||||
|
func NewClusterRoleBindingWithArgs(r k8s.Res) *ClusterRoleBinding {
|
||||||
|
ctx := &ClusterRoleBinding{
|
||||||
|
Base: &Base{
|
||||||
|
caller: r,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ctx.creator = ctx
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInstance builds a new Context instance from a k8s resource.
|
||||||
|
func (r *ClusterRoleBinding) NewInstance(i interface{}) Columnar {
|
||||||
|
c := NewClusterRoleBinding()
|
||||||
|
switch i.(type) {
|
||||||
|
case *v1.ClusterRoleBinding:
|
||||||
|
c.instance = i.(*v1.ClusterRoleBinding)
|
||||||
|
case v1.ClusterRoleBinding:
|
||||||
|
ii := i.(v1.ClusterRoleBinding)
|
||||||
|
c.instance = &ii
|
||||||
|
default:
|
||||||
|
log.Fatalf("unknown context type %#v", i)
|
||||||
|
}
|
||||||
|
c.path = c.instance.Name
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal resource to yaml.
|
||||||
|
func (r *ClusterRoleBinding) Marshal(path string) (string, error) {
|
||||||
|
ns, n := namespaced(path)
|
||||||
|
i, err := r.caller.Get(ns, n)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
crb := i.(*v1.ClusterRoleBinding)
|
||||||
|
crb.TypeMeta.APIVersion = "rbac.authorization.k8s.io/v1"
|
||||||
|
crb.TypeMeta.Kind = "ClusterRoleBinding"
|
||||||
|
raw, err := yaml.Marshal(crb)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(raw), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header return resource header.
|
||||||
|
func (*ClusterRoleBinding) Header(_ string) Row {
|
||||||
|
return append(Row{}, "NAME", "AGE")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fields retrieves displayable fields.
|
||||||
|
func (r *ClusterRoleBinding) Fields(ns string) Row {
|
||||||
|
ff := make(Row, 0, len(r.Header(ns)))
|
||||||
|
i := r.instance
|
||||||
|
|
||||||
|
return append(ff,
|
||||||
|
i.Name,
|
||||||
|
toAge(i.ObjectMeta.CreationTimestamp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtFields returns extended fields in relation to headers.
|
||||||
|
func (*ClusterRoleBinding) ExtFields() Properties {
|
||||||
|
return Properties{}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
package resource_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/resource"
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
m "github.com/petergtz/pegomock"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCRBHeader(t *testing.T) {
|
||||||
|
assert.Equal(t, resource.Row{"NAME", "AGE"}, newCRB().Header("default"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCRBHeaderAllNS(t *testing.T) {
|
||||||
|
assert.Equal(t, resource.Row{"NAME", "AGE"}, newCRB().Header(resource.AllNamespaces))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCRBFields(t *testing.T) {
|
||||||
|
r := newCRB().Fields(resource.AllNamespaces)
|
||||||
|
assert.Equal(t, "fred", r[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCRBMarshal(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.Get("blee", "fred")).ThenReturn(k8sCRB(), nil)
|
||||||
|
|
||||||
|
cm := resource.NewClusterRoleBindingWithArgs(ca)
|
||||||
|
ma, err := cm.Marshal("blee/fred")
|
||||||
|
|
||||||
|
ca.VerifyWasCalledOnce().Get("blee", "fred")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, crbYaml(), ma)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCRBListData(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.List(resource.NotNamespaced)).ThenReturn(k8s.Collection{*k8sCRB()}, nil)
|
||||||
|
|
||||||
|
l := resource.NewClusterRoleBindingListWithArgs("-", resource.NewClusterRoleBindingWithArgs(ca))
|
||||||
|
// Make sure we can get deltas!
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
err := l.Reconcile()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ca.VerifyWasCalled(m.Times(2)).List(resource.NotNamespaced)
|
||||||
|
td := l.Data()
|
||||||
|
assert.Equal(t, 1, len(td.Rows))
|
||||||
|
assert.Equal(t, resource.NotNamespaced, l.GetNamespace())
|
||||||
|
assert.False(t, l.HasXRay())
|
||||||
|
row := td.Rows["fred"]
|
||||||
|
assert.Equal(t, 2, len(row.Deltas))
|
||||||
|
for _, d := range row.Deltas {
|
||||||
|
assert.Equal(t, "", d)
|
||||||
|
}
|
||||||
|
assert.Equal(t, resource.Row{"fred"}, row.Fields[:1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
func k8sCRB() *rbacv1.ClusterRoleBinding {
|
||||||
|
return &rbacv1.ClusterRoleBinding{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "fred",
|
||||||
|
Namespace: "blee",
|
||||||
|
CreationTimestamp: metav1.Time{testTime()},
|
||||||
|
},
|
||||||
|
Subjects: []rbacv1.Subject{
|
||||||
|
rbacv1.Subject{Kind: "test", Name: "fred", Namespace: "blee"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCRB() resource.Columnar {
|
||||||
|
return resource.NewClusterRoleBinding().NewInstance(k8sCRB())
|
||||||
|
}
|
||||||
|
|
||||||
|
func crbYaml() string {
|
||||||
|
return `typemeta:
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
apiversion: rbac.authorization.k8s.io/v1
|
||||||
|
objectmeta:
|
||||||
|
name: fred
|
||||||
|
generatename: ""
|
||||||
|
namespace: blee
|
||||||
|
selflink: ""
|
||||||
|
uid: ""
|
||||||
|
resourceversion: ""
|
||||||
|
generation: 0
|
||||||
|
creationtimestamp: "2018-12-14T10:36:43.326972-07:00"
|
||||||
|
deletiontimestamp: null
|
||||||
|
deletiongraceperiodseconds: null
|
||||||
|
labels: {}
|
||||||
|
annotations: {}
|
||||||
|
ownerreferences: []
|
||||||
|
initializers: null
|
||||||
|
finalizers: []
|
||||||
|
clustername: ""
|
||||||
|
subjects:
|
||||||
|
- kind: test
|
||||||
|
apigroup: ""
|
||||||
|
name: fred
|
||||||
|
namespace: blee
|
||||||
|
roleref:
|
||||||
|
apigroup: ""
|
||||||
|
kind: ""
|
||||||
|
name: ""
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,163 @@
|
||||||
|
package resource_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/resource"
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
m "github.com/petergtz/pegomock"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCRListAccess(t *testing.T) {
|
||||||
|
ns := "blee"
|
||||||
|
l := resource.NewClusterRoleList(resource.AllNamespaces)
|
||||||
|
l.SetNamespace(ns)
|
||||||
|
|
||||||
|
assert.Equal(t, resource.NotNamespaced, l.GetNamespace())
|
||||||
|
assert.Equal(t, "clusterrole", l.GetName())
|
||||||
|
for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} {
|
||||||
|
assert.True(t, l.Access(a))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCRHeader(t *testing.T) {
|
||||||
|
assert.Equal(t, resource.Row{"NAME", "AGE"}, newClusterRole().Header("default"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCRHeaderAllNS(t *testing.T) {
|
||||||
|
assert.Equal(t, resource.Row{"NAME", "AGE"}, newClusterRole().Header(resource.AllNamespaces))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCRFields(t *testing.T) {
|
||||||
|
r := newClusterRole().Fields("blee")
|
||||||
|
assert.Equal(t, "fred"+strings.Repeat(" ", 66), r[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCRFieldsAllNS(t *testing.T) {
|
||||||
|
r := newClusterRole().Fields(resource.AllNamespaces)
|
||||||
|
assert.Equal(t, "fred"+strings.Repeat(" ", 66), r[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCRMarshal(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.Get("blee", "fred")).ThenReturn(k8sCR(), nil)
|
||||||
|
|
||||||
|
cm := resource.NewClusterRoleWithArgs(ca)
|
||||||
|
ma, err := cm.Marshal("blee/fred")
|
||||||
|
ca.VerifyWasCalledOnce().Get("blee", "fred")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, crYaml(), ma)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCRListData(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.List(resource.NotNamespaced)).ThenReturn(k8s.Collection{*k8sCR()}, nil)
|
||||||
|
|
||||||
|
l := resource.NewClusterRoleListWithArgs("-", resource.NewClusterRoleWithArgs(ca))
|
||||||
|
// Make sure we can get deltas!
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
err := l.Reconcile()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ca.VerifyWasCalled(m.Times(2)).List(resource.NotNamespaced)
|
||||||
|
td := l.Data()
|
||||||
|
assert.Equal(t, 1, len(td.Rows))
|
||||||
|
assert.Equal(t, resource.NotNamespaced, l.GetNamespace())
|
||||||
|
assert.False(t, l.HasXRay())
|
||||||
|
row := td.Rows["fred"]
|
||||||
|
assert.Equal(t, 2, len(row.Deltas))
|
||||||
|
for _, d := range row.Deltas {
|
||||||
|
assert.Equal(t, "", d)
|
||||||
|
}
|
||||||
|
assert.Equal(t, resource.Row{"fred" + strings.Repeat(" ", 66)}, row.Fields[:1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCRListDescribe(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.Get("blee", "fred")).ThenReturn(k8sCR(), nil)
|
||||||
|
l := resource.NewClusterRoleListWithArgs("blee", resource.NewClusterRoleWithArgs(ca))
|
||||||
|
props, err := l.Describe("blee/fred")
|
||||||
|
|
||||||
|
ca.VerifyWasCalledOnce().Get("blee", "fred")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, 0, len(props))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
func k8sCR() *rbacv1.ClusterRole {
|
||||||
|
return &rbacv1.ClusterRole{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "fred",
|
||||||
|
Namespace: "blee",
|
||||||
|
CreationTimestamp: metav1.Time{testTime()},
|
||||||
|
},
|
||||||
|
Rules: []rbacv1.PolicyRule{
|
||||||
|
rbacv1.PolicyRule{
|
||||||
|
Verbs: []string{"get", "list"},
|
||||||
|
APIGroups: []string{""},
|
||||||
|
ResourceNames: []string{"pod"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newClusterRole() resource.Columnar {
|
||||||
|
return resource.NewClusterRole().NewInstance(k8sCR())
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTime() time.Time {
|
||||||
|
t, err := time.Parse(time.RFC3339, "2018-12-14T10:36:43.326972-07:00")
|
||||||
|
if err != nil {
|
||||||
|
log.Println("TestTime Failed", err)
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func crYaml() string {
|
||||||
|
return `typemeta:
|
||||||
|
kind: ClusterRole
|
||||||
|
apiversion: rbac.authorization.k8s.io/v1
|
||||||
|
objectmeta:
|
||||||
|
name: fred
|
||||||
|
generatename: ""
|
||||||
|
namespace: blee
|
||||||
|
selflink: ""
|
||||||
|
uid: ""
|
||||||
|
resourceversion: ""
|
||||||
|
generation: 0
|
||||||
|
creationtimestamp: "2018-12-14T10:36:43.326972-07:00"
|
||||||
|
deletiontimestamp: null
|
||||||
|
deletiongraceperiodseconds: null
|
||||||
|
labels: {}
|
||||||
|
annotations: {}
|
||||||
|
ownerreferences: []
|
||||||
|
initializers: null
|
||||||
|
finalizers: []
|
||||||
|
clustername: ""
|
||||||
|
rules:
|
||||||
|
- verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
apigroups:
|
||||||
|
- ""
|
||||||
|
resources: []
|
||||||
|
resourcenames:
|
||||||
|
- pod
|
||||||
|
nonresourceurls: []
|
||||||
|
aggregationrule: null
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CRD tracks a kubernetes resource.
|
||||||
|
type CRD struct {
|
||||||
|
*Base
|
||||||
|
instance *unstructured.Unstructured
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCRDList returns a new resource list.
|
||||||
|
func NewCRDList(ns string) List {
|
||||||
|
return NewCRDListWithArgs(ns, NewCRD())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCRDListWithArgs returns a new resource list.
|
||||||
|
func NewCRDListWithArgs(ns string, res Resource) List {
|
||||||
|
return newList(NotNamespaced, "crd", res, CRUDAccess)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCRD instantiates a new CRD.
|
||||||
|
func NewCRD() *CRD {
|
||||||
|
return NewCRDWithArgs(k8s.NewCRD())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCRDWithArgs instantiates a new Context.
|
||||||
|
func NewCRDWithArgs(r k8s.Res) *CRD {
|
||||||
|
ctx := &CRD{
|
||||||
|
Base: &Base{
|
||||||
|
caller: r,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ctx.creator = ctx
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInstance builds a new Context instance from a k8s resource.
|
||||||
|
func (r *CRD) NewInstance(i interface{}) Columnar {
|
||||||
|
c := NewCRD()
|
||||||
|
switch i.(type) {
|
||||||
|
case *unstructured.Unstructured:
|
||||||
|
c.instance = i.(*unstructured.Unstructured)
|
||||||
|
case unstructured.Unstructured:
|
||||||
|
ii := i.(unstructured.Unstructured)
|
||||||
|
c.instance = &ii
|
||||||
|
default:
|
||||||
|
log.Fatalf("unknown context type %#v", i)
|
||||||
|
}
|
||||||
|
meta := c.instance.Object["metadata"].(map[string]interface{})
|
||||||
|
c.path = meta["name"].(string)
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal a resource.
|
||||||
|
func (r *CRD) Marshal(path string) (string, error) {
|
||||||
|
ns, n := namespaced(path)
|
||||||
|
i, err := r.caller.Get(ns, n)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
raw, err := yaml.Marshal(i)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(raw), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header return the resource header.
|
||||||
|
func (*CRD) Header(ns string) Row {
|
||||||
|
return Row{"NAME", "AGE"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fields retrieves displayable fields.
|
||||||
|
func (r *CRD) Fields(ns string) Row {
|
||||||
|
ff := make(Row, 0, len(r.Header(ns)))
|
||||||
|
|
||||||
|
i := r.instance
|
||||||
|
meta := i.Object["metadata"].(map[string]interface{})
|
||||||
|
t, err := time.Parse(time.RFC3339, meta["creationTimestamp"].(string))
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Fields timestamp", err)
|
||||||
|
}
|
||||||
|
return append(ff, meta["name"].(string), toAge(metav1.Time{t}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtFields returns extended fields.
|
||||||
|
func (r *CRD) ExtFields() Properties {
|
||||||
|
var (
|
||||||
|
pp = Properties{}
|
||||||
|
i = r.instance
|
||||||
|
)
|
||||||
|
|
||||||
|
meta := i.Object["metadata"].(map[string]interface{})
|
||||||
|
|
||||||
|
if spec, ok := i.Object["spec"].(map[string]interface{}); ok {
|
||||||
|
pp["name"] = meta["name"]
|
||||||
|
pp["group"], pp["version"] = spec["group"], spec["version"]
|
||||||
|
names := spec["names"].(map[string]interface{})
|
||||||
|
pp["kind"] = names["kind"]
|
||||||
|
pp["singular"], pp["plural"] = names["singular"], names["plural"]
|
||||||
|
pp["aliases"] = names["shortNames"]
|
||||||
|
}
|
||||||
|
return pp
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
package resource_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/resource"
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
m "github.com/petergtz/pegomock"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCRDListAccess(t *testing.T) {
|
||||||
|
ns := "blee"
|
||||||
|
l := resource.NewCRDList(resource.AllNamespaces)
|
||||||
|
l.SetNamespace(ns)
|
||||||
|
|
||||||
|
assert.Equal(t, resource.NotNamespaced, l.GetNamespace())
|
||||||
|
assert.Equal(t, "crd", l.GetName())
|
||||||
|
for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} {
|
||||||
|
assert.True(t, l.Access(a))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCRDHeader(t *testing.T) {
|
||||||
|
assert.Equal(t, resource.Row{"NAME", "AGE"}, newCRD().Header("default"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCRDHeaderAllNS(t *testing.T) {
|
||||||
|
assert.Equal(t, resource.Row{"NAME", "AGE"}, newCRD().Header(resource.AllNamespaces))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCRDFields(t *testing.T) {
|
||||||
|
r := newCRD().Fields("blee")
|
||||||
|
assert.Equal(t, "fred", r[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCRDFieldsAllNS(t *testing.T) {
|
||||||
|
r := newCRD().Fields(resource.AllNamespaces)
|
||||||
|
assert.Equal(t, "fred", r[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCRDMarshal(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.Get("blee", "fred")).ThenReturn(k8sCRD(), nil)
|
||||||
|
|
||||||
|
cm := resource.NewCRDWithArgs(ca)
|
||||||
|
ma, err := cm.Marshal("blee/fred")
|
||||||
|
ca.VerifyWasCalledOnce().Get("blee", "fred")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, crdYaml(), ma)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCRDListData(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.List(resource.NotNamespaced)).ThenReturn(k8s.Collection{*k8sCRD()}, nil)
|
||||||
|
|
||||||
|
l := resource.NewCRDListWithArgs("-", resource.NewCRDWithArgs(ca))
|
||||||
|
// Make sure we can get deltas!
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
err := l.Reconcile()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ca.VerifyWasCalled(m.Times(2)).List(resource.NotNamespaced)
|
||||||
|
td := l.Data()
|
||||||
|
assert.Equal(t, 1, len(td.Rows))
|
||||||
|
assert.Equal(t, resource.NotNamespaced, l.GetNamespace())
|
||||||
|
assert.False(t, l.HasXRay())
|
||||||
|
row := td.Rows["fred"]
|
||||||
|
assert.Equal(t, 2, len(row.Deltas))
|
||||||
|
for _, d := range row.Deltas {
|
||||||
|
assert.Equal(t, "", d)
|
||||||
|
}
|
||||||
|
assert.Equal(t, resource.Row{"fred"}, row.Fields[:1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCRDListDescribe(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.Get("blee", "fred")).ThenReturn(k8sCRD(), nil)
|
||||||
|
l := resource.NewCRDListWithArgs("blee", resource.NewCRDWithArgs(ca))
|
||||||
|
props, err := l.Describe("blee/fred")
|
||||||
|
|
||||||
|
ca.VerifyWasCalledOnce().Get("blee", "fred")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, 0, len(props))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
func k8sCRD() *unstructured.Unstructured {
|
||||||
|
return &unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"namespace": "blee",
|
||||||
|
"name": "fred",
|
||||||
|
"creationTimestamp": "2018-12-14T10:36:43.326972Z",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCRD() resource.Columnar {
|
||||||
|
return resource.NewCRD().NewInstance(k8sCRD())
|
||||||
|
}
|
||||||
|
|
||||||
|
func crdYaml() string {
|
||||||
|
return `object:
|
||||||
|
metadata:
|
||||||
|
creationTimestamp: "2018-12-14T10:36:43.326972Z"
|
||||||
|
name: fred
|
||||||
|
namespace: blee
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
batchv1beta1 "k8s.io/api/batch/v1beta1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CronJob tracks a kubernetes resource.
|
||||||
|
type CronJob struct {
|
||||||
|
*Base
|
||||||
|
instance *batchv1beta1.CronJob
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCronJobList returns a new resource list.
|
||||||
|
func NewCronJobList(ns string) List {
|
||||||
|
return NewCronJobListWithArgs(ns, NewCronJob())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCronJobListWithArgs returns a new resource list.
|
||||||
|
func NewCronJobListWithArgs(ns string, res Resource) List {
|
||||||
|
return newList(ns, "job", res, AllVerbsAccess)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCronJob instantiates a new CronJob.
|
||||||
|
func NewCronJob() *CronJob {
|
||||||
|
return NewCronJobWithArgs(k8s.NewCronJob())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCronJobWithArgs instantiates a new CronJob.
|
||||||
|
func NewCronJobWithArgs(r k8s.Res) *CronJob {
|
||||||
|
cm := &CronJob{
|
||||||
|
Base: &Base{
|
||||||
|
caller: r,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cm.creator = cm
|
||||||
|
return cm
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInstance builds a new CronJob instance from a k8s resource.
|
||||||
|
func (*CronJob) NewInstance(i interface{}) Columnar {
|
||||||
|
job := NewCronJob()
|
||||||
|
switch i.(type) {
|
||||||
|
case *batchv1beta1.CronJob:
|
||||||
|
job.instance = i.(*batchv1beta1.CronJob)
|
||||||
|
case batchv1beta1.CronJob:
|
||||||
|
ii := i.(batchv1beta1.CronJob)
|
||||||
|
job.instance = &ii
|
||||||
|
default:
|
||||||
|
log.Fatalf("Unknown %#v", i)
|
||||||
|
}
|
||||||
|
job.path = job.namespacedName(job.instance.ObjectMeta)
|
||||||
|
return job
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal resource to yaml.
|
||||||
|
func (r *CronJob) Marshal(path string) (string, error) {
|
||||||
|
ns, n := namespaced(path)
|
||||||
|
i, err := r.caller.Get(ns, n)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
dp := i.(*batchv1beta1.CronJob)
|
||||||
|
dp.TypeMeta.APIVersion = "extensions/batchv1beta1"
|
||||||
|
dp.TypeMeta.Kind = "CronJob"
|
||||||
|
raw, err := yaml.Marshal(i)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(raw), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header return resource header.
|
||||||
|
func (*CronJob) Header(ns string) Row {
|
||||||
|
hh := Row{}
|
||||||
|
if ns == AllNamespaces {
|
||||||
|
hh = append(hh, "NAMESPACE")
|
||||||
|
}
|
||||||
|
return append(hh, "NAME", "SCHEDULE", "SUSPEND", "ACTIVE", "LAST_SCHEDULE", "AGE")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fields retrieves displayable fields.
|
||||||
|
func (r *CronJob) Fields(ns string) Row {
|
||||||
|
ff := make([]string, 0, len(r.Header(ns)))
|
||||||
|
|
||||||
|
i := r.instance
|
||||||
|
if ns == AllNamespaces {
|
||||||
|
ff = append(ff, i.Namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
lastScheduled := "<none>"
|
||||||
|
if i.Status.LastScheduleTime != nil {
|
||||||
|
lastScheduled = toAge(*i.Status.LastScheduleTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(ff,
|
||||||
|
i.Name,
|
||||||
|
i.Spec.Schedule,
|
||||||
|
boolToStr(*i.Spec.Suspend),
|
||||||
|
strconv.Itoa(len(i.Status.Active)),
|
||||||
|
lastScheduled,
|
||||||
|
toAge(i.ObjectMeta.CreationTimestamp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtFields returns extended fields in relation to headers.
|
||||||
|
func (*CronJob) ExtFields() Properties {
|
||||||
|
return Properties{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
@ -0,0 +1,218 @@
|
||||||
|
package resource_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/resource"
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
m "github.com/petergtz/pegomock"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
batchv1beta1 "k8s.io/api/batch/v1beta1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCronJobListAccess(t *testing.T) {
|
||||||
|
ns := "blee"
|
||||||
|
l := resource.NewCronJobList(resource.AllNamespaces)
|
||||||
|
l.SetNamespace(ns)
|
||||||
|
|
||||||
|
assert.Equal(t, "blee", l.GetNamespace())
|
||||||
|
assert.Equal(t, "job", l.GetName())
|
||||||
|
for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} {
|
||||||
|
assert.True(t, l.Access(a))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCronJobHeader(t *testing.T) {
|
||||||
|
assert.Equal(t, resource.Row{"NAME", "SCHEDULE", "SUSPEND", "ACTIVE", "LAST_SCHEDULE", "AGE"}, newCronJob().Header("default"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCronJobFields(t *testing.T) {
|
||||||
|
r := newCronJob().Fields("blee")
|
||||||
|
assert.Equal(t, "fred", r[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCronJobMarshal(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.Get("blee", "fred")).ThenReturn(k8sCronJob(), nil)
|
||||||
|
|
||||||
|
cm := resource.NewCronJobWithArgs(ca)
|
||||||
|
ma, err := cm.Marshal("blee/fred")
|
||||||
|
ca.VerifyWasCalledOnce().Get("blee", "fred")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, cronjobYaml(), ma)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCronJobListData(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.List(resource.NotNamespaced)).ThenReturn(k8s.Collection{*k8sCronJob()}, nil)
|
||||||
|
|
||||||
|
l := resource.NewCronJobListWithArgs("-", resource.NewCronJobWithArgs(ca))
|
||||||
|
// Make sure we can get deltas!
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
err := l.Reconcile()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ca.VerifyWasCalled(m.Times(2)).List(resource.NotNamespaced)
|
||||||
|
td := l.Data()
|
||||||
|
assert.Equal(t, 1, len(td.Rows))
|
||||||
|
assert.Equal(t, resource.NotNamespaced, l.GetNamespace())
|
||||||
|
assert.False(t, l.HasXRay())
|
||||||
|
row := td.Rows["blee/fred"]
|
||||||
|
assert.Equal(t, 6, len(row.Deltas))
|
||||||
|
for _, d := range row.Deltas {
|
||||||
|
assert.Equal(t, "", d)
|
||||||
|
}
|
||||||
|
assert.Equal(t, resource.Row{"fred"}, row.Fields[:1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCronJobListDescribe(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.Get("blee", "fred")).ThenReturn(k8sCronJob(), nil)
|
||||||
|
l := resource.NewCronJobListWithArgs("blee", resource.NewCronJobWithArgs(ca))
|
||||||
|
props, err := l.Describe("blee/fred")
|
||||||
|
|
||||||
|
ca.VerifyWasCalledOnce().Get("blee", "fred")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, 0, len(props))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
func k8sCronJob() *batchv1beta1.CronJob {
|
||||||
|
var b bool
|
||||||
|
return &batchv1beta1.CronJob{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "blee",
|
||||||
|
Name: "fred",
|
||||||
|
CreationTimestamp: metav1.Time{Time: testTime()},
|
||||||
|
},
|
||||||
|
Spec: batchv1beta1.CronJobSpec{
|
||||||
|
Schedule: "*/1 * * * *",
|
||||||
|
Suspend: &b,
|
||||||
|
},
|
||||||
|
Status: batchv1beta1.CronJobStatus{
|
||||||
|
LastScheduleTime: &metav1.Time{Time: testTime()},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCronJob() resource.Columnar {
|
||||||
|
return resource.NewCronJob().NewInstance(k8sCronJob())
|
||||||
|
}
|
||||||
|
|
||||||
|
func cronjobYaml() string {
|
||||||
|
return `typemeta:
|
||||||
|
kind: CronJob
|
||||||
|
apiversion: extensions/batchv1beta1
|
||||||
|
objectmeta:
|
||||||
|
name: fred
|
||||||
|
generatename: ""
|
||||||
|
namespace: blee
|
||||||
|
selflink: ""
|
||||||
|
uid: ""
|
||||||
|
resourceversion: ""
|
||||||
|
generation: 0
|
||||||
|
creationtimestamp: "2018-12-14T10:36:43.326972-07:00"
|
||||||
|
deletiontimestamp: null
|
||||||
|
deletiongraceperiodseconds: null
|
||||||
|
labels: {}
|
||||||
|
annotations: {}
|
||||||
|
ownerreferences: []
|
||||||
|
initializers: null
|
||||||
|
finalizers: []
|
||||||
|
clustername: ""
|
||||||
|
spec:
|
||||||
|
schedule: '*/1 * * * *'
|
||||||
|
startingdeadlineseconds: null
|
||||||
|
concurrencypolicy: ""
|
||||||
|
suspend: false
|
||||||
|
jobtemplate:
|
||||||
|
objectmeta:
|
||||||
|
name: ""
|
||||||
|
generatename: ""
|
||||||
|
namespace: ""
|
||||||
|
selflink: ""
|
||||||
|
uid: ""
|
||||||
|
resourceversion: ""
|
||||||
|
generation: 0
|
||||||
|
creationtimestamp: "0001-01-01T00:00:00Z"
|
||||||
|
deletiontimestamp: null
|
||||||
|
deletiongraceperiodseconds: null
|
||||||
|
labels: {}
|
||||||
|
annotations: {}
|
||||||
|
ownerreferences: []
|
||||||
|
initializers: null
|
||||||
|
finalizers: []
|
||||||
|
clustername: ""
|
||||||
|
spec:
|
||||||
|
parallelism: null
|
||||||
|
completions: null
|
||||||
|
activedeadlineseconds: null
|
||||||
|
backofflimit: null
|
||||||
|
selector: null
|
||||||
|
manualselector: null
|
||||||
|
template:
|
||||||
|
objectmeta:
|
||||||
|
name: ""
|
||||||
|
generatename: ""
|
||||||
|
namespace: ""
|
||||||
|
selflink: ""
|
||||||
|
uid: ""
|
||||||
|
resourceversion: ""
|
||||||
|
generation: 0
|
||||||
|
creationtimestamp: "0001-01-01T00:00:00Z"
|
||||||
|
deletiontimestamp: null
|
||||||
|
deletiongraceperiodseconds: null
|
||||||
|
labels: {}
|
||||||
|
annotations: {}
|
||||||
|
ownerreferences: []
|
||||||
|
initializers: null
|
||||||
|
finalizers: []
|
||||||
|
clustername: ""
|
||||||
|
spec:
|
||||||
|
volumes: []
|
||||||
|
initcontainers: []
|
||||||
|
containers: []
|
||||||
|
restartpolicy: ""
|
||||||
|
terminationgraceperiodseconds: null
|
||||||
|
activedeadlineseconds: null
|
||||||
|
dnspolicy: ""
|
||||||
|
nodeselector: {}
|
||||||
|
serviceaccountname: ""
|
||||||
|
deprecatedserviceaccount: ""
|
||||||
|
automountserviceaccounttoken: null
|
||||||
|
nodename: ""
|
||||||
|
hostnetwork: false
|
||||||
|
hostpid: false
|
||||||
|
hostipc: false
|
||||||
|
shareprocessnamespace: null
|
||||||
|
securitycontext: null
|
||||||
|
imagepullsecrets: []
|
||||||
|
hostname: ""
|
||||||
|
subdomain: ""
|
||||||
|
affinity: null
|
||||||
|
schedulername: ""
|
||||||
|
tolerations: []
|
||||||
|
hostaliases: []
|
||||||
|
priorityclassname: ""
|
||||||
|
priority: null
|
||||||
|
dnsconfig: null
|
||||||
|
readinessgates: []
|
||||||
|
runtimeclassname: null
|
||||||
|
enableservicelinks: null
|
||||||
|
ttlsecondsafterfinished: null
|
||||||
|
successfuljobshistorylimit: null
|
||||||
|
failedjobshistorylimit: null
|
||||||
|
status:
|
||||||
|
active: []
|
||||||
|
lastscheduletime: "2018-12-14T10:36:43.326972-07:00"
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,191 @@
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Custom tracks a kubernetes resource.
|
||||||
|
type Custom struct {
|
||||||
|
*Base
|
||||||
|
// instance *unstructured.Unstructured
|
||||||
|
instance *metav1beta1.TableRow
|
||||||
|
group, version, name string
|
||||||
|
headers Row
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCustomList returns a new resource list.
|
||||||
|
func NewCustomList(ns, g, v, n string) List {
|
||||||
|
return NewCustomListWithArgs(ns, n, NewCustom(g, v, n))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCustomListWithArgs returns a new resource list.
|
||||||
|
func NewCustomListWithArgs(ns, n string, res Resource) List {
|
||||||
|
return newList(ns, n, res, AllVerbsAccess)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCustom instantiates a new Kubernetes Resource.
|
||||||
|
func NewCustom(g, v, n string) *Custom {
|
||||||
|
return NewCustomWithArgs(k8s.NewResource(g, v, n))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCustomWithArgs instantiates a new Custom.
|
||||||
|
func NewCustomWithArgs(r k8s.Res) *Custom {
|
||||||
|
cr := &Custom{
|
||||||
|
Base: &Base{
|
||||||
|
caller: r,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cr.creator = cr
|
||||||
|
|
||||||
|
cr.group, cr.version, cr.name = r.(*k8s.Resource).GetInfo()
|
||||||
|
return cr
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInstance builds a new Custom instance from a k8s resource.
|
||||||
|
func (*Custom) NewInstance(i interface{}) Columnar {
|
||||||
|
cr := NewCustom("", "", "")
|
||||||
|
switch i.(type) {
|
||||||
|
case *metav1beta1.TableRow:
|
||||||
|
cr.instance = i.(*metav1beta1.TableRow)
|
||||||
|
case metav1beta1.TableRow:
|
||||||
|
t := i.(metav1beta1.TableRow)
|
||||||
|
cr.instance = &t
|
||||||
|
default:
|
||||||
|
log.Fatalf("Unknown %#v", i)
|
||||||
|
}
|
||||||
|
var obj map[string]interface{}
|
||||||
|
err := json.Unmarshal(cr.instance.Object.Raw, &obj)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
meta := obj["metadata"].(map[string]interface{})
|
||||||
|
cr.path = path.Join(meta["namespace"].(string), meta["name"].(string))
|
||||||
|
cr.group, cr.version, cr.name = obj["kind"].(string), obj["apiVersion"].(string), meta["name"].(string)
|
||||||
|
return cr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal resource to yaml.
|
||||||
|
func (r *Custom) Marshal(path string) (string, error) {
|
||||||
|
ns, n := namespaced(path)
|
||||||
|
i, err := r.caller.Get(ns, n)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
raw, err := yaml.Marshal(i)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(raw), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all resources
|
||||||
|
func (r *Custom) List(ns string) (Columnars, error) {
|
||||||
|
ii, err := r.caller.List(ns)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ii) != 1 {
|
||||||
|
return Columnars{}, errors.New("no resources found")
|
||||||
|
}
|
||||||
|
|
||||||
|
table := ii[0].(*metav1beta1.Table)
|
||||||
|
r.headers = make(Row, len(table.ColumnDefinitions))
|
||||||
|
for i, h := range table.ColumnDefinitions {
|
||||||
|
r.headers[i] = h.Name
|
||||||
|
}
|
||||||
|
rows := table.Rows
|
||||||
|
cc := make(Columnars, 0, len(rows))
|
||||||
|
for i := 0; i < len(rows); i++ {
|
||||||
|
cc = append(cc, r.creator.NewInstance(rows[i]))
|
||||||
|
}
|
||||||
|
return cc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header return resource header.
|
||||||
|
func (r *Custom) Header(ns string) Row {
|
||||||
|
hh := make(Row, 0, len(r.headers)+1)
|
||||||
|
if ns == AllNamespaces {
|
||||||
|
hh = append(hh, "NAMESPACE")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, h := range r.headers {
|
||||||
|
hh = append(hh, strings.ToUpper(h))
|
||||||
|
}
|
||||||
|
return hh
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fields retrieves displayable fields.
|
||||||
|
func (r *Custom) Fields(ns string) Row {
|
||||||
|
ff := make(Row, 0, len(r.Header(ns)))
|
||||||
|
|
||||||
|
var obj map[string]interface{}
|
||||||
|
err := json.Unmarshal(r.instance.Object.Raw, &obj)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return Row{}
|
||||||
|
}
|
||||||
|
|
||||||
|
meta := obj["metadata"].(map[string]interface{})
|
||||||
|
|
||||||
|
if ns == AllNamespaces {
|
||||||
|
ff = append(ff, meta["namespace"].(string))
|
||||||
|
}
|
||||||
|
for _, c := range r.instance.Cells {
|
||||||
|
ff = append(ff, fmt.Sprintf("%v", c))
|
||||||
|
}
|
||||||
|
|
||||||
|
return ff
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtFields returns extended fields in relation to headers.
|
||||||
|
func (*Custom) ExtFields() Properties {
|
||||||
|
return Properties{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCRDS() map[string]k8s.ApiGroup {
|
||||||
|
m := map[string]k8s.ApiGroup{}
|
||||||
|
list := NewCRDList("")
|
||||||
|
ll, _ := list.Resource().List("")
|
||||||
|
for _, l := range ll {
|
||||||
|
ff := l.ExtFields()
|
||||||
|
grp := k8s.ApiGroup{
|
||||||
|
Resource: ff["name"].(string),
|
||||||
|
Version: ff["version"].(string),
|
||||||
|
Group: ff["group"].(string),
|
||||||
|
Kind: ff["kind"].(string),
|
||||||
|
}
|
||||||
|
if aa, ok := ff["aliases"].([]interface{}); ok {
|
||||||
|
if n, ok := ff["plural"].(string); ok {
|
||||||
|
grp.Plural = n
|
||||||
|
}
|
||||||
|
if n, ok := ff["singular"].(string); ok {
|
||||||
|
grp.Singular = n
|
||||||
|
}
|
||||||
|
aliases := make([]string, len(aa))
|
||||||
|
for i, a := range aa {
|
||||||
|
aliases[i] = a.(string)
|
||||||
|
}
|
||||||
|
grp.Aliases = aliases
|
||||||
|
} else if s, ok := ff["singular"].(string); ok {
|
||||||
|
grp.Singular = s
|
||||||
|
if p, ok := ff["plural"].(string); ok {
|
||||||
|
grp.Plural = p
|
||||||
|
}
|
||||||
|
} else if s, ok := ff["plural"].(string); ok {
|
||||||
|
grp.Plural = s
|
||||||
|
}
|
||||||
|
m[grp.Kind] = grp
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
"k8s.io/api/apps/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Deployment tracks a kubernetes resource.
|
||||||
|
type Deployment struct {
|
||||||
|
*Base
|
||||||
|
instance *v1.Deployment
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDeploymentList returns a new resource list.
|
||||||
|
func NewDeploymentList(ns string) List {
|
||||||
|
return NewDeploymentListWithArgs(ns, NewDeployment())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDeploymentListWithArgs returns a new resource list.
|
||||||
|
func NewDeploymentListWithArgs(ns string, res Resource) List {
|
||||||
|
return newList(ns, "deploy", res, AllVerbsAccess)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDeployment instantiates a new Deployment.
|
||||||
|
func NewDeployment() *Deployment {
|
||||||
|
return NewDeploymentWithArgs(k8s.NewDeployment())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDeploymentWithArgs instantiates a new Deployment.
|
||||||
|
func NewDeploymentWithArgs(r k8s.Res) *Deployment {
|
||||||
|
cm := &Deployment{
|
||||||
|
Base: &Base{
|
||||||
|
caller: r,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cm.creator = cm
|
||||||
|
return cm
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInstance builds a new Deployment instance from a k8s resource.
|
||||||
|
func (*Deployment) NewInstance(i interface{}) Columnar {
|
||||||
|
cm := NewDeployment()
|
||||||
|
switch i.(type) {
|
||||||
|
case *v1.Deployment:
|
||||||
|
cm.instance = i.(*v1.Deployment)
|
||||||
|
case v1.Deployment:
|
||||||
|
ii := i.(v1.Deployment)
|
||||||
|
cm.instance = &ii
|
||||||
|
default:
|
||||||
|
log.Fatalf("Unknown %#v", i)
|
||||||
|
}
|
||||||
|
cm.path = cm.namespacedName(cm.instance.ObjectMeta)
|
||||||
|
return cm
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal resource to yaml.
|
||||||
|
func (r *Deployment) Marshal(path string) (string, error) {
|
||||||
|
ns, n := namespaced(path)
|
||||||
|
i, err := r.caller.Get(ns, n)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
dp := i.(*v1.Deployment)
|
||||||
|
dp.TypeMeta.APIVersion = "apps/v1"
|
||||||
|
dp.TypeMeta.Kind = "Deployment"
|
||||||
|
raw, err := yaml.Marshal(i)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(raw), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header return resource header.
|
||||||
|
func (*Deployment) Header(ns string) Row {
|
||||||
|
hh := Row{}
|
||||||
|
if ns == AllNamespaces {
|
||||||
|
hh = append(hh, "NAMESPACE")
|
||||||
|
}
|
||||||
|
return append(hh, "NAME", "DESIRED", "CURRENT", "UP-TO-DATE", "AVAILABLE", "AGE")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fields retrieves displayable fields.
|
||||||
|
func (r *Deployment) Fields(ns string) Row {
|
||||||
|
ff := make([]string, 0, len(r.Header(ns)))
|
||||||
|
|
||||||
|
i := r.instance
|
||||||
|
if ns == AllNamespaces {
|
||||||
|
ff = append(ff, i.Namespace)
|
||||||
|
}
|
||||||
|
return append(ff,
|
||||||
|
i.Name,
|
||||||
|
strconv.Itoa(int(*i.Spec.Replicas)),
|
||||||
|
strconv.Itoa(int(i.Status.Replicas)),
|
||||||
|
strconv.Itoa(int(i.Status.UpdatedReplicas)),
|
||||||
|
strconv.Itoa(int(i.Status.AvailableReplicas)),
|
||||||
|
toAge(i.ObjectMeta.CreationTimestamp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtFields returns extended fields in relation to headers.
|
||||||
|
func (*Deployment) ExtFields() Properties {
|
||||||
|
return Properties{}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,197 @@
|
||||||
|
package resource_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/resource"
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
m "github.com/petergtz/pegomock"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDeploymentListAccess(t *testing.T) {
|
||||||
|
ns := "blee"
|
||||||
|
l := resource.NewDeploymentList(resource.AllNamespaces)
|
||||||
|
l.SetNamespace(ns)
|
||||||
|
|
||||||
|
assert.Equal(t, "blee", l.GetNamespace())
|
||||||
|
assert.Equal(t, "deploy", l.GetName())
|
||||||
|
for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} {
|
||||||
|
assert.True(t, l.Access(a))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeploymentHeader(t *testing.T) {
|
||||||
|
assert.Equal(t, resource.Row{"NAME", "DESIRED", "CURRENT", "UP-TO-DATE", "AVAILABLE", "AGE"}, newDeployment().Header("default"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeploymentFields(t *testing.T) {
|
||||||
|
r := newDeployment().Fields("blee")
|
||||||
|
assert.Equal(t, "fred", r[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeploymentMarshal(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.Get("blee", "fred")).ThenReturn(k8sDeployment(), nil)
|
||||||
|
|
||||||
|
cm := resource.NewDeploymentWithArgs(ca)
|
||||||
|
ma, err := cm.Marshal("blee/fred")
|
||||||
|
ca.VerifyWasCalledOnce().Get("blee", "fred")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, dpYaml(), ma)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeploymentListData(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.List(resource.NotNamespaced)).ThenReturn(k8s.Collection{*k8sDeployment()}, nil)
|
||||||
|
|
||||||
|
l := resource.NewDeploymentListWithArgs("-", resource.NewDeploymentWithArgs(ca))
|
||||||
|
// Make sure we can get deltas!
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
err := l.Reconcile()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ca.VerifyWasCalled(m.Times(2)).List(resource.NotNamespaced)
|
||||||
|
td := l.Data()
|
||||||
|
assert.Equal(t, 1, len(td.Rows))
|
||||||
|
assert.Equal(t, resource.NotNamespaced, l.GetNamespace())
|
||||||
|
assert.False(t, l.HasXRay())
|
||||||
|
row := td.Rows["blee/fred"]
|
||||||
|
assert.Equal(t, 6, len(row.Deltas))
|
||||||
|
for _, d := range row.Deltas {
|
||||||
|
assert.Equal(t, "", d)
|
||||||
|
}
|
||||||
|
assert.Equal(t, resource.Row{"fred"}, row.Fields[:1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeploymentListDescribe(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.Get("blee", "fred")).ThenReturn(k8sDeployment(), nil)
|
||||||
|
l := resource.NewDeploymentListWithArgs("blee", resource.NewDeploymentWithArgs(ca))
|
||||||
|
props, err := l.Describe("blee/fred")
|
||||||
|
|
||||||
|
ca.VerifyWasCalledOnce().Get("blee", "fred")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, 0, len(props))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
func k8sDeployment() *appsv1.Deployment {
|
||||||
|
var i int32 = 1
|
||||||
|
return &appsv1.Deployment{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "blee",
|
||||||
|
Name: "fred",
|
||||||
|
CreationTimestamp: metav1.Time{Time: testTime()},
|
||||||
|
},
|
||||||
|
Spec: appsv1.DeploymentSpec{
|
||||||
|
Replicas: &i,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDeployment() resource.Columnar {
|
||||||
|
return resource.NewDeployment().NewInstance(k8sDeployment())
|
||||||
|
}
|
||||||
|
|
||||||
|
func dpYaml() string {
|
||||||
|
return `typemeta:
|
||||||
|
kind: Deployment
|
||||||
|
apiversion: apps/v1
|
||||||
|
objectmeta:
|
||||||
|
name: fred
|
||||||
|
generatename: ""
|
||||||
|
namespace: blee
|
||||||
|
selflink: ""
|
||||||
|
uid: ""
|
||||||
|
resourceversion: ""
|
||||||
|
generation: 0
|
||||||
|
creationtimestamp: "2018-12-14T10:36:43.326972-07:00"
|
||||||
|
deletiontimestamp: null
|
||||||
|
deletiongraceperiodseconds: null
|
||||||
|
labels: {}
|
||||||
|
annotations: {}
|
||||||
|
ownerreferences: []
|
||||||
|
initializers: null
|
||||||
|
finalizers: []
|
||||||
|
clustername: ""
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector: null
|
||||||
|
template:
|
||||||
|
objectmeta:
|
||||||
|
name: ""
|
||||||
|
generatename: ""
|
||||||
|
namespace: ""
|
||||||
|
selflink: ""
|
||||||
|
uid: ""
|
||||||
|
resourceversion: ""
|
||||||
|
generation: 0
|
||||||
|
creationtimestamp: "0001-01-01T00:00:00Z"
|
||||||
|
deletiontimestamp: null
|
||||||
|
deletiongraceperiodseconds: null
|
||||||
|
labels: {}
|
||||||
|
annotations: {}
|
||||||
|
ownerreferences: []
|
||||||
|
initializers: null
|
||||||
|
finalizers: []
|
||||||
|
clustername: ""
|
||||||
|
spec:
|
||||||
|
volumes: []
|
||||||
|
initcontainers: []
|
||||||
|
containers: []
|
||||||
|
restartpolicy: ""
|
||||||
|
terminationgraceperiodseconds: null
|
||||||
|
activedeadlineseconds: null
|
||||||
|
dnspolicy: ""
|
||||||
|
nodeselector: {}
|
||||||
|
serviceaccountname: ""
|
||||||
|
deprecatedserviceaccount: ""
|
||||||
|
automountserviceaccounttoken: null
|
||||||
|
nodename: ""
|
||||||
|
hostnetwork: false
|
||||||
|
hostpid: false
|
||||||
|
hostipc: false
|
||||||
|
shareprocessnamespace: null
|
||||||
|
securitycontext: null
|
||||||
|
imagepullsecrets: []
|
||||||
|
hostname: ""
|
||||||
|
subdomain: ""
|
||||||
|
affinity: null
|
||||||
|
schedulername: ""
|
||||||
|
tolerations: []
|
||||||
|
hostaliases: []
|
||||||
|
priorityclassname: ""
|
||||||
|
priority: null
|
||||||
|
dnsconfig: null
|
||||||
|
readinessgates: []
|
||||||
|
runtimeclassname: null
|
||||||
|
enableservicelinks: null
|
||||||
|
strategy:
|
||||||
|
type: ""
|
||||||
|
rollingupdate: null
|
||||||
|
minreadyseconds: 0
|
||||||
|
revisionhistorylimit: null
|
||||||
|
paused: false
|
||||||
|
progressdeadlineseconds: null
|
||||||
|
status:
|
||||||
|
observedgeneration: 0
|
||||||
|
replicas: 0
|
||||||
|
updatedreplicas: 0
|
||||||
|
readyreplicas: 0
|
||||||
|
availablereplicas: 0
|
||||||
|
unavailablereplicas: 0
|
||||||
|
conditions: []
|
||||||
|
collisioncount: null
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
extv1beta1 "k8s.io/api/extensions/v1beta1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DaemonSet tracks a kubernetes resource.
|
||||||
|
type DaemonSet struct {
|
||||||
|
*Base
|
||||||
|
instance *extv1beta1.DaemonSet
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDaemonSetList returns a new resource list.
|
||||||
|
func NewDaemonSetList(ns string) List {
|
||||||
|
return NewDaemonSetListWithArgs(ns, NewDaemonSet())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDaemonSetListWithArgs returns a new resource list.
|
||||||
|
func NewDaemonSetListWithArgs(ns string, res Resource) List {
|
||||||
|
return newList(ns, "ds", res, AllVerbsAccess)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDaemonSet instantiates a new DaemonSet.
|
||||||
|
func NewDaemonSet() *DaemonSet {
|
||||||
|
return NewDaemonSetWithArgs(k8s.NewDaemonSet())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDaemonSetWithArgs instantiates a new DaemonSet.
|
||||||
|
func NewDaemonSetWithArgs(r k8s.Res) *DaemonSet {
|
||||||
|
cm := &DaemonSet{
|
||||||
|
Base: &Base{
|
||||||
|
caller: r,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cm.creator = cm
|
||||||
|
return cm
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInstance builds a new DaemonSet instance from a k8s resource.
|
||||||
|
func (*DaemonSet) NewInstance(i interface{}) Columnar {
|
||||||
|
cm := NewDaemonSet()
|
||||||
|
switch i.(type) {
|
||||||
|
case *extv1beta1.DaemonSet:
|
||||||
|
cm.instance = i.(*extv1beta1.DaemonSet)
|
||||||
|
case extv1beta1.DaemonSet:
|
||||||
|
ii := i.(extv1beta1.DaemonSet)
|
||||||
|
cm.instance = &ii
|
||||||
|
default:
|
||||||
|
log.Fatalf("Unknown %#v", i)
|
||||||
|
}
|
||||||
|
cm.path = cm.namespacedName(cm.instance.ObjectMeta)
|
||||||
|
return cm
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal resource to yaml.
|
||||||
|
func (r *DaemonSet) Marshal(path string) (string, error) {
|
||||||
|
ns, n := namespaced(path)
|
||||||
|
i, err := r.caller.Get(ns, n)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
dp := i.(*extv1beta1.DaemonSet)
|
||||||
|
dp.TypeMeta.APIVersion = "extensions/v1beta1"
|
||||||
|
dp.TypeMeta.Kind = "DaemonSet"
|
||||||
|
raw, err := yaml.Marshal(i)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(raw), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header return resource header.
|
||||||
|
func (*DaemonSet) Header(ns string) Row {
|
||||||
|
hh := Row{}
|
||||||
|
if ns == AllNamespaces {
|
||||||
|
hh = append(hh, "NAMESPACE")
|
||||||
|
}
|
||||||
|
hh = append(hh, "NAME", "DESIRED", "CURRENT", "READY", "UP-TO-DATE")
|
||||||
|
hh = append(hh, "AVAILABLE", "NODE_SELECTOR", "AGE")
|
||||||
|
return hh
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fields retrieves displayable fields.
|
||||||
|
func (r *DaemonSet) Fields(ns string) Row {
|
||||||
|
ff := make([]string, 0, len(r.Header(ns)))
|
||||||
|
|
||||||
|
i := r.instance
|
||||||
|
if ns == AllNamespaces {
|
||||||
|
ff = append(ff, i.Namespace)
|
||||||
|
}
|
||||||
|
return append(ff,
|
||||||
|
i.Name,
|
||||||
|
strconv.Itoa(int(i.Status.DesiredNumberScheduled)),
|
||||||
|
strconv.Itoa(int(i.Status.CurrentNumberScheduled)),
|
||||||
|
strconv.Itoa(int(i.Status.NumberReady)),
|
||||||
|
strconv.Itoa(int(i.Status.UpdatedNumberScheduled)),
|
||||||
|
strconv.Itoa(int(i.Status.NumberAvailable)),
|
||||||
|
mapToStr(i.Spec.Template.Spec.NodeSelector),
|
||||||
|
toAge(i.ObjectMeta.CreationTimestamp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtFields returns extended fields in relation to headers.
|
||||||
|
func (*DaemonSet) ExtFields() Properties {
|
||||||
|
return Properties{}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,207 @@
|
||||||
|
package resource_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/resource"
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
m "github.com/petergtz/pegomock"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
extv1beta1 "k8s.io/api/extensions/v1beta1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDSListAccess(t *testing.T) {
|
||||||
|
ns := "blee"
|
||||||
|
l := resource.NewDaemonSetList(resource.AllNamespaces)
|
||||||
|
l.SetNamespace(ns)
|
||||||
|
|
||||||
|
assert.Equal(t, "blee", l.GetNamespace())
|
||||||
|
assert.Equal(t, "ds", l.GetName())
|
||||||
|
for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} {
|
||||||
|
assert.True(t, l.Access(a))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDSHeader(t *testing.T) {
|
||||||
|
assert.Equal(t, resource.Row{"NAME", "DESIRED", "CURRENT", "READY", "UP-TO-DATE", "AVAILABLE", "NODE_SELECTOR", "AGE"}, newDS().Header("default"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDSFields(t *testing.T) {
|
||||||
|
r := newDS().Fields("blee")
|
||||||
|
assert.Equal(t, "fred", r[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDSMarshal(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.Get("blee", "fred")).ThenReturn(k8sDS(), nil)
|
||||||
|
|
||||||
|
cm := resource.NewDaemonSetWithArgs(ca)
|
||||||
|
ma, err := cm.Marshal("blee/fred")
|
||||||
|
ca.VerifyWasCalledOnce().Get("blee", "fred")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, dsYaml(), ma)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDSListData(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.List(resource.NotNamespaced)).ThenReturn(k8s.Collection{*k8sDS()}, nil)
|
||||||
|
|
||||||
|
l := resource.NewDaemonSetListWithArgs("-", resource.NewDaemonSetWithArgs(ca))
|
||||||
|
// Make sure we can get deltas!
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
err := l.Reconcile()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ca.VerifyWasCalled(m.Times(2)).List(resource.NotNamespaced)
|
||||||
|
td := l.Data()
|
||||||
|
assert.Equal(t, 1, len(td.Rows))
|
||||||
|
assert.Equal(t, resource.NotNamespaced, l.GetNamespace())
|
||||||
|
assert.False(t, l.HasXRay())
|
||||||
|
row := td.Rows["blee/fred"]
|
||||||
|
assert.Equal(t, 8, len(row.Deltas))
|
||||||
|
for _, d := range row.Deltas {
|
||||||
|
assert.Equal(t, "", d)
|
||||||
|
}
|
||||||
|
assert.Equal(t, resource.Row{"fred"}, row.Fields[:1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDSListDescribe(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.Get("blee", "fred")).ThenReturn(k8sDS(), nil)
|
||||||
|
l := resource.NewDaemonSetListWithArgs("blee", resource.NewDaemonSetWithArgs(ca))
|
||||||
|
props, err := l.Describe("blee/fred")
|
||||||
|
|
||||||
|
ca.VerifyWasCalledOnce().Get("blee", "fred")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, 0, len(props))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
func k8sDS() *extv1beta1.DaemonSet {
|
||||||
|
return &extv1beta1.DaemonSet{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "blee",
|
||||||
|
Name: "fred",
|
||||||
|
CreationTimestamp: metav1.Time{Time: testTime()},
|
||||||
|
},
|
||||||
|
Spec: extv1beta1.DaemonSetSpec{
|
||||||
|
Selector: &metav1.LabelSelector{
|
||||||
|
MatchLabels: map[string]string{"fred": "blee"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Status: extv1beta1.DaemonSetStatus{
|
||||||
|
DesiredNumberScheduled: 1,
|
||||||
|
CurrentNumberScheduled: 1,
|
||||||
|
NumberReady: 1,
|
||||||
|
NumberAvailable: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDS() resource.Columnar {
|
||||||
|
return resource.NewDaemonSet().NewInstance(k8sDS())
|
||||||
|
}
|
||||||
|
|
||||||
|
func dsYaml() string {
|
||||||
|
return `typemeta:
|
||||||
|
kind: DaemonSet
|
||||||
|
apiversion: extensions/v1beta1
|
||||||
|
objectmeta:
|
||||||
|
name: fred
|
||||||
|
generatename: ""
|
||||||
|
namespace: blee
|
||||||
|
selflink: ""
|
||||||
|
uid: ""
|
||||||
|
resourceversion: ""
|
||||||
|
generation: 0
|
||||||
|
creationtimestamp: "2018-12-14T10:36:43.326972-07:00"
|
||||||
|
deletiontimestamp: null
|
||||||
|
deletiongraceperiodseconds: null
|
||||||
|
labels: {}
|
||||||
|
annotations: {}
|
||||||
|
ownerreferences: []
|
||||||
|
initializers: null
|
||||||
|
finalizers: []
|
||||||
|
clustername: ""
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
matchlabels:
|
||||||
|
fred: blee
|
||||||
|
matchexpressions: []
|
||||||
|
template:
|
||||||
|
objectmeta:
|
||||||
|
name: ""
|
||||||
|
generatename: ""
|
||||||
|
namespace: ""
|
||||||
|
selflink: ""
|
||||||
|
uid: ""
|
||||||
|
resourceversion: ""
|
||||||
|
generation: 0
|
||||||
|
creationtimestamp: "0001-01-01T00:00:00Z"
|
||||||
|
deletiontimestamp: null
|
||||||
|
deletiongraceperiodseconds: null
|
||||||
|
labels: {}
|
||||||
|
annotations: {}
|
||||||
|
ownerreferences: []
|
||||||
|
initializers: null
|
||||||
|
finalizers: []
|
||||||
|
clustername: ""
|
||||||
|
spec:
|
||||||
|
volumes: []
|
||||||
|
initcontainers: []
|
||||||
|
containers: []
|
||||||
|
restartpolicy: ""
|
||||||
|
terminationgraceperiodseconds: null
|
||||||
|
activedeadlineseconds: null
|
||||||
|
dnspolicy: ""
|
||||||
|
nodeselector: {}
|
||||||
|
serviceaccountname: ""
|
||||||
|
deprecatedserviceaccount: ""
|
||||||
|
automountserviceaccounttoken: null
|
||||||
|
nodename: ""
|
||||||
|
hostnetwork: false
|
||||||
|
hostpid: false
|
||||||
|
hostipc: false
|
||||||
|
shareprocessnamespace: null
|
||||||
|
securitycontext: null
|
||||||
|
imagepullsecrets: []
|
||||||
|
hostname: ""
|
||||||
|
subdomain: ""
|
||||||
|
affinity: null
|
||||||
|
schedulername: ""
|
||||||
|
tolerations: []
|
||||||
|
hostaliases: []
|
||||||
|
priorityclassname: ""
|
||||||
|
priority: null
|
||||||
|
dnsconfig: null
|
||||||
|
readinessgates: []
|
||||||
|
runtimeclassname: null
|
||||||
|
enableservicelinks: null
|
||||||
|
updatestrategy:
|
||||||
|
type: ""
|
||||||
|
rollingupdate: null
|
||||||
|
minreadyseconds: 0
|
||||||
|
templategeneration: 0
|
||||||
|
revisionhistorylimit: null
|
||||||
|
status:
|
||||||
|
currentnumberscheduled: 1
|
||||||
|
numbermisscheduled: 0
|
||||||
|
desirednumberscheduled: 1
|
||||||
|
numberready: 1
|
||||||
|
observedgeneration: 0
|
||||||
|
updatednumberscheduled: 0
|
||||||
|
numberavailable: 1
|
||||||
|
numberunavailable: 0
|
||||||
|
collisioncount: null
|
||||||
|
conditions: []
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,133 @@
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
"k8s.io/api/core/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Endpoints tracks a kubernetes resource.
|
||||||
|
type Endpoints struct {
|
||||||
|
*Base
|
||||||
|
instance *v1.Endpoints
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEndpointsList returns a new resource list.
|
||||||
|
func NewEndpointsList(ns string) List {
|
||||||
|
return NewEndpointsListWithArgs(ns, NewEndpoints())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEndpointsListWithArgs returns a new resource list.
|
||||||
|
func NewEndpointsListWithArgs(ns string, res Resource) List {
|
||||||
|
return newList(ns, "ep", res, AllVerbsAccess)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEndpoints instantiates a new Endpoint.
|
||||||
|
func NewEndpoints() *Endpoints {
|
||||||
|
return NewEndpointsWithArgs(k8s.NewEndpoints())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEndpointsWithArgs instantiates a new Endpoint.
|
||||||
|
func NewEndpointsWithArgs(r k8s.Res) *Endpoints {
|
||||||
|
ep := &Endpoints{
|
||||||
|
Base: &Base{
|
||||||
|
caller: r,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ep.creator = ep
|
||||||
|
return ep
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInstance builds a new Endpoint instance from a k8s resource.
|
||||||
|
func (*Endpoints) NewInstance(i interface{}) Columnar {
|
||||||
|
cm := NewEndpoints()
|
||||||
|
switch i.(type) {
|
||||||
|
case *v1.Endpoints:
|
||||||
|
cm.instance = i.(*v1.Endpoints)
|
||||||
|
case v1.Endpoints:
|
||||||
|
ii := i.(v1.Endpoints)
|
||||||
|
cm.instance = &ii
|
||||||
|
default:
|
||||||
|
log.Fatalf("Unknown %#v", i)
|
||||||
|
}
|
||||||
|
cm.path = cm.namespacedName(cm.instance.ObjectMeta)
|
||||||
|
return cm
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal resource to yaml.
|
||||||
|
func (r *Endpoints) Marshal(path string) (string, error) {
|
||||||
|
ns, n := namespaced(path)
|
||||||
|
i, err := r.caller.Get(ns, n)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
ep := i.(*v1.Endpoints)
|
||||||
|
ep.TypeMeta.APIVersion = "v1"
|
||||||
|
ep.TypeMeta.Kind = "Endpoints"
|
||||||
|
raw, err := yaml.Marshal(ep)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(raw), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header return resource header.
|
||||||
|
func (*Endpoints) Header(ns string) Row {
|
||||||
|
hh := Row{}
|
||||||
|
if ns == AllNamespaces {
|
||||||
|
hh = append(hh, "NAMESPACE")
|
||||||
|
}
|
||||||
|
return append(hh, "NAME", "ENDPOINTS", "AGE")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fields retrieves displayable fields.
|
||||||
|
func (r *Endpoints) Fields(ns string) Row {
|
||||||
|
ff := make(Row, 0, len(r.Header(ns)))
|
||||||
|
i := r.instance
|
||||||
|
if ns == AllNamespaces {
|
||||||
|
ff = append(ff, i.Namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(ff,
|
||||||
|
i.Name,
|
||||||
|
missing(r.toEPs(i.Subsets)),
|
||||||
|
toAge(i.ObjectMeta.CreationTimestamp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtFields returns extended fields in relation to headers.
|
||||||
|
func (*Endpoints) ExtFields() Properties {
|
||||||
|
return Properties{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Endpoints) toEPs(ss []v1.EndpointSubset) string {
|
||||||
|
aa := make([]string, 0, len(ss))
|
||||||
|
max := 3
|
||||||
|
for _, s := range ss {
|
||||||
|
pp := make([]string, 0, len(s.Ports))
|
||||||
|
for _, p := range s.Ports {
|
||||||
|
pp = append(pp, strconv.Itoa(int(p.Port)))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, a := range s.Addresses {
|
||||||
|
if len(a.IP) != 0 {
|
||||||
|
if len(pp) == 0 {
|
||||||
|
aa = append(aa, fmt.Sprintf("%s", a.IP))
|
||||||
|
} else {
|
||||||
|
add := fmt.Sprintf("%s:%s", a.IP, strings.Join(pp, ","))
|
||||||
|
if len(pp) > max {
|
||||||
|
add = fmt.Sprintf("%s:%s...", a.IP, strings.Join(pp[:max], ","))
|
||||||
|
}
|
||||||
|
aa = append(aa, add)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(aa, ",")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,146 @@
|
||||||
|
package resource_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/resource"
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
m "github.com/petergtz/pegomock"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEndpointsListAccess(t *testing.T) {
|
||||||
|
ns := "blee"
|
||||||
|
l := resource.NewEndpointsList(resource.AllNamespaces)
|
||||||
|
l.SetNamespace(ns)
|
||||||
|
|
||||||
|
assert.Equal(t, "blee", l.GetNamespace())
|
||||||
|
assert.Equal(t, "ep", l.GetName())
|
||||||
|
for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} {
|
||||||
|
assert.True(t, l.Access(a))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEndpointsHeader(t *testing.T) {
|
||||||
|
assert.Equal(t, resource.Row{"NAME", "ENDPOINTS", "AGE"}, newEndpoints().Header("default"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEndpointsFields(t *testing.T) {
|
||||||
|
r := newEndpoints().Fields("blee")
|
||||||
|
assert.Equal(t, "fred", r[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEndpointsMarshal(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.Get("blee", "fred")).ThenReturn(k8sEndpoints(), nil)
|
||||||
|
|
||||||
|
cm := resource.NewEndpointsWithArgs(ca)
|
||||||
|
ma, err := cm.Marshal("blee/fred")
|
||||||
|
ca.VerifyWasCalledOnce().Get("blee", "fred")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, epYaml(), ma)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEndpointsListData(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.List(resource.NotNamespaced)).ThenReturn(k8s.Collection{*k8sEndpoints()}, nil)
|
||||||
|
|
||||||
|
l := resource.NewEndpointsListWithArgs("-", resource.NewEndpointsWithArgs(ca))
|
||||||
|
// Make sure we can get deltas!
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
err := l.Reconcile()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ca.VerifyWasCalled(m.Times(2)).List(resource.NotNamespaced)
|
||||||
|
td := l.Data()
|
||||||
|
assert.Equal(t, 1, len(td.Rows))
|
||||||
|
assert.Equal(t, resource.NotNamespaced, l.GetNamespace())
|
||||||
|
assert.False(t, l.HasXRay())
|
||||||
|
row := td.Rows["blee/fred"]
|
||||||
|
assert.Equal(t, 3, len(row.Deltas))
|
||||||
|
for _, d := range row.Deltas {
|
||||||
|
assert.Equal(t, "", d)
|
||||||
|
}
|
||||||
|
assert.Equal(t, resource.Row{"fred"}, row.Fields[:1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEndpointsListDescribe(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.Get("blee", "fred")).ThenReturn(k8sEndpoints(), nil)
|
||||||
|
l := resource.NewEndpointsListWithArgs("blee", resource.NewEndpointsWithArgs(ca))
|
||||||
|
props, err := l.Describe("blee/fred")
|
||||||
|
|
||||||
|
ca.VerifyWasCalledOnce().Get("blee", "fred")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, 0, len(props))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
func k8sEndpoints() *v1.Endpoints {
|
||||||
|
return &v1.Endpoints{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "blee",
|
||||||
|
Name: "fred",
|
||||||
|
CreationTimestamp: metav1.Time{Time: testTime()},
|
||||||
|
},
|
||||||
|
Subsets: []v1.EndpointSubset{
|
||||||
|
{
|
||||||
|
Addresses: []v1.EndpointAddress{
|
||||||
|
{IP: "1.1.1.1"},
|
||||||
|
},
|
||||||
|
Ports: []v1.EndpointPort{
|
||||||
|
{Port: 80, Protocol: "TCP"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newEndpoints() resource.Columnar {
|
||||||
|
return resource.NewEndpoints().NewInstance(k8sEndpoints())
|
||||||
|
}
|
||||||
|
|
||||||
|
func epYaml() string {
|
||||||
|
return `typemeta:
|
||||||
|
kind: Endpoints
|
||||||
|
apiversion: v1
|
||||||
|
objectmeta:
|
||||||
|
name: fred
|
||||||
|
generatename: ""
|
||||||
|
namespace: blee
|
||||||
|
selflink: ""
|
||||||
|
uid: ""
|
||||||
|
resourceversion: ""
|
||||||
|
generation: 0
|
||||||
|
creationtimestamp: "2018-12-14T10:36:43.326972-07:00"
|
||||||
|
deletiontimestamp: null
|
||||||
|
deletiongraceperiodseconds: null
|
||||||
|
labels: {}
|
||||||
|
annotations: {}
|
||||||
|
ownerreferences: []
|
||||||
|
initializers: null
|
||||||
|
finalizers: []
|
||||||
|
clustername: ""
|
||||||
|
subsets:
|
||||||
|
- addresses:
|
||||||
|
- ip: 1.1.1.1
|
||||||
|
hostname: ""
|
||||||
|
nodename: null
|
||||||
|
targetref: null
|
||||||
|
notreadyaddresses: []
|
||||||
|
ports:
|
||||||
|
- name: ""
|
||||||
|
port: 80
|
||||||
|
protocol: TCP
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,152 @@
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
"k8s.io/api/core/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Event tracks a kubernetes resource.
|
||||||
|
type Event struct {
|
||||||
|
*Base
|
||||||
|
instance *v1.Event
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEventList returns a new resource list.
|
||||||
|
func NewEventList(ns string) List {
|
||||||
|
return NewEventListWithArgs(ns, NewEvent())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEventListWithArgs returns a new resource list.
|
||||||
|
func NewEventListWithArgs(ns string, res Resource) List {
|
||||||
|
return newList(ns, "event", res, ListAccess+NamespaceAccess)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEvent instantiates a new Endpoint.
|
||||||
|
func NewEvent() *Event {
|
||||||
|
return NewEventWithArgs(k8s.NewEvent())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEventWithArgs instantiates a new Endpoint.
|
||||||
|
func NewEventWithArgs(r k8s.Res) *Event {
|
||||||
|
ep := &Event{
|
||||||
|
Base: &Base{
|
||||||
|
caller: r,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ep.creator = ep
|
||||||
|
return ep
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInstance builds a new Endpoint instance from a k8s resource.
|
||||||
|
func (*Event) NewInstance(i interface{}) Columnar {
|
||||||
|
cm := NewEvent()
|
||||||
|
switch i.(type) {
|
||||||
|
case *v1.Event:
|
||||||
|
cm.instance = i.(*v1.Event)
|
||||||
|
case v1.Event:
|
||||||
|
ii := i.(v1.Event)
|
||||||
|
cm.instance = &ii
|
||||||
|
default:
|
||||||
|
log.Fatalf("Unknown %#v", i)
|
||||||
|
}
|
||||||
|
cm.path = cm.namespacedName(cm.instance.ObjectMeta)
|
||||||
|
return cm
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal resource to yaml.
|
||||||
|
func (r *Event) Marshal(path string) (string, error) {
|
||||||
|
ns, n := namespaced(path)
|
||||||
|
i, err := r.caller.Get(ns, n)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
ev := i.(*v1.Event)
|
||||||
|
ev.TypeMeta.APIVersion = "v1"
|
||||||
|
ev.TypeMeta.Kind = "Event"
|
||||||
|
raw, err := yaml.Marshal(ev)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(raw), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// // Get resource given a namespaced name.
|
||||||
|
// func (r *Event) Get(path string) (Columnar, error) {
|
||||||
|
// ns, n := namespaced(path)
|
||||||
|
// i, err := r.caller.Get(ns, n)
|
||||||
|
// if err != nil {
|
||||||
|
// return nil, err
|
||||||
|
// }
|
||||||
|
// return r.NewInstance(i), nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Delete a resource by name.
|
||||||
|
func (r *Event) Delete(path string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header return resource header.
|
||||||
|
func (*Event) Header(ns string) Row {
|
||||||
|
ff := Row{""}
|
||||||
|
if ns == AllNamespaces {
|
||||||
|
ff = append(ff, "NAMESPACE")
|
||||||
|
}
|
||||||
|
return append(ff, "NAME", "REASON", "SOURCE", "COUNT", "MESSAGE", "AGE")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fields returns display fields.
|
||||||
|
func (r *Event) Fields(ns string) Row {
|
||||||
|
ff := make(Row, 0, len(r.Header(ns)))
|
||||||
|
i := r.instance
|
||||||
|
|
||||||
|
ff = append(ff, r.toEmoji(i.Type, i.Reason))
|
||||||
|
if ns == AllNamespaces {
|
||||||
|
ff = append(ff, i.Namespace)
|
||||||
|
}
|
||||||
|
rx := regexp.MustCompile(`(.+)\.(.+)`)
|
||||||
|
return append(ff,
|
||||||
|
// i.Name,
|
||||||
|
rx.ReplaceAllString(i.Name, `$1`),
|
||||||
|
i.Reason,
|
||||||
|
i.Source.Component,
|
||||||
|
strconv.Itoa(int(i.Count)),
|
||||||
|
Truncate(i.Message, 50),
|
||||||
|
toAge(i.LastTimestamp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtFields returns extended fields in relation to headers.
|
||||||
|
func (*Event) ExtFields() Properties {
|
||||||
|
return Properties{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
func (*Event) toEmoji(t, r string) string {
|
||||||
|
switch t {
|
||||||
|
case "Warning":
|
||||||
|
switch r {
|
||||||
|
case "Failed":
|
||||||
|
return "😡"
|
||||||
|
case "Killing":
|
||||||
|
return "👿"
|
||||||
|
default:
|
||||||
|
return "😡"
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
switch r {
|
||||||
|
case "Killing":
|
||||||
|
return "👿"
|
||||||
|
case "BackOff":
|
||||||
|
return "👹"
|
||||||
|
default:
|
||||||
|
return "😮"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
package resource_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/resource"
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
m "github.com/petergtz/pegomock"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEventListAccess(t *testing.T) {
|
||||||
|
ns := "blee"
|
||||||
|
l := resource.NewEventList(resource.AllNamespaces)
|
||||||
|
l.SetNamespace(ns)
|
||||||
|
|
||||||
|
assert.Equal(t, "blee", l.GetNamespace())
|
||||||
|
assert.Equal(t, "event", l.GetName())
|
||||||
|
for _, a := range []int{resource.ListAccess, resource.NamespaceAccess} {
|
||||||
|
assert.True(t, l.Access(a))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEventHeader(t *testing.T) {
|
||||||
|
assert.Equal(t, resource.Row{"", "NAME", "REASON", "SOURCE", "COUNT", "MESSAGE", "AGE"}, newEvent().Header("default"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEventFields(t *testing.T) {
|
||||||
|
r := newEvent().Fields("blee")
|
||||||
|
assert.Equal(t, resource.Row{"😮", "fred", "blah", "", "1"}, r[:5])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEventMarshal(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.Get("blee", "fred")).ThenReturn(k8sEvent(), nil)
|
||||||
|
|
||||||
|
cm := resource.NewEventWithArgs(ca)
|
||||||
|
ma, err := cm.Marshal("blee/fred")
|
||||||
|
ca.VerifyWasCalledOnce().Get("blee", "fred")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, evYaml(), ma)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEventListData(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.List(resource.NotNamespaced)).ThenReturn(k8s.Collection{*k8sEvent()}, nil)
|
||||||
|
|
||||||
|
l := resource.NewEventListWithArgs("-", resource.NewEventWithArgs(ca))
|
||||||
|
// Make sure we can get deltas!
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
err := l.Reconcile()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ca.VerifyWasCalled(m.Times(2)).List(resource.NotNamespaced)
|
||||||
|
td := l.Data()
|
||||||
|
assert.Equal(t, 1, len(td.Rows))
|
||||||
|
assert.Equal(t, resource.NotNamespaced, l.GetNamespace())
|
||||||
|
assert.False(t, l.HasXRay())
|
||||||
|
row := td.Rows["blee/fred"]
|
||||||
|
assert.Equal(t, 7, len(row.Deltas))
|
||||||
|
for _, d := range row.Deltas {
|
||||||
|
assert.Equal(t, "", d)
|
||||||
|
}
|
||||||
|
assert.Equal(t, resource.Row{"😮"}, row.Fields[:1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEventListDescribe(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.Get("blee", "fred")).ThenReturn(k8sEvent(), nil)
|
||||||
|
l := resource.NewEventListWithArgs("blee", resource.NewEventWithArgs(ca))
|
||||||
|
props, err := l.Describe("blee/fred")
|
||||||
|
|
||||||
|
ca.VerifyWasCalledOnce().Get("blee", "fred")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, 0, len(props))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
func k8sEvent() *v1.Event {
|
||||||
|
return &v1.Event{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "blee",
|
||||||
|
Name: "fred",
|
||||||
|
CreationTimestamp: metav1.Time{Time: testTime()},
|
||||||
|
},
|
||||||
|
Reason: "blah",
|
||||||
|
Message: "blee",
|
||||||
|
Count: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newEvent() resource.Columnar {
|
||||||
|
return resource.NewEvent().NewInstance(k8sEvent())
|
||||||
|
}
|
||||||
|
|
||||||
|
func evYaml() string {
|
||||||
|
return `typemeta:
|
||||||
|
kind: Event
|
||||||
|
apiversion: v1
|
||||||
|
objectmeta:
|
||||||
|
name: fred
|
||||||
|
generatename: ""
|
||||||
|
namespace: blee
|
||||||
|
selflink: ""
|
||||||
|
uid: ""
|
||||||
|
resourceversion: ""
|
||||||
|
generation: 0
|
||||||
|
creationtimestamp: "2018-12-14T10:36:43.326972-07:00"
|
||||||
|
deletiontimestamp: null
|
||||||
|
deletiongraceperiodseconds: null
|
||||||
|
labels: {}
|
||||||
|
annotations: {}
|
||||||
|
ownerreferences: []
|
||||||
|
initializers: null
|
||||||
|
finalizers: []
|
||||||
|
clustername: ""
|
||||||
|
involvedobject:
|
||||||
|
kind: ""
|
||||||
|
namespace: ""
|
||||||
|
name: ""
|
||||||
|
uid: ""
|
||||||
|
apiversion: ""
|
||||||
|
resourceversion: ""
|
||||||
|
fieldpath: ""
|
||||||
|
reason: blah
|
||||||
|
message: blee
|
||||||
|
source:
|
||||||
|
component: ""
|
||||||
|
host: ""
|
||||||
|
firsttimestamp: "0001-01-01T00:00:00Z"
|
||||||
|
lasttimestamp: "0001-01-01T00:00:00Z"
|
||||||
|
count: 1
|
||||||
|
type: ""
|
||||||
|
eventtime: "0001-01-01T00:00:00Z"
|
||||||
|
series: null
|
||||||
|
action: ""
|
||||||
|
related: null
|
||||||
|
reportingcontroller: ""
|
||||||
|
reportinginstance: ""
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/k8sland/tview"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/util/duration"
|
||||||
|
"k8s.io/apimachinery/pkg/watch"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// AllNamespaces indicator to retrieve K8s resource for all namespaces
|
||||||
|
AllNamespaces = ""
|
||||||
|
// NotNamespaced indicator for non namespaced resource.
|
||||||
|
NotNamespaced = "-"
|
||||||
|
|
||||||
|
// New track new resource events.
|
||||||
|
New watch.EventType = "NEW"
|
||||||
|
// Unchanged provides no change events.
|
||||||
|
Unchanged watch.EventType = "UNCHANGED"
|
||||||
|
|
||||||
|
// MissingValue indicates an unset value.
|
||||||
|
MissingValue = "<none>"
|
||||||
|
// NAValue indicates a value that does not pertain.
|
||||||
|
NAValue = "<n/a>"
|
||||||
|
)
|
||||||
|
|
||||||
|
func namespaced(n string) (string, string) {
|
||||||
|
ns, po := path.Split(n)
|
||||||
|
return strings.Trim(ns, "/"), po
|
||||||
|
}
|
||||||
|
|
||||||
|
func missing(s string) string {
|
||||||
|
return check(s, MissingValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func na(s string) string {
|
||||||
|
return check(s, NAValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func check(s, sub string) string {
|
||||||
|
if len(s) == 0 {
|
||||||
|
return sub
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func intToStr(i int64) string {
|
||||||
|
return strconv.Itoa(int(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
func boolToStr(b bool) string {
|
||||||
|
switch b {
|
||||||
|
case true:
|
||||||
|
return "true"
|
||||||
|
default:
|
||||||
|
return "false"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toAge(timestamp metav1.Time) string {
|
||||||
|
if timestamp.IsZero() {
|
||||||
|
return "<unknown>"
|
||||||
|
}
|
||||||
|
return duration.HumanDuration(time.Since(timestamp.Time))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pad a string up to the given length.
|
||||||
|
func Pad(s string, l int) string {
|
||||||
|
fmat := "%-" + strconv.Itoa(l) + "s"
|
||||||
|
return fmt.Sprintf(fmat, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Truncate a string to the given l and suffix ellipsis if needed.
|
||||||
|
func Truncate(s string, l int) string {
|
||||||
|
if len(s) > l {
|
||||||
|
fmat := "%." + strconv.Itoa(l) + "s%s"
|
||||||
|
return fmt.Sprintf(fmat, s, string(tview.SemigraphicsHorizontalEllipsis))
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapToStr(m map[string]string) (s string) {
|
||||||
|
if len(m) == 0 {
|
||||||
|
return MissingValue
|
||||||
|
}
|
||||||
|
|
||||||
|
kk := make([]string, 0, len(m))
|
||||||
|
for k := range m {
|
||||||
|
kk = append(kk, k)
|
||||||
|
}
|
||||||
|
sort.Strings(kk)
|
||||||
|
|
||||||
|
for i, k := range kk {
|
||||||
|
s += k + "=" + m[k]
|
||||||
|
if i < len(kk)-1 {
|
||||||
|
s += ","
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNamespaced(t *testing.T) {
|
||||||
|
uu := []struct {
|
||||||
|
p, ns, n string
|
||||||
|
}{
|
||||||
|
{"fred/blee", "fred", "blee"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, u := range uu {
|
||||||
|
ns, n := namespaced(u.p)
|
||||||
|
assert.Equal(t, u.ns, ns)
|
||||||
|
assert.Equal(t, u.n, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMissing(t *testing.T) {
|
||||||
|
uu := []struct {
|
||||||
|
i, e string
|
||||||
|
}{
|
||||||
|
{"fred", "fred"},
|
||||||
|
{"", MissingValue},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, u := range uu {
|
||||||
|
assert.Equal(t, u.e, missing(u.i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoolToStr(t *testing.T) {
|
||||||
|
uu := []struct {
|
||||||
|
i bool
|
||||||
|
e string
|
||||||
|
}{
|
||||||
|
{true, "true"},
|
||||||
|
{false, "false"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, u := range uu {
|
||||||
|
assert.Equal(t, u.e, boolToStr(u.i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNa(t *testing.T) {
|
||||||
|
uu := []struct {
|
||||||
|
i, e string
|
||||||
|
}{
|
||||||
|
{"fred", "fred"},
|
||||||
|
{"", NAValue},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, u := range uu {
|
||||||
|
assert.Equal(t, u.e, na(u.i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPad(t *testing.T) {
|
||||||
|
uu := []struct {
|
||||||
|
s string
|
||||||
|
l int
|
||||||
|
e string
|
||||||
|
}{
|
||||||
|
{"fred", 10, "fred "},
|
||||||
|
{"fred", 6, "fred "},
|
||||||
|
{"fred", 4, "fred"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, u := range uu {
|
||||||
|
assert.Equal(t, u.e, Pad(u.s, u.l))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTruncate(t *testing.T) {
|
||||||
|
uu := []struct {
|
||||||
|
s string
|
||||||
|
l int
|
||||||
|
e string
|
||||||
|
}{
|
||||||
|
{"fred", 2, "fr…"},
|
||||||
|
{"fred", 1, "f…"},
|
||||||
|
{"fred", 10, "fred"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, u := range uu {
|
||||||
|
assert.Equal(t, u.e, Truncate(u.s, u.l))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMapToStr(t *testing.T) {
|
||||||
|
uu := []struct {
|
||||||
|
i map[string]string
|
||||||
|
e string
|
||||||
|
}{
|
||||||
|
{map[string]string{"blee": "duh", "aa": "bb"}, "aa=bb,blee=duh"},
|
||||||
|
{map[string]string{}, MissingValue},
|
||||||
|
}
|
||||||
|
for _, u := range uu {
|
||||||
|
assert.Equal(t, u.e, mapToStr(u.i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMapToStr(b *testing.B) {
|
||||||
|
ll := map[string]string{
|
||||||
|
"blee": "duh",
|
||||||
|
"aa": "bb",
|
||||||
|
}
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
mapToStr(ll)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
"k8s.io/api/autoscaling/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HPA tracks a kubernetes resource.
|
||||||
|
type HPA struct {
|
||||||
|
*Base
|
||||||
|
instance *v1.HorizontalPodAutoscaler
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHPAList returns a new resource list.
|
||||||
|
func NewHPAList(ns string) List {
|
||||||
|
return NewHPAListWithArgs(ns, NewHPA())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHPAListWithArgs returns a new resource list.
|
||||||
|
func NewHPAListWithArgs(ns string, res Resource) List {
|
||||||
|
return newList(ns, "hpa", res, AllVerbsAccess)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHPA instantiates a new Endpoint.
|
||||||
|
func NewHPA() *HPA {
|
||||||
|
return NewHPAWithArgs(k8s.NewHPA())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHPAWithArgs instantiates a new Endpoint.
|
||||||
|
func NewHPAWithArgs(r k8s.Res) *HPA {
|
||||||
|
ep := &HPA{
|
||||||
|
Base: &Base{
|
||||||
|
caller: r,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ep.creator = ep
|
||||||
|
return ep
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInstance builds a new Endpoint instance from a k8s resource.
|
||||||
|
func (*HPA) NewInstance(i interface{}) Columnar {
|
||||||
|
cm := NewHPA()
|
||||||
|
switch i.(type) {
|
||||||
|
case *v1.HorizontalPodAutoscaler:
|
||||||
|
cm.instance = i.(*v1.HorizontalPodAutoscaler)
|
||||||
|
case v1.HorizontalPodAutoscaler:
|
||||||
|
ii := i.(v1.HorizontalPodAutoscaler)
|
||||||
|
cm.instance = &ii
|
||||||
|
default:
|
||||||
|
log.Fatalf("Unknown %#v", i)
|
||||||
|
}
|
||||||
|
cm.path = cm.namespacedName(cm.instance.ObjectMeta)
|
||||||
|
return cm
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal resource to yaml.
|
||||||
|
func (r *HPA) Marshal(path string) (string, error) {
|
||||||
|
ns, n := namespaced(path)
|
||||||
|
i, err := r.caller.Get(ns, n)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
hpa := i.(*v1.HorizontalPodAutoscaler)
|
||||||
|
hpa.TypeMeta.APIVersion = "autoscaling/v1"
|
||||||
|
hpa.TypeMeta.Kind = "HPA"
|
||||||
|
raw, err := yaml.Marshal(i)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(raw), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header return resource header.
|
||||||
|
func (*HPA) Header(ns string) Row {
|
||||||
|
hh := Row{}
|
||||||
|
if ns == AllNamespaces {
|
||||||
|
hh = append(hh, "NAMESPACE")
|
||||||
|
}
|
||||||
|
return append(hh,
|
||||||
|
"NAME",
|
||||||
|
"REFERENCE",
|
||||||
|
"TARGETS",
|
||||||
|
"MINPODS",
|
||||||
|
"MAXPODS",
|
||||||
|
"REPLICAS",
|
||||||
|
"AGE")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fields retrieves displayable fields.
|
||||||
|
func (r *HPA) Fields(ns string) Row {
|
||||||
|
ff := make(Row, 0, len(r.Header(ns)))
|
||||||
|
|
||||||
|
i := r.instance
|
||||||
|
if ns == AllNamespaces {
|
||||||
|
ff = append(ff, i.Namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
target := "<unknown>"
|
||||||
|
if i.Status.CurrentCPUUtilizationPercentage != nil {
|
||||||
|
target = strconv.Itoa(int(*i.Status.CurrentCPUUtilizationPercentage))
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(ff,
|
||||||
|
i.ObjectMeta.Name,
|
||||||
|
i.Spec.ScaleTargetRef.Name,
|
||||||
|
fmt.Sprintf("%s٪/%d٪", target, *i.Spec.TargetCPUUtilizationPercentage),
|
||||||
|
strconv.Itoa(int(*i.Spec.MinReplicas)),
|
||||||
|
strconv.Itoa(int(i.Spec.MaxReplicas)),
|
||||||
|
strconv.Itoa(int(i.Status.CurrentReplicas)),
|
||||||
|
toAge(i.ObjectMeta.CreationTimestamp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtFields returns extended fields in relation to headers.
|
||||||
|
func (*HPA) ExtFields() Properties {
|
||||||
|
return Properties{}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,153 @@
|
||||||
|
package resource_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/resource"
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
m "github.com/petergtz/pegomock"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"k8s.io/api/autoscaling/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHPAListAccess(t *testing.T) {
|
||||||
|
ns := "blee"
|
||||||
|
l := resource.NewHPAList(resource.AllNamespaces)
|
||||||
|
l.SetNamespace(ns)
|
||||||
|
|
||||||
|
assert.Equal(t, "blee", l.GetNamespace())
|
||||||
|
assert.Equal(t, "hpa", l.GetName())
|
||||||
|
for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} {
|
||||||
|
assert.True(t, l.Access(a))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHPAHeader(t *testing.T) {
|
||||||
|
assert.Equal(t, resource.Row{"NAME", "REFERENCE", "TARGETS", "MINPODS", "MAXPODS", "REPLICAS", "AGE"}, newHPA().Header("default"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHPAFields(t *testing.T) {
|
||||||
|
r := newHPA().Fields("blee")
|
||||||
|
assert.Equal(t, "fred", r[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHPAMarshal(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.Get("blee", "fred")).ThenReturn(k8sHPA(), nil)
|
||||||
|
|
||||||
|
cm := resource.NewHPAWithArgs(ca)
|
||||||
|
ma, err := cm.Marshal("blee/fred")
|
||||||
|
ca.VerifyWasCalledOnce().Get("blee", "fred")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, hpaYaml(), ma)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHPAListData(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.List(resource.NotNamespaced)).ThenReturn(k8s.Collection{*k8sHPA()}, nil)
|
||||||
|
|
||||||
|
l := resource.NewHPAListWithArgs("-", resource.NewHPAWithArgs(ca))
|
||||||
|
// Make sure we can get deltas!
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
err := l.Reconcile()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ca.VerifyWasCalled(m.Times(2)).List(resource.NotNamespaced)
|
||||||
|
td := l.Data()
|
||||||
|
assert.Equal(t, 1, len(td.Rows))
|
||||||
|
assert.Equal(t, resource.NotNamespaced, l.GetNamespace())
|
||||||
|
assert.False(t, l.HasXRay())
|
||||||
|
row := td.Rows["blee/fred"]
|
||||||
|
assert.Equal(t, 7, len(row.Deltas))
|
||||||
|
for _, d := range row.Deltas {
|
||||||
|
assert.Equal(t, "", d)
|
||||||
|
}
|
||||||
|
assert.Equal(t, resource.Row{"fred"}, row.Fields[:1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHPAListDescribe(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.Get("blee", "fred")).ThenReturn(k8sHPA(), nil)
|
||||||
|
l := resource.NewHPAListWithArgs("blee", resource.NewHPAWithArgs(ca))
|
||||||
|
props, err := l.Describe("blee/fred")
|
||||||
|
|
||||||
|
ca.VerifyWasCalledOnce().Get("blee", "fred")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, 0, len(props))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
func k8sHPA() *v1.HorizontalPodAutoscaler {
|
||||||
|
var i int32 = 1
|
||||||
|
return &v1.HorizontalPodAutoscaler{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "blee",
|
||||||
|
Name: "fred",
|
||||||
|
CreationTimestamp: metav1.Time{Time: testTime()},
|
||||||
|
},
|
||||||
|
Spec: v1.HorizontalPodAutoscalerSpec{
|
||||||
|
ScaleTargetRef: v1.CrossVersionObjectReference{
|
||||||
|
Kind: "fred",
|
||||||
|
Name: "blee",
|
||||||
|
},
|
||||||
|
MinReplicas: &i,
|
||||||
|
MaxReplicas: 1,
|
||||||
|
TargetCPUUtilizationPercentage: &i,
|
||||||
|
},
|
||||||
|
Status: v1.HorizontalPodAutoscalerStatus{
|
||||||
|
CurrentReplicas: 1,
|
||||||
|
CurrentCPUUtilizationPercentage: &i,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHPA() resource.Columnar {
|
||||||
|
return resource.NewHPA().NewInstance(k8sHPA())
|
||||||
|
}
|
||||||
|
|
||||||
|
func hpaYaml() string {
|
||||||
|
return `typemeta:
|
||||||
|
kind: HPA
|
||||||
|
apiversion: autoscaling/v1
|
||||||
|
objectmeta:
|
||||||
|
name: fred
|
||||||
|
generatename: ""
|
||||||
|
namespace: blee
|
||||||
|
selflink: ""
|
||||||
|
uid: ""
|
||||||
|
resourceversion: ""
|
||||||
|
generation: 0
|
||||||
|
creationtimestamp: "2018-12-14T10:36:43.326972-07:00"
|
||||||
|
deletiontimestamp: null
|
||||||
|
deletiongraceperiodseconds: null
|
||||||
|
labels: {}
|
||||||
|
annotations: {}
|
||||||
|
ownerreferences: []
|
||||||
|
initializers: null
|
||||||
|
finalizers: []
|
||||||
|
clustername: ""
|
||||||
|
spec:
|
||||||
|
scaletargetref:
|
||||||
|
kind: fred
|
||||||
|
name: blee
|
||||||
|
apiversion: ""
|
||||||
|
minreplicas: 1
|
||||||
|
maxreplicas: 1
|
||||||
|
targetcpuutilizationpercentage: 1
|
||||||
|
status:
|
||||||
|
observedgeneration: null
|
||||||
|
lastscaletime: null
|
||||||
|
currentreplicas: 1
|
||||||
|
desiredreplicas: 0
|
||||||
|
currentcpuutilizationpercentage: 1
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,144 @@
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
"k8s.io/api/core/v1"
|
||||||
|
"k8s.io/api/extensions/v1beta1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ingress tracks a kubernetes resource.
|
||||||
|
type Ingress struct {
|
||||||
|
*Base
|
||||||
|
instance *v1beta1.Ingress
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewIngressList returns a new resource list.
|
||||||
|
func NewIngressList(ns string) List {
|
||||||
|
return NewIngressListWithArgs(ns, NewIngress())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewIngressListWithArgs returns a new resource list.
|
||||||
|
func NewIngressListWithArgs(ns string, res Resource) List {
|
||||||
|
return newList(ns, "ing", res, AllVerbsAccess)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewIngress instantiates a new Endpoint.
|
||||||
|
func NewIngress() *Ingress {
|
||||||
|
return NewIngressWithArgs(k8s.NewIngress())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewIngressWithArgs instantiates a new Endpoint.
|
||||||
|
func NewIngressWithArgs(r k8s.Res) *Ingress {
|
||||||
|
ep := &Ingress{
|
||||||
|
Base: &Base{
|
||||||
|
caller: r,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ep.creator = ep
|
||||||
|
return ep
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInstance builds a new Endpoint instance from a k8s resource.
|
||||||
|
func (*Ingress) NewInstance(i interface{}) Columnar {
|
||||||
|
cm := NewIngress()
|
||||||
|
switch i.(type) {
|
||||||
|
case *v1beta1.Ingress:
|
||||||
|
cm.instance = i.(*v1beta1.Ingress)
|
||||||
|
case v1beta1.Ingress:
|
||||||
|
ii := i.(v1beta1.Ingress)
|
||||||
|
cm.instance = &ii
|
||||||
|
default:
|
||||||
|
log.Fatalf("Unknown %#v", i)
|
||||||
|
}
|
||||||
|
cm.path = cm.namespacedName(cm.instance.ObjectMeta)
|
||||||
|
return cm
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal resource to yaml.
|
||||||
|
func (r *Ingress) Marshal(path string) (string, error) {
|
||||||
|
ns, n := namespaced(path)
|
||||||
|
i, err := r.caller.Get(ns, n)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
ing := i.(*v1beta1.Ingress)
|
||||||
|
ing.TypeMeta.APIVersion = "extensions/v1beta1"
|
||||||
|
ing.TypeMeta.Kind = "Ingress"
|
||||||
|
raw, err := yaml.Marshal(i)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(raw), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header return resource header.
|
||||||
|
func (*Ingress) Header(ns string) Row {
|
||||||
|
hh := Row{}
|
||||||
|
if ns == AllNamespaces {
|
||||||
|
hh = append(hh, "NAMESPACE")
|
||||||
|
}
|
||||||
|
return append(hh, "NAME", "HOSTS", "ADDRESS", "PORT", "AGE")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fields retrieves displayable fields.
|
||||||
|
func (r *Ingress) Fields(ns string) Row {
|
||||||
|
ff := make([]string, 0, len(r.Header(ns)))
|
||||||
|
|
||||||
|
i := r.instance
|
||||||
|
if ns == AllNamespaces {
|
||||||
|
ff = append(ff, i.Namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(ff,
|
||||||
|
i.Name,
|
||||||
|
r.toHosts(i.Spec.Rules),
|
||||||
|
r.toAddress(i.Status.LoadBalancer),
|
||||||
|
r.toPorts(i.Spec.TLS),
|
||||||
|
toAge(i.ObjectMeta.CreationTimestamp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtFields returns extended fields in relation to headers.
|
||||||
|
func (*Ingress) ExtFields() Properties {
|
||||||
|
return Properties{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
func (*Ingress) toAddress(lbs v1.LoadBalancerStatus) string {
|
||||||
|
ings := lbs.Ingress
|
||||||
|
res := make([]string, 0, len(ings))
|
||||||
|
for _, lb := range ings {
|
||||||
|
if len(lb.IP) > 0 {
|
||||||
|
res = append(res, lb.IP)
|
||||||
|
} else if len(lb.Hostname) != 0 {
|
||||||
|
res = append(res, lb.Hostname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(res, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Ingress) toPorts(tls []v1beta1.IngressTLS) string {
|
||||||
|
if len(tls) != 0 {
|
||||||
|
return "80, 443"
|
||||||
|
}
|
||||||
|
return "80"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Ingress) toHosts(rr []v1beta1.IngressRule) string {
|
||||||
|
var s string
|
||||||
|
var i int
|
||||||
|
for _, r := range rr {
|
||||||
|
s += r.Host
|
||||||
|
if i < len(rr)-1 {
|
||||||
|
s += ","
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
package resource_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/resource"
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
m "github.com/petergtz/pegomock"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"k8s.io/api/extensions/v1beta1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIngressListAccess(t *testing.T) {
|
||||||
|
ns := "blee"
|
||||||
|
l := resource.NewIngressList(resource.AllNamespaces)
|
||||||
|
l.SetNamespace(ns)
|
||||||
|
|
||||||
|
assert.Equal(t, "blee", l.GetNamespace())
|
||||||
|
assert.Equal(t, "ing", l.GetName())
|
||||||
|
for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} {
|
||||||
|
assert.True(t, l.Access(a))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIngressHeader(t *testing.T) {
|
||||||
|
assert.Equal(t, resource.Row{"NAME", "HOSTS", "ADDRESS", "PORT", "AGE"}, newIngress().Header("default"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIngressFields(t *testing.T) {
|
||||||
|
r := newIngress().Fields("blee")
|
||||||
|
assert.Equal(t, "fred", r[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIngressMarshal(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.Get("blee", "fred")).ThenReturn(k8sIngress(), nil)
|
||||||
|
|
||||||
|
cm := resource.NewIngressWithArgs(ca)
|
||||||
|
ma, err := cm.Marshal("blee/fred")
|
||||||
|
ca.VerifyWasCalledOnce().Get("blee", "fred")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, ingYaml(), ma)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIngressListData(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.List(resource.NotNamespaced)).ThenReturn(k8s.Collection{*k8sIngress()}, nil)
|
||||||
|
|
||||||
|
l := resource.NewIngressListWithArgs("-", resource.NewIngressWithArgs(ca))
|
||||||
|
// Make sure we can get deltas!
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
err := l.Reconcile()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ca.VerifyWasCalled(m.Times(2)).List(resource.NotNamespaced)
|
||||||
|
td := l.Data()
|
||||||
|
assert.Equal(t, 1, len(td.Rows))
|
||||||
|
assert.Equal(t, resource.NotNamespaced, l.GetNamespace())
|
||||||
|
assert.False(t, l.HasXRay())
|
||||||
|
row := td.Rows["blee/fred"]
|
||||||
|
assert.Equal(t, 5, len(row.Deltas))
|
||||||
|
for _, d := range row.Deltas {
|
||||||
|
assert.Equal(t, "", d)
|
||||||
|
}
|
||||||
|
assert.Equal(t, resource.Row{"fred"}, row.Fields[:1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIngressListDescribe(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.Get("blee", "fred")).ThenReturn(k8sIngress(), nil)
|
||||||
|
l := resource.NewIngressListWithArgs("blee", resource.NewIngressWithArgs(ca))
|
||||||
|
props, err := l.Describe("blee/fred")
|
||||||
|
|
||||||
|
ca.VerifyWasCalledOnce().Get("blee", "fred")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, 0, len(props))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
func k8sIngress() *v1beta1.Ingress {
|
||||||
|
return &v1beta1.Ingress{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "blee",
|
||||||
|
Name: "fred",
|
||||||
|
CreationTimestamp: metav1.Time{Time: testTime()},
|
||||||
|
},
|
||||||
|
Spec: v1beta1.IngressSpec{
|
||||||
|
Rules: []v1beta1.IngressRule{
|
||||||
|
{Host: "blee"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newIngress() resource.Columnar {
|
||||||
|
return resource.NewIngress().NewInstance(k8sIngress())
|
||||||
|
}
|
||||||
|
|
||||||
|
func ingYaml() string {
|
||||||
|
return `typemeta:
|
||||||
|
kind: Ingress
|
||||||
|
apiversion: extensions/v1beta1
|
||||||
|
objectmeta:
|
||||||
|
name: fred
|
||||||
|
generatename: ""
|
||||||
|
namespace: blee
|
||||||
|
selflink: ""
|
||||||
|
uid: ""
|
||||||
|
resourceversion: ""
|
||||||
|
generation: 0
|
||||||
|
creationtimestamp: "2018-12-14T10:36:43.326972-07:00"
|
||||||
|
deletiontimestamp: null
|
||||||
|
deletiongraceperiodseconds: null
|
||||||
|
labels: {}
|
||||||
|
annotations: {}
|
||||||
|
ownerreferences: []
|
||||||
|
initializers: null
|
||||||
|
finalizers: []
|
||||||
|
clustername: ""
|
||||||
|
spec:
|
||||||
|
backend: null
|
||||||
|
tls: []
|
||||||
|
rules:
|
||||||
|
- host: blee
|
||||||
|
ingressrulevalue:
|
||||||
|
http: null
|
||||||
|
status:
|
||||||
|
loadbalancer:
|
||||||
|
ingress: []
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,133 @@
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
"k8s.io/api/batch/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/util/duration"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Job tracks a kubernetes resource.
|
||||||
|
type Job struct {
|
||||||
|
*Base
|
||||||
|
instance *v1.Job
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewJobList returns a new resource list.
|
||||||
|
func NewJobList(ns string) List {
|
||||||
|
return NewJobListWithArgs(ns, NewJob())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewJobListWithArgs returns a new resource list.
|
||||||
|
func NewJobListWithArgs(ns string, res Resource) List {
|
||||||
|
return newList(ns, "job", res, AllVerbsAccess)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewJob instantiates a new Job.
|
||||||
|
func NewJob() *Job {
|
||||||
|
return NewJobWithArgs(k8s.NewJob())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewJobWithArgs instantiates a new Job.
|
||||||
|
func NewJobWithArgs(r k8s.Res) *Job {
|
||||||
|
cm := &Job{
|
||||||
|
Base: &Base{
|
||||||
|
caller: r,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cm.creator = cm
|
||||||
|
return cm
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInstance builds a new Job instance from a k8s resource.
|
||||||
|
func (*Job) NewInstance(i interface{}) Columnar {
|
||||||
|
job := NewJob()
|
||||||
|
switch i.(type) {
|
||||||
|
case *v1.Job:
|
||||||
|
job.instance = i.(*v1.Job)
|
||||||
|
case v1.Job:
|
||||||
|
ii := i.(v1.Job)
|
||||||
|
job.instance = &ii
|
||||||
|
default:
|
||||||
|
log.Fatalf("Unknown %#v", i)
|
||||||
|
}
|
||||||
|
job.path = job.namespacedName(job.instance.ObjectMeta)
|
||||||
|
return job
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal resource to yaml.
|
||||||
|
func (r *Job) Marshal(path string) (string, error) {
|
||||||
|
ns, n := namespaced(path)
|
||||||
|
i, err := r.caller.Get(ns, n)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
dp := i.(*v1.Job)
|
||||||
|
dp.TypeMeta.APIVersion = "extensions/v1beta1"
|
||||||
|
dp.TypeMeta.Kind = "Job"
|
||||||
|
raw, err := yaml.Marshal(i)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(raw), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header return resource header.
|
||||||
|
func (*Job) Header(ns string) Row {
|
||||||
|
hh := Row{}
|
||||||
|
if ns == AllNamespaces {
|
||||||
|
hh = append(hh, "NAMESPACE")
|
||||||
|
}
|
||||||
|
return append(hh, "NAME", "COMPLETIONS", "DURATION", "AGE")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fields retrieves displayable fields.
|
||||||
|
func (r *Job) Fields(ns string) Row {
|
||||||
|
ff := make([]string, 0, len(r.Header(ns)))
|
||||||
|
|
||||||
|
i := r.instance
|
||||||
|
if ns == AllNamespaces {
|
||||||
|
ff = append(ff, i.Namespace)
|
||||||
|
}
|
||||||
|
return append(ff,
|
||||||
|
i.Name,
|
||||||
|
r.toCompletion(i.Spec, i.Status),
|
||||||
|
r.toDuration(i.Status),
|
||||||
|
toAge(i.ObjectMeta.CreationTimestamp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtFields returns extended fields in relation to headers.
|
||||||
|
func (*Job) ExtFields() Properties {
|
||||||
|
return Properties{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
func (*Job) toCompletion(spec v1.JobSpec, status v1.JobStatus) (s string) {
|
||||||
|
if spec.Completions != nil {
|
||||||
|
return fmt.Sprintf("%d/%d", status.Succeeded, *spec.Completions)
|
||||||
|
}
|
||||||
|
var parallelism int32
|
||||||
|
if spec.Parallelism != nil {
|
||||||
|
parallelism = *spec.Parallelism
|
||||||
|
}
|
||||||
|
if parallelism > 1 {
|
||||||
|
return fmt.Sprintf("%d/1 of %d", status.Succeeded, parallelism)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d/1", status.Succeeded)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Job) toDuration(status v1.JobStatus) string {
|
||||||
|
switch {
|
||||||
|
case status.StartTime == nil:
|
||||||
|
case status.CompletionTime == nil:
|
||||||
|
return duration.HumanDuration(time.Now().Sub(status.StartTime.Time))
|
||||||
|
}
|
||||||
|
return duration.HumanDuration(status.CompletionTime.Sub(status.StartTime.Time))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,198 @@
|
||||||
|
package resource_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/resource"
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
m "github.com/petergtz/pegomock"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"k8s.io/api/batch/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestJobListAccess(t *testing.T) {
|
||||||
|
ns := "blee"
|
||||||
|
l := resource.NewJobList(resource.AllNamespaces)
|
||||||
|
l.SetNamespace(ns)
|
||||||
|
|
||||||
|
assert.Equal(t, "blee", l.GetNamespace())
|
||||||
|
assert.Equal(t, "job", l.GetName())
|
||||||
|
for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} {
|
||||||
|
assert.True(t, l.Access(a))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJobHeader(t *testing.T) {
|
||||||
|
assert.Equal(t, resource.Row{"NAME", "COMPLETIONS", "DURATION", "AGE"}, newJob().Header("default"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJobFields(t *testing.T) {
|
||||||
|
r := newJob().Fields("blee")
|
||||||
|
assert.Equal(t, "fred", r[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJobMarshal(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.Get("blee", "fred")).ThenReturn(k8sJob(), nil)
|
||||||
|
|
||||||
|
cm := resource.NewJobWithArgs(ca)
|
||||||
|
ma, err := cm.Marshal("blee/fred")
|
||||||
|
ca.VerifyWasCalledOnce().Get("blee", "fred")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, jobYaml(), ma)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJobListData(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.List(resource.NotNamespaced)).ThenReturn(k8s.Collection{*k8sJob()}, nil)
|
||||||
|
|
||||||
|
l := resource.NewJobListWithArgs("-", resource.NewJobWithArgs(ca))
|
||||||
|
// Make sure we can get deltas!
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
err := l.Reconcile()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ca.VerifyWasCalled(m.Times(2)).List(resource.NotNamespaced)
|
||||||
|
td := l.Data()
|
||||||
|
assert.Equal(t, 1, len(td.Rows))
|
||||||
|
assert.Equal(t, resource.NotNamespaced, l.GetNamespace())
|
||||||
|
assert.False(t, l.HasXRay())
|
||||||
|
row := td.Rows["blee/fred"]
|
||||||
|
assert.Equal(t, 4, len(row.Deltas))
|
||||||
|
for _, d := range row.Deltas {
|
||||||
|
assert.Equal(t, "", d)
|
||||||
|
}
|
||||||
|
assert.Equal(t, resource.Row{"fred"}, row.Fields[:1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJobListDescribe(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.Get("blee", "fred")).ThenReturn(k8sJob(), nil)
|
||||||
|
l := resource.NewJobListWithArgs("blee", resource.NewJobWithArgs(ca))
|
||||||
|
props, err := l.Describe("blee/fred")
|
||||||
|
|
||||||
|
ca.VerifyWasCalledOnce().Get("blee", "fred")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, 0, len(props))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
func k8sJob() *v1.Job {
|
||||||
|
var i int32
|
||||||
|
return &v1.Job{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "blee",
|
||||||
|
Name: "fred",
|
||||||
|
CreationTimestamp: metav1.Time{Time: testTime()},
|
||||||
|
},
|
||||||
|
Spec: v1.JobSpec{
|
||||||
|
Completions: &i,
|
||||||
|
Parallelism: &i,
|
||||||
|
},
|
||||||
|
Status: v1.JobStatus{
|
||||||
|
StartTime: &metav1.Time{Time: testTime()},
|
||||||
|
CompletionTime: &metav1.Time{Time: testTime()},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newJob() resource.Columnar {
|
||||||
|
return resource.NewJob().NewInstance(k8sJob())
|
||||||
|
}
|
||||||
|
|
||||||
|
func jobYaml() string {
|
||||||
|
return `typemeta:
|
||||||
|
kind: Job
|
||||||
|
apiversion: extensions/v1beta1
|
||||||
|
objectmeta:
|
||||||
|
name: fred
|
||||||
|
generatename: ""
|
||||||
|
namespace: blee
|
||||||
|
selflink: ""
|
||||||
|
uid: ""
|
||||||
|
resourceversion: ""
|
||||||
|
generation: 0
|
||||||
|
creationtimestamp: "2018-12-14T10:36:43.326972-07:00"
|
||||||
|
deletiontimestamp: null
|
||||||
|
deletiongraceperiodseconds: null
|
||||||
|
labels: {}
|
||||||
|
annotations: {}
|
||||||
|
ownerreferences: []
|
||||||
|
initializers: null
|
||||||
|
finalizers: []
|
||||||
|
clustername: ""
|
||||||
|
spec:
|
||||||
|
parallelism: 0
|
||||||
|
completions: 0
|
||||||
|
activedeadlineseconds: null
|
||||||
|
backofflimit: null
|
||||||
|
selector: null
|
||||||
|
manualselector: null
|
||||||
|
template:
|
||||||
|
objectmeta:
|
||||||
|
name: ""
|
||||||
|
generatename: ""
|
||||||
|
namespace: ""
|
||||||
|
selflink: ""
|
||||||
|
uid: ""
|
||||||
|
resourceversion: ""
|
||||||
|
generation: 0
|
||||||
|
creationtimestamp: "0001-01-01T00:00:00Z"
|
||||||
|
deletiontimestamp: null
|
||||||
|
deletiongraceperiodseconds: null
|
||||||
|
labels: {}
|
||||||
|
annotations: {}
|
||||||
|
ownerreferences: []
|
||||||
|
initializers: null
|
||||||
|
finalizers: []
|
||||||
|
clustername: ""
|
||||||
|
spec:
|
||||||
|
volumes: []
|
||||||
|
initcontainers: []
|
||||||
|
containers: []
|
||||||
|
restartpolicy: ""
|
||||||
|
terminationgraceperiodseconds: null
|
||||||
|
activedeadlineseconds: null
|
||||||
|
dnspolicy: ""
|
||||||
|
nodeselector: {}
|
||||||
|
serviceaccountname: ""
|
||||||
|
deprecatedserviceaccount: ""
|
||||||
|
automountserviceaccounttoken: null
|
||||||
|
nodename: ""
|
||||||
|
hostnetwork: false
|
||||||
|
hostpid: false
|
||||||
|
hostipc: false
|
||||||
|
shareprocessnamespace: null
|
||||||
|
securitycontext: null
|
||||||
|
imagepullsecrets: []
|
||||||
|
hostname: ""
|
||||||
|
subdomain: ""
|
||||||
|
affinity: null
|
||||||
|
schedulername: ""
|
||||||
|
tolerations: []
|
||||||
|
hostaliases: []
|
||||||
|
priorityclassname: ""
|
||||||
|
priority: null
|
||||||
|
dnsconfig: null
|
||||||
|
readinessgates: []
|
||||||
|
runtimeclassname: null
|
||||||
|
enableservicelinks: null
|
||||||
|
ttlsecondsafterfinished: null
|
||||||
|
status:
|
||||||
|
conditions: []
|
||||||
|
starttime: "2018-12-14T10:36:43.326972-07:00"
|
||||||
|
completiontime: "2018-12-14T10:36:43.326972-07:00"
|
||||||
|
active: 0
|
||||||
|
succeeded: 0
|
||||||
|
failed: 0
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,244 @@
|
||||||
|
package k8s
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/client-go/dynamic"
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
|
"k8s.io/client-go/rest"
|
||||||
|
restclient "k8s.io/client-go/rest"
|
||||||
|
"k8s.io/client-go/tools/clientcmd"
|
||||||
|
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||||
|
"k8s.io/kubernetes/pkg/kubectl/metricsutil"
|
||||||
|
"k8s.io/kubernetes/staging/src/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
|
||||||
|
metricsapi "k8s.io/metrics/pkg/apis/metrics"
|
||||||
|
versioned "k8s.io/metrics/pkg/client/clientset/versioned"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NA Not available
|
||||||
|
const NA = "n/a"
|
||||||
|
|
||||||
|
var (
|
||||||
|
conn connection
|
||||||
|
supportedMetricsAPIVersions = []string{"v1beta1"}
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
conn = &apiServer{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
ApiGroup struct {
|
||||||
|
Resource string
|
||||||
|
Group, Kind, Version string
|
||||||
|
Plural, Singular string
|
||||||
|
Aliases []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collection of empty interfaces.
|
||||||
|
Collection []interface{}
|
||||||
|
|
||||||
|
// Res K8s api server calls
|
||||||
|
Res interface {
|
||||||
|
Get(ns string, name string) (interface{}, error)
|
||||||
|
List(ns string) (Collection, error)
|
||||||
|
Delete(ns string, name string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
connection interface {
|
||||||
|
configOrDie() *restclient.Config
|
||||||
|
dialConfigOrDie() *clientcmdapi.Config
|
||||||
|
dialOrDie() kubernetes.Interface
|
||||||
|
dynDialOrDie() dynamic.Interface
|
||||||
|
nsDialOrDie() dynamic.NamespaceableResourceInterface
|
||||||
|
mxsDial() (*versioned.Clientset, error)
|
||||||
|
heapsterDial() (*metricsutil.HeapsterMetricsClient, error)
|
||||||
|
getClusterName() string
|
||||||
|
setClusterName(n string)
|
||||||
|
hasMetricsServer() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
apiServer struct {
|
||||||
|
client kubernetes.Interface
|
||||||
|
dClient dynamic.Interface
|
||||||
|
csClient *clientset.Clientset
|
||||||
|
nsClient dynamic.NamespaceableResourceInterface
|
||||||
|
heapsterClient *metricsutil.HeapsterMetricsClient
|
||||||
|
mxsClient *versioned.Clientset
|
||||||
|
cluster string
|
||||||
|
useMetricServer bool
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a *apiServer) getClusterName() string {
|
||||||
|
return a.cluster
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *apiServer) setClusterName(n string) {
|
||||||
|
a.cluster = n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *apiServer) hasMetricsServer() bool {
|
||||||
|
return a.useMetricServer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *apiServer) dialConfigOrDie() *clientcmdapi.Config {
|
||||||
|
c, err := clientcmd.NewDefaultPathOptions().GetStartingConfig()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialOrDie returns a handle to api server or die.
|
||||||
|
func (a *apiServer) dialOrDie() kubernetes.Interface {
|
||||||
|
a.checkCurrentConfig()
|
||||||
|
if a.client != nil {
|
||||||
|
return a.client
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if a.client, err = kubernetes.NewForConfig(a.configOrDie()); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *apiServer) csDialOrDie() *clientset.Clientset {
|
||||||
|
a.checkCurrentConfig()
|
||||||
|
if a.csClient != nil {
|
||||||
|
return a.csClient
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg *rest.Config
|
||||||
|
// cfg := clientcmd.NewNonInteractiveClientConfig(config, contextName, overrides, configAccess)
|
||||||
|
var err error
|
||||||
|
if a.csClient, err = clientset.NewForConfig(cfg); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.csClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// DynDial returns a handle to the api server.
|
||||||
|
func (a *apiServer) dynDialOrDie() dynamic.Interface {
|
||||||
|
a.checkCurrentConfig()
|
||||||
|
if a.dClient != nil {
|
||||||
|
return a.dClient
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if a.dClient, err = dynamic.NewForConfig(a.configOrDie()); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.dClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *apiServer) nsDialOrDie() dynamic.NamespaceableResourceInterface {
|
||||||
|
a.checkCurrentConfig()
|
||||||
|
if a.nsClient != nil {
|
||||||
|
return a.nsClient
|
||||||
|
}
|
||||||
|
|
||||||
|
a.nsClient = a.dynDialOrDie().Resource(schema.GroupVersionResource{
|
||||||
|
Group: "apiextensions.k8s.io",
|
||||||
|
Version: "v1beta1",
|
||||||
|
Resource: "customresourcedefinitions",
|
||||||
|
})
|
||||||
|
return a.nsClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *apiServer) heapsterDial() (*metricsutil.HeapsterMetricsClient, error) {
|
||||||
|
a.checkCurrentConfig()
|
||||||
|
if a.heapsterClient != nil {
|
||||||
|
return a.heapsterClient, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
a.heapsterClient = metricsutil.NewHeapsterMetricsClient(
|
||||||
|
a.dialOrDie().CoreV1(),
|
||||||
|
metricsutil.DefaultHeapsterNamespace,
|
||||||
|
metricsutil.DefaultHeapsterScheme,
|
||||||
|
metricsutil.DefaultHeapsterService,
|
||||||
|
metricsutil.DefaultHeapsterPort,
|
||||||
|
)
|
||||||
|
return a.heapsterClient, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *apiServer) mxsDial() (*versioned.Clientset, error) {
|
||||||
|
a.checkCurrentConfig()
|
||||||
|
if a.mxsClient != nil {
|
||||||
|
return a.mxsClient, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := clientcmd.NewDefaultPathOptions()
|
||||||
|
dat, err := ioutil.ReadFile(opts.GetDefaultFilename())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rc, err := clientcmd.RESTConfigFromKubeConfig(dat)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.mxsClient, err = versioned.NewForConfig(rc)
|
||||||
|
return a.mxsClient, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*apiServer) configOrDie() *restclient.Config {
|
||||||
|
opts := clientcmd.NewDefaultPathOptions()
|
||||||
|
cfg, err := clientcmd.BuildConfigFromFlags("", opts.GetDefaultFilename())
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *apiServer) checkCurrentConfig() {
|
||||||
|
cfg, err := clientcmd.NewDefaultPathOptions().GetStartingConfig()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(a.getClusterName()) == 0 {
|
||||||
|
a.setClusterName(cfg.CurrentContext)
|
||||||
|
a.useMetricServer = a.supportsMxServer()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.getClusterName() != cfg.CurrentContext {
|
||||||
|
a.reset()
|
||||||
|
a.setClusterName(cfg.CurrentContext)
|
||||||
|
a.useMetricServer = a.supportsMxServer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *apiServer) reset() {
|
||||||
|
a.client, a.dClient, a.nsClient = nil, nil, nil
|
||||||
|
a.heapsterClient, a.mxsClient = nil, nil
|
||||||
|
a.setClusterName("")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *apiServer) supportsMxServer() bool {
|
||||||
|
apiGroups, err := a.dialOrDie().Discovery().ServerGroups()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, discoveredAPIGroup := range apiGroups.Groups {
|
||||||
|
if discoveredAPIGroup.Name != metricsapi.GroupName {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, version := range discoveredAPIGroup.Versions {
|
||||||
|
for _, supportedVersion := range supportedMetricsAPIVersions {
|
||||||
|
if version.Version == supportedVersion {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
package k8s
|
||||||
|
|
||||||
|
// Cluster manages a Kubernetes ClusterRole.
|
||||||
|
type Cluster struct{}
|
||||||
|
|
||||||
|
// NewCluster instantiates a new ClusterRole.
|
||||||
|
func NewCluster() *Cluster {
|
||||||
|
return &Cluster{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version retrieves cluster git version.
|
||||||
|
func (c *Cluster) Version() (string, error) {
|
||||||
|
rev, err := conn.dialOrDie().Discovery().ServerVersion()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return rev.GitVersion, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClusterName retrieves cluster name.
|
||||||
|
func (c *Cluster) ClusterName() string {
|
||||||
|
return conn.getClusterName()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
package k8s
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ClusterRole represents a Kubernetes ClusterRole
|
||||||
|
type ClusterRole struct{}
|
||||||
|
|
||||||
|
// NewClusterRole returns a new ClusterRole.
|
||||||
|
func NewClusterRole() Res {
|
||||||
|
return &ClusterRole{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a service.
|
||||||
|
func (*ClusterRole) Get(_, n string) (interface{}, error) {
|
||||||
|
opts := metav1.GetOptions{}
|
||||||
|
return conn.dialOrDie().RbacV1().ClusterRoles().Get(n, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all ClusterRoles in a given namespace
|
||||||
|
func (*ClusterRole) List(_ string) (Collection, error) {
|
||||||
|
opts := metav1.ListOptions{}
|
||||||
|
|
||||||
|
rr, err := conn.dialOrDie().RbacV1().ClusterRoles().List(opts)
|
||||||
|
if err != nil {
|
||||||
|
return Collection{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := make(Collection, len(rr.Items))
|
||||||
|
for i, r := range rr.Items {
|
||||||
|
cc[i] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
return cc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a ClusterRole
|
||||||
|
func (*ClusterRole) Delete(_, n string) error {
|
||||||
|
opts := metav1.DeleteOptions{}
|
||||||
|
return conn.dialOrDie().RbacV1().ClusterRoles().Delete(n, &opts)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
package k8s
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ClusterRoleBinding represents a Kubernetes ClusterRoleBinding
|
||||||
|
type ClusterRoleBinding struct{}
|
||||||
|
|
||||||
|
// NewClusterRoleBinding returns a new ClusterRoleBinding.
|
||||||
|
func NewClusterRoleBinding() Res {
|
||||||
|
return &ClusterRoleBinding{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a service.
|
||||||
|
func (*ClusterRoleBinding) Get(_, n string) (interface{}, error) {
|
||||||
|
opts := metav1.GetOptions{}
|
||||||
|
return conn.dialOrDie().RbacV1().ClusterRoleBindings().Get(n, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all ClusterRoleBindings in a given namespace
|
||||||
|
func (*ClusterRoleBinding) List(_ string) (Collection, error) {
|
||||||
|
opts := metav1.ListOptions{}
|
||||||
|
|
||||||
|
rr, err := conn.dialOrDie().RbacV1().ClusterRoleBindings().List(opts)
|
||||||
|
if err != nil {
|
||||||
|
return Collection{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := make(Collection, len(rr.Items))
|
||||||
|
for i, r := range rr.Items {
|
||||||
|
cc[i] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
return cc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a ClusterRoleBinding
|
||||||
|
func (*ClusterRoleBinding) Delete(_, n string) error {
|
||||||
|
opts := metav1.DeleteOptions{}
|
||||||
|
return conn.dialOrDie().RbacV1().ClusterRoleBindings().Delete(n, &opts)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
package k8s
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConfigMap represents a Kubernetes ConfigMap
|
||||||
|
type ConfigMap struct{}
|
||||||
|
|
||||||
|
// NewConfigMap returns a new ConfigMap.
|
||||||
|
func NewConfigMap() Res {
|
||||||
|
return &ConfigMap{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a ConfigMap.
|
||||||
|
func (c *ConfigMap) Get(ns, n string) (interface{}, error) {
|
||||||
|
opts := metav1.GetOptions{}
|
||||||
|
return conn.dialOrDie().CoreV1().ConfigMaps(ns).Get(n, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all ConfigMaps in a given namespace
|
||||||
|
func (c *ConfigMap) List(ns string) (Collection, error) {
|
||||||
|
opts := metav1.ListOptions{}
|
||||||
|
|
||||||
|
rr, err := conn.dialOrDie().CoreV1().ConfigMaps(ns).List(opts)
|
||||||
|
if err != nil {
|
||||||
|
return Collection{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := make(Collection, len(rr.Items))
|
||||||
|
for i, r := range rr.Items {
|
||||||
|
cc[i] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
return cc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a ConfigMap
|
||||||
|
func (c *ConfigMap) Delete(ns, n string) error {
|
||||||
|
opts := metav1.DeleteOptions{}
|
||||||
|
return conn.dialOrDie().CoreV1().ConfigMaps(ns).Delete(n, &opts)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
package k8s
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"k8s.io/client-go/tools/clientcmd"
|
||||||
|
"k8s.io/client-go/tools/clientcmd/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ContextRes represents a kubernetes clusters configurations.
|
||||||
|
type ContextRes interface {
|
||||||
|
Res
|
||||||
|
Switch(n string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// NamedContext represents a named cluster context.
|
||||||
|
type NamedContext struct {
|
||||||
|
Name string
|
||||||
|
Context *api.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
// CurrentCluster return active cluster name
|
||||||
|
func (c *NamedContext) CurrentCluster() string {
|
||||||
|
return conn.getClusterName()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context represents a Kubernetes Context.
|
||||||
|
type Context struct{}
|
||||||
|
|
||||||
|
// NewContext returns a new Context.
|
||||||
|
func NewContext() Res {
|
||||||
|
return &Context{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a Context.
|
||||||
|
func (*Context) Get(_, n string) (interface{}, error) {
|
||||||
|
return &NamedContext{Name: n, Context: conn.dialConfigOrDie().Contexts[n]}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all Contexts in a given namespace
|
||||||
|
func (*Context) List(string) (Collection, error) {
|
||||||
|
conn := conn.dialConfigOrDie()
|
||||||
|
|
||||||
|
cc := make([]interface{}, 0, len(conn.Contexts))
|
||||||
|
for k, v := range conn.Contexts {
|
||||||
|
cc = append(cc, &NamedContext{k, v})
|
||||||
|
}
|
||||||
|
return cc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a Context
|
||||||
|
func (*Context) Delete(_, n string) error {
|
||||||
|
conn := conn.dialConfigOrDie()
|
||||||
|
|
||||||
|
if conn.CurrentContext == n {
|
||||||
|
return fmt.Errorf("trying to delete your current context %s", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
acc := clientcmd.NewDefaultPathOptions()
|
||||||
|
|
||||||
|
delete(conn.Contexts, n)
|
||||||
|
return clientcmd.ModifyConfig(acc, *conn, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch cluster Context.
|
||||||
|
func (*Context) Switch(n string) error {
|
||||||
|
conn := conn.dialConfigOrDie()
|
||||||
|
|
||||||
|
conn.CurrentContext = n
|
||||||
|
acc := clientcmd.NewDefaultPathOptions()
|
||||||
|
return clientcmd.ModifyConfig(acc, *conn, true)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
package k8s
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CRD represents a Kubernetes CRD
|
||||||
|
type CRD struct{}
|
||||||
|
|
||||||
|
// NewCRD returns a new CRD.
|
||||||
|
func NewCRD() Res {
|
||||||
|
return &CRD{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a CRD.
|
||||||
|
func (*CRD) Get(_, n string) (interface{}, error) {
|
||||||
|
opts := metav1.GetOptions{}
|
||||||
|
return conn.nsDialOrDie().Get(n, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all CRDs in a given namespace
|
||||||
|
func (*CRD) List(string) (Collection, error) {
|
||||||
|
opts := metav1.ListOptions{}
|
||||||
|
|
||||||
|
rr, err := conn.nsDialOrDie().List(opts)
|
||||||
|
if err != nil {
|
||||||
|
return Collection{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := make(Collection, len(rr.Items))
|
||||||
|
for i, r := range rr.Items {
|
||||||
|
cc[i] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
return cc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a CRD
|
||||||
|
func (*CRD) Delete(_, n string) error {
|
||||||
|
opts := metav1.DeleteOptions{}
|
||||||
|
return conn.nsDialOrDie().Delete(n, &opts)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
package k8s
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CronJob represents a Kubernetes CronJob
|
||||||
|
type CronJob struct{}
|
||||||
|
|
||||||
|
// NewCronJob returns a new CronJob.
|
||||||
|
func NewCronJob() Res {
|
||||||
|
return &CronJob{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a CronJob.
|
||||||
|
func (c *CronJob) Get(ns, n string) (interface{}, error) {
|
||||||
|
opts := metav1.GetOptions{}
|
||||||
|
return conn.dialOrDie().BatchV1beta1().CronJobs(ns).Get(n, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all CronJobs in a given namespace
|
||||||
|
func (c *CronJob) List(ns string) (Collection, error) {
|
||||||
|
opts := metav1.ListOptions{}
|
||||||
|
|
||||||
|
rr, err := conn.dialOrDie().BatchV1beta1().CronJobs(ns).List(opts)
|
||||||
|
if err != nil {
|
||||||
|
return Collection{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := make(Collection, len(rr.Items))
|
||||||
|
for i, r := range rr.Items {
|
||||||
|
cc[i] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
return cc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a CronJob
|
||||||
|
func (c *CronJob) Delete(ns, n string) error {
|
||||||
|
opts := metav1.DeleteOptions{}
|
||||||
|
return conn.dialOrDie().BatchV1beta1().CronJobs(ns).Delete(n, &opts)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
package k8s
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Deployment represents a Kubernetes Deployment
|
||||||
|
type Deployment struct{}
|
||||||
|
|
||||||
|
// NewDeployment returns a new Deployment.
|
||||||
|
func NewDeployment() Res {
|
||||||
|
return &Deployment{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a service.
|
||||||
|
func (*Deployment) Get(ns, n string) (interface{}, error) {
|
||||||
|
opts := metav1.GetOptions{}
|
||||||
|
return conn.dialOrDie().Apps().Deployments(ns).Get(n, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all Deployments in a given namespace
|
||||||
|
func (*Deployment) List(ns string) (Collection, error) {
|
||||||
|
opts := metav1.ListOptions{}
|
||||||
|
|
||||||
|
rr, err := conn.dialOrDie().Apps().Deployments(ns).List(opts)
|
||||||
|
if err != nil {
|
||||||
|
return Collection{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := make(Collection, len(rr.Items))
|
||||||
|
for i, r := range rr.Items {
|
||||||
|
cc[i] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
return cc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a Deployment
|
||||||
|
func (*Deployment) Delete(ns, n string) error {
|
||||||
|
opts := metav1.DeleteOptions{}
|
||||||
|
return conn.dialOrDie().Apps().Deployments(ns).Delete(n, &opts)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
package k8s
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DaemonSet represents a Kubernetes DaemonSet
|
||||||
|
type DaemonSet struct{}
|
||||||
|
|
||||||
|
// NewDaemonSet returns a new DaemonSet.
|
||||||
|
func NewDaemonSet() Res {
|
||||||
|
return &DaemonSet{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a service.
|
||||||
|
func (*DaemonSet) Get(ns, n string) (interface{}, error) {
|
||||||
|
opts := metav1.GetOptions{}
|
||||||
|
return conn.dialOrDie().ExtensionsV1beta1().DaemonSets(ns).Get(n, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all DaemonSets in a given namespace
|
||||||
|
func (*DaemonSet) List(ns string) (Collection, error) {
|
||||||
|
opts := metav1.ListOptions{}
|
||||||
|
|
||||||
|
rr, err := conn.dialOrDie().ExtensionsV1beta1().DaemonSets(ns).List(opts)
|
||||||
|
if err != nil {
|
||||||
|
return Collection{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := make(Collection, len(rr.Items))
|
||||||
|
for i, r := range rr.Items {
|
||||||
|
cc[i] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
return cc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a DaemonSet
|
||||||
|
func (*DaemonSet) Delete(ns, n string) error {
|
||||||
|
opts := metav1.DeleteOptions{}
|
||||||
|
return conn.dialOrDie().ExtensionsV1beta1().DaemonSets(ns).Delete(n, &opts)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
package k8s
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Endpoints represents a Kubernetes Endpoints
|
||||||
|
type Endpoints struct{}
|
||||||
|
|
||||||
|
// NewEndpoints returns a new Endpoints.
|
||||||
|
func NewEndpoints() Res {
|
||||||
|
return &Endpoints{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a service.
|
||||||
|
func (*Endpoints) Get(ns, n string) (interface{}, error) {
|
||||||
|
opts := metav1.GetOptions{}
|
||||||
|
return conn.dialOrDie().CoreV1().Endpoints(ns).Get(n, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all Endpointss in a given namespace
|
||||||
|
func (*Endpoints) List(ns string) (Collection, error) {
|
||||||
|
opts := metav1.ListOptions{}
|
||||||
|
|
||||||
|
rr, err := conn.dialOrDie().CoreV1().Endpoints(ns).List(opts)
|
||||||
|
if err != nil {
|
||||||
|
return Collection{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := make(Collection, len(rr.Items))
|
||||||
|
for i, r := range rr.Items {
|
||||||
|
cc[i] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
return cc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a Endpoints
|
||||||
|
func (*Endpoints) Delete(ns, n string) error {
|
||||||
|
opts := metav1.DeleteOptions{}
|
||||||
|
return conn.dialOrDie().CoreV1().Endpoints(ns).Delete(n, &opts)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
package k8s
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Event represents a Kubernetes Event
|
||||||
|
type Event struct{}
|
||||||
|
|
||||||
|
// NewEvent returns a new Event.
|
||||||
|
func NewEvent() Res {
|
||||||
|
return &Event{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a service.
|
||||||
|
func (*Event) Get(ns, n string) (interface{}, error) {
|
||||||
|
opts := metav1.GetOptions{}
|
||||||
|
return conn.dialOrDie().CoreV1().Events(ns).Get(n, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all services in a given namespace
|
||||||
|
func (*Event) List(ns string) (Collection, error) {
|
||||||
|
opts := metav1.ListOptions{}
|
||||||
|
|
||||||
|
rr, err := conn.dialOrDie().CoreV1().Events(ns).List(opts)
|
||||||
|
if err != nil {
|
||||||
|
return Collection{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := make(Collection, len(rr.Items))
|
||||||
|
for i, r := range rr.Items {
|
||||||
|
cc[i] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
return cc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a service
|
||||||
|
func (*Event) Delete(ns, n string) error {
|
||||||
|
opts := metav1.DeleteOptions{}
|
||||||
|
return conn.dialOrDie().CoreV1().Events(ns).Delete(n, &opts)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
package k8s
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HPA represents am HorizontalPodAutoscaler.
|
||||||
|
type HPA struct{}
|
||||||
|
|
||||||
|
// NewHPA returns a new HPA.
|
||||||
|
func NewHPA() Res {
|
||||||
|
return &HPA{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a service.
|
||||||
|
func (*HPA) Get(ns, n string) (interface{}, error) {
|
||||||
|
opts := metav1.GetOptions{}
|
||||||
|
return conn.dialOrDie().Autoscaling().HorizontalPodAutoscalers(ns).Get(n, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all services in a given namespace
|
||||||
|
func (*HPA) List(ns string) (Collection, error) {
|
||||||
|
opts := metav1.ListOptions{}
|
||||||
|
|
||||||
|
rr, err := conn.dialOrDie().Autoscaling().HorizontalPodAutoscalers(ns).List(opts)
|
||||||
|
if err != nil {
|
||||||
|
return Collection{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := make(Collection, len(rr.Items))
|
||||||
|
for i, r := range rr.Items {
|
||||||
|
cc[i] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
return cc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a service
|
||||||
|
func (*HPA) Delete(ns, n string) error {
|
||||||
|
opts := metav1.DeleteOptions{}
|
||||||
|
return conn.dialOrDie().Autoscaling().HorizontalPodAutoscalers(ns).Delete(n, &opts)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
package k8s
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ingress represents a Kubernetes Ingress
|
||||||
|
type Ingress struct{}
|
||||||
|
|
||||||
|
// NewIngress returns a new Ingress.
|
||||||
|
func NewIngress() Res {
|
||||||
|
return &Ingress{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a service.
|
||||||
|
func (*Ingress) Get(ns, n string) (interface{}, error) {
|
||||||
|
opts := metav1.GetOptions{}
|
||||||
|
return conn.dialOrDie().ExtensionsV1beta1().Ingresses(ns).Get(n, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all Ingresss in a given namespace
|
||||||
|
func (*Ingress) List(ns string) (Collection, error) {
|
||||||
|
opts := metav1.ListOptions{}
|
||||||
|
|
||||||
|
rr, err := conn.dialOrDie().ExtensionsV1beta1().Ingresses(ns).List(opts)
|
||||||
|
if err != nil {
|
||||||
|
return Collection{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := make(Collection, len(rr.Items))
|
||||||
|
for i, r := range rr.Items {
|
||||||
|
cc[i] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
return cc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a Ingress
|
||||||
|
func (*Ingress) Delete(ns, n string) error {
|
||||||
|
opts := metav1.DeleteOptions{}
|
||||||
|
return conn.dialOrDie().ExtensionsV1beta1().Ingresses(ns).Delete(n, &opts)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
package k8s
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Job represents a Kubernetes Job
|
||||||
|
type Job struct{}
|
||||||
|
|
||||||
|
// NewJob returns a new Job.
|
||||||
|
func NewJob() Res {
|
||||||
|
return &Job{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a Job.
|
||||||
|
func (c *Job) Get(ns, n string) (interface{}, error) {
|
||||||
|
opts := metav1.GetOptions{}
|
||||||
|
return conn.dialOrDie().BatchV1().Jobs(ns).Get(n, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all Jobs in a given namespace
|
||||||
|
func (c *Job) List(ns string) (Collection, error) {
|
||||||
|
opts := metav1.ListOptions{}
|
||||||
|
|
||||||
|
rr, err := conn.dialOrDie().BatchV1().Jobs(ns).List(opts)
|
||||||
|
if err != nil {
|
||||||
|
return Collection{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := make(Collection, len(rr.Items))
|
||||||
|
for i, r := range rr.Items {
|
||||||
|
cc[i] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
return cc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a Job
|
||||||
|
func (c *Job) Delete(ns, n string) error {
|
||||||
|
opts := metav1.DeleteOptions{}
|
||||||
|
return conn.dialOrDie().BatchV1().Jobs(ns).Delete(n, &opts)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,195 @@
|
||||||
|
package k8s
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
|
metricsapi "k8s.io/metrics/pkg/apis/metrics"
|
||||||
|
metricsV1beta1api "k8s.io/metrics/pkg/apis/metrics/v1beta1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// MetricsServer serves cluster metrics for nodes and pods.
|
||||||
|
MetricsServer struct{}
|
||||||
|
|
||||||
|
// Metric tracks resource metrics.
|
||||||
|
Metric struct {
|
||||||
|
CPU string
|
||||||
|
Mem string
|
||||||
|
AvailCPU string
|
||||||
|
AvailMem string
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewMetricsServer return a metric server instance.
|
||||||
|
func NewMetricsServer() *MetricsServer {
|
||||||
|
return &MetricsServer{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NodeMetrics retrieves all nodes metrics
|
||||||
|
func (m *MetricsServer) NodeMetrics() (Metric, error) {
|
||||||
|
var mx Metric
|
||||||
|
|
||||||
|
opts := metav1.ListOptions{}
|
||||||
|
nn, err := conn.dialOrDie().CoreV1().Nodes().List(opts)
|
||||||
|
if err != nil {
|
||||||
|
return mx, err
|
||||||
|
}
|
||||||
|
nods := make([]string, len(nn.Items))
|
||||||
|
var maxCPU, maxMem float64
|
||||||
|
for i, n := range nn.Items {
|
||||||
|
nods[i] = n.Name
|
||||||
|
c := n.Status.Allocatable["cpu"]
|
||||||
|
maxCPU += float64(c.MilliValue())
|
||||||
|
m := n.Status.Allocatable["memory"]
|
||||||
|
maxMem += float64(m.Value() / (1024 * 1024))
|
||||||
|
}
|
||||||
|
|
||||||
|
mm, err := m.getNodeMetrics()
|
||||||
|
if err != nil {
|
||||||
|
return mx, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var cpu, mem float64
|
||||||
|
for _, n := range nods {
|
||||||
|
for _, m := range mm.Items {
|
||||||
|
if m.Name == n {
|
||||||
|
cpu += float64(m.Usage.Cpu().MilliValue())
|
||||||
|
mem += float64(m.Usage.Memory().Value() / (1024 * 1024))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mx = Metric{
|
||||||
|
CPU: fmt.Sprintf("%0.f%%", math.Round((cpu/maxCPU)*100)),
|
||||||
|
Mem: fmt.Sprintf("%0.f%%", math.Round((mem/maxMem)*100)),
|
||||||
|
}
|
||||||
|
return mx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PodMetrics retrieves all pods metrics
|
||||||
|
func (m *MetricsServer) PodMetrics() (map[string]Metric, error) {
|
||||||
|
mx := map[string]Metric{}
|
||||||
|
|
||||||
|
mm, err := m.getPodMetrics()
|
||||||
|
if err != nil {
|
||||||
|
return mx, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range mm.Items {
|
||||||
|
var cpu, mem int64
|
||||||
|
for _, c := range m.Containers {
|
||||||
|
cpu += c.Usage.Cpu().MilliValue()
|
||||||
|
mem += c.Usage.Memory().Value() / (1024 * 1024)
|
||||||
|
}
|
||||||
|
pa := path.Join(m.Namespace, m.Name)
|
||||||
|
mx[pa] = Metric{CPU: fmt.Sprintf("%dm", cpu), Mem: fmt.Sprintf("%dMi", mem)}
|
||||||
|
}
|
||||||
|
return mx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PerNodeMetrics retrieves all nodes metrics
|
||||||
|
func (m *MetricsServer) PerNodeMetrics(nn []v1.Node) (map[string]Metric, error) {
|
||||||
|
log.Println("Getting per node metrics...")
|
||||||
|
mx := map[string]Metric{}
|
||||||
|
|
||||||
|
mm, err := m.getNodeMetrics()
|
||||||
|
if err != nil {
|
||||||
|
return mx, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, n := range nn {
|
||||||
|
acpu := n.Status.Allocatable["cpu"]
|
||||||
|
amem := n.Status.Allocatable["memory"]
|
||||||
|
var cpu, mem int64
|
||||||
|
for _, m := range mm.Items {
|
||||||
|
if m.Name == n.Name {
|
||||||
|
cpu += m.Usage.Cpu().MilliValue()
|
||||||
|
mem += m.Usage.Memory().Value() / (1024 * 1024)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mx[n.Name] = Metric{
|
||||||
|
CPU: fmt.Sprintf("%dm", cpu),
|
||||||
|
Mem: fmt.Sprintf("%dMi", mem),
|
||||||
|
AvailCPU: fmt.Sprintf("%dm", acpu.MilliValue()),
|
||||||
|
AvailMem: fmt.Sprintf("%dMi", amem.Value()/(1024*1024)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MetricsServer) getPodMetrics() (*metricsapi.PodMetricsList, error) {
|
||||||
|
if conn.hasMetricsServer() {
|
||||||
|
return m.podMetricsViaService()
|
||||||
|
}
|
||||||
|
selector := labels.Everything()
|
||||||
|
|
||||||
|
var mx *metricsapi.PodMetricsList
|
||||||
|
conn, err := conn.heapsterDial()
|
||||||
|
if err != nil {
|
||||||
|
return mx, err
|
||||||
|
}
|
||||||
|
return conn.GetPodMetrics("", "", true, selector)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MetricsServer) getNodeMetrics() (*metricsapi.NodeMetricsList, error) {
|
||||||
|
if conn.hasMetricsServer() {
|
||||||
|
return m.nodeMetricsViaService()
|
||||||
|
}
|
||||||
|
selector := labels.Everything()
|
||||||
|
|
||||||
|
var mx *metricsapi.NodeMetricsList
|
||||||
|
conn, err := conn.heapsterDial()
|
||||||
|
if err != nil {
|
||||||
|
return mx, err
|
||||||
|
}
|
||||||
|
return conn.GetNodeMetrics("", selector.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*MetricsServer) nodeMetricsViaService() (*metricsapi.NodeMetricsList, error) {
|
||||||
|
var mx *metricsapi.NodeMetricsList
|
||||||
|
clt, err := conn.mxsDial()
|
||||||
|
if err != nil {
|
||||||
|
return mx, err
|
||||||
|
}
|
||||||
|
selector := labels.Everything()
|
||||||
|
var versionedMetrics *metricsV1beta1api.NodeMetricsList
|
||||||
|
mc := clt.Metrics()
|
||||||
|
nm := mc.NodeMetricses()
|
||||||
|
versionedMetrics, err = nm.List(metav1.ListOptions{LabelSelector: selector.String()})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
metrics := &metricsapi.NodeMetricsList{}
|
||||||
|
err = metricsV1beta1api.Convert_v1beta1_NodeMetricsList_To_metrics_NodeMetricsList(versionedMetrics, metrics, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return metrics, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*MetricsServer) podMetricsViaService() (*metricsapi.PodMetricsList, error) {
|
||||||
|
var mx *metricsapi.PodMetricsList
|
||||||
|
clt, err := conn.mxsDial()
|
||||||
|
if err != nil {
|
||||||
|
return mx, err
|
||||||
|
}
|
||||||
|
selector := labels.Everything()
|
||||||
|
var versionedMetrics *metricsV1beta1api.PodMetricsList
|
||||||
|
mc := clt.Metrics()
|
||||||
|
nm := mc.PodMetricses("")
|
||||||
|
versionedMetrics, err = nm.List(metav1.ListOptions{LabelSelector: selector.String()})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
metrics := &metricsapi.PodMetricsList{}
|
||||||
|
err = metricsV1beta1api.Convert_v1beta1_PodMetricsList_To_metrics_PodMetricsList(versionedMetrics, metrics, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return metrics, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
package k8s
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Node represents a Kubernetes service
|
||||||
|
type Node struct{}
|
||||||
|
|
||||||
|
// NewNode returns a new Node.
|
||||||
|
func NewNode() Res {
|
||||||
|
return &Node{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a service.
|
||||||
|
func (*Node) Get(_, n string) (interface{}, error) {
|
||||||
|
opts := metav1.GetOptions{}
|
||||||
|
return conn.dialOrDie().CoreV1().Nodes().Get(n, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all services in a given namespace
|
||||||
|
func (*Node) List(_ string) (Collection, error) {
|
||||||
|
opts := metav1.ListOptions{}
|
||||||
|
|
||||||
|
rr, err := conn.dialOrDie().CoreV1().Nodes().List(opts)
|
||||||
|
if err != nil {
|
||||||
|
return Collection{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := make(Collection, len(rr.Items))
|
||||||
|
for i, r := range rr.Items {
|
||||||
|
cc[i] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
return cc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a service
|
||||||
|
func (*Node) Delete(_, n string) error {
|
||||||
|
opts := metav1.DeleteOptions{}
|
||||||
|
return conn.dialOrDie().CoreV1().Nodes().Delete(n, &opts)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
package k8s
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Namespace represents a Kubernetes service
|
||||||
|
type Namespace struct{}
|
||||||
|
|
||||||
|
// NewNamespace returns a new Namespace.
|
||||||
|
func NewNamespace() Res {
|
||||||
|
return &Namespace{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a service.
|
||||||
|
func (*Namespace) Get(_, n string) (interface{}, error) {
|
||||||
|
opts := metav1.GetOptions{}
|
||||||
|
return conn.dialOrDie().CoreV1().Namespaces().Get(n, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all services in a given namespace
|
||||||
|
func (*Namespace) List(_ string) (Collection, error) {
|
||||||
|
opts := metav1.ListOptions{}
|
||||||
|
|
||||||
|
rr, err := conn.dialOrDie().CoreV1().Namespaces().List(opts)
|
||||||
|
if err != nil {
|
||||||
|
return Collection{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := make(Collection, len(rr.Items))
|
||||||
|
for i, r := range rr.Items {
|
||||||
|
cc[i] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
return cc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a service
|
||||||
|
func (*Namespace) Delete(_, n string) error {
|
||||||
|
opts := metav1.DeleteOptions{}
|
||||||
|
return conn.dialOrDie().CoreV1().Namespaces().Delete(n, &opts)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
package k8s
|
||||||
|
|
||||||
|
import (
|
||||||
|
"k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
restclient "k8s.io/client-go/rest"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultTailLines = 10
|
||||||
|
|
||||||
|
// PodRes represents a K8s pod resource.
|
||||||
|
type PodRes interface {
|
||||||
|
Res
|
||||||
|
Containers(ns, n string) ([]string, error)
|
||||||
|
Logs(ns, n, co string) *restclient.Request
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pod represents a Kubernetes service
|
||||||
|
type Pod struct{}
|
||||||
|
|
||||||
|
// NewPod returns a new Pod.
|
||||||
|
func NewPod() Res {
|
||||||
|
return &Pod{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a service.
|
||||||
|
func (*Pod) Get(ns, n string) (interface{}, error) {
|
||||||
|
opts := metav1.GetOptions{}
|
||||||
|
return conn.dialOrDie().CoreV1().Pods(ns).Get(n, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all services in a given namespace
|
||||||
|
func (*Pod) List(ns string) (Collection, error) {
|
||||||
|
opts := metav1.ListOptions{}
|
||||||
|
|
||||||
|
rr, err := conn.dialOrDie().CoreV1().Pods(ns).List(opts)
|
||||||
|
if err != nil {
|
||||||
|
return Collection{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := make(Collection, len(rr.Items))
|
||||||
|
for i, r := range rr.Items {
|
||||||
|
cc[i] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
return cc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a service
|
||||||
|
func (*Pod) Delete(ns, n string) error {
|
||||||
|
opts := metav1.DeleteOptions{}
|
||||||
|
return conn.dialOrDie().CoreV1().Pods(ns).Delete(n, &opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Containers returns all container names on pod
|
||||||
|
func (*Pod) Containers(ns, n string) ([]string, error) {
|
||||||
|
opts := metav1.GetOptions{}
|
||||||
|
cc := []string{}
|
||||||
|
po, err := conn.dialOrDie().CoreV1().Pods(ns).Get(n, opts)
|
||||||
|
if err != nil {
|
||||||
|
return cc, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range po.Spec.Containers {
|
||||||
|
cc = append(cc, c.Name)
|
||||||
|
}
|
||||||
|
return cc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logs fetch container logs for a given pod and container.
|
||||||
|
func (*Pod) Logs(ns, n, co string) *restclient.Request {
|
||||||
|
tl := int64(defaultTailLines)
|
||||||
|
opts := &v1.PodLogOptions{
|
||||||
|
Container: co,
|
||||||
|
Follow: true,
|
||||||
|
TailLines: &tl,
|
||||||
|
}
|
||||||
|
return conn.dialOrDie().CoreV1().Pods(ns).GetLogs(n, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Events retrieved pod's events.
|
||||||
|
func (*Pod) Events(ns, n string) (*v1.EventList, error) {
|
||||||
|
e := conn.dialOrDie().Core().Events(ns)
|
||||||
|
sel := e.GetFieldSelector(&n, &ns, nil, nil)
|
||||||
|
opts := metav1.ListOptions{FieldSelector: sel.String()}
|
||||||
|
ee, err := e.List(opts)
|
||||||
|
return ee, err
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
package k8s
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PV represents a Kubernetes service
|
||||||
|
type PV struct{}
|
||||||
|
|
||||||
|
// NewPV returns a new PV.
|
||||||
|
func NewPV() Res {
|
||||||
|
return &PV{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a service.
|
||||||
|
func (*PV) Get(_, n string) (interface{}, error) {
|
||||||
|
opts := metav1.GetOptions{}
|
||||||
|
return conn.dialOrDie().CoreV1().PersistentVolumes().Get(n, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all services in a given namespace
|
||||||
|
func (*PV) List(_ string) (Collection, error) {
|
||||||
|
opts := metav1.ListOptions{}
|
||||||
|
|
||||||
|
rr, err := conn.dialOrDie().CoreV1().PersistentVolumes().List(opts)
|
||||||
|
if err != nil {
|
||||||
|
return Collection{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := make(Collection, len(rr.Items))
|
||||||
|
for i, r := range rr.Items {
|
||||||
|
cc[i] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
return cc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a service
|
||||||
|
func (*PV) Delete(_, n string) error {
|
||||||
|
opts := metav1.DeleteOptions{}
|
||||||
|
return conn.dialOrDie().CoreV1().PersistentVolumes().Delete(n, &opts)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
package k8s
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PVC represents a Kubernetes service
|
||||||
|
type PVC struct{}
|
||||||
|
|
||||||
|
// NewPVC returns a new PVC.
|
||||||
|
func NewPVC() Res {
|
||||||
|
return &PVC{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a service.
|
||||||
|
func (*PVC) Get(ns, n string) (interface{}, error) {
|
||||||
|
opts := metav1.GetOptions{}
|
||||||
|
return conn.dialOrDie().CoreV1().PersistentVolumeClaims(ns).Get(n, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all services in a given namespace
|
||||||
|
func (*PVC) List(ns string) (Collection, error) {
|
||||||
|
opts := metav1.ListOptions{}
|
||||||
|
|
||||||
|
rr, err := conn.dialOrDie().CoreV1().PersistentVolumeClaims(ns).List(opts)
|
||||||
|
if err != nil {
|
||||||
|
return Collection{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := make(Collection, len(rr.Items))
|
||||||
|
for i, r := range rr.Items {
|
||||||
|
cc[i] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
return cc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a service
|
||||||
|
func (*PVC) Delete(ns, n string) error {
|
||||||
|
opts := metav1.DeleteOptions{}
|
||||||
|
return conn.dialOrDie().CoreV1().PersistentVolumeClaims(ns).Delete(n, &opts)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
package k8s
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||||
|
"k8s.io/client-go/dynamic"
|
||||||
|
"k8s.io/client-go/rest"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Resource represents a Kubernetes Resource
|
||||||
|
type Resource struct {
|
||||||
|
group, version, name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewResource returns a new Resource.
|
||||||
|
func NewResource(group, version, name string) Res {
|
||||||
|
return &Resource{group: group, version: version, name: name}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInfo returns info about apigroup.
|
||||||
|
func (r *Resource) GetInfo() (string, string, string) {
|
||||||
|
return r.group, r.version, r.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Resource) base() dynamic.NamespaceableResourceInterface {
|
||||||
|
g := schema.GroupVersionResource{
|
||||||
|
Group: r.group,
|
||||||
|
Version: r.version,
|
||||||
|
Resource: r.name,
|
||||||
|
}
|
||||||
|
return conn.dynDialOrDie().Resource(g)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a Resource.
|
||||||
|
func (r *Resource) Get(ns, n string) (interface{}, error) {
|
||||||
|
opts := metav1.GetOptions{}
|
||||||
|
return r.base().Namespace(ns).Get(n, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all Resources in a given namespace
|
||||||
|
func (r *Resource) List(ns string) (Collection, error) {
|
||||||
|
obj, err := r.listAll(ns, r.name)
|
||||||
|
if err != nil {
|
||||||
|
return Collection{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return Collection{obj.(*metav1beta1.Table)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a Resource
|
||||||
|
func (r *Resource) Delete(ns, n string) error {
|
||||||
|
opts := metav1.DeleteOptions{}
|
||||||
|
return r.base().Namespace(ns).Delete(n, &opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
func (r *Resource) getClient() *rest.RESTClient {
|
||||||
|
gv := schema.GroupVersion{Group: r.group, Version: r.version}
|
||||||
|
codecs, _ := r.codecs()
|
||||||
|
crConfig := *conn.configOrDie()
|
||||||
|
crConfig.GroupVersion = &gv
|
||||||
|
crConfig.APIPath = "/apis"
|
||||||
|
if len(r.group) == 0 {
|
||||||
|
crConfig.APIPath = "/api"
|
||||||
|
}
|
||||||
|
crConfig.NegotiatedSerializer = serializer.DirectCodecFactory{CodecFactory: codecs}
|
||||||
|
crRestClient, err := rest.RESTClientFor(&crConfig)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
return crRestClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Resource) codecs() (serializer.CodecFactory, runtime.ParameterCodec) {
|
||||||
|
scheme := runtime.NewScheme()
|
||||||
|
gv := schema.GroupVersion{Group: r.group, Version: r.version}
|
||||||
|
metav1.AddToGroupVersion(scheme, gv)
|
||||||
|
scheme.AddKnownTypes(gv, &metav1beta1.Table{}, &metav1beta1.TableOptions{})
|
||||||
|
scheme.AddKnownTypes(metav1beta1.SchemeGroupVersion, &metav1beta1.Table{}, &metav1beta1.TableOptions{})
|
||||||
|
codecs := serializer.NewCodecFactory(scheme)
|
||||||
|
return codecs, runtime.NewParameterCodec(scheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Resource) listAll(ns, n string) (runtime.Object, error) {
|
||||||
|
group := metav1beta1.GroupName
|
||||||
|
version := metav1beta1.SchemeGroupVersion.Version
|
||||||
|
a := fmt.Sprintf("application/json;as=Table;v=%s;g=%s, application/json", version, group)
|
||||||
|
|
||||||
|
_, codec := r.codecs()
|
||||||
|
return r.getClient().Get().
|
||||||
|
SetHeader("Accept", a).
|
||||||
|
Namespace(ns).
|
||||||
|
Resource(n).
|
||||||
|
VersionedParams(&metav1beta1.TableOptions{}, codec).
|
||||||
|
Do().
|
||||||
|
Get()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Resource) getOne(ns, n string) (runtime.Object, error) {
|
||||||
|
group := metav1beta1.GroupName
|
||||||
|
version := metav1beta1.SchemeGroupVersion.Version
|
||||||
|
a := fmt.Sprintf("application/json;as=Table;v=%s;g=%s, application/json", version, group)
|
||||||
|
|
||||||
|
_, codec := r.codecs()
|
||||||
|
return r.getClient().Get().
|
||||||
|
SetHeader("Accept", a).
|
||||||
|
Namespace(ns).
|
||||||
|
Resource(n).
|
||||||
|
VersionedParams(&metav1beta1.TableOptions{}, codec).
|
||||||
|
Do().
|
||||||
|
Get()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
package k8s
|
||||||
|
|
||||||
|
import (
|
||||||
|
// rbacv1 "k8s.io/api/rbac/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Role represents a Kubernetes service
|
||||||
|
type Role struct{}
|
||||||
|
|
||||||
|
// NewRole returns a new Role.
|
||||||
|
func NewRole() Res {
|
||||||
|
return &Role{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a service.
|
||||||
|
func (*Role) Get(ns, n string) (interface{}, error) {
|
||||||
|
opts := metav1.GetOptions{}
|
||||||
|
return conn.dialOrDie().RbacV1().Roles(ns).Get(n, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all services in a given namespace
|
||||||
|
func (*Role) List(ns string) (Collection, error) {
|
||||||
|
opts := metav1.ListOptions{}
|
||||||
|
|
||||||
|
rr, err := conn.dialOrDie().RbacV1().Roles(ns).List(opts)
|
||||||
|
if err != nil {
|
||||||
|
return Collection{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := make(Collection, len(rr.Items))
|
||||||
|
for i, r := range rr.Items {
|
||||||
|
cc[i] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
return cc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a service
|
||||||
|
func (*Role) Delete(ns, n string) error {
|
||||||
|
opts := metav1.DeleteOptions{}
|
||||||
|
return conn.dialOrDie().RbacV1().Roles(ns).Delete(n, &opts)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
package k8s
|
||||||
|
|
||||||
|
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
|
// RoleBinding represents a Kubernetes service
|
||||||
|
type RoleBinding struct{}
|
||||||
|
|
||||||
|
// NewRoleBinding returns a new RoleBinding.
|
||||||
|
func NewRoleBinding() Res {
|
||||||
|
return &RoleBinding{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a service.
|
||||||
|
func (*RoleBinding) Get(ns, n string) (interface{}, error) {
|
||||||
|
opts := metav1.GetOptions{}
|
||||||
|
return conn.dialOrDie().RbacV1().RoleBindings(ns).Get(n, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all services in a given namespace
|
||||||
|
func (*RoleBinding) List(ns string) (Collection, error) {
|
||||||
|
opts := metav1.ListOptions{}
|
||||||
|
|
||||||
|
rr, err := conn.dialOrDie().RbacV1().RoleBindings(ns).List(opts)
|
||||||
|
if err != nil {
|
||||||
|
return Collection{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := make(Collection, len(rr.Items))
|
||||||
|
for i, r := range rr.Items {
|
||||||
|
cc[i] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
return cc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a service
|
||||||
|
func (*RoleBinding) Delete(ns, n string) error {
|
||||||
|
opts := metav1.DeleteOptions{}
|
||||||
|
return conn.dialOrDie().RbacV1().RoleBindings(ns).Delete(n, &opts)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
package k8s
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReplicaSet represents a Kubernetes service
|
||||||
|
type ReplicaSet struct{}
|
||||||
|
|
||||||
|
// NewReplicaSet returns a new ReplicaSet.
|
||||||
|
func NewReplicaSet() Res {
|
||||||
|
return &ReplicaSet{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a service.
|
||||||
|
func (*ReplicaSet) Get(ns, n string) (interface{}, error) {
|
||||||
|
opts := metav1.GetOptions{}
|
||||||
|
return conn.dialOrDie().Apps().ReplicaSets(ns).Get(n, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all services in a given namespace
|
||||||
|
func (*ReplicaSet) List(ns string) (Collection, error) {
|
||||||
|
opts := metav1.ListOptions{}
|
||||||
|
|
||||||
|
rr, err := conn.dialOrDie().Apps().ReplicaSets(ns).List(opts)
|
||||||
|
if err != nil {
|
||||||
|
return Collection{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := make(Collection, len(rr.Items))
|
||||||
|
for i, r := range rr.Items {
|
||||||
|
cc[i] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
return cc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a service
|
||||||
|
func (*ReplicaSet) Delete(ns, n string) error {
|
||||||
|
opts := metav1.DeleteOptions{}
|
||||||
|
return conn.dialOrDie().Apps().ReplicaSets(ns).Delete(n, &opts)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
package k8s
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ServiceAccount manages a Kubernetes ServiceAccount.
|
||||||
|
type ServiceAccount struct{}
|
||||||
|
|
||||||
|
// NewServiceAccount instantiates a new ServiceAccount.
|
||||||
|
func NewServiceAccount() Res {
|
||||||
|
return &ServiceAccount{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a ServiceAccount
|
||||||
|
func (*ServiceAccount) Get(ns, n string) (interface{}, error) {
|
||||||
|
opts := metav1.GetOptions{}
|
||||||
|
o, err := conn.dialOrDie().CoreV1().ServiceAccounts(ns).Get(n, opts)
|
||||||
|
if err != nil {
|
||||||
|
return o, err
|
||||||
|
}
|
||||||
|
return o, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all ServiceAccounts in a given namespace
|
||||||
|
func (*ServiceAccount) List(ns string) (Collection, error) {
|
||||||
|
opts := metav1.ListOptions{}
|
||||||
|
|
||||||
|
rr, err := conn.dialOrDie().CoreV1().ServiceAccounts(ns).List(opts)
|
||||||
|
if err != nil {
|
||||||
|
return Collection{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := make(Collection, len(rr.Items))
|
||||||
|
for i, r := range rr.Items {
|
||||||
|
cc[i] = r
|
||||||
|
}
|
||||||
|
return cc, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a ServiceAccount
|
||||||
|
func (*ServiceAccount) Delete(ns, n string) error {
|
||||||
|
opts := metav1.DeleteOptions{}
|
||||||
|
return conn.dialOrDie().CoreV1().ServiceAccounts(ns).Delete(n, &opts)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
package k8s
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Secret represents a Kubernetes Secret
|
||||||
|
type Secret struct{}
|
||||||
|
|
||||||
|
// NewSecret returns a new Secret.
|
||||||
|
func NewSecret() Res {
|
||||||
|
return &Secret{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a Secret.
|
||||||
|
func (c *Secret) Get(ns, n string) (interface{}, error) {
|
||||||
|
opts := metav1.GetOptions{}
|
||||||
|
return conn.dialOrDie().CoreV1().Secrets(ns).Get(n, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all Secrets in a given namespace
|
||||||
|
func (c *Secret) List(ns string) (Collection, error) {
|
||||||
|
opts := metav1.ListOptions{}
|
||||||
|
|
||||||
|
rr, err := conn.dialOrDie().CoreV1().Secrets(ns).List(opts)
|
||||||
|
if err != nil {
|
||||||
|
return Collection{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := make(Collection, len(rr.Items))
|
||||||
|
for i, r := range rr.Items {
|
||||||
|
cc[i] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
return cc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a Secret
|
||||||
|
func (c *Secret) Delete(ns, n string) error {
|
||||||
|
opts := metav1.DeleteOptions{}
|
||||||
|
return conn.dialOrDie().CoreV1().Secrets(ns).Delete(n, &opts)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
package k8s
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StatefulSet manages a Kubernetes StatefulSet.
|
||||||
|
type StatefulSet struct{}
|
||||||
|
|
||||||
|
// NewStatefulSet instantiates a new StatefulSet.
|
||||||
|
func NewStatefulSet() Res {
|
||||||
|
return &StatefulSet{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a StatefulSet
|
||||||
|
func (*StatefulSet) Get(ns, n string) (interface{}, error) {
|
||||||
|
opts := metav1.GetOptions{}
|
||||||
|
o, err := conn.dialOrDie().AppsV1().StatefulSets(ns).Get(n, opts)
|
||||||
|
if err != nil {
|
||||||
|
return o, err
|
||||||
|
}
|
||||||
|
return o, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all StatefulSets in a given namespace
|
||||||
|
func (*StatefulSet) List(ns string) (Collection, error) {
|
||||||
|
opts := metav1.ListOptions{}
|
||||||
|
|
||||||
|
rr, err := conn.dialOrDie().AppsV1().StatefulSets(ns).List(opts)
|
||||||
|
if err != nil {
|
||||||
|
return Collection{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := make(Collection, len(rr.Items))
|
||||||
|
for i, r := range rr.Items {
|
||||||
|
cc[i] = r
|
||||||
|
}
|
||||||
|
return cc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a StatefulSet
|
||||||
|
func (*StatefulSet) Delete(ns, n string) error {
|
||||||
|
opts := metav1.DeleteOptions{}
|
||||||
|
return conn.dialOrDie().AppsV1().StatefulSets(ns).Delete(n, &opts)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
package k8s
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Service represents a Kubernetes Service
|
||||||
|
type Service struct{}
|
||||||
|
|
||||||
|
// NewService returns a new Service.
|
||||||
|
func NewService() Res {
|
||||||
|
return &Service{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a service.
|
||||||
|
func (*Service) Get(ns, n string) (interface{}, error) {
|
||||||
|
opts := metav1.GetOptions{}
|
||||||
|
return conn.dialOrDie().CoreV1().Services(ns).Get(n, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all Services in a given namespace
|
||||||
|
func (*Service) List(ns string) (Collection, error) {
|
||||||
|
opts := metav1.ListOptions{}
|
||||||
|
|
||||||
|
rr, err := conn.dialOrDie().CoreV1().Services(ns).List(opts)
|
||||||
|
if err != nil {
|
||||||
|
return Collection{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := make(Collection, len(rr.Items))
|
||||||
|
for i, r := range rr.Items {
|
||||||
|
cc[i] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
return cc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a Service
|
||||||
|
func (*Service) Delete(ns, n string) error {
|
||||||
|
opts := metav1.DeleteOptions{}
|
||||||
|
return conn.dialOrDie().CoreV1().Services(ns).Delete(n, &opts)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,246 @@
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
"k8s.io/apimachinery/pkg/watch"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// GetAccess set if resource can be fetched.
|
||||||
|
GetAccess = 1 << iota
|
||||||
|
// ListAccess set if resource can be listed.
|
||||||
|
ListAccess
|
||||||
|
// EditAccess set if resource can be edited.
|
||||||
|
EditAccess
|
||||||
|
// DeleteAccess set if resource can be deleted.
|
||||||
|
DeleteAccess
|
||||||
|
// ViewAccess set if resource can be viewed.
|
||||||
|
ViewAccess
|
||||||
|
// NamespaceAccess set if namespaced resource.
|
||||||
|
NamespaceAccess
|
||||||
|
// DescribeAccess set if resource can be described.
|
||||||
|
DescribeAccess
|
||||||
|
// SwitchAccess set if resource can be switched (Context).
|
||||||
|
SwitchAccess
|
||||||
|
|
||||||
|
// CRUDAccess Verbs.
|
||||||
|
CRUDAccess = GetAccess | ListAccess | DeleteAccess | ViewAccess | EditAccess
|
||||||
|
|
||||||
|
// AllVerbsAccess super powers.
|
||||||
|
AllVerbsAccess = CRUDAccess | NamespaceAccess
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// SortFn provides for sorting items in list.
|
||||||
|
SortFn func([]string)
|
||||||
|
|
||||||
|
// RowEvent represents a call for action after a resource reconciliation.
|
||||||
|
// Tracks whether a resource got added, deleted or updated.
|
||||||
|
RowEvent struct {
|
||||||
|
Action watch.EventType
|
||||||
|
Fields Row
|
||||||
|
Deltas Row
|
||||||
|
}
|
||||||
|
|
||||||
|
// RowEvents tracks resource update events.
|
||||||
|
RowEvents map[string]*RowEvent
|
||||||
|
|
||||||
|
// Properties a collection of extra properties on a K8s resource.
|
||||||
|
Properties map[string]interface{}
|
||||||
|
|
||||||
|
// TableData tracks a K8s resource for tabular display.
|
||||||
|
TableData struct {
|
||||||
|
Header Row
|
||||||
|
Rows RowEvents
|
||||||
|
Namespace string
|
||||||
|
}
|
||||||
|
|
||||||
|
// List protocol to display and update a collection of resources
|
||||||
|
List interface {
|
||||||
|
Data() TableData
|
||||||
|
Resource() Resource
|
||||||
|
GetNamespace() string
|
||||||
|
SetNamespace(string)
|
||||||
|
Reconcile() error
|
||||||
|
Describe(pa string) (Properties, error)
|
||||||
|
GetName() string
|
||||||
|
Access(flag int) bool
|
||||||
|
HasXRay() bool
|
||||||
|
SortFn() SortFn
|
||||||
|
}
|
||||||
|
|
||||||
|
// Columnar tracks resources that can be diplayed in a tabular fashion.
|
||||||
|
Columnar interface {
|
||||||
|
Header(ns string) Row
|
||||||
|
Fields(ns string) Row
|
||||||
|
ExtFields() Properties
|
||||||
|
Name() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row represents a collection of string fields.
|
||||||
|
Row []string
|
||||||
|
|
||||||
|
// Columnars a collection of columnars.
|
||||||
|
Columnars []Columnar
|
||||||
|
|
||||||
|
// MxColumnar tracks resource metrics.
|
||||||
|
MxColumnar interface {
|
||||||
|
Columnar
|
||||||
|
Metrics() k8s.Metric
|
||||||
|
SetMetrics(k8s.Metric)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resource tracks generic Kubernetes resources.
|
||||||
|
Resource interface {
|
||||||
|
NewInstance(interface{}) Columnar
|
||||||
|
Get(path string) (Columnar, error)
|
||||||
|
List(ns string) (Columnars, error)
|
||||||
|
Delete(path string) error
|
||||||
|
Marshal(pa string) (string, error)
|
||||||
|
Header(ns string) Row
|
||||||
|
}
|
||||||
|
|
||||||
|
list struct {
|
||||||
|
namespace, name string
|
||||||
|
verbs int
|
||||||
|
xray bool
|
||||||
|
api Resource
|
||||||
|
cache RowEvents
|
||||||
|
sortFn func([]string)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func newRowEvent(a watch.EventType, f, d Row) *RowEvent {
|
||||||
|
return &RowEvent{Action: a, Fields: f, Deltas: d}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newList(ns, name string, api Resource, v int) *list {
|
||||||
|
return &list{
|
||||||
|
namespace: ns,
|
||||||
|
name: name,
|
||||||
|
verbs: v,
|
||||||
|
api: api,
|
||||||
|
cache: RowEvents{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *list) SortFn() SortFn {
|
||||||
|
if l.sortFn == nil {
|
||||||
|
return sort.Strings
|
||||||
|
}
|
||||||
|
return l.sortFn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *list) HasXRay() bool {
|
||||||
|
return l.xray
|
||||||
|
}
|
||||||
|
|
||||||
|
// Access check access control on a given resource.
|
||||||
|
func (l *list) Access(f int) bool {
|
||||||
|
return l.verbs&f == f
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNamespace associated with the resource.
|
||||||
|
func (l *list) GetNamespace() string {
|
||||||
|
if !l.Access(NamespaceAccess) {
|
||||||
|
l.namespace = NotNamespaced
|
||||||
|
}
|
||||||
|
return l.namespace
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNamespace updates the namespace on the list. Default ns is "" for all
|
||||||
|
// namespaces.
|
||||||
|
func (l *list) SetNamespace(n string) {
|
||||||
|
l.cache = RowEvents{}
|
||||||
|
if l.Access(NamespaceAccess) {
|
||||||
|
l.namespace = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName returns the kubernetes resource name.
|
||||||
|
func (l *list) GetName() string {
|
||||||
|
return l.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resource returns a resource api connection.
|
||||||
|
func (l *list) Resource() Resource {
|
||||||
|
return l.api
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache tracks previous resource state.
|
||||||
|
func (l *list) Data() TableData {
|
||||||
|
return TableData{
|
||||||
|
Header: l.api.Header(l.namespace),
|
||||||
|
Rows: l.cache,
|
||||||
|
Namespace: l.namespace,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *list) Describe(pa string) (Properties, error) {
|
||||||
|
var p Properties
|
||||||
|
i, err := l.api.Get(pa)
|
||||||
|
if err != nil {
|
||||||
|
return p, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return i.ExtFields(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reconcile previous vs current state and emits delta events.
|
||||||
|
func (l *list) Reconcile() error {
|
||||||
|
var (
|
||||||
|
items Columnars
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if items, err = l.api.List(l.namespace); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(l.cache) == 0 {
|
||||||
|
for _, i := range items {
|
||||||
|
ff := i.Fields(l.namespace)
|
||||||
|
l.cache[i.Name()] = newRowEvent(New, ff, make(Row, len(ff)))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
kk := make([]string, 0, len(items))
|
||||||
|
for _, i := range items {
|
||||||
|
a := watch.Added
|
||||||
|
ff := i.Fields(l.namespace)
|
||||||
|
dd := make(Row, len(ff))
|
||||||
|
kk = append(kk, i.Name())
|
||||||
|
if evt, ok := l.cache[i.Name()]; ok {
|
||||||
|
f1, f2 := evt.Fields[:len(evt.Fields)-2], ff[:len(ff)-2]
|
||||||
|
a = Unchanged
|
||||||
|
if !reflect.DeepEqual(f1, f2) {
|
||||||
|
for i, f := range f1 {
|
||||||
|
if f != f2[i] {
|
||||||
|
dd[i] = f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a = watch.Modified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
l.cache[i.Name()] = newRowEvent(a, ff, dd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for deletions!
|
||||||
|
for k := range l.cache {
|
||||||
|
var found bool
|
||||||
|
for _, key := range kk {
|
||||||
|
if k == key {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
delete(l.cache, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,198 @@
|
||||||
|
// Code generated by pegomock. DO NOT EDIT.
|
||||||
|
// Source: github.com/k8sland/k9s/resource (interfaces: Caller)
|
||||||
|
|
||||||
|
package resource_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
k8s "github.com/k8sland/k9s/resource/k8s"
|
||||||
|
pegomock "github.com/petergtz/pegomock"
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MockCaller struct {
|
||||||
|
fail func(message string, callerSkip ...int)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMockCaller() *MockCaller {
|
||||||
|
return &MockCaller{fail: pegomock.GlobalFailHandler}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mock *MockCaller) Delete(_param0 string, _param1 string) error {
|
||||||
|
if mock == nil {
|
||||||
|
panic("mock must not be nil. Use myMock := NewMockCaller().")
|
||||||
|
}
|
||||||
|
params := []pegomock.Param{_param0, _param1}
|
||||||
|
result := pegomock.GetGenericMockFrom(mock).Invoke("Delete", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})
|
||||||
|
var ret0 error
|
||||||
|
if len(result) != 0 {
|
||||||
|
if result[0] != nil {
|
||||||
|
ret0 = result[0].(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mock *MockCaller) Get(_param0 string, _param1 string) (interface{}, error) {
|
||||||
|
if mock == nil {
|
||||||
|
panic("mock must not be nil. Use myMock := NewMockCaller().")
|
||||||
|
}
|
||||||
|
params := []pegomock.Param{_param0, _param1}
|
||||||
|
result := pegomock.GetGenericMockFrom(mock).Invoke("Get", params, []reflect.Type{reflect.TypeOf((*interface{})(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
|
||||||
|
var ret0 interface{}
|
||||||
|
var ret1 error
|
||||||
|
if len(result) != 0 {
|
||||||
|
if result[0] != nil {
|
||||||
|
ret0 = result[0].(interface{})
|
||||||
|
}
|
||||||
|
if result[1] != nil {
|
||||||
|
ret1 = result[1].(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mock *MockCaller) List(_param0 string) (k8s.Collection, error) {
|
||||||
|
if mock == nil {
|
||||||
|
panic("mock must not be nil. Use myMock := NewMockCaller().")
|
||||||
|
}
|
||||||
|
params := []pegomock.Param{_param0}
|
||||||
|
result := pegomock.GetGenericMockFrom(mock).Invoke("List", params, []reflect.Type{reflect.TypeOf((*k8s.Collection)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
|
||||||
|
var ret0 k8s.Collection
|
||||||
|
var ret1 error
|
||||||
|
if len(result) != 0 {
|
||||||
|
if result[0] != nil {
|
||||||
|
ret0 = result[0].(k8s.Collection)
|
||||||
|
}
|
||||||
|
if result[1] != nil {
|
||||||
|
ret1 = result[1].(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mock *MockCaller) VerifyWasCalledOnce() *VerifierCaller {
|
||||||
|
return &VerifierCaller{
|
||||||
|
mock: mock,
|
||||||
|
invocationCountMatcher: pegomock.Times(1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mock *MockCaller) VerifyWasCalled(invocationCountMatcher pegomock.Matcher) *VerifierCaller {
|
||||||
|
return &VerifierCaller{
|
||||||
|
mock: mock,
|
||||||
|
invocationCountMatcher: invocationCountMatcher,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mock *MockCaller) VerifyWasCalledInOrder(invocationCountMatcher pegomock.Matcher, inOrderContext *pegomock.InOrderContext) *VerifierCaller {
|
||||||
|
return &VerifierCaller{
|
||||||
|
mock: mock,
|
||||||
|
invocationCountMatcher: invocationCountMatcher,
|
||||||
|
inOrderContext: inOrderContext,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mock *MockCaller) VerifyWasCalledEventually(invocationCountMatcher pegomock.Matcher, timeout time.Duration) *VerifierCaller {
|
||||||
|
return &VerifierCaller{
|
||||||
|
mock: mock,
|
||||||
|
invocationCountMatcher: invocationCountMatcher,
|
||||||
|
timeout: timeout,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type VerifierCaller struct {
|
||||||
|
mock *MockCaller
|
||||||
|
invocationCountMatcher pegomock.Matcher
|
||||||
|
inOrderContext *pegomock.InOrderContext
|
||||||
|
timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (verifier *VerifierCaller) Delete(_param0 string, _param1 string) *Caller_Delete_OngoingVerification {
|
||||||
|
params := []pegomock.Param{_param0, _param1}
|
||||||
|
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Delete", params, verifier.timeout)
|
||||||
|
return &Caller_Delete_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Caller_Delete_OngoingVerification struct {
|
||||||
|
mock *MockCaller
|
||||||
|
methodInvocations []pegomock.MethodInvocation
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Caller_Delete_OngoingVerification) GetCapturedArguments() (string, string) {
|
||||||
|
_param0, _param1 := c.GetAllCapturedArguments()
|
||||||
|
return _param0[len(_param0)-1], _param1[len(_param1)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Caller_Delete_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []string) {
|
||||||
|
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
|
||||||
|
if len(params) > 0 {
|
||||||
|
_param0 = make([]string, len(params[0]))
|
||||||
|
for u, param := range params[0] {
|
||||||
|
_param0[u] = param.(string)
|
||||||
|
}
|
||||||
|
_param1 = make([]string, len(params[1]))
|
||||||
|
for u, param := range params[1] {
|
||||||
|
_param1[u] = param.(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (verifier *VerifierCaller) Get(_param0 string, _param1 string) *Caller_Get_OngoingVerification {
|
||||||
|
params := []pegomock.Param{_param0, _param1}
|
||||||
|
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Get", params, verifier.timeout)
|
||||||
|
return &Caller_Get_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Caller_Get_OngoingVerification struct {
|
||||||
|
mock *MockCaller
|
||||||
|
methodInvocations []pegomock.MethodInvocation
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Caller_Get_OngoingVerification) GetCapturedArguments() (string, string) {
|
||||||
|
_param0, _param1 := c.GetAllCapturedArguments()
|
||||||
|
return _param0[len(_param0)-1], _param1[len(_param1)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Caller_Get_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []string) {
|
||||||
|
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
|
||||||
|
if len(params) > 0 {
|
||||||
|
_param0 = make([]string, len(params[0]))
|
||||||
|
for u, param := range params[0] {
|
||||||
|
_param0[u] = param.(string)
|
||||||
|
}
|
||||||
|
_param1 = make([]string, len(params[1]))
|
||||||
|
for u, param := range params[1] {
|
||||||
|
_param1[u] = param.(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (verifier *VerifierCaller) List(_param0 string) *Caller_List_OngoingVerification {
|
||||||
|
params := []pegomock.Param{_param0}
|
||||||
|
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "List", params, verifier.timeout)
|
||||||
|
return &Caller_List_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Caller_List_OngoingVerification struct {
|
||||||
|
mock *MockCaller
|
||||||
|
methodInvocations []pegomock.MethodInvocation
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Caller_List_OngoingVerification) GetCapturedArguments() string {
|
||||||
|
_param0 := c.GetAllCapturedArguments()
|
||||||
|
return _param0[len(_param0)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Caller_List_OngoingVerification) GetAllCapturedArguments() (_param0 []string) {
|
||||||
|
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
|
||||||
|
if len(params) > 0 {
|
||||||
|
_param0 = make([]string, len(params[0]))
|
||||||
|
for u, param := range params[0] {
|
||||||
|
_param0[u] = param.(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
// Code generated by pegomock. DO NOT EDIT.
|
||||||
|
// Source: github.com/k8sland/k9s/resource (interfaces: ClusterIfc)
|
||||||
|
|
||||||
|
package resource_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
pegomock "github.com/petergtz/pegomock"
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MockClusterIfc struct {
|
||||||
|
fail func(message string, callerSkip ...int)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMockClusterIfc() *MockClusterIfc {
|
||||||
|
return &MockClusterIfc{fail: pegomock.GlobalFailHandler}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mock *MockClusterIfc) ClusterName() string {
|
||||||
|
if mock == nil {
|
||||||
|
panic("mock must not be nil. Use myMock := NewMockClusterIfc().")
|
||||||
|
}
|
||||||
|
params := []pegomock.Param{}
|
||||||
|
result := pegomock.GetGenericMockFrom(mock).Invoke("ClusterName", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem()})
|
||||||
|
var ret0 string
|
||||||
|
if len(result) != 0 {
|
||||||
|
if result[0] != nil {
|
||||||
|
ret0 = result[0].(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mock *MockClusterIfc) Version() (string, error) {
|
||||||
|
if mock == nil {
|
||||||
|
panic("mock must not be nil. Use myMock := NewMockClusterIfc().")
|
||||||
|
}
|
||||||
|
params := []pegomock.Param{}
|
||||||
|
result := pegomock.GetGenericMockFrom(mock).Invoke("Version", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
|
||||||
|
var ret0 string
|
||||||
|
var ret1 error
|
||||||
|
if len(result) != 0 {
|
||||||
|
if result[0] != nil {
|
||||||
|
ret0 = result[0].(string)
|
||||||
|
}
|
||||||
|
if result[1] != nil {
|
||||||
|
ret1 = result[1].(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mock *MockClusterIfc) VerifyWasCalledOnce() *VerifierClusterIfc {
|
||||||
|
return &VerifierClusterIfc{
|
||||||
|
mock: mock,
|
||||||
|
invocationCountMatcher: pegomock.Times(1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mock *MockClusterIfc) VerifyWasCalled(invocationCountMatcher pegomock.Matcher) *VerifierClusterIfc {
|
||||||
|
return &VerifierClusterIfc{
|
||||||
|
mock: mock,
|
||||||
|
invocationCountMatcher: invocationCountMatcher,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mock *MockClusterIfc) VerifyWasCalledInOrder(invocationCountMatcher pegomock.Matcher, inOrderContext *pegomock.InOrderContext) *VerifierClusterIfc {
|
||||||
|
return &VerifierClusterIfc{
|
||||||
|
mock: mock,
|
||||||
|
invocationCountMatcher: invocationCountMatcher,
|
||||||
|
inOrderContext: inOrderContext,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mock *MockClusterIfc) VerifyWasCalledEventually(invocationCountMatcher pegomock.Matcher, timeout time.Duration) *VerifierClusterIfc {
|
||||||
|
return &VerifierClusterIfc{
|
||||||
|
mock: mock,
|
||||||
|
invocationCountMatcher: invocationCountMatcher,
|
||||||
|
timeout: timeout,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type VerifierClusterIfc struct {
|
||||||
|
mock *MockClusterIfc
|
||||||
|
invocationCountMatcher pegomock.Matcher
|
||||||
|
inOrderContext *pegomock.InOrderContext
|
||||||
|
timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (verifier *VerifierClusterIfc) ClusterName() *ClusterIfc_ClusterName_OngoingVerification {
|
||||||
|
params := []pegomock.Param{}
|
||||||
|
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ClusterName", params, verifier.timeout)
|
||||||
|
return &ClusterIfc_ClusterName_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClusterIfc_ClusterName_OngoingVerification struct {
|
||||||
|
mock *MockClusterIfc
|
||||||
|
methodInvocations []pegomock.MethodInvocation
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClusterIfc_ClusterName_OngoingVerification) GetCapturedArguments() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClusterIfc_ClusterName_OngoingVerification) GetAllCapturedArguments() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (verifier *VerifierClusterIfc) Version() *ClusterIfc_Version_OngoingVerification {
|
||||||
|
params := []pegomock.Param{}
|
||||||
|
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Version", params, verifier.timeout)
|
||||||
|
return &ClusterIfc_Version_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClusterIfc_Version_OngoingVerification struct {
|
||||||
|
mock *MockClusterIfc
|
||||||
|
methodInvocations []pegomock.MethodInvocation
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClusterIfc_Version_OngoingVerification) GetCapturedArguments() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClusterIfc_Version_OngoingVerification) GetAllCapturedArguments() {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,175 @@
|
||||||
|
// Code generated by pegomock. DO NOT EDIT.
|
||||||
|
// Source: github.com/k8sland/k9s/resource (interfaces: MetricsIfc)
|
||||||
|
|
||||||
|
package resource_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
k8s "github.com/k8sland/k9s/resource/k8s"
|
||||||
|
pegomock "github.com/petergtz/pegomock"
|
||||||
|
v1 "k8s.io/api/core/v1"
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MockMetricsIfc struct {
|
||||||
|
fail func(message string, callerSkip ...int)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMockMetricsIfc() *MockMetricsIfc {
|
||||||
|
return &MockMetricsIfc{fail: pegomock.GlobalFailHandler}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mock *MockMetricsIfc) NodeMetrics() (k8s.Metric, error) {
|
||||||
|
if mock == nil {
|
||||||
|
panic("mock must not be nil. Use myMock := NewMockMetricsIfc().")
|
||||||
|
}
|
||||||
|
params := []pegomock.Param{}
|
||||||
|
result := pegomock.GetGenericMockFrom(mock).Invoke("NodeMetrics", params, []reflect.Type{reflect.TypeOf((*k8s.Metric)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
|
||||||
|
var ret0 k8s.Metric
|
||||||
|
var ret1 error
|
||||||
|
if len(result) != 0 {
|
||||||
|
if result[0] != nil {
|
||||||
|
ret0 = result[0].(k8s.Metric)
|
||||||
|
}
|
||||||
|
if result[1] != nil {
|
||||||
|
ret1 = result[1].(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mock *MockMetricsIfc) PerNodeMetrics(_param0 []v1.Node) (map[string]k8s.Metric, error) {
|
||||||
|
if mock == nil {
|
||||||
|
panic("mock must not be nil. Use myMock := NewMockMetricsIfc().")
|
||||||
|
}
|
||||||
|
params := []pegomock.Param{_param0}
|
||||||
|
result := pegomock.GetGenericMockFrom(mock).Invoke("PerNodeMetrics", params, []reflect.Type{reflect.TypeOf((*map[string]k8s.Metric)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
|
||||||
|
var ret0 map[string]k8s.Metric
|
||||||
|
var ret1 error
|
||||||
|
if len(result) != 0 {
|
||||||
|
if result[0] != nil {
|
||||||
|
ret0 = result[0].(map[string]k8s.Metric)
|
||||||
|
}
|
||||||
|
if result[1] != nil {
|
||||||
|
ret1 = result[1].(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mock *MockMetricsIfc) PodMetrics() (map[string]k8s.Metric, error) {
|
||||||
|
if mock == nil {
|
||||||
|
panic("mock must not be nil. Use myMock := NewMockMetricsIfc().")
|
||||||
|
}
|
||||||
|
params := []pegomock.Param{}
|
||||||
|
result := pegomock.GetGenericMockFrom(mock).Invoke("PodMetrics", params, []reflect.Type{reflect.TypeOf((*map[string]k8s.Metric)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
|
||||||
|
var ret0 map[string]k8s.Metric
|
||||||
|
var ret1 error
|
||||||
|
if len(result) != 0 {
|
||||||
|
if result[0] != nil {
|
||||||
|
ret0 = result[0].(map[string]k8s.Metric)
|
||||||
|
}
|
||||||
|
if result[1] != nil {
|
||||||
|
ret1 = result[1].(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mock *MockMetricsIfc) VerifyWasCalledOnce() *VerifierMetricsIfc {
|
||||||
|
return &VerifierMetricsIfc{
|
||||||
|
mock: mock,
|
||||||
|
invocationCountMatcher: pegomock.Times(1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mock *MockMetricsIfc) VerifyWasCalled(invocationCountMatcher pegomock.Matcher) *VerifierMetricsIfc {
|
||||||
|
return &VerifierMetricsIfc{
|
||||||
|
mock: mock,
|
||||||
|
invocationCountMatcher: invocationCountMatcher,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mock *MockMetricsIfc) VerifyWasCalledInOrder(invocationCountMatcher pegomock.Matcher, inOrderContext *pegomock.InOrderContext) *VerifierMetricsIfc {
|
||||||
|
return &VerifierMetricsIfc{
|
||||||
|
mock: mock,
|
||||||
|
invocationCountMatcher: invocationCountMatcher,
|
||||||
|
inOrderContext: inOrderContext,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mock *MockMetricsIfc) VerifyWasCalledEventually(invocationCountMatcher pegomock.Matcher, timeout time.Duration) *VerifierMetricsIfc {
|
||||||
|
return &VerifierMetricsIfc{
|
||||||
|
mock: mock,
|
||||||
|
invocationCountMatcher: invocationCountMatcher,
|
||||||
|
timeout: timeout,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type VerifierMetricsIfc struct {
|
||||||
|
mock *MockMetricsIfc
|
||||||
|
invocationCountMatcher pegomock.Matcher
|
||||||
|
inOrderContext *pegomock.InOrderContext
|
||||||
|
timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (verifier *VerifierMetricsIfc) NodeMetrics() *MetricsIfc_NodeMetrics_OngoingVerification {
|
||||||
|
params := []pegomock.Param{}
|
||||||
|
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "NodeMetrics", params, verifier.timeout)
|
||||||
|
return &MetricsIfc_NodeMetrics_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
|
||||||
|
}
|
||||||
|
|
||||||
|
type MetricsIfc_NodeMetrics_OngoingVerification struct {
|
||||||
|
mock *MockMetricsIfc
|
||||||
|
methodInvocations []pegomock.MethodInvocation
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MetricsIfc_NodeMetrics_OngoingVerification) GetCapturedArguments() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MetricsIfc_NodeMetrics_OngoingVerification) GetAllCapturedArguments() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (verifier *VerifierMetricsIfc) PerNodeMetrics(_param0 []v1.Node) *MetricsIfc_PerNodeMetrics_OngoingVerification {
|
||||||
|
params := []pegomock.Param{_param0}
|
||||||
|
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "PerNodeMetrics", params, verifier.timeout)
|
||||||
|
return &MetricsIfc_PerNodeMetrics_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
|
||||||
|
}
|
||||||
|
|
||||||
|
type MetricsIfc_PerNodeMetrics_OngoingVerification struct {
|
||||||
|
mock *MockMetricsIfc
|
||||||
|
methodInvocations []pegomock.MethodInvocation
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MetricsIfc_PerNodeMetrics_OngoingVerification) GetCapturedArguments() []v1.Node {
|
||||||
|
_param0 := c.GetAllCapturedArguments()
|
||||||
|
return _param0[len(_param0)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MetricsIfc_PerNodeMetrics_OngoingVerification) GetAllCapturedArguments() (_param0 [][]v1.Node) {
|
||||||
|
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
|
||||||
|
if len(params) > 0 {
|
||||||
|
_param0 = make([][]v1.Node, len(params[0]))
|
||||||
|
for u, param := range params[0] {
|
||||||
|
_param0[u] = param.([]v1.Node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (verifier *VerifierMetricsIfc) PodMetrics() *MetricsIfc_PodMetrics_OngoingVerification {
|
||||||
|
params := []pegomock.Param{}
|
||||||
|
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "PodMetrics", params, verifier.timeout)
|
||||||
|
return &MetricsIfc_PodMetrics_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
|
||||||
|
}
|
||||||
|
|
||||||
|
type MetricsIfc_PodMetrics_OngoingVerification struct {
|
||||||
|
mock *MockMetricsIfc
|
||||||
|
methodInvocations []pegomock.MethodInvocation
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MetricsIfc_PodMetrics_OngoingVerification) GetCapturedArguments() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MetricsIfc_PodMetrics_OngoingVerification) GetAllCapturedArguments() {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,278 @@
|
||||||
|
// Code generated by pegomock. DO NOT EDIT.
|
||||||
|
// Source: github.com/k8sland/k9s/resource (interfaces: Resource)
|
||||||
|
|
||||||
|
package resource_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
resource "github.com/k8sland/k9s/resource"
|
||||||
|
pegomock "github.com/petergtz/pegomock"
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MockResource struct {
|
||||||
|
fail func(message string, callerSkip ...int)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMockResource() *MockResource {
|
||||||
|
return &MockResource{fail: pegomock.GlobalFailHandler}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mock *MockResource) Delete(_param0 string) error {
|
||||||
|
if mock == nil {
|
||||||
|
panic("mock must not be nil. Use myMock := NewMockResource().")
|
||||||
|
}
|
||||||
|
params := []pegomock.Param{_param0}
|
||||||
|
result := pegomock.GetGenericMockFrom(mock).Invoke("Delete", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})
|
||||||
|
var ret0 error
|
||||||
|
if len(result) != 0 {
|
||||||
|
if result[0] != nil {
|
||||||
|
ret0 = result[0].(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mock *MockResource) Get(_param0 string) (resource.Columnar, error) {
|
||||||
|
if mock == nil {
|
||||||
|
panic("mock must not be nil. Use myMock := NewMockResource().")
|
||||||
|
}
|
||||||
|
params := []pegomock.Param{_param0}
|
||||||
|
result := pegomock.GetGenericMockFrom(mock).Invoke("Get", params, []reflect.Type{reflect.TypeOf((*resource.Columnar)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
|
||||||
|
var ret0 resource.Columnar
|
||||||
|
var ret1 error
|
||||||
|
if len(result) != 0 {
|
||||||
|
if result[0] != nil {
|
||||||
|
ret0 = result[0].(resource.Columnar)
|
||||||
|
}
|
||||||
|
if result[1] != nil {
|
||||||
|
ret1 = result[1].(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mock *MockResource) Header(_param0 string) resource.Row {
|
||||||
|
if mock == nil {
|
||||||
|
panic("mock must not be nil. Use myMock := NewMockResource().")
|
||||||
|
}
|
||||||
|
params := []pegomock.Param{_param0}
|
||||||
|
result := pegomock.GetGenericMockFrom(mock).Invoke("Header", params, []reflect.Type{reflect.TypeOf((*resource.Row)(nil)).Elem()})
|
||||||
|
var ret0 resource.Row
|
||||||
|
if len(result) != 0 {
|
||||||
|
if result[0] != nil {
|
||||||
|
ret0 = result[0].(resource.Row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mock *MockResource) List(_param0 string) (resource.Columnars, error) {
|
||||||
|
if mock == nil {
|
||||||
|
panic("mock must not be nil. Use myMock := NewMockResource().")
|
||||||
|
}
|
||||||
|
params := []pegomock.Param{_param0}
|
||||||
|
result := pegomock.GetGenericMockFrom(mock).Invoke("List", params, []reflect.Type{reflect.TypeOf((*resource.Columnars)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
|
||||||
|
var ret0 resource.Columnars
|
||||||
|
var ret1 error
|
||||||
|
if len(result) != 0 {
|
||||||
|
if result[0] != nil {
|
||||||
|
ret0 = result[0].(resource.Columnars)
|
||||||
|
}
|
||||||
|
if result[1] != nil {
|
||||||
|
ret1 = result[1].(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mock *MockResource) Marshal(_param0 string) (string, error) {
|
||||||
|
if mock == nil {
|
||||||
|
panic("mock must not be nil. Use myMock := NewMockResource().")
|
||||||
|
}
|
||||||
|
params := []pegomock.Param{_param0}
|
||||||
|
result := pegomock.GetGenericMockFrom(mock).Invoke("Marshal", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
|
||||||
|
var ret0 string
|
||||||
|
var ret1 error
|
||||||
|
if len(result) != 0 {
|
||||||
|
if result[0] != nil {
|
||||||
|
ret0 = result[0].(string)
|
||||||
|
}
|
||||||
|
if result[1] != nil {
|
||||||
|
ret1 = result[1].(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mock *MockResource) VerifyWasCalledOnce() *VerifierResource {
|
||||||
|
return &VerifierResource{
|
||||||
|
mock: mock,
|
||||||
|
invocationCountMatcher: pegomock.Times(1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mock *MockResource) VerifyWasCalled(invocationCountMatcher pegomock.Matcher) *VerifierResource {
|
||||||
|
return &VerifierResource{
|
||||||
|
mock: mock,
|
||||||
|
invocationCountMatcher: invocationCountMatcher,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mock *MockResource) VerifyWasCalledInOrder(invocationCountMatcher pegomock.Matcher, inOrderContext *pegomock.InOrderContext) *VerifierResource {
|
||||||
|
return &VerifierResource{
|
||||||
|
mock: mock,
|
||||||
|
invocationCountMatcher: invocationCountMatcher,
|
||||||
|
inOrderContext: inOrderContext,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mock *MockResource) VerifyWasCalledEventually(invocationCountMatcher pegomock.Matcher, timeout time.Duration) *VerifierResource {
|
||||||
|
return &VerifierResource{
|
||||||
|
mock: mock,
|
||||||
|
invocationCountMatcher: invocationCountMatcher,
|
||||||
|
timeout: timeout,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type VerifierResource struct {
|
||||||
|
mock *MockResource
|
||||||
|
invocationCountMatcher pegomock.Matcher
|
||||||
|
inOrderContext *pegomock.InOrderContext
|
||||||
|
timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (verifier *VerifierResource) Delete(_param0 string) *Resource_Delete_OngoingVerification {
|
||||||
|
params := []pegomock.Param{_param0}
|
||||||
|
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Delete", params, verifier.timeout)
|
||||||
|
return &Resource_Delete_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Resource_Delete_OngoingVerification struct {
|
||||||
|
mock *MockResource
|
||||||
|
methodInvocations []pegomock.MethodInvocation
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Resource_Delete_OngoingVerification) GetCapturedArguments() string {
|
||||||
|
_param0 := c.GetAllCapturedArguments()
|
||||||
|
return _param0[len(_param0)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Resource_Delete_OngoingVerification) GetAllCapturedArguments() (_param0 []string) {
|
||||||
|
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
|
||||||
|
if len(params) > 0 {
|
||||||
|
_param0 = make([]string, len(params[0]))
|
||||||
|
for u, param := range params[0] {
|
||||||
|
_param0[u] = param.(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (verifier *VerifierResource) Get(_param0 string) *Resource_Get_OngoingVerification {
|
||||||
|
params := []pegomock.Param{_param0}
|
||||||
|
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Get", params, verifier.timeout)
|
||||||
|
return &Resource_Get_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Resource_Get_OngoingVerification struct {
|
||||||
|
mock *MockResource
|
||||||
|
methodInvocations []pegomock.MethodInvocation
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Resource_Get_OngoingVerification) GetCapturedArguments() string {
|
||||||
|
_param0 := c.GetAllCapturedArguments()
|
||||||
|
return _param0[len(_param0)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Resource_Get_OngoingVerification) GetAllCapturedArguments() (_param0 []string) {
|
||||||
|
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
|
||||||
|
if len(params) > 0 {
|
||||||
|
_param0 = make([]string, len(params[0]))
|
||||||
|
for u, param := range params[0] {
|
||||||
|
_param0[u] = param.(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (verifier *VerifierResource) Header(_param0 string) *Resource_Header_OngoingVerification {
|
||||||
|
params := []pegomock.Param{_param0}
|
||||||
|
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Header", params, verifier.timeout)
|
||||||
|
return &Resource_Header_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Resource_Header_OngoingVerification struct {
|
||||||
|
mock *MockResource
|
||||||
|
methodInvocations []pegomock.MethodInvocation
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Resource_Header_OngoingVerification) GetCapturedArguments() string {
|
||||||
|
_param0 := c.GetAllCapturedArguments()
|
||||||
|
return _param0[len(_param0)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Resource_Header_OngoingVerification) GetAllCapturedArguments() (_param0 []string) {
|
||||||
|
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
|
||||||
|
if len(params) > 0 {
|
||||||
|
_param0 = make([]string, len(params[0]))
|
||||||
|
for u, param := range params[0] {
|
||||||
|
_param0[u] = param.(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (verifier *VerifierResource) List(_param0 string) *Resource_List_OngoingVerification {
|
||||||
|
params := []pegomock.Param{_param0}
|
||||||
|
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "List", params, verifier.timeout)
|
||||||
|
return &Resource_List_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Resource_List_OngoingVerification struct {
|
||||||
|
mock *MockResource
|
||||||
|
methodInvocations []pegomock.MethodInvocation
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Resource_List_OngoingVerification) GetCapturedArguments() string {
|
||||||
|
_param0 := c.GetAllCapturedArguments()
|
||||||
|
return _param0[len(_param0)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Resource_List_OngoingVerification) GetAllCapturedArguments() (_param0 []string) {
|
||||||
|
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
|
||||||
|
if len(params) > 0 {
|
||||||
|
_param0 = make([]string, len(params[0]))
|
||||||
|
for u, param := range params[0] {
|
||||||
|
_param0[u] = param.(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (verifier *VerifierResource) Marshal(_param0 string) *Resource_Marshal_OngoingVerification {
|
||||||
|
params := []pegomock.Param{_param0}
|
||||||
|
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Marshal", params, verifier.timeout)
|
||||||
|
return &Resource_Marshal_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Resource_Marshal_OngoingVerification struct {
|
||||||
|
mock *MockResource
|
||||||
|
methodInvocations []pegomock.MethodInvocation
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Resource_Marshal_OngoingVerification) GetCapturedArguments() string {
|
||||||
|
_param0 := c.GetAllCapturedArguments()
|
||||||
|
return _param0[len(_param0)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Resource_Marshal_OngoingVerification) GetAllCapturedArguments() (_param0 []string) {
|
||||||
|
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
|
||||||
|
if len(params) > 0 {
|
||||||
|
_param0 = make([]string, len(params[0]))
|
||||||
|
for u, param := range params[0] {
|
||||||
|
_param0[u] = param.(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,240 @@
|
||||||
|
// Code generated by pegomock. DO NOT EDIT.
|
||||||
|
// Source: github.com/k8sland/k9s/resource (interfaces: SwitchableRes)
|
||||||
|
|
||||||
|
package resource_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
k8s "github.com/k8sland/k9s/resource/k8s"
|
||||||
|
pegomock "github.com/petergtz/pegomock"
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MockSwitchableRes struct {
|
||||||
|
fail func(message string, callerSkip ...int)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMockSwitchableRes() *MockSwitchableRes {
|
||||||
|
return &MockSwitchableRes{fail: pegomock.GlobalFailHandler}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mock *MockSwitchableRes) Delete(_param0 string, _param1 string) error {
|
||||||
|
if mock == nil {
|
||||||
|
panic("mock must not be nil. Use myMock := NewMockSwitchableRes().")
|
||||||
|
}
|
||||||
|
params := []pegomock.Param{_param0, _param1}
|
||||||
|
result := pegomock.GetGenericMockFrom(mock).Invoke("Delete", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})
|
||||||
|
var ret0 error
|
||||||
|
if len(result) != 0 {
|
||||||
|
if result[0] != nil {
|
||||||
|
ret0 = result[0].(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mock *MockSwitchableRes) Get(_param0 string, _param1 string) (interface{}, error) {
|
||||||
|
if mock == nil {
|
||||||
|
panic("mock must not be nil. Use myMock := NewMockSwitchableRes().")
|
||||||
|
}
|
||||||
|
params := []pegomock.Param{_param0, _param1}
|
||||||
|
result := pegomock.GetGenericMockFrom(mock).Invoke("Get", params, []reflect.Type{reflect.TypeOf((*interface{})(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
|
||||||
|
var ret0 interface{}
|
||||||
|
var ret1 error
|
||||||
|
if len(result) != 0 {
|
||||||
|
if result[0] != nil {
|
||||||
|
ret0 = result[0].(interface{})
|
||||||
|
}
|
||||||
|
if result[1] != nil {
|
||||||
|
ret1 = result[1].(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mock *MockSwitchableRes) List(_param0 string) (k8s.Collection, error) {
|
||||||
|
if mock == nil {
|
||||||
|
panic("mock must not be nil. Use myMock := NewMockSwitchableRes().")
|
||||||
|
}
|
||||||
|
params := []pegomock.Param{_param0}
|
||||||
|
result := pegomock.GetGenericMockFrom(mock).Invoke("List", params, []reflect.Type{reflect.TypeOf((*k8s.Collection)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
|
||||||
|
var ret0 k8s.Collection
|
||||||
|
var ret1 error
|
||||||
|
if len(result) != 0 {
|
||||||
|
if result[0] != nil {
|
||||||
|
ret0 = result[0].(k8s.Collection)
|
||||||
|
}
|
||||||
|
if result[1] != nil {
|
||||||
|
ret1 = result[1].(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mock *MockSwitchableRes) Switch(_param0 string) error {
|
||||||
|
if mock == nil {
|
||||||
|
panic("mock must not be nil. Use myMock := NewMockSwitchableRes().")
|
||||||
|
}
|
||||||
|
params := []pegomock.Param{_param0}
|
||||||
|
result := pegomock.GetGenericMockFrom(mock).Invoke("Switch", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})
|
||||||
|
var ret0 error
|
||||||
|
if len(result) != 0 {
|
||||||
|
if result[0] != nil {
|
||||||
|
ret0 = result[0].(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mock *MockSwitchableRes) VerifyWasCalledOnce() *VerifierSwitchableRes {
|
||||||
|
return &VerifierSwitchableRes{
|
||||||
|
mock: mock,
|
||||||
|
invocationCountMatcher: pegomock.Times(1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mock *MockSwitchableRes) VerifyWasCalled(invocationCountMatcher pegomock.Matcher) *VerifierSwitchableRes {
|
||||||
|
return &VerifierSwitchableRes{
|
||||||
|
mock: mock,
|
||||||
|
invocationCountMatcher: invocationCountMatcher,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mock *MockSwitchableRes) VerifyWasCalledInOrder(invocationCountMatcher pegomock.Matcher, inOrderContext *pegomock.InOrderContext) *VerifierSwitchableRes {
|
||||||
|
return &VerifierSwitchableRes{
|
||||||
|
mock: mock,
|
||||||
|
invocationCountMatcher: invocationCountMatcher,
|
||||||
|
inOrderContext: inOrderContext,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mock *MockSwitchableRes) VerifyWasCalledEventually(invocationCountMatcher pegomock.Matcher, timeout time.Duration) *VerifierSwitchableRes {
|
||||||
|
return &VerifierSwitchableRes{
|
||||||
|
mock: mock,
|
||||||
|
invocationCountMatcher: invocationCountMatcher,
|
||||||
|
timeout: timeout,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type VerifierSwitchableRes struct {
|
||||||
|
mock *MockSwitchableRes
|
||||||
|
invocationCountMatcher pegomock.Matcher
|
||||||
|
inOrderContext *pegomock.InOrderContext
|
||||||
|
timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (verifier *VerifierSwitchableRes) Delete(_param0 string, _param1 string) *SwitchableRes_Delete_OngoingVerification {
|
||||||
|
params := []pegomock.Param{_param0, _param1}
|
||||||
|
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Delete", params, verifier.timeout)
|
||||||
|
return &SwitchableRes_Delete_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
|
||||||
|
}
|
||||||
|
|
||||||
|
type SwitchableRes_Delete_OngoingVerification struct {
|
||||||
|
mock *MockSwitchableRes
|
||||||
|
methodInvocations []pegomock.MethodInvocation
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SwitchableRes_Delete_OngoingVerification) GetCapturedArguments() (string, string) {
|
||||||
|
_param0, _param1 := c.GetAllCapturedArguments()
|
||||||
|
return _param0[len(_param0)-1], _param1[len(_param1)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SwitchableRes_Delete_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []string) {
|
||||||
|
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
|
||||||
|
if len(params) > 0 {
|
||||||
|
_param0 = make([]string, len(params[0]))
|
||||||
|
for u, param := range params[0] {
|
||||||
|
_param0[u] = param.(string)
|
||||||
|
}
|
||||||
|
_param1 = make([]string, len(params[1]))
|
||||||
|
for u, param := range params[1] {
|
||||||
|
_param1[u] = param.(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (verifier *VerifierSwitchableRes) Get(_param0 string, _param1 string) *SwitchableRes_Get_OngoingVerification {
|
||||||
|
params := []pegomock.Param{_param0, _param1}
|
||||||
|
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Get", params, verifier.timeout)
|
||||||
|
return &SwitchableRes_Get_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
|
||||||
|
}
|
||||||
|
|
||||||
|
type SwitchableRes_Get_OngoingVerification struct {
|
||||||
|
mock *MockSwitchableRes
|
||||||
|
methodInvocations []pegomock.MethodInvocation
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SwitchableRes_Get_OngoingVerification) GetCapturedArguments() (string, string) {
|
||||||
|
_param0, _param1 := c.GetAllCapturedArguments()
|
||||||
|
return _param0[len(_param0)-1], _param1[len(_param1)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SwitchableRes_Get_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []string) {
|
||||||
|
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
|
||||||
|
if len(params) > 0 {
|
||||||
|
_param0 = make([]string, len(params[0]))
|
||||||
|
for u, param := range params[0] {
|
||||||
|
_param0[u] = param.(string)
|
||||||
|
}
|
||||||
|
_param1 = make([]string, len(params[1]))
|
||||||
|
for u, param := range params[1] {
|
||||||
|
_param1[u] = param.(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (verifier *VerifierSwitchableRes) List(_param0 string) *SwitchableRes_List_OngoingVerification {
|
||||||
|
params := []pegomock.Param{_param0}
|
||||||
|
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "List", params, verifier.timeout)
|
||||||
|
return &SwitchableRes_List_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
|
||||||
|
}
|
||||||
|
|
||||||
|
type SwitchableRes_List_OngoingVerification struct {
|
||||||
|
mock *MockSwitchableRes
|
||||||
|
methodInvocations []pegomock.MethodInvocation
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SwitchableRes_List_OngoingVerification) GetCapturedArguments() string {
|
||||||
|
_param0 := c.GetAllCapturedArguments()
|
||||||
|
return _param0[len(_param0)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SwitchableRes_List_OngoingVerification) GetAllCapturedArguments() (_param0 []string) {
|
||||||
|
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
|
||||||
|
if len(params) > 0 {
|
||||||
|
_param0 = make([]string, len(params[0]))
|
||||||
|
for u, param := range params[0] {
|
||||||
|
_param0[u] = param.(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (verifier *VerifierSwitchableRes) Switch(_param0 string) *SwitchableRes_Switch_OngoingVerification {
|
||||||
|
params := []pegomock.Param{_param0}
|
||||||
|
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Switch", params, verifier.timeout)
|
||||||
|
return &SwitchableRes_Switch_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
|
||||||
|
}
|
||||||
|
|
||||||
|
type SwitchableRes_Switch_OngoingVerification struct {
|
||||||
|
mock *MockSwitchableRes
|
||||||
|
methodInvocations []pegomock.MethodInvocation
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SwitchableRes_Switch_OngoingVerification) GetCapturedArguments() string {
|
||||||
|
_param0 := c.GetAllCapturedArguments()
|
||||||
|
return _param0[len(_param0)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SwitchableRes_Switch_OngoingVerification) GetAllCapturedArguments() (_param0 []string) {
|
||||||
|
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
|
||||||
|
if len(params) > 0 {
|
||||||
|
_param0 = make([]string, len(params[0]))
|
||||||
|
for u, param := range params[0] {
|
||||||
|
_param0[u] = param.(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,217 @@
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
"k8s.io/api/core/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
labelNodeRolePrefix = "node-role.kubernetes.io/"
|
||||||
|
nodeLabelRole = "kubernetes.io/role"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Node tracks a kubernetes resource.
|
||||||
|
type Node struct {
|
||||||
|
*Base
|
||||||
|
instance *v1.Node
|
||||||
|
metricSvc MetricsIfc
|
||||||
|
metrics k8s.Metric
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNodeList returns a new resource list.
|
||||||
|
func NewNodeList(ns string) List {
|
||||||
|
return NewNodeListWithArgs(ns, NewNode())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNodeListWithArgs returns a new resource list.
|
||||||
|
func NewNodeListWithArgs(ns string, res Resource) List {
|
||||||
|
return newList(NotNamespaced, "no", res, ViewAccess)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNode instantiates a new Endpoint.
|
||||||
|
func NewNode() *Node {
|
||||||
|
return NewNodeWithArgs(k8s.NewNode(), k8s.NewMetricsServer())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNodeWithArgs instantiates a new Endpoint.
|
||||||
|
func NewNodeWithArgs(r k8s.Res, mx MetricsIfc) *Node {
|
||||||
|
ep := &Node{
|
||||||
|
metricSvc: mx,
|
||||||
|
Base: &Base{
|
||||||
|
caller: r,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ep.creator = ep
|
||||||
|
return ep
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInstance builds a new Endpoint instance from a k8s resource.
|
||||||
|
func (*Node) NewInstance(i interface{}) Columnar {
|
||||||
|
cm := NewNode()
|
||||||
|
switch i.(type) {
|
||||||
|
case *v1.Node:
|
||||||
|
cm.instance = i.(*v1.Node)
|
||||||
|
case v1.Node:
|
||||||
|
ii := i.(v1.Node)
|
||||||
|
cm.instance = &ii
|
||||||
|
default:
|
||||||
|
log.Fatalf("Unknown %#v", i)
|
||||||
|
}
|
||||||
|
cm.path = cm.namespacedName(cm.instance.ObjectMeta)
|
||||||
|
return cm
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all resources for a given namespace.
|
||||||
|
func (r *Node) List(ns string) (Columnars, error) {
|
||||||
|
ii, err := r.caller.List(AllNamespaces)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nn := make([]v1.Node, len(ii))
|
||||||
|
for k, i := range ii {
|
||||||
|
nn[k] = i.(v1.Node)
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := make(Columnars, 0, len(nn))
|
||||||
|
mx, err := r.metricSvc.PerNodeMetrics(nn)
|
||||||
|
if err != nil {
|
||||||
|
return cc, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(nn); i++ {
|
||||||
|
n := r.NewInstance(&nn[i]).(*Node)
|
||||||
|
n.metrics = mx[nn[i].Name]
|
||||||
|
cc = append(cc, n)
|
||||||
|
}
|
||||||
|
return cc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal a resource to yaml.
|
||||||
|
func (r *Node) Marshal(path string) (string, error) {
|
||||||
|
ns, n := namespaced(path)
|
||||||
|
i, err := r.caller.Get(ns, n)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
no := i.(*v1.Node)
|
||||||
|
no.TypeMeta.APIVersion = "v1"
|
||||||
|
no.TypeMeta.Kind = "Node"
|
||||||
|
raw, err := yaml.Marshal(no)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(raw), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header returns resource header.
|
||||||
|
func (*Node) Header(ns string) Row {
|
||||||
|
return Row{
|
||||||
|
"NAME",
|
||||||
|
"STATUS",
|
||||||
|
"ROLES",
|
||||||
|
"VERSION",
|
||||||
|
"INTERNAL-IP",
|
||||||
|
"EXTERNAL-IP",
|
||||||
|
"CPU",
|
||||||
|
"MEM",
|
||||||
|
"AVAILABLE_CPU",
|
||||||
|
"AVAILABLE_MEM",
|
||||||
|
"AGE",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fields returns displayable fields.
|
||||||
|
func (r *Node) Fields(ns string) Row {
|
||||||
|
ff := make(Row, 0, len(r.Header(ns)))
|
||||||
|
i := r.instance
|
||||||
|
|
||||||
|
status := r.status(i)
|
||||||
|
iIP, eIP := r.getIPs(i.Status.Addresses)
|
||||||
|
iIP, eIP = missing(iIP), missing(eIP)
|
||||||
|
|
||||||
|
roles := missing(strings.Join(findNodeRoles(i), ","))
|
||||||
|
cpu, mem, acpu, amem := na(r.metrics.CPU), na(r.metrics.Mem), na(r.metrics.AvailCPU), na(r.metrics.AvailMem)
|
||||||
|
|
||||||
|
return append(ff,
|
||||||
|
i.Name,
|
||||||
|
status,
|
||||||
|
roles,
|
||||||
|
i.Status.NodeInfo.KernelVersion,
|
||||||
|
iIP,
|
||||||
|
eIP,
|
||||||
|
cpu,
|
||||||
|
mem,
|
||||||
|
acpu,
|
||||||
|
amem,
|
||||||
|
toAge(i.ObjectMeta.CreationTimestamp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtFields returns extended fields in relation to headers.
|
||||||
|
func (*Node) ExtFields() Properties {
|
||||||
|
return Properties{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
func (*Node) getIPs(addrs []v1.NodeAddress) (iIP, eIP string) {
|
||||||
|
for _, a := range addrs {
|
||||||
|
switch a.Type {
|
||||||
|
case v1.NodeExternalIP:
|
||||||
|
eIP = a.Address
|
||||||
|
case v1.NodeInternalIP:
|
||||||
|
iIP = a.Address
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Node) status(i *v1.Node) string {
|
||||||
|
conditionMap := make(map[v1.NodeConditionType]*v1.NodeCondition)
|
||||||
|
NodeAllConditions := []v1.NodeConditionType{v1.NodeReady}
|
||||||
|
for n := range i.Status.Conditions {
|
||||||
|
cond := i.Status.Conditions[n]
|
||||||
|
conditionMap[cond.Type] = &cond
|
||||||
|
}
|
||||||
|
var status []string
|
||||||
|
for _, validCondition := range NodeAllConditions {
|
||||||
|
if condition, ok := conditionMap[validCondition]; ok {
|
||||||
|
if condition.Status == v1.ConditionTrue {
|
||||||
|
status = append(status, string(condition.Type))
|
||||||
|
} else {
|
||||||
|
status = append(status, "Not"+string(condition.Type))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(status) == 0 {
|
||||||
|
status = append(status, "Unknown")
|
||||||
|
}
|
||||||
|
if i.Spec.Unschedulable {
|
||||||
|
status = append(status, "SchedulingDisabled")
|
||||||
|
}
|
||||||
|
return strings.Join(status, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
func findNodeRoles(i *v1.Node) []string {
|
||||||
|
roles := sets.NewString()
|
||||||
|
for k, v := range i.Labels {
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(k, labelNodeRolePrefix):
|
||||||
|
if role := strings.TrimPrefix(k, labelNodeRolePrefix); len(role) > 0 {
|
||||||
|
roles.Insert(role)
|
||||||
|
}
|
||||||
|
case k == nodeLabelRole && v != "":
|
||||||
|
roles.Insert(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return roles.List()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,168 @@
|
||||||
|
package resource_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/resource"
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
m "github.com/petergtz/pegomock"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNodeListAccess(t *testing.T) {
|
||||||
|
ns := "blee"
|
||||||
|
l := resource.NewNodeList(resource.AllNamespaces)
|
||||||
|
l.SetNamespace(ns)
|
||||||
|
|
||||||
|
assert.Equal(t, resource.NotNamespaced, l.GetNamespace())
|
||||||
|
assert.Equal(t, "no", l.GetName())
|
||||||
|
for _, a := range []int{resource.ViewAccess} {
|
||||||
|
assert.True(t, l.Access(a))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNodeFields(t *testing.T) {
|
||||||
|
r := newNode().Fields("blee")
|
||||||
|
assert.Equal(t, "fred", r[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNodeMarshal(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
mx := NewMockMetricsIfc()
|
||||||
|
m.When(mx.PerNodeMetrics([]v1.Node{*k8sNode()})).
|
||||||
|
ThenReturn(map[string]k8s.Metric{"fred": k8s.Metric{}}, nil)
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.Get("blee", "fred")).ThenReturn(k8sNode(), nil)
|
||||||
|
|
||||||
|
cm := resource.NewNodeWithArgs(ca, mx)
|
||||||
|
ma, err := cm.Marshal("blee/fred")
|
||||||
|
ca.VerifyWasCalledOnce().Get("blee", "fred")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, noYaml(), ma)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNodeListData(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
mx := NewMockMetricsIfc()
|
||||||
|
m.When(mx.PerNodeMetrics([]v1.Node{*k8sNode()})).
|
||||||
|
ThenReturn(map[string]k8s.Metric{"fred": k8s.Metric{}}, nil)
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.List("")).ThenReturn(k8s.Collection{*k8sNode()}, nil)
|
||||||
|
|
||||||
|
l := resource.NewNodeListWithArgs("", resource.NewNodeWithArgs(ca, mx))
|
||||||
|
// Make sure we can get deltas!
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
err := l.Reconcile()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ca.VerifyWasCalled(m.Times(2)).List("")
|
||||||
|
td := l.Data()
|
||||||
|
assert.Equal(t, 1, len(td.Rows))
|
||||||
|
assert.Equal(t, resource.NotNamespaced, l.GetNamespace())
|
||||||
|
assert.False(t, l.HasXRay())
|
||||||
|
row := td.Rows["fred"]
|
||||||
|
assert.Equal(t, 11, len(row.Deltas))
|
||||||
|
for _, d := range row.Deltas {
|
||||||
|
assert.Equal(t, "", d)
|
||||||
|
}
|
||||||
|
assert.Equal(t, resource.Row{"fred"}, row.Fields[:1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNodeListDescribe(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
mx := NewMockMetricsIfc()
|
||||||
|
m.When(mx.PerNodeMetrics([]v1.Node{*k8sNode()})).
|
||||||
|
ThenReturn(map[string]k8s.Metric{"fred": k8s.Metric{}}, nil)
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.Get("blee", "fred")).ThenReturn(k8sNode(), nil)
|
||||||
|
l := resource.NewNodeListWithArgs("blee", resource.NewNodeWithArgs(ca, mx))
|
||||||
|
props, err := l.Describe("blee/fred")
|
||||||
|
|
||||||
|
ca.VerifyWasCalledOnce().Get("blee", "fred")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, 0, len(props))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
func k8sNode() *v1.Node {
|
||||||
|
return &v1.Node{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "fred",
|
||||||
|
CreationTimestamp: metav1.Time{Time: testTime()},
|
||||||
|
},
|
||||||
|
Spec: v1.NodeSpec{},
|
||||||
|
Status: v1.NodeStatus{
|
||||||
|
Addresses: []v1.NodeAddress{
|
||||||
|
{Address: "1.1.1.1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newNode() resource.Columnar {
|
||||||
|
return resource.NewNode().NewInstance(k8sNode())
|
||||||
|
}
|
||||||
|
|
||||||
|
func noYaml() string {
|
||||||
|
return `typemeta:
|
||||||
|
kind: Node
|
||||||
|
apiversion: v1
|
||||||
|
objectmeta:
|
||||||
|
name: fred
|
||||||
|
generatename: ""
|
||||||
|
namespace: ""
|
||||||
|
selflink: ""
|
||||||
|
uid: ""
|
||||||
|
resourceversion: ""
|
||||||
|
generation: 0
|
||||||
|
creationtimestamp: "2018-12-14T10:36:43.326972-07:00"
|
||||||
|
deletiontimestamp: null
|
||||||
|
deletiongraceperiodseconds: null
|
||||||
|
labels: {}
|
||||||
|
annotations: {}
|
||||||
|
ownerreferences: []
|
||||||
|
initializers: null
|
||||||
|
finalizers: []
|
||||||
|
clustername: ""
|
||||||
|
spec:
|
||||||
|
podcidr: ""
|
||||||
|
providerid: ""
|
||||||
|
unschedulable: false
|
||||||
|
taints: []
|
||||||
|
configsource: null
|
||||||
|
donotuse_externalid: ""
|
||||||
|
status:
|
||||||
|
capacity: {}
|
||||||
|
allocatable: {}
|
||||||
|
phase: ""
|
||||||
|
conditions: []
|
||||||
|
addresses:
|
||||||
|
- type: ""
|
||||||
|
address: 1.1.1.1
|
||||||
|
daemonendpoints:
|
||||||
|
kubeletendpoint:
|
||||||
|
port: 0
|
||||||
|
nodeinfo:
|
||||||
|
machineid: ""
|
||||||
|
systemuuid: ""
|
||||||
|
bootid: ""
|
||||||
|
kernelversion: ""
|
||||||
|
osimage: ""
|
||||||
|
containerruntimeversion: ""
|
||||||
|
kubeletversion: ""
|
||||||
|
kubeproxyversion: ""
|
||||||
|
operatingsystem: ""
|
||||||
|
architecture: ""
|
||||||
|
images: []
|
||||||
|
volumesinuse: []
|
||||||
|
volumesattached: []
|
||||||
|
config: null
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
"k8s.io/api/core/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Namespace tracks a kubernetes resource.
|
||||||
|
type Namespace struct {
|
||||||
|
*Base
|
||||||
|
instance *v1.Namespace
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNamespaceList returns a new resource list.
|
||||||
|
func NewNamespaceList(ns string) List {
|
||||||
|
return NewNamespaceListWithArgs(ns, NewNamespace())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNamespaceListWithArgs returns a new resource list.
|
||||||
|
func NewNamespaceListWithArgs(ns string, res Resource) List {
|
||||||
|
return newList(NotNamespaced, "ns", res, CRUDAccess)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNamespace instantiates a new Endpoint.
|
||||||
|
func NewNamespace() *Namespace {
|
||||||
|
return NewNamespaceWithArgs(k8s.NewNamespace())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNamespaceWithArgs instantiates a new Endpoint.
|
||||||
|
func NewNamespaceWithArgs(r k8s.Res) *Namespace {
|
||||||
|
ep := &Namespace{
|
||||||
|
Base: &Base{
|
||||||
|
caller: r,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ep.creator = ep
|
||||||
|
return ep
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInstance builds a new Endpoint instance from a k8s resource.
|
||||||
|
func (*Namespace) NewInstance(i interface{}) Columnar {
|
||||||
|
cm := NewNamespace()
|
||||||
|
switch i.(type) {
|
||||||
|
case *v1.Namespace:
|
||||||
|
cm.instance = i.(*v1.Namespace)
|
||||||
|
case v1.Namespace:
|
||||||
|
ii := i.(v1.Namespace)
|
||||||
|
cm.instance = &ii
|
||||||
|
default:
|
||||||
|
log.Fatalf("Unknown %#v", i)
|
||||||
|
}
|
||||||
|
cm.path = cm.namespacedName(cm.instance.ObjectMeta)
|
||||||
|
return cm
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal a resource to yaml.
|
||||||
|
func (r *Namespace) Marshal(path string) (string, error) {
|
||||||
|
ns, n := namespaced(path)
|
||||||
|
i, err := r.caller.Get(ns, n)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
nss := i.(*v1.Namespace)
|
||||||
|
nss.TypeMeta.APIVersion = "v1"
|
||||||
|
nss.TypeMeta.Kind = "Namespace"
|
||||||
|
raw, err := yaml.Marshal(nss)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(raw), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header returns resource header.
|
||||||
|
func (*Namespace) Header(ns string) Row {
|
||||||
|
return Row{"NAME", "STATUS", "AGE"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fields returns displayable fields.
|
||||||
|
func (r *Namespace) Fields(ns string) Row {
|
||||||
|
ff := make(Row, 0, len(r.Header(ns)))
|
||||||
|
i := r.instance
|
||||||
|
return append(ff,
|
||||||
|
i.Name,
|
||||||
|
string(i.Status.Phase),
|
||||||
|
toAge(i.ObjectMeta.CreationTimestamp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtFields returns extended fields in relation to headers.
|
||||||
|
func (*Namespace) ExtFields() Properties {
|
||||||
|
return Properties{}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,129 @@
|
||||||
|
package resource_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/resource"
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
m "github.com/petergtz/pegomock"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNamespaceListAccess(t *testing.T) {
|
||||||
|
ns := "blee"
|
||||||
|
l := resource.NewNamespaceList(resource.AllNamespaces)
|
||||||
|
l.SetNamespace(ns)
|
||||||
|
|
||||||
|
assert.Equal(t, resource.NotNamespaced, l.GetNamespace())
|
||||||
|
assert.Equal(t, "ns", l.GetName())
|
||||||
|
for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} {
|
||||||
|
assert.True(t, l.Access(a))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNamespaceHeader(t *testing.T) {
|
||||||
|
assert.Equal(t, resource.Row{"NAME", "STATUS", "AGE"}, newNamespace().Header("default"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNamespaceFields(t *testing.T) {
|
||||||
|
r := newNamespace().Fields("blee")
|
||||||
|
assert.Equal(t, "fred", r[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNamespaceMarshal(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.Get("", "fred")).ThenReturn(k8sNamespace(), nil)
|
||||||
|
|
||||||
|
cm := resource.NewNamespaceWithArgs(ca)
|
||||||
|
ma, err := cm.Marshal("fred")
|
||||||
|
ca.VerifyWasCalledOnce().Get("", "fred")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, nsYaml(), ma)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNamespaceListData(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.List(resource.NotNamespaced)).ThenReturn(k8s.Collection{*k8sNamespace()}, nil)
|
||||||
|
|
||||||
|
l := resource.NewNamespaceListWithArgs("-", resource.NewNamespaceWithArgs(ca))
|
||||||
|
// Make sure we can get deltas!
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
err := l.Reconcile()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ca.VerifyWasCalled(m.Times(2)).List(resource.NotNamespaced)
|
||||||
|
td := l.Data()
|
||||||
|
assert.Equal(t, 1, len(td.Rows))
|
||||||
|
assert.Equal(t, resource.NotNamespaced, l.GetNamespace())
|
||||||
|
assert.False(t, l.HasXRay())
|
||||||
|
row := td.Rows["blee/fred"]
|
||||||
|
assert.Equal(t, 3, len(row.Deltas))
|
||||||
|
for _, d := range row.Deltas {
|
||||||
|
assert.Equal(t, "", d)
|
||||||
|
}
|
||||||
|
assert.Equal(t, resource.Row{"fred"}, row.Fields[:1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNamespaceListDescribe(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.Get("blee", "fred")).ThenReturn(k8sNamespace(), nil)
|
||||||
|
l := resource.NewNamespaceListWithArgs("blee", resource.NewNamespaceWithArgs(ca))
|
||||||
|
props, err := l.Describe("blee/fred")
|
||||||
|
|
||||||
|
ca.VerifyWasCalledOnce().Get("blee", "fred")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, 0, len(props))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
func k8sNamespace() *v1.Namespace {
|
||||||
|
return &v1.Namespace{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "blee",
|
||||||
|
Name: "fred",
|
||||||
|
CreationTimestamp: metav1.Time{Time: testTime()},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newNamespace() resource.Columnar {
|
||||||
|
return resource.NewNamespace().NewInstance(k8sNamespace())
|
||||||
|
}
|
||||||
|
|
||||||
|
func nsYaml() string {
|
||||||
|
return `typemeta:
|
||||||
|
kind: Namespace
|
||||||
|
apiversion: v1
|
||||||
|
objectmeta:
|
||||||
|
name: fred
|
||||||
|
generatename: ""
|
||||||
|
namespace: blee
|
||||||
|
selflink: ""
|
||||||
|
uid: ""
|
||||||
|
resourceversion: ""
|
||||||
|
generation: 0
|
||||||
|
creationtimestamp: "2018-12-14T10:36:43.326972-07:00"
|
||||||
|
deletiontimestamp: null
|
||||||
|
deletiongraceperiodseconds: null
|
||||||
|
labels: {}
|
||||||
|
annotations: {}
|
||||||
|
ownerreferences: []
|
||||||
|
initializers: null
|
||||||
|
finalizers: []
|
||||||
|
clustername: ""
|
||||||
|
spec:
|
||||||
|
finalizers: []
|
||||||
|
status:
|
||||||
|
phase: ""
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,382 @@
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
"k8s.io/api/core/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultTimeout = 3
|
||||||
|
|
||||||
|
type (
|
||||||
|
// Container represents a resource that encompass multiple containers.
|
||||||
|
Container interface {
|
||||||
|
Containers(path string) ([]string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tailable represents a resource with tailable logs.
|
||||||
|
Tailable interface {
|
||||||
|
Logs(c chan<- string, ns, na, co string) (context.CancelFunc, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TailableResource is a resource that have tailable logs.
|
||||||
|
TailableResource interface {
|
||||||
|
Resource
|
||||||
|
Tailable
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pod that can be displayed in a table and interacted with.
|
||||||
|
Pod struct {
|
||||||
|
*Base
|
||||||
|
instance *v1.Pod
|
||||||
|
metricSvc MetricsIfc
|
||||||
|
metrics k8s.Metric
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewPodList returns a new resource list.
|
||||||
|
func NewPodList(ns string) List {
|
||||||
|
return NewPodListWithArgs(ns, NewPod())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPodListWithArgs returns a new resource list.
|
||||||
|
func NewPodListWithArgs(ns string, res Resource) List {
|
||||||
|
l := newList(ns, "po", res, AllVerbsAccess)
|
||||||
|
l.xray = true
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPod returns a new Pod instance.
|
||||||
|
func NewPod() *Pod {
|
||||||
|
return NewPodWithArgs(k8s.NewPod(), k8s.NewMetricsServer())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPodWithArgs returns a new Pod instance.
|
||||||
|
func NewPodWithArgs(r k8s.Res, mx MetricsIfc) *Pod {
|
||||||
|
p := &Pod{
|
||||||
|
metricSvc: mx,
|
||||||
|
Base: &Base{
|
||||||
|
caller: r,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
p.creator = p
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInstance builds a new Pod instance from a k8s resource.
|
||||||
|
func (r *Pod) NewInstance(i interface{}) Columnar {
|
||||||
|
pod := NewPod()
|
||||||
|
switch i.(type) {
|
||||||
|
case *v1.Pod:
|
||||||
|
pod.instance = i.(*v1.Pod)
|
||||||
|
case v1.Pod:
|
||||||
|
ii := i.(v1.Pod)
|
||||||
|
pod.instance = &ii
|
||||||
|
case *interface{}:
|
||||||
|
ptr := *i.(*interface{})
|
||||||
|
po := ptr.(v1.Pod)
|
||||||
|
pod.instance = &po
|
||||||
|
default:
|
||||||
|
log.Fatalf("Unknown %#v", i)
|
||||||
|
}
|
||||||
|
pod.path = r.namespacedName(pod.instance.ObjectMeta)
|
||||||
|
return pod
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metrics retrieves cpu/mem resource consumption on a pod.
|
||||||
|
func (r *Pod) Metrics() k8s.Metric {
|
||||||
|
return r.metrics
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMetrics set the current k8s resource metrics on a given pod.
|
||||||
|
func (r *Pod) SetMetrics(m k8s.Metric) {
|
||||||
|
r.metrics = m
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal resource to yaml.
|
||||||
|
func (r *Pod) Marshal(path string) (string, error) {
|
||||||
|
ns, n := namespaced(path)
|
||||||
|
i, err := r.caller.Get(ns, n)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
po := i.(*v1.Pod)
|
||||||
|
po.TypeMeta.APIVersion = "v1"
|
||||||
|
po.TypeMeta.Kind = "Pod"
|
||||||
|
raw, err := yaml.Marshal(po)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(raw), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Containers lists out all the docker contrainers name contained in a pod.
|
||||||
|
func (r *Pod) Containers(path string) ([]string, error) {
|
||||||
|
ns, po := namespaced(path)
|
||||||
|
return r.caller.(k8s.PodRes).Containers(ns, po)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logs tails a given container logs
|
||||||
|
func (r *Pod) Logs(c chan<- string, ns, n, co string) (context.CancelFunc, error) {
|
||||||
|
req := r.caller.(k8s.PodRes).Logs(ns, n, co)
|
||||||
|
ctx, cancel := context.WithCancel(context.TODO())
|
||||||
|
req.Context(ctx)
|
||||||
|
|
||||||
|
blocked := true
|
||||||
|
go func() {
|
||||||
|
select {
|
||||||
|
case <-time.After(1 * time.Second):
|
||||||
|
if blocked {
|
||||||
|
close(c)
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
// This call will block if nothing is in the stream!!
|
||||||
|
stream, err := req.Stream()
|
||||||
|
blocked = false
|
||||||
|
if err != nil {
|
||||||
|
return cancel, fmt.Errorf("Log tail request failed for pod `%s/%s:%s", ns, n, co)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
stream.Close()
|
||||||
|
cancel()
|
||||||
|
close(c)
|
||||||
|
}()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(stream)
|
||||||
|
for scanner.Scan() {
|
||||||
|
c <- scanner.Text()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return cancel, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// List resources for a given namespace.
|
||||||
|
func (r *Pod) List(ns string) (Columnars, error) {
|
||||||
|
ii, err := r.caller.List(ns)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
metrics, err := r.metricSvc.PodMetrics()
|
||||||
|
if err != nil {
|
||||||
|
log.Warn(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := make(Columnars, 0, len(ii))
|
||||||
|
for i := 0; i < len(ii); i++ {
|
||||||
|
po := r.NewInstance(&ii[i]).(MxColumnar)
|
||||||
|
po.SetMetrics(metrics[po.Name()])
|
||||||
|
cc = append(cc, po)
|
||||||
|
}
|
||||||
|
return cc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header return resource header.
|
||||||
|
func (*Pod) Header(ns string) Row {
|
||||||
|
hh := Row{}
|
||||||
|
if ns == AllNamespaces {
|
||||||
|
hh = append(hh, "NAMESPACE")
|
||||||
|
}
|
||||||
|
return append(hh,
|
||||||
|
"NAME",
|
||||||
|
"READY",
|
||||||
|
"STATUS",
|
||||||
|
"RESTARTS",
|
||||||
|
"CPU",
|
||||||
|
"MEM",
|
||||||
|
"IP",
|
||||||
|
"NODE",
|
||||||
|
"QOS",
|
||||||
|
"AGE",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fields retrieves displayable fields.
|
||||||
|
func (r *Pod) Fields(ns string) Row {
|
||||||
|
ff := make(Row, 0, len(r.Header(ns)))
|
||||||
|
i := r.instance
|
||||||
|
|
||||||
|
if ns == AllNamespaces {
|
||||||
|
ff = append(ff, i.Namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
cr, _, rc, cc := r.statuses()
|
||||||
|
return append(ff,
|
||||||
|
i.ObjectMeta.Name,
|
||||||
|
strconv.Itoa(cr)+"/"+strconv.Itoa(len(cc)),
|
||||||
|
r.phase(i.Status),
|
||||||
|
strconv.Itoa(rc),
|
||||||
|
r.metrics.CPU,
|
||||||
|
r.metrics.Mem,
|
||||||
|
i.Status.PodIP,
|
||||||
|
i.Status.HostIP,
|
||||||
|
string(i.Status.QOSClass),
|
||||||
|
toAge(i.ObjectMeta.CreationTimestamp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtFields returns extra info about the resource.
|
||||||
|
func (r *Pod) ExtFields() Properties {
|
||||||
|
i := r.instance
|
||||||
|
|
||||||
|
// po := k8s.Pod{}
|
||||||
|
// e, err := po.Events(i.Namespace, i.Name)
|
||||||
|
// if err != nil {
|
||||||
|
// log.Error("Boom!", err)
|
||||||
|
// }
|
||||||
|
// if len(e.Items) > 0 {
|
||||||
|
// log.Println("Events", ee.Items)
|
||||||
|
// }
|
||||||
|
|
||||||
|
return Properties{
|
||||||
|
"Priority": strconv.Itoa(int(*i.Spec.Priority)),
|
||||||
|
"Priority Class": missing(i.Spec.PriorityClassName),
|
||||||
|
"Labels": mapToStr(i.Labels),
|
||||||
|
"Annotations": mapToStr(i.ObjectMeta.Annotations),
|
||||||
|
"Containers": r.toContainers(i.Spec.Containers),
|
||||||
|
"Init Containers": r.toContainers(i.Spec.InitContainers),
|
||||||
|
"Node Selectors": mapToStr(i.Spec.NodeSelector),
|
||||||
|
"Volumes": r.toVolumes(i.Spec.Volumes),
|
||||||
|
// "Events": r.toEvents(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// func (r *Pod) toEvents(e *v1.EventList) []string {
|
||||||
|
// ss := make([]string, 0, len(e.Items)+1)
|
||||||
|
// for _, h := range([]string{"Type", "Reason", "From", "Message", "Age"}) {
|
||||||
|
// ss[0] = fmt.Printf("%10s %10s %20s %30s", )
|
||||||
|
// }
|
||||||
|
// for i, e := range e.Items {
|
||||||
|
// ss[i] = e.
|
||||||
|
|
||||||
|
// }
|
||||||
|
// return ss
|
||||||
|
// }
|
||||||
|
|
||||||
|
func (r *Pod) toVolumes(vv []v1.Volume) map[string]interface{} {
|
||||||
|
m := make(map[string]interface{}, len(vv))
|
||||||
|
for _, v := range vv {
|
||||||
|
m[v.Name] = r.toVolume(v)
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Pod) toVolume(v v1.Volume) map[string]interface{} {
|
||||||
|
switch {
|
||||||
|
case v.Secret != nil:
|
||||||
|
return map[string]interface{}{
|
||||||
|
"Type": "Secret",
|
||||||
|
"Name": v.Secret.SecretName,
|
||||||
|
"Optional": r.boolPtrToStr(v.Secret.Optional),
|
||||||
|
}
|
||||||
|
case v.AWSElasticBlockStore != nil:
|
||||||
|
return map[string]interface{}{
|
||||||
|
"Type": v.AWSElasticBlockStore.FSType,
|
||||||
|
"VolumeID": v.AWSElasticBlockStore.VolumeID,
|
||||||
|
"Partition": strconv.Itoa(int(v.AWSElasticBlockStore.Partition)),
|
||||||
|
"ReadOnly": boolToStr(v.AWSElasticBlockStore.ReadOnly),
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return map[string]interface{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Pod) toContainers(cc []v1.Container) map[string]interface{} {
|
||||||
|
m := make(map[string]interface{}, len(cc))
|
||||||
|
for _, c := range cc {
|
||||||
|
m[c.Name] = map[string]interface{}{
|
||||||
|
"Image": c.Image,
|
||||||
|
"Environment": r.toEnv(c.Env),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Pod) toEnv(ee []v1.EnvVar) []string {
|
||||||
|
if len(ee) == 0 {
|
||||||
|
return []string{MissingValue}
|
||||||
|
}
|
||||||
|
|
||||||
|
ss := make([]string, len(ee))
|
||||||
|
for i, e := range ee {
|
||||||
|
s := r.toEnvFrom(e.ValueFrom)
|
||||||
|
if len(s) == 0 {
|
||||||
|
ss[i] = e.Name + "=" + e.Value
|
||||||
|
} else {
|
||||||
|
ss[i] = e.Name + "=" + e.Value + "(" + s + ")"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ss
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Pod) toEnvFrom(e *v1.EnvVarSource) string {
|
||||||
|
if e == nil {
|
||||||
|
return MissingValue
|
||||||
|
}
|
||||||
|
|
||||||
|
var s string
|
||||||
|
switch {
|
||||||
|
case e.ConfigMapKeyRef != nil:
|
||||||
|
f := e.ConfigMapKeyRef
|
||||||
|
s += f.Name + ":" + f.Key + "(" + r.boolPtrToStr(f.Optional) + ")"
|
||||||
|
case e.FieldRef != nil:
|
||||||
|
f := e.FieldRef
|
||||||
|
s += f.FieldPath + ":" + f.APIVersion
|
||||||
|
case e.SecretKeyRef != nil:
|
||||||
|
f := e.SecretKeyRef
|
||||||
|
s += f.Name + ":" + f.Key + "(" + r.boolPtrToStr(f.Optional) + ")"
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Pod) boolPtrToStr(b *bool) string {
|
||||||
|
if b == nil {
|
||||||
|
return "false"
|
||||||
|
}
|
||||||
|
|
||||||
|
return boolToStr(*b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Pod) statuses() (cr, ct, rc int, cc []v1.ContainerStatus) {
|
||||||
|
cc = r.instance.Status.ContainerStatuses
|
||||||
|
for _, c := range cc {
|
||||||
|
if c.State.Terminated != nil {
|
||||||
|
ct++
|
||||||
|
}
|
||||||
|
if c.Ready {
|
||||||
|
cr = cr + 1
|
||||||
|
}
|
||||||
|
rc += int(c.RestartCount)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Pod) phase(s v1.PodStatus) string {
|
||||||
|
status := "Pending"
|
||||||
|
for _, cs := range s.ContainerStatuses {
|
||||||
|
switch {
|
||||||
|
case cs.State.Running != nil:
|
||||||
|
status = "Running"
|
||||||
|
case cs.State.Waiting != nil:
|
||||||
|
status = cs.State.Waiting.Reason
|
||||||
|
case cs.State.Terminated != nil:
|
||||||
|
status = "Terminating"
|
||||||
|
if len(cs.State.Terminated.Reason) != 0 {
|
||||||
|
status = cs.State.Terminated.Reason
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,298 @@
|
||||||
|
package resource_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/resource"
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
m "github.com/petergtz/pegomock"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPodListAccess(t *testing.T) {
|
||||||
|
ns := "blee"
|
||||||
|
l := resource.NewPodList(resource.AllNamespaces)
|
||||||
|
l.SetNamespace(ns)
|
||||||
|
|
||||||
|
assert.Equal(t, "blee", l.GetNamespace())
|
||||||
|
assert.Equal(t, "po", l.GetName())
|
||||||
|
for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} {
|
||||||
|
assert.True(t, l.Access(a))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPodHeader(t *testing.T) {
|
||||||
|
assert.Equal(t, resource.Row{"NAME", "READY", "STATUS", "RESTARTS", "CPU", "MEM", "IP", "NODE", "QOS", "AGE"}, newPod().Header("default"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPodFields(t *testing.T) {
|
||||||
|
r := newPod().Fields("blee")
|
||||||
|
assert.Equal(t, "fred", r[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPodMarshal(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
mx := NewMockMetricsIfc()
|
||||||
|
m.When(mx.PodMetrics()).ThenReturn(map[string]k8s.Metric{"fred": k8s.Metric{}}, nil)
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.Get("blee", "fred")).ThenReturn(k8sPod(), nil)
|
||||||
|
|
||||||
|
cm := resource.NewPodWithArgs(ca, mx)
|
||||||
|
ma, err := cm.Marshal("blee/fred")
|
||||||
|
ca.VerifyWasCalledOnce().Get("blee", "fred")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, poYaml(), ma)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPodListData(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
mx := NewMockMetricsIfc()
|
||||||
|
m.When(mx.PodMetrics()).ThenReturn(map[string]k8s.Metric{"fred": k8s.Metric{}}, nil)
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.List("")).ThenReturn(k8s.Collection{*k8sPod()}, nil)
|
||||||
|
|
||||||
|
l := resource.NewPodListWithArgs("", resource.NewPodWithArgs(ca, mx))
|
||||||
|
// Make sure we can get deltas!
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
err := l.Reconcile()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ca.VerifyWasCalled(m.Times(2)).List(resource.AllNamespaces)
|
||||||
|
td := l.Data()
|
||||||
|
assert.Equal(t, 1, len(td.Rows))
|
||||||
|
assert.Equal(t, resource.AllNamespaces, l.GetNamespace())
|
||||||
|
assert.True(t, l.HasXRay())
|
||||||
|
row := td.Rows["blee/fred"]
|
||||||
|
assert.Equal(t, 11, len(row.Deltas))
|
||||||
|
for _, d := range row.Deltas {
|
||||||
|
assert.Equal(t, "", d)
|
||||||
|
}
|
||||||
|
assert.Equal(t, resource.Row{"blee"}, row.Fields[:1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPodListDescribe(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
mx := NewMockMetricsIfc()
|
||||||
|
m.When(mx.PodMetrics()).ThenReturn(map[string]k8s.Metric{"fred": k8s.Metric{}}, nil)
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.Get("blee", "fred")).ThenReturn(k8sPod(), nil)
|
||||||
|
l := resource.NewPodListWithArgs("blee", resource.NewPodWithArgs(ca, mx))
|
||||||
|
props, err := l.Describe("blee/fred")
|
||||||
|
|
||||||
|
ca.VerifyWasCalledOnce().Get("blee", "fred")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, 8, len(props))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
func k8sPod() *v1.Pod {
|
||||||
|
var i int32 = 1
|
||||||
|
var t = v1.HostPathDirectory
|
||||||
|
return &v1.Pod{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "blee",
|
||||||
|
Name: "fred",
|
||||||
|
Labels: map[string]string{"blee": "duh"},
|
||||||
|
CreationTimestamp: metav1.Time{Time: testTime()},
|
||||||
|
},
|
||||||
|
Spec: v1.PodSpec{
|
||||||
|
Priority: &i,
|
||||||
|
PriorityClassName: "bozo",
|
||||||
|
Containers: []v1.Container{
|
||||||
|
{
|
||||||
|
Name: "fred",
|
||||||
|
Image: "blee",
|
||||||
|
Env: []v1.EnvVar{
|
||||||
|
{
|
||||||
|
Name: "fred",
|
||||||
|
Value: "1",
|
||||||
|
ValueFrom: &v1.EnvVarSource{
|
||||||
|
ConfigMapKeyRef: &v1.ConfigMapKeySelector{Key: "blee"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Volumes: []v1.Volume{
|
||||||
|
{
|
||||||
|
Name: "fred",
|
||||||
|
VolumeSource: v1.VolumeSource{
|
||||||
|
HostPath: &v1.HostPathVolumeSource{
|
||||||
|
Path: "/blee",
|
||||||
|
Type: &t,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Status: v1.PodStatus{
|
||||||
|
Phase: "Running",
|
||||||
|
ContainerStatuses: []v1.ContainerStatus{
|
||||||
|
{
|
||||||
|
Name: "fred",
|
||||||
|
State: v1.ContainerState{Running: &v1.ContainerStateRunning{}},
|
||||||
|
RestartCount: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPod() resource.Columnar {
|
||||||
|
return resource.NewPod().NewInstance(k8sPod())
|
||||||
|
}
|
||||||
|
|
||||||
|
func poYaml() string {
|
||||||
|
return `typemeta:
|
||||||
|
kind: Pod
|
||||||
|
apiversion: v1
|
||||||
|
objectmeta:
|
||||||
|
name: fred
|
||||||
|
generatename: ""
|
||||||
|
namespace: blee
|
||||||
|
selflink: ""
|
||||||
|
uid: ""
|
||||||
|
resourceversion: ""
|
||||||
|
generation: 0
|
||||||
|
creationtimestamp: "2018-12-14T10:36:43.326972-07:00"
|
||||||
|
deletiontimestamp: null
|
||||||
|
deletiongraceperiodseconds: null
|
||||||
|
labels:
|
||||||
|
blee: duh
|
||||||
|
annotations: {}
|
||||||
|
ownerreferences: []
|
||||||
|
initializers: null
|
||||||
|
finalizers: []
|
||||||
|
clustername: ""
|
||||||
|
spec:
|
||||||
|
volumes:
|
||||||
|
- name: fred
|
||||||
|
volumesource:
|
||||||
|
hostpath:
|
||||||
|
path: /blee
|
||||||
|
type: Directory
|
||||||
|
emptydir: null
|
||||||
|
gcepersistentdisk: null
|
||||||
|
awselasticblockstore: null
|
||||||
|
gitrepo: null
|
||||||
|
secret: null
|
||||||
|
nfs: null
|
||||||
|
iscsi: null
|
||||||
|
glusterfs: null
|
||||||
|
persistentvolumeclaim: null
|
||||||
|
rbd: null
|
||||||
|
flexvolume: null
|
||||||
|
cinder: null
|
||||||
|
cephfs: null
|
||||||
|
flocker: null
|
||||||
|
downwardapi: null
|
||||||
|
fc: null
|
||||||
|
azurefile: null
|
||||||
|
configmap: null
|
||||||
|
vspherevolume: null
|
||||||
|
quobyte: null
|
||||||
|
azuredisk: null
|
||||||
|
photonpersistentdisk: null
|
||||||
|
projected: null
|
||||||
|
portworxvolume: null
|
||||||
|
scaleio: null
|
||||||
|
storageos: null
|
||||||
|
initcontainers: []
|
||||||
|
containers:
|
||||||
|
- name: fred
|
||||||
|
image: blee
|
||||||
|
command: []
|
||||||
|
args: []
|
||||||
|
workingdir: ""
|
||||||
|
ports: []
|
||||||
|
envfrom: []
|
||||||
|
env:
|
||||||
|
- name: fred
|
||||||
|
value: "1"
|
||||||
|
valuefrom:
|
||||||
|
fieldref: null
|
||||||
|
resourcefieldref: null
|
||||||
|
configmapkeyref:
|
||||||
|
localobjectreference:
|
||||||
|
name: ""
|
||||||
|
key: blee
|
||||||
|
optional: null
|
||||||
|
secretkeyref: null
|
||||||
|
resources:
|
||||||
|
limits: {}
|
||||||
|
requests: {}
|
||||||
|
volumemounts: []
|
||||||
|
volumedevices: []
|
||||||
|
livenessprobe: null
|
||||||
|
readinessprobe: null
|
||||||
|
lifecycle: null
|
||||||
|
terminationmessagepath: ""
|
||||||
|
terminationmessagepolicy: ""
|
||||||
|
imagepullpolicy: ""
|
||||||
|
securitycontext: null
|
||||||
|
stdin: false
|
||||||
|
stdinonce: false
|
||||||
|
tty: false
|
||||||
|
restartpolicy: ""
|
||||||
|
terminationgraceperiodseconds: null
|
||||||
|
activedeadlineseconds: null
|
||||||
|
dnspolicy: ""
|
||||||
|
nodeselector: {}
|
||||||
|
serviceaccountname: ""
|
||||||
|
deprecatedserviceaccount: ""
|
||||||
|
automountserviceaccounttoken: null
|
||||||
|
nodename: ""
|
||||||
|
hostnetwork: false
|
||||||
|
hostpid: false
|
||||||
|
hostipc: false
|
||||||
|
shareprocessnamespace: null
|
||||||
|
securitycontext: null
|
||||||
|
imagepullsecrets: []
|
||||||
|
hostname: ""
|
||||||
|
subdomain: ""
|
||||||
|
affinity: null
|
||||||
|
schedulername: ""
|
||||||
|
tolerations: []
|
||||||
|
hostaliases: []
|
||||||
|
priorityclassname: bozo
|
||||||
|
priority: 1
|
||||||
|
dnsconfig: null
|
||||||
|
readinessgates: []
|
||||||
|
runtimeclassname: null
|
||||||
|
enableservicelinks: null
|
||||||
|
status:
|
||||||
|
phase: Running
|
||||||
|
conditions: []
|
||||||
|
message: ""
|
||||||
|
reason: ""
|
||||||
|
nominatednodename: ""
|
||||||
|
hostip: ""
|
||||||
|
podip: ""
|
||||||
|
starttime: null
|
||||||
|
initcontainerstatuses: []
|
||||||
|
containerstatuses:
|
||||||
|
- name: fred
|
||||||
|
state:
|
||||||
|
waiting: null
|
||||||
|
running:
|
||||||
|
startedat: "0001-01-01T00:00:00Z"
|
||||||
|
terminated: null
|
||||||
|
lastterminationstate:
|
||||||
|
waiting: null
|
||||||
|
running: null
|
||||||
|
terminated: null
|
||||||
|
ready: false
|
||||||
|
restartcount: 0
|
||||||
|
image: ""
|
||||||
|
imageid: ""
|
||||||
|
containerid: ""
|
||||||
|
qosclass: ""
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,166 @@
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
"k8s.io/api/core/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PV tracks a kubernetes resource.
|
||||||
|
type PV struct {
|
||||||
|
*Base
|
||||||
|
instance *v1.PersistentVolume
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPVList returns a new resource list.
|
||||||
|
func NewPVList(ns string) List {
|
||||||
|
return NewPVListWithArgs(ns, NewPV())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPVListWithArgs returns a new resource list.
|
||||||
|
func NewPVListWithArgs(ns string, res Resource) List {
|
||||||
|
return newList(NotNamespaced, "pv", res, CRUDAccess)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPV instantiates a new Endpoint.
|
||||||
|
func NewPV() *PV {
|
||||||
|
return NewPVWithArgs(k8s.NewPV())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPVWithArgs instantiates a new Endpoint.
|
||||||
|
func NewPVWithArgs(r k8s.Res) *PV {
|
||||||
|
ep := &PV{
|
||||||
|
Base: &Base{
|
||||||
|
caller: r,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ep.creator = ep
|
||||||
|
return ep
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInstance builds a new Endpoint instance from a k8s resource.
|
||||||
|
func (*PV) NewInstance(i interface{}) Columnar {
|
||||||
|
cm := NewPV()
|
||||||
|
switch i.(type) {
|
||||||
|
case *v1.PersistentVolume:
|
||||||
|
cm.instance = i.(*v1.PersistentVolume)
|
||||||
|
case v1.PersistentVolume:
|
||||||
|
ii := i.(v1.PersistentVolume)
|
||||||
|
cm.instance = &ii
|
||||||
|
default:
|
||||||
|
log.Fatalf("Unknown %#v", i)
|
||||||
|
}
|
||||||
|
cm.path = cm.namespacedName(cm.instance.ObjectMeta)
|
||||||
|
return cm
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal resource to yaml.
|
||||||
|
func (r *PV) Marshal(path string) (string, error) {
|
||||||
|
ns, n := namespaced(path)
|
||||||
|
i, err := r.caller.Get(ns, n)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
pv := i.(*v1.PersistentVolume)
|
||||||
|
pv.TypeMeta.APIVersion = "v1"
|
||||||
|
pv.TypeMeta.Kind = "PV"
|
||||||
|
raw, err := yaml.Marshal(pv)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(raw), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header return resource header.
|
||||||
|
func (*PV) Header(ns string) Row {
|
||||||
|
hh := Row{}
|
||||||
|
if ns == AllNamespaces {
|
||||||
|
hh = append(hh, "NAMESPACE")
|
||||||
|
}
|
||||||
|
return append(hh, "NAME", "CAPACITY", "ACCESS MODES", "RECLAIM POLICY", "STATUS", "CLAIM", "STORAGECLASS", "REASON", "AGE")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fields retrieves displayable fields.
|
||||||
|
func (r *PV) Fields(ns string) Row {
|
||||||
|
ff := make(Row, 0, len(r.Header(ns)))
|
||||||
|
i := r.instance
|
||||||
|
if ns == AllNamespaces {
|
||||||
|
ff = append(ff, i.Namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
phase := i.Status.Phase
|
||||||
|
if i.ObjectMeta.DeletionTimestamp != nil {
|
||||||
|
phase = "Terminating"
|
||||||
|
}
|
||||||
|
|
||||||
|
var claim string
|
||||||
|
if i.Spec.ClaimRef != nil {
|
||||||
|
claim = path.Join(i.Spec.ClaimRef.Namespace, i.Spec.ClaimRef.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
class, found := i.Annotations[v1.BetaStorageClassAnnotation]
|
||||||
|
if !found {
|
||||||
|
class = i.Spec.StorageClassName
|
||||||
|
}
|
||||||
|
|
||||||
|
size := i.Spec.Capacity[v1.ResourceStorage]
|
||||||
|
|
||||||
|
return append(ff,
|
||||||
|
i.Name,
|
||||||
|
size.String(),
|
||||||
|
r.accessMode(i.Spec.AccessModes),
|
||||||
|
string(i.Spec.PersistentVolumeReclaimPolicy),
|
||||||
|
string(phase),
|
||||||
|
claim,
|
||||||
|
class,
|
||||||
|
i.Status.Reason,
|
||||||
|
toAge(i.ObjectMeta.CreationTimestamp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtFields returns extended fields in relation to headers.
|
||||||
|
func (*PV) ExtFields() Properties {
|
||||||
|
return Properties{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
func (r *PV) accessMode(aa []v1.PersistentVolumeAccessMode) string {
|
||||||
|
dd := r.accessDedup(aa)
|
||||||
|
s := make([]string, 0, len(dd))
|
||||||
|
for i := 0; i < len(aa); i++ {
|
||||||
|
switch {
|
||||||
|
case r.accessContains(dd, v1.ReadWriteOnce):
|
||||||
|
s = append(s, "RWO")
|
||||||
|
case r.accessContains(dd, v1.ReadOnlyMany):
|
||||||
|
s = append(s, "ROX")
|
||||||
|
case r.accessContains(dd, v1.ReadWriteMany):
|
||||||
|
s = append(s, "RWX")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(s, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PV) accessContains(cc []v1.PersistentVolumeAccessMode, a v1.PersistentVolumeAccessMode) bool {
|
||||||
|
for _, c := range cc {
|
||||||
|
if c == a {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PV) accessDedup(cc []v1.PersistentVolumeAccessMode) []v1.PersistentVolumeAccessMode {
|
||||||
|
set := []v1.PersistentVolumeAccessMode{}
|
||||||
|
for _, c := range cc {
|
||||||
|
if !r.accessContains(set, c) {
|
||||||
|
set = append(set, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return set
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,162 @@
|
||||||
|
package resource_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/resource"
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
m "github.com/petergtz/pegomock"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPVListAccess(t *testing.T) {
|
||||||
|
ns := "blee"
|
||||||
|
l := resource.NewPVList(resource.AllNamespaces)
|
||||||
|
l.SetNamespace(ns)
|
||||||
|
|
||||||
|
assert.Equal(t, resource.NotNamespaced, l.GetNamespace())
|
||||||
|
assert.Equal(t, "pv", l.GetName())
|
||||||
|
for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} {
|
||||||
|
assert.True(t, l.Access(a))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPVHeader(t *testing.T) {
|
||||||
|
assert.Equal(t, resource.Row{"NAME", "CAPACITY", "ACCESS MODES", "RECLAIM POLICY", "STATUS", "CLAIM", "STORAGECLASS", "REASON", "AGE"}, newPV().Header("default"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPVFields(t *testing.T) {
|
||||||
|
r := newPV().Fields("blee")
|
||||||
|
assert.Equal(t, "fred", r[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPVMarshal(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.Get("blee", "fred")).ThenReturn(k8sPV(), nil)
|
||||||
|
|
||||||
|
cm := resource.NewPVWithArgs(ca)
|
||||||
|
ma, err := cm.Marshal("blee/fred")
|
||||||
|
ca.VerifyWasCalledOnce().Get("blee", "fred")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, pvYaml(), ma)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPVListData(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.List(resource.NotNamespaced)).ThenReturn(k8s.Collection{*k8sPV()}, nil)
|
||||||
|
|
||||||
|
l := resource.NewPVListWithArgs("-", resource.NewPVWithArgs(ca))
|
||||||
|
// Make sure we can get deltas!
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
err := l.Reconcile()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ca.VerifyWasCalled(m.Times(2)).List(resource.NotNamespaced)
|
||||||
|
td := l.Data()
|
||||||
|
assert.Equal(t, 1, len(td.Rows))
|
||||||
|
assert.Equal(t, resource.NotNamespaced, l.GetNamespace())
|
||||||
|
assert.False(t, l.HasXRay())
|
||||||
|
row := td.Rows["blee/fred"]
|
||||||
|
assert.Equal(t, 9, len(row.Deltas))
|
||||||
|
for _, d := range row.Deltas {
|
||||||
|
assert.Equal(t, "", d)
|
||||||
|
}
|
||||||
|
assert.Equal(t, resource.Row{"fred"}, row.Fields[:1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPVListDescribe(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.Get("blee", "fred")).ThenReturn(k8sPV(), nil)
|
||||||
|
l := resource.NewPVListWithArgs("blee", resource.NewPVWithArgs(ca))
|
||||||
|
props, err := l.Describe("blee/fred")
|
||||||
|
|
||||||
|
ca.VerifyWasCalledOnce().Get("blee", "fred")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, 0, len(props))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
func k8sPV() *v1.PersistentVolume {
|
||||||
|
return &v1.PersistentVolume{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "blee",
|
||||||
|
Name: "fred",
|
||||||
|
CreationTimestamp: metav1.Time{Time: testTime()},
|
||||||
|
},
|
||||||
|
Spec: v1.PersistentVolumeSpec{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPV() resource.Columnar {
|
||||||
|
return resource.NewPV().NewInstance(k8sPV())
|
||||||
|
}
|
||||||
|
|
||||||
|
func pvYaml() string {
|
||||||
|
return `typemeta:
|
||||||
|
kind: PV
|
||||||
|
apiversion: v1
|
||||||
|
objectmeta:
|
||||||
|
name: fred
|
||||||
|
generatename: ""
|
||||||
|
namespace: blee
|
||||||
|
selflink: ""
|
||||||
|
uid: ""
|
||||||
|
resourceversion: ""
|
||||||
|
generation: 0
|
||||||
|
creationtimestamp: "2018-12-14T10:36:43.326972-07:00"
|
||||||
|
deletiontimestamp: null
|
||||||
|
deletiongraceperiodseconds: null
|
||||||
|
labels: {}
|
||||||
|
annotations: {}
|
||||||
|
ownerreferences: []
|
||||||
|
initializers: null
|
||||||
|
finalizers: []
|
||||||
|
clustername: ""
|
||||||
|
spec:
|
||||||
|
capacity: {}
|
||||||
|
persistentvolumesource:
|
||||||
|
gcepersistentdisk: null
|
||||||
|
awselasticblockstore: null
|
||||||
|
hostpath: null
|
||||||
|
glusterfs: null
|
||||||
|
nfs: null
|
||||||
|
rbd: null
|
||||||
|
iscsi: null
|
||||||
|
cinder: null
|
||||||
|
cephfs: null
|
||||||
|
fc: null
|
||||||
|
flocker: null
|
||||||
|
flexvolume: null
|
||||||
|
azurefile: null
|
||||||
|
vspherevolume: null
|
||||||
|
quobyte: null
|
||||||
|
azuredisk: null
|
||||||
|
photonpersistentdisk: null
|
||||||
|
portworxvolume: null
|
||||||
|
scaleio: null
|
||||||
|
local: null
|
||||||
|
storageos: null
|
||||||
|
csi: null
|
||||||
|
accessmodes: []
|
||||||
|
claimref: null
|
||||||
|
persistentvolumereclaimpolicy: ""
|
||||||
|
storageclassname: ""
|
||||||
|
mountoptions: []
|
||||||
|
volumemode: null
|
||||||
|
nodeaffinity: null
|
||||||
|
status:
|
||||||
|
phase: ""
|
||||||
|
message: ""
|
||||||
|
reason: ""
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
"k8s.io/api/core/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PVC tracks a kubernetes resource.
|
||||||
|
type PVC struct {
|
||||||
|
*Base
|
||||||
|
instance *v1.PersistentVolumeClaim
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPVCList returns a new resource list.
|
||||||
|
func NewPVCList(ns string) List {
|
||||||
|
return NewPVCListWithArgs(ns, NewPVC())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPVCListWithArgs returns a new resource list.
|
||||||
|
func NewPVCListWithArgs(ns string, res Resource) List {
|
||||||
|
return newList(ns, "pvc", res, AllVerbsAccess)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPVC instantiates a new Endpoint.
|
||||||
|
func NewPVC() *PVC {
|
||||||
|
return NewPVCWithArgs(k8s.NewPVC())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPVCWithArgs instantiates a new Endpoint.
|
||||||
|
func NewPVCWithArgs(r k8s.Res) *PVC {
|
||||||
|
ep := &PVC{
|
||||||
|
Base: &Base{
|
||||||
|
caller: r,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ep.creator = ep
|
||||||
|
return ep
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInstance builds a new Endpoint instance from a k8s resource.
|
||||||
|
func (*PVC) NewInstance(i interface{}) Columnar {
|
||||||
|
cm := NewPVC()
|
||||||
|
switch i.(type) {
|
||||||
|
case *v1.PersistentVolumeClaim:
|
||||||
|
cm.instance = i.(*v1.PersistentVolumeClaim)
|
||||||
|
case v1.PersistentVolumeClaim:
|
||||||
|
ii := i.(v1.PersistentVolumeClaim)
|
||||||
|
cm.instance = &ii
|
||||||
|
default:
|
||||||
|
log.Fatalf("Unknown %#v", i)
|
||||||
|
}
|
||||||
|
cm.path = cm.namespacedName(cm.instance.ObjectMeta)
|
||||||
|
return cm
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal resource to yaml.
|
||||||
|
func (r *PVC) Marshal(path string) (string, error) {
|
||||||
|
ns, n := namespaced(path)
|
||||||
|
i, err := r.caller.Get(ns, n)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
pvc := i.(*v1.PersistentVolumeClaim)
|
||||||
|
pvc.TypeMeta.APIVersion = "v1"
|
||||||
|
pvc.TypeMeta.Kind = "PersistentVolumeClaim"
|
||||||
|
raw, err := yaml.Marshal(pvc)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(raw), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header return resource header.
|
||||||
|
func (*PVC) Header(ns string) Row {
|
||||||
|
hh := Row{}
|
||||||
|
if ns == AllNamespaces {
|
||||||
|
hh = append(hh, "NAMESPACE")
|
||||||
|
}
|
||||||
|
return append(hh, "NAME", "STATUS", "VOLUME", "CAPACITY", "ACCESS MODES", "STORAGECLASS", "AGE")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fields retrieves displayable fields.
|
||||||
|
func (r *PVC) Fields(ns string) Row {
|
||||||
|
ff := make(Row, 0, len(r.Header(ns)))
|
||||||
|
i := r.instance
|
||||||
|
if ns == AllNamespaces {
|
||||||
|
ff = append(ff, i.Namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
phase := i.Status.Phase
|
||||||
|
if i.ObjectMeta.DeletionTimestamp != nil {
|
||||||
|
phase = "Terminating"
|
||||||
|
}
|
||||||
|
|
||||||
|
pv := PV{}
|
||||||
|
storage := i.Spec.Resources.Requests[v1.ResourceStorage]
|
||||||
|
var capacity, accessModes string
|
||||||
|
if i.Spec.VolumeName != "" {
|
||||||
|
accessModes = pv.accessMode(i.Status.AccessModes)
|
||||||
|
storage = i.Status.Capacity[v1.ResourceStorage]
|
||||||
|
capacity = storage.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
class, found := i.Annotations[v1.BetaStorageClassAnnotation]
|
||||||
|
if !found {
|
||||||
|
if i.Spec.StorageClassName != nil {
|
||||||
|
class = *i.Spec.StorageClassName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(ff,
|
||||||
|
i.Name,
|
||||||
|
string(phase),
|
||||||
|
i.Spec.VolumeName,
|
||||||
|
capacity,
|
||||||
|
accessModes,
|
||||||
|
class,
|
||||||
|
toAge(i.ObjectMeta.CreationTimestamp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtFields returns extended fields in relation to headers.
|
||||||
|
func (*PVC) ExtFields() Properties {
|
||||||
|
return Properties{}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
package resource_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/resource"
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
m "github.com/petergtz/pegomock"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"k8s.io/api/core/v1"
|
||||||
|
resv1 "k8s.io/apimachinery/pkg/api/resource"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPVCListAccess(t *testing.T) {
|
||||||
|
ns := "blee"
|
||||||
|
l := resource.NewPVCList(resource.AllNamespaces)
|
||||||
|
l.SetNamespace(ns)
|
||||||
|
|
||||||
|
assert.Equal(t, "blee", l.GetNamespace())
|
||||||
|
assert.Equal(t, "pvc", l.GetName())
|
||||||
|
for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} {
|
||||||
|
assert.True(t, l.Access(a))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPVCHeader(t *testing.T) {
|
||||||
|
assert.Equal(t, resource.Row{"NAME", "STATUS", "VOLUME", "CAPACITY", "ACCESS MODES", "STORAGECLASS", "AGE"}, newPVC().Header("default"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPVCFields(t *testing.T) {
|
||||||
|
r := newPVC().Fields("blee")
|
||||||
|
assert.Equal(t, "fred", r[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPVCMarshal(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.Get("blee", "fred")).ThenReturn(k8sPVC(), nil)
|
||||||
|
|
||||||
|
cm := resource.NewPVCWithArgs(ca)
|
||||||
|
ma, err := cm.Marshal("blee/fred")
|
||||||
|
ca.VerifyWasCalledOnce().Get("blee", "fred")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, pvcYaml(), ma)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPVCListData(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.List(resource.NotNamespaced)).ThenReturn(k8s.Collection{*k8sPVC()}, nil)
|
||||||
|
|
||||||
|
l := resource.NewPVCListWithArgs("-", resource.NewPVCWithArgs(ca))
|
||||||
|
// Make sure we can get deltas!
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
err := l.Reconcile()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ca.VerifyWasCalled(m.Times(2)).List(resource.NotNamespaced)
|
||||||
|
td := l.Data()
|
||||||
|
assert.Equal(t, 1, len(td.Rows))
|
||||||
|
assert.Equal(t, resource.NotNamespaced, l.GetNamespace())
|
||||||
|
assert.False(t, l.HasXRay())
|
||||||
|
row := td.Rows["blee/fred"]
|
||||||
|
assert.Equal(t, 7, len(row.Deltas))
|
||||||
|
for _, d := range row.Deltas {
|
||||||
|
assert.Equal(t, "", d)
|
||||||
|
}
|
||||||
|
assert.Equal(t, resource.Row{"fred"}, row.Fields[:1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPVCListDescribe(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.Get("blee", "fred")).ThenReturn(k8sPVC(), nil)
|
||||||
|
l := resource.NewPVCListWithArgs("blee", resource.NewPVCWithArgs(ca))
|
||||||
|
props, err := l.Describe("blee/fred")
|
||||||
|
|
||||||
|
ca.VerifyWasCalledOnce().Get("blee", "fred")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, 0, len(props))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
func k8sPVC() *v1.PersistentVolumeClaim {
|
||||||
|
return &v1.PersistentVolumeClaim{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "blee",
|
||||||
|
Name: "fred",
|
||||||
|
CreationTimestamp: metav1.Time{Time: testTime()},
|
||||||
|
},
|
||||||
|
Spec: v1.PersistentVolumeClaimSpec{
|
||||||
|
VolumeName: "duh",
|
||||||
|
Resources: v1.ResourceRequirements{
|
||||||
|
Requests: v1.ResourceList{
|
||||||
|
v1.ResourceStorage: resv1.Quantity{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPVC() resource.Columnar {
|
||||||
|
return resource.NewPVC().NewInstance(k8sPVC())
|
||||||
|
}
|
||||||
|
|
||||||
|
func pvcYaml() string {
|
||||||
|
return `typemeta:
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
apiversion: v1
|
||||||
|
objectmeta:
|
||||||
|
name: fred
|
||||||
|
generatename: ""
|
||||||
|
namespace: blee
|
||||||
|
selflink: ""
|
||||||
|
uid: ""
|
||||||
|
resourceversion: ""
|
||||||
|
generation: 0
|
||||||
|
creationtimestamp: "2018-12-14T10:36:43.326972-07:00"
|
||||||
|
deletiontimestamp: null
|
||||||
|
deletiongraceperiodseconds: null
|
||||||
|
labels: {}
|
||||||
|
annotations: {}
|
||||||
|
ownerreferences: []
|
||||||
|
initializers: null
|
||||||
|
finalizers: []
|
||||||
|
clustername: ""
|
||||||
|
spec:
|
||||||
|
accessmodes: []
|
||||||
|
selector: null
|
||||||
|
resources:
|
||||||
|
limits: {}
|
||||||
|
requests:
|
||||||
|
storage:
|
||||||
|
format: ""
|
||||||
|
volumename: duh
|
||||||
|
storageclassname: null
|
||||||
|
volumemode: null
|
||||||
|
datasource: null
|
||||||
|
status:
|
||||||
|
phase: ""
|
||||||
|
accessmodes: []
|
||||||
|
capacity: {}
|
||||||
|
conditions: []
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
"k8s.io/api/rbac/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Role tracks a kubernetes resource.
|
||||||
|
type Role struct {
|
||||||
|
*Base
|
||||||
|
instance *v1.Role
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRoleList returns a new resource list.
|
||||||
|
func NewRoleList(ns string) List {
|
||||||
|
return NewRoleListWithArgs(ns, NewRole())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRoleListWithArgs returns a new resource list.
|
||||||
|
func NewRoleListWithArgs(ns string, res Resource) List {
|
||||||
|
l := newList(ns, "role", res, AllVerbsAccess|DescribeAccess)
|
||||||
|
l.xray = true
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRole instantiates a new Endpoint.
|
||||||
|
func NewRole() *Role {
|
||||||
|
return NewRoleWithArgs(k8s.NewRole())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRoleWithArgs instantiates a new Endpoint.
|
||||||
|
func NewRoleWithArgs(r k8s.Res) *Role {
|
||||||
|
ep := &Role{
|
||||||
|
Base: &Base{
|
||||||
|
caller: r,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ep.creator = ep
|
||||||
|
return ep
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInstance builds a new Endpoint instance from a k8s resource.
|
||||||
|
func (*Role) NewInstance(i interface{}) Columnar {
|
||||||
|
cm := NewRole()
|
||||||
|
switch i.(type) {
|
||||||
|
case *v1.Role:
|
||||||
|
cm.instance = i.(*v1.Role)
|
||||||
|
case v1.Role:
|
||||||
|
ii := i.(v1.Role)
|
||||||
|
cm.instance = &ii
|
||||||
|
default:
|
||||||
|
log.Fatalf("Unknown %#v", i)
|
||||||
|
}
|
||||||
|
cm.path = cm.namespacedName(cm.instance.ObjectMeta)
|
||||||
|
return cm
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal resource to yaml.
|
||||||
|
func (r *Role) Marshal(path string) (string, error) {
|
||||||
|
ns, n := namespaced(path)
|
||||||
|
i, err := r.caller.Get(ns, n)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
role := i.(*v1.Role)
|
||||||
|
role.TypeMeta.APIVersion = "rbac.authorization.k8s.io/v1"
|
||||||
|
role.TypeMeta.Kind = "Role"
|
||||||
|
raw, err := yaml.Marshal(role)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(raw), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header return resource header.
|
||||||
|
func (*Role) Header(ns string) Row {
|
||||||
|
hh := Row{}
|
||||||
|
if ns == AllNamespaces {
|
||||||
|
hh = append(hh, "NAMESPACE")
|
||||||
|
}
|
||||||
|
return append(hh, "NAME", "AGE")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fields retrieves displayable fields.
|
||||||
|
func (r *Role) Fields(ns string) Row {
|
||||||
|
ff := make(Row, 0, len(r.Header(ns)))
|
||||||
|
i := r.instance
|
||||||
|
if ns == AllNamespaces {
|
||||||
|
ff = append(ff, i.Namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(ff,
|
||||||
|
i.Name,
|
||||||
|
toAge(i.ObjectMeta.CreationTimestamp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtFields returns extended fields in relation to headers.
|
||||||
|
func (r *Role) ExtFields() Properties {
|
||||||
|
i := r.instance
|
||||||
|
|
||||||
|
return Properties{
|
||||||
|
"Headers": Row{"RESOURCES", "NON-RESOURCE URLS", "RESOURCE NAMES", "VERBS"},
|
||||||
|
"Rows": r.parseRules(i.Rules),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
func (r *Role) parseRules(pp []v1.PolicyRule) []Row {
|
||||||
|
acc := make([]Row, len(pp))
|
||||||
|
for i, p := range pp {
|
||||||
|
acc[i] = make(Row, 4)
|
||||||
|
acc[i][0] = strings.Join(p.Resources, ", ")
|
||||||
|
acc[i][1] = strings.Join(p.NonResourceURLs, ", ")
|
||||||
|
acc[i][2] = strings.Join(p.ResourceNames, ", ")
|
||||||
|
acc[i][3] = strings.Join(p.Verbs, ", ")
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
"k8s.io/api/rbac/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RoleBinding tracks a kubernetes resource.
|
||||||
|
type RoleBinding struct {
|
||||||
|
*Base
|
||||||
|
instance *v1.RoleBinding
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRoleBindingList returns a new resource list.
|
||||||
|
func NewRoleBindingList(ns string) List {
|
||||||
|
return NewRoleBindingListWithArgs(ns, NewRoleBinding())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRoleBindingListWithArgs returns a new resource list.
|
||||||
|
func NewRoleBindingListWithArgs(ns string, res Resource) List {
|
||||||
|
return newList(ns, "rolebinding", res, AllVerbsAccess)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRoleBinding instantiates a new Endpoint.
|
||||||
|
func NewRoleBinding() *RoleBinding {
|
||||||
|
return NewRoleBindingWithArgs(k8s.NewRoleBinding())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRoleBindingWithArgs instantiates a new Endpoint.
|
||||||
|
func NewRoleBindingWithArgs(r k8s.Res) *RoleBinding {
|
||||||
|
ep := &RoleBinding{
|
||||||
|
Base: &Base{
|
||||||
|
caller: r,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ep.creator = ep
|
||||||
|
return ep
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInstance builds a new Endpoint instance from a k8s resource.
|
||||||
|
func (*RoleBinding) NewInstance(i interface{}) Columnar {
|
||||||
|
cm := NewRoleBinding()
|
||||||
|
switch i.(type) {
|
||||||
|
case *v1.RoleBinding:
|
||||||
|
cm.instance = i.(*v1.RoleBinding)
|
||||||
|
case v1.RoleBinding:
|
||||||
|
ii := i.(v1.RoleBinding)
|
||||||
|
cm.instance = &ii
|
||||||
|
default:
|
||||||
|
log.Fatalf("Unknown %#v", i)
|
||||||
|
}
|
||||||
|
cm.path = cm.namespacedName(cm.instance.ObjectMeta)
|
||||||
|
return cm
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal resource to yaml.
|
||||||
|
func (r *RoleBinding) Marshal(path string) (string, error) {
|
||||||
|
ns, n := namespaced(path)
|
||||||
|
i, err := r.caller.Get(ns, n)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
rb := i.(*v1.RoleBinding)
|
||||||
|
rb.TypeMeta.APIVersion = "rbac.authorization.k8s.io/v1"
|
||||||
|
rb.TypeMeta.Kind = "RoleBinding"
|
||||||
|
raw, err := yaml.Marshal(rb)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(raw), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header return resource header.
|
||||||
|
func (*RoleBinding) Header(ns string) Row {
|
||||||
|
hh := Row{}
|
||||||
|
if ns == AllNamespaces {
|
||||||
|
hh = append(hh, "NAMESPACE")
|
||||||
|
}
|
||||||
|
return append(hh, "NAME", "ROLE", "SUBJECTS", "AGE")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fields retrieves displayable fields.
|
||||||
|
func (r *RoleBinding) Fields(ns string) Row {
|
||||||
|
ff := make(Row, 0, len(r.Header(ns)))
|
||||||
|
i := r.instance
|
||||||
|
if ns == AllNamespaces {
|
||||||
|
ff = append(ff, i.Namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(ff,
|
||||||
|
i.Name,
|
||||||
|
i.RoleRef.Name,
|
||||||
|
r.toSubjects(i.Subjects),
|
||||||
|
toAge(i.ObjectMeta.CreationTimestamp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtFields returns extended fields in relation to headers.
|
||||||
|
func (*RoleBinding) ExtFields() Properties {
|
||||||
|
return Properties{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
func (r *RoleBinding) toSubjects(ss []v1.Subject) string {
|
||||||
|
var acc string
|
||||||
|
for i, s := range ss {
|
||||||
|
acc += s.Name + "/" + r.toSubjectAlias(s.Kind)
|
||||||
|
if i < len(ss)-1 {
|
||||||
|
acc += ","
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RoleBinding) toSubjectAlias(s string) string {
|
||||||
|
switch s {
|
||||||
|
case v1.UserKind:
|
||||||
|
return "USR"
|
||||||
|
case v1.GroupKind:
|
||||||
|
return "GRP"
|
||||||
|
case v1.ServiceAccountKind:
|
||||||
|
return "SA"
|
||||||
|
default:
|
||||||
|
return strings.ToUpper(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestToSubjectAlias(t *testing.T) {
|
||||||
|
r := RoleBinding{}
|
||||||
|
|
||||||
|
uu := []struct {
|
||||||
|
i string
|
||||||
|
e string
|
||||||
|
}{
|
||||||
|
{rbacv1.UserKind, "USR"},
|
||||||
|
{rbacv1.GroupKind, "GRP"},
|
||||||
|
{rbacv1.ServiceAccountKind, "SA"},
|
||||||
|
{"fred", "FRED"},
|
||||||
|
}
|
||||||
|
for _, u := range uu {
|
||||||
|
assert.Equal(t, u.e, r.toSubjectAlias(u.i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToSubjects(t *testing.T) {
|
||||||
|
r := RoleBinding{}
|
||||||
|
|
||||||
|
uu := []struct {
|
||||||
|
i []rbacv1.Subject
|
||||||
|
e string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
[]rbacv1.Subject{
|
||||||
|
{Name: "blee", Kind: rbacv1.UserKind},
|
||||||
|
},
|
||||||
|
"blee/USR",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, u := range uu {
|
||||||
|
assert.Equal(t, u.e, r.toSubjects(u.i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkToSubjects(b *testing.B) {
|
||||||
|
var r RoleBinding
|
||||||
|
ss := []rbacv1.Subject{
|
||||||
|
{Name: "blee", Kind: rbacv1.UserKind},
|
||||||
|
}
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
r.toSubjects(ss)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
package resource_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/resource"
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
m "github.com/petergtz/pegomock"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"k8s.io/api/rbac/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRBMarshal(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.Get("blee", "fred")).ThenReturn(k8sRB(), nil)
|
||||||
|
|
||||||
|
cm := resource.NewRoleBindingWithArgs(ca)
|
||||||
|
ma, err := cm.Marshal("blee/fred")
|
||||||
|
ca.VerifyWasCalledOnce().Get("blee", "fred")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, rbYaml(), ma)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRBListData(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.List("blee")).ThenReturn(k8s.Collection{*k8sRB()}, nil)
|
||||||
|
|
||||||
|
l := resource.NewRoleBindingListWithArgs("blee", resource.NewRoleBindingWithArgs(ca))
|
||||||
|
// Make sure we can get deltas!
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
err := l.Reconcile()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ca.VerifyWasCalled(m.Times(2)).List("blee")
|
||||||
|
td := l.Data()
|
||||||
|
assert.Equal(t, 1, len(td.Rows))
|
||||||
|
assert.Equal(t, "blee", l.GetNamespace())
|
||||||
|
assert.False(t, l.HasXRay())
|
||||||
|
row := td.Rows["blee/fred"]
|
||||||
|
assert.Equal(t, 4, len(row.Deltas))
|
||||||
|
for _, d := range row.Deltas {
|
||||||
|
assert.Equal(t, "", d)
|
||||||
|
}
|
||||||
|
assert.Equal(t, resource.Row{"fred"}, row.Fields[:1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRBListDescribe(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.Get("blee", "fred")).ThenReturn(k8sRB(), nil)
|
||||||
|
l := resource.NewRoleBindingListWithArgs("blee", resource.NewRoleBindingWithArgs(ca))
|
||||||
|
props, err := l.Describe("blee/fred")
|
||||||
|
|
||||||
|
ca.VerifyWasCalledOnce().Get("blee", "fred")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, 0, len(props))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
func k8sRB() *v1.RoleBinding {
|
||||||
|
return &v1.RoleBinding{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "blee",
|
||||||
|
Name: "fred",
|
||||||
|
CreationTimestamp: metav1.Time{Time: testTime()},
|
||||||
|
},
|
||||||
|
Subjects: []v1.Subject{
|
||||||
|
{
|
||||||
|
Kind: v1.UserKind,
|
||||||
|
Name: "fred",
|
||||||
|
Namespace: "blee",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RoleRef: v1.RoleRef{
|
||||||
|
Kind: v1.UserKind,
|
||||||
|
Name: "duh",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRB() resource.Columnar {
|
||||||
|
return resource.NewRoleBinding().NewInstance(k8sRB())
|
||||||
|
}
|
||||||
|
|
||||||
|
func rbYaml() string {
|
||||||
|
return `typemeta:
|
||||||
|
kind: RoleBinding
|
||||||
|
apiversion: rbac.authorization.k8s.io/v1
|
||||||
|
objectmeta:
|
||||||
|
name: fred
|
||||||
|
generatename: ""
|
||||||
|
namespace: blee
|
||||||
|
selflink: ""
|
||||||
|
uid: ""
|
||||||
|
resourceversion: ""
|
||||||
|
generation: 0
|
||||||
|
creationtimestamp: "2018-12-14T10:36:43.326972-07:00"
|
||||||
|
deletiontimestamp: null
|
||||||
|
deletiongraceperiodseconds: null
|
||||||
|
labels: {}
|
||||||
|
annotations: {}
|
||||||
|
ownerreferences: []
|
||||||
|
initializers: null
|
||||||
|
finalizers: []
|
||||||
|
clustername: ""
|
||||||
|
subjects:
|
||||||
|
- kind: User
|
||||||
|
apigroup: ""
|
||||||
|
name: fred
|
||||||
|
namespace: blee
|
||||||
|
roleref:
|
||||||
|
apigroup: ""
|
||||||
|
kind: User
|
||||||
|
name: duh
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
package resource_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/resource"
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
m "github.com/petergtz/pegomock"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"k8s.io/api/rbac/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRoleMarshal(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.Get("blee", "fred")).ThenReturn(k8sRole(), nil)
|
||||||
|
|
||||||
|
cm := resource.NewRoleWithArgs(ca)
|
||||||
|
ma, err := cm.Marshal("blee/fred")
|
||||||
|
ca.VerifyWasCalledOnce().Get("blee", "fred")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, roleYaml(), ma)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRoleListData(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.List("blee")).ThenReturn(k8s.Collection{*k8sRole()}, nil)
|
||||||
|
|
||||||
|
l := resource.NewRoleListWithArgs("blee", resource.NewRoleWithArgs(ca))
|
||||||
|
// Make sure we can get deltas!
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
err := l.Reconcile()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ca.VerifyWasCalled(m.Times(2)).List("blee")
|
||||||
|
td := l.Data()
|
||||||
|
assert.Equal(t, 1, len(td.Rows))
|
||||||
|
assert.Equal(t, "blee", l.GetNamespace())
|
||||||
|
assert.True(t, l.HasXRay())
|
||||||
|
row := td.Rows["blee/fred"]
|
||||||
|
assert.Equal(t, 2, len(row.Deltas))
|
||||||
|
for _, d := range row.Deltas {
|
||||||
|
assert.Equal(t, "", d)
|
||||||
|
}
|
||||||
|
assert.Equal(t, resource.Row{"fred"}, row.Fields[:1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRoleListDescribe(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
ca := NewMockCaller()
|
||||||
|
m.When(ca.Get("blee", "fred")).ThenReturn(k8sRole(), nil)
|
||||||
|
l := resource.NewRoleListWithArgs("blee", resource.NewRoleWithArgs(ca))
|
||||||
|
props, err := l.Describe("blee/fred")
|
||||||
|
|
||||||
|
ca.VerifyWasCalledOnce().Get("blee", "fred")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, 2, len(props))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
func k8sRole() *v1.Role {
|
||||||
|
return &v1.Role{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "blee",
|
||||||
|
Name: "fred",
|
||||||
|
CreationTimestamp: metav1.Time{Time: testTime()},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRole() resource.Columnar {
|
||||||
|
return resource.NewRole().NewInstance(k8sRole())
|
||||||
|
}
|
||||||
|
|
||||||
|
func roleYaml() string {
|
||||||
|
return `typemeta:
|
||||||
|
kind: Role
|
||||||
|
apiversion: rbac.authorization.k8s.io/v1
|
||||||
|
objectmeta:
|
||||||
|
name: fred
|
||||||
|
generatename: ""
|
||||||
|
namespace: blee
|
||||||
|
selflink: ""
|
||||||
|
uid: ""
|
||||||
|
resourceversion: ""
|
||||||
|
generation: 0
|
||||||
|
creationtimestamp: "2018-12-14T10:36:43.326972-07:00"
|
||||||
|
deletiontimestamp: null
|
||||||
|
deletiongraceperiodseconds: null
|
||||||
|
labels: {}
|
||||||
|
annotations: {}
|
||||||
|
ownerreferences: []
|
||||||
|
initializers: null
|
||||||
|
finalizers: []
|
||||||
|
clustername: ""
|
||||||
|
rules: []
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/k8sland/k9s/resource/k8s"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
"k8s.io/api/apps/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReplicaSet tracks a kubernetes resource.
|
||||||
|
type ReplicaSet struct {
|
||||||
|
*Base
|
||||||
|
instance *v1.ReplicaSet
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewReplicaSetList returns a new resource list.
|
||||||
|
func NewReplicaSetList(ns string) List {
|
||||||
|
return NewReplicaSetListWithArgs(ns, NewReplicaSet())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewReplicaSetListWithArgs returns a new resource list.
|
||||||
|
func NewReplicaSetListWithArgs(ns string, res Resource) List {
|
||||||
|
return newList(ns, "rs", res, AllVerbsAccess)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewReplicaSet instantiates a new Endpoint.
|
||||||
|
func NewReplicaSet() *ReplicaSet {
|
||||||
|
return NewReplicaSetWithArgs(k8s.NewReplicaSet())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewReplicaSetWithArgs instantiates a new Endpoint.
|
||||||
|
func NewReplicaSetWithArgs(r k8s.Res) *ReplicaSet {
|
||||||
|
ep := &ReplicaSet{
|
||||||
|
Base: &Base{
|
||||||
|
caller: r,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ep.creator = ep
|
||||||
|
return ep
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInstance builds a new Endpoint instance from a k8s resource.
|
||||||
|
func (*ReplicaSet) NewInstance(i interface{}) Columnar {
|
||||||
|
cm := NewReplicaSet()
|
||||||
|
switch i.(type) {
|
||||||
|
case *v1.ReplicaSet:
|
||||||
|
cm.instance = i.(*v1.ReplicaSet)
|
||||||
|
case v1.ReplicaSet:
|
||||||
|
ii := i.(v1.ReplicaSet)
|
||||||
|
cm.instance = &ii
|
||||||
|
default:
|
||||||
|
log.Fatalf("Unknown %#v", i)
|
||||||
|
}
|
||||||
|
cm.path = cm.namespacedName(cm.instance.ObjectMeta)
|
||||||
|
return cm
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal a deployment given a namespaced name.
|
||||||
|
func (r *ReplicaSet) Marshal(path string) (string, error) {
|
||||||
|
ns, n := namespaced(path)
|
||||||
|
i, err := r.caller.Get(ns, n)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
rs := i.(*v1.ReplicaSet)
|
||||||
|
rs.TypeMeta.APIVersion = "extensions/v1beta"
|
||||||
|
rs.TypeMeta.Kind = "ReplicaSet"
|
||||||
|
raw, err := yaml.Marshal(rs)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(raw), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header return resource header.
|
||||||
|
func (*ReplicaSet) Header(ns string) Row {
|
||||||
|
hh := Row{}
|
||||||
|
if ns == AllNamespaces {
|
||||||
|
hh = append(hh, "NAMESPACE")
|
||||||
|
}
|
||||||
|
return append(hh, "NAME", "DESIRED", "CURRENT", "READY", "AGE")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fields retrieves displayable fields.
|
||||||
|
func (r *ReplicaSet) Fields(ns string) Row {
|
||||||
|
ff := make(Row, 0, len(r.Header(ns)))
|
||||||
|
if ns == AllNamespaces {
|
||||||
|
ff = append(ff, r.instance.Namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
i := r.instance
|
||||||
|
return append(ff,
|
||||||
|
i.Name,
|
||||||
|
strconv.Itoa(int(*i.Spec.Replicas)),
|
||||||
|
strconv.Itoa(int(i.Status.Replicas)),
|
||||||
|
strconv.Itoa(int(i.Status.ReadyReplicas)),
|
||||||
|
toAge(i.ObjectMeta.CreationTimestamp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtFields returns extended fields in relation to headers.
|
||||||
|
func (*ReplicaSet) ExtFields() Properties {
|
||||||
|
return Properties{}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue