init drop

mine
derailed 2019-01-25 11:53:08 -07:00
commit da190c204c
1841 changed files with 678774 additions and 0 deletions

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
.vscode
k9s.log
.envrc
cov.out
execs
k9s
samples
dist
notes
styles

56
.goreleaser.yml Normal file
View File

@ -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"

63
.semaphore/semaphore.yml Normal file
View File

@ -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"

21
.travis.yml Normal file
View File

@ -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 ./...

1
CNAME Normal file
View File

@ -0,0 +1 @@
k9ss.io

13
LICENSE Normal file
View File

@ -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

18
Makefile Normal file
View File

@ -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}'

132
README.md Normal file
View File

@ -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/>
---
[![Go Report Card](https://goreportcard.com/badge/github.com/k8sland/k9s)](https://goreportcard.com/report/github.com/k8sland/k9s)
[![Build Status](https://travis-ci.org/k8sland/k9s.svg?branch=master)](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)

BIN
assets/imhotep_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

BIN
assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
assets/logo_w.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

BIN
assets/screen_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 KiB

BIN
assets/screen_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

124
cmd/root.go Normal file
View File

@ -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
}

48
go.mod Normal file
View File

@ -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
)

119
go.sum Normal file
View File

@ -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=

22
main.go Normal file
View File

@ -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()
}

73
resource/base.go Normal file
View File

@ -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)
}

56
resource/cluster.go Normal file
View File

@ -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()
}

72
resource/cluster_test.go Normal file
View File

@ -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",
}
}

105
resource/cm.go Normal file
View File

@ -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{}
}

211
resource/cm_test.go Normal file
View File

@ -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: {}
`
}

97
resource/context.go Normal file
View File

@ -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{}
}

160
resource/context_test.go Normal file
View File

@ -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
}

96
resource/cr.go Normal file
View File

@ -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{}
}

95
resource/cr_binding.go Normal file
View File

@ -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{}
}

117
resource/cr_binding_test.go Normal file
View File

@ -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: ""
`
}

163
resource/cr_test.go Normal file
View File

@ -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
`
}

114
resource/crd.go Normal file
View File

@ -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
}

120
resource/crd_test.go Normal file
View File

@ -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
`
}

116
resource/cronjob.go Normal file
View File

@ -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...

218
resource/cronjob_test.go Normal file
View File

@ -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"
`
}

191
resource/custom.go Normal file
View File

@ -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
}

108
resource/dp.go Normal file
View File

@ -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{}
}

197
resource/dp_test.go Normal file
View File

@ -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
`
}

112
resource/ds.go Normal file
View File

@ -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{}
}

207
resource/ds_test.go Normal file
View File

@ -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: []
`
}

133
resource/ep.go Normal file
View File

@ -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, ",")
}

146
resource/ep_test.go Normal file
View File

@ -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
`
}

152
resource/evt.go Normal file
View File

@ -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 "😮"
}
}
}

151
resource/evt_test.go Normal file
View File

@ -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: ""
`
}

107
resource/helpers.go Normal file
View File

@ -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
}

119
resource/helpers_test.go Normal file
View File

@ -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)
}
}

123
resource/hpa.go Normal file
View File

@ -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{}
}

153
resource/hpa_test.go Normal file
View File

@ -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
`
}

144
resource/ing.go Normal file
View File

@ -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
}

140
resource/ing_test.go Normal file
View File

@ -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: []
`
}

133
resource/job.go Normal file
View File

@ -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))
}

198
resource/job_test.go Normal file
View File

@ -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
`
}

244
resource/k8s/api.go Normal file
View File

@ -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
}

23
resource/k8s/cluster.go Normal file
View File

@ -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()
}

View File

@ -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)
}

View File

@ -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)
}

42
resource/k8s/cm.go Normal file
View File

@ -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)
}

72
resource/k8s/context.go Normal file
View File

@ -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)
}

42
resource/k8s/crd.go Normal file
View File

@ -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)
}

42
resource/k8s/cronjob.go Normal file
View File

@ -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)
}

42
resource/k8s/dp.go Normal file
View File

@ -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)
}

42
resource/k8s/ds.go Normal file
View File

@ -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)
}

42
resource/k8s/ep.go Normal file
View File

@ -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)
}

42
resource/k8s/evt.go Normal file
View File

@ -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)
}

42
resource/k8s/hpa.go Normal file
View File

@ -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)
}

42
resource/k8s/ing.go Normal file
View File

@ -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)
}

42
resource/k8s/job.go Normal file
View File

@ -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)
}

195
resource/k8s/metrics.go Normal file
View File

@ -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
}

42
resource/k8s/no.go Normal file
View File

@ -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)
}

42
resource/k8s/ns.go Normal file
View File

@ -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)
}

88
resource/k8s/pod.go Normal file
View File

@ -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
}

42
resource/k8s/pv.go Normal file
View File

@ -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)
}

42
resource/k8s/pvc.go Normal file
View File

@ -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)
}

119
resource/k8s/resource.go Normal file
View File

@ -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()
}

43
resource/k8s/role.go Normal file
View File

@ -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)
}

View File

@ -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)
}

42
resource/k8s/rs.go Normal file
View File

@ -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)
}

46
resource/k8s/sa.go Normal file
View File

@ -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)
}

42
resource/k8s/secret.go Normal file
View File

@ -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)
}

45
resource/k8s/sts.go Normal file
View File

@ -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)
}

42
resource/k8s/svc.go Normal file
View File

@ -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)
}

246
resource/list.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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() {
}

View File

@ -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() {
}

View File

@ -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
}

View File

@ -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
}

217
resource/no.go Normal file
View File

@ -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()
}

168
resource/no_test.go Normal file
View File

@ -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
`
}

96
resource/ns.go Normal file
View File

@ -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{}
}

129
resource/ns_test.go Normal file
View File

@ -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: ""
`
}

382
resource/pod.go Normal file
View File

@ -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
}

298
resource/pod_test.go Normal file
View File

@ -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: ""
`
}

166
resource/pv.go Normal file
View File

@ -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
}

162
resource/pv_test.go Normal file
View File

@ -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: ""
`
}

128
resource/pvc.go Normal file
View File

@ -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{}
}

151
resource/pvc_test.go Normal file
View File

@ -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: []
`
}

125
resource/ro.go Normal file
View File

@ -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
}

132
resource/ro_binding.go Normal file
View File

@ -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)
}
}

View File

@ -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)
}
}

124
resource/ro_binding_test.go Normal file
View File

@ -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
`
}

105
resource/ro_test.go Normal file
View File

@ -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: []
`
}

107
resource/rs.go Normal file
View File

@ -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