pax_global_header00006660000000000000000000000064140451625200014511gustar00rootroot0000000000000052 comment=d00a6598db30da791b4ac2205bbf81ed911145b7 kubecolor-0.0.20/000077500000000000000000000000001404516252000135555ustar00rootroot00000000000000kubecolor-0.0.20/.github/000077500000000000000000000000001404516252000151155ustar00rootroot00000000000000kubecolor-0.0.20/.github/issue_template.md000066400000000000000000000015711404516252000204660ustar00rootroot00000000000000 kubecolor-0.0.20/.github/pull_request_template.md000066400000000000000000000000561404516252000220570ustar00rootroot00000000000000## WHAT ## WHY ## Related issue (if exists) kubecolor-0.0.20/.github/workflows/000077500000000000000000000000001404516252000171525ustar00rootroot00000000000000kubecolor-0.0.20/.github/workflows/release.yml000066400000000000000000000007421404516252000213200ustar00rootroot00000000000000name: goreleaser on: push: tags: - '*' jobs: goreleaser: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v1 - name: Set up Go uses: actions/setup-go@v1 with: go-version: 1.16.x - name: Run GoReleaser uses: goreleaser/goreleaser-action@v1 with: version: latest args: release --rm-dist env: GITHUB_TOKEN: ${{ secrets.GO_RELEASER }} kubecolor-0.0.20/.github/workflows/test.yml000066400000000000000000000010211404516252000206460ustar00rootroot00000000000000name: test on: push: branches: - "main" pull_request: {} jobs: test: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: - ubuntu-latest - macOS-latest - windows-latest steps: - name: checkout uses: actions/checkout@v2 - name: setup go uses: actions/setup-go@v2 - name: test run: make coverage - name: upload coverage uses: codecov/codecov-action@v1 with: token: ${{ secrets.CODECOV_TOKEN }} kubecolor-0.0.20/.gitignore000066400000000000000000000000061404516252000155410ustar00rootroot00000000000000dist/ kubecolor-0.0.20/.goreleaser.yml000066400000000000000000000015201404516252000165040ustar00rootroot00000000000000release: before: hooks: - go mod tidy - make testshort builds: - id: kubecolor main: ./cmd/kubecolor/main.go binary: kubecolor ldflags: - -s -w - -X main.Version={{.Version}} goos: - windows - darwin - linux goarch: - arm64 - amd64 - ppc64le archives: - builds: - kubecolor replacements: darwin: Darwin linux: Linux windows: Windows amd64: x86_64 format: tar.gz format_overrides: - goos: windows format: zip brews: - name: kubecolor tap: owner: dty1er name: homebrew-tap homepage: "https://github.com/dty1er/kubecolor" description: "Colorize your kubectl output" folder: Formula install: | bin.install "kubecolor" checksum: name_template: 'checksums.txt' changelog: sort: asc filters: exclude: - '^docs:' - '^test:' kubecolor-0.0.20/LICENSE000066400000000000000000000020461404516252000145640ustar00rootroot00000000000000Copyright (c) 2020 Hidetatsu Yaginuma Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. kubecolor-0.0.20/Makefile000066400000000000000000000003741404516252000152210ustar00rootroot00000000000000.PHONY: testshort testshort: go test -timeout 30s -count=1 ./... -test.short .PHONY: coverage coverage: go test -timeout 30s -count=1 ./... -test.short -coverprofile=coverage.txt .PHONY: e2etest e2etest: go test -timeout 30s -count=1 ./e2etest -v kubecolor-0.0.20/README.md000066400000000000000000000157611404516252000150460ustar00rootroot00000000000000# kubecolor ![test](https://github.com/dty1er/kubecolor/workflows/test/badge.svg?branch=main) [![Go Report Card](https://goreportcard.com/badge/github.com/dty1er/kubecolor)](https://goreportcard.com/report/github.com/dty1er/kubecolor) [![codecov](https://codecov.io/gh/dty1er/kubecolor/branch/main/graph/badge.svg?token=k6ysAa5ghD)](https://codecov.io/gh/dty1er/kubecolor/) Colorize your kubectl output * get pods ![image](https://user-images.githubusercontent.com/60682957/95733375-04929680-0cbd-11eb-82f3-adbcfecf4a3e.png) * describe pods ![image](https://user-images.githubusercontent.com/60682957/95733389-08beb400-0cbd-11eb-983b-cf5138277fe3.png) * something wrong ![image](https://user-images.githubusercontent.com/60682957/95733397-0a887780-0cbd-11eb-8875-bb1000e0e597.png) * You can change color theme for light-backgrounded environment ![image](https://user-images.githubusercontent.com/60682957/95733403-0c523b00-0cbd-11eb-9ff9-abc5469e97ca.png) ## What's this? kubecolor colorizes your `kubectl` command output and does nothing else. kubecolor internally calls `kubectl` command and try to colorizes the output so you can use kubecolor as a complete alternative of kubectl. It means you can write this in your .bash_profile: ```sh alias kubectl="kubecolor" ``` If you use your .bash_profile on more than one computer (e.g. synced via git) that might not all have `kubecolor` installed, you can avoid breaking `kubectl` like so: ```sh command -v kubecolor >/dev/null 2>&1 && alias kubectl="kubecolor" ``` kubecolor is developed to colorize the output of only READ commands (get, describe...). So if the given subcommand was for WRITE operations (apply, edit...), it doesn't give great decorations on it. For now, not all subcommands are supported and will be done in the future. What is supported can be found below. Even if what you want to do is not supported by kubecolor now, kubecolor still can just show `kubectl` output without any decorations, so you don't need to switch kubecolor and kubectl but you always can use kubecolor. Additionally, if `kubectl` resulted an error, kubecolor just shows the error message in red or yellow. ## Installation ### Download binary via GitHub release Go to [Release page](https://github.com/dty1er/kubecolor/releases) then download the binary which fits your environment. ### Mac and Linux users via Homebrew ```sh brew install dty1er/tap/kubecolor ``` ### Manually via go command *Note: if you install kubecolor via go command, --kubecolor-version command might not work* ```sh go install github.com/dty1er/kubecolor/cmd/kubecolor@latest ``` If you are not using module mode (or if just above doesn't work), try below: ```sh go get -u github.com/dty1er/kubecolor/cmd/kubecolor ``` ## Usage kubecolor understands every subcommands and options which are available for `kubectl`. What you have to do is just using `kubecolor` instead of `kubectl` like: ```sh kubecolor --context=your_context get pods -o json ``` If you want to make the colorized kubectl default on your shell, just add this line into your shell configuration file: ```sh alias kubectl="kubecolor" ``` ### Flags * `--kubecolor-version` Prints the version of kubecolor (not kubectl one). * `--plain` When you don't want to colorize output, you can specify `--plain`. Kubecolor understands this option and outputs the result without colorizing. Of course, given `--plain` will never be passed to `kubectl`. This option will help you when you want to save the output onto a file and edit them by editors. * `--light-background` When your terminal's background color is something light (e.g white), default color preset might look too bright and not readable. If so, specify `--light-background` as a command line argument. kubecolor will use a color preset for light-backgrounded environment. * `--force-colors` By default, kubecolor never output the result in colors when the tty is not a terminal. If you want to force kubecolor to show the result in colors for non-terminal tty, you can specify this flag. For example, when you want to pass kubecolor result to grep (`kubecolor get pods | grep pod_name`), this option is useful. ### Autocompletion kubectl provides [autocompletion feature](https://kubernetes.io/docs/tasks/tools/install-kubectl/#enable-kubectl-autocompletion). If you are already using it, you might have to configure it for kubecolor. Please also refer to [kubectl official doc for kubectl autorcomplete](https://kubernetes.io/docs/reference/kubectl/cheatsheet/#kubectl-autocomplete). #### Bash For Bash, configuring autocompletion requires adding following line in your shell config file. ```shell # autocomplete for kubecolor complete -o default -F __start_kubectl kubecolor ``` If you are using an alias like `k="kubecolor"`, then just change above like: ```shell complete -o default -F __start_kubectl k ``` #### Zsh For zsh make sure these lines are present in your zsh config file: ```shell # get zsh complete kubectl source <(kubectl completion zsh) alias kubectl=kubecolor # make completion work with kubecolor compdef kubecolor=kubectl ``` #### fish Fish completion is officially unsupported by `kubectl`, so it is unsupported by `kubecolor` as well. However, there are 2 ways we can make them work. Please keep in mind these are a kind of "hack" and not officially supported. 1. Use [evanlucas/fish-kubectl-completions](https://github.com/evanlucas/fish-kubectl-completions) with `kubecolor`: * Install `kubectl` completions (https://github.com/evanlucas/fish-kubectl-completions): ``` fisher install evanlucas/fish-kubectl-completions ``` * Add the following function to your `config.fish` file: ``` function kubectl kubecolor $argv end ``` 2. Use [awinecki/fish-kubecolor-completions](https://github.com/awinecki/fish-kubecolor-completions) The first way will override `kubectl` command. If you wish to preserve both `kubectl` and `kubecolor` with completions, you need to copy [evanlucas/fish-kubectl-completions](https://github.com/evanlucas/fish-kubectl-completions) for the `kubecolor` command. For this purpose, you can use [awinecki/fish-kubecolor-completions](https://github.com/awinecki/fish-kubecolor-completions). ### Specify what command to execute as kubectl Sometimes, you may want to specify which command to use as `kubectl` internally in kubecolor. For example, when you want to use a versioned-kubectl `kubectl.1.19`, you can do that by an environment variable: ```shell KUBECTL_COMMAND="kubectl.1.19" kubecolor get po ``` When you don't set `KUBECTL_COMMAND`, then `kubectl` is used by default. ## Supported kubectl version Because kubecolor internally calls `kubectl` command, if you are using unsupported kubectl version, it's also not supported by kubecolor. Kubernetes version support policy can be found in [official doc](https://kubernetes.io/docs/setup/release/version-skew-policy/). ## Krew [Krew](https://krew.sigs.k8s.io/) is unsupported for now. ## Contributions Always welcome. Just opening an issue should be also greatful. ## LICENSE MIT kubecolor-0.0.20/cmd/000077500000000000000000000000001404516252000143205ustar00rootroot00000000000000kubecolor-0.0.20/cmd/kubecolor/000077500000000000000000000000001404516252000163055ustar00rootroot00000000000000kubecolor-0.0.20/cmd/kubecolor/main.go000066400000000000000000000005231404516252000175600ustar00rootroot00000000000000package main import ( "errors" "os" "github.com/dty1er/kubecolor/command" ) // this is overridden on build time by GoReleaser var Version string = "unset" func main() { err := command.Run(os.Args[1:], Version) if err != nil { var ke *command.KubectlError if errors.As(err, &ke) { os.Exit(ke.ExitCode) } os.Exit(1) } } kubecolor-0.0.20/color/000077500000000000000000000000001404516252000146735ustar00rootroot00000000000000kubecolor-0.0.20/color/color.go000066400000000000000000000005021404516252000163350ustar00rootroot00000000000000package color import ( "fmt" ) type Color int const escape = "\x1b" const ( Black Color = iota + 30 Red Green Yellow Blue Magenta Cyan White ) func (c Color) sequence() int { return int(c) } func Apply(val string, c Color) string { return fmt.Sprintf("%s[%dm%s%s[0m", escape, c.sequence(), val, escape) } kubecolor-0.0.20/color/color_test.go000066400000000000000000000003241404516252000173760ustar00rootroot00000000000000package color import ( "testing" ) func TestApply(t *testing.T) { val := "test" expected := "\x1b[31mtest\x1b[0m" if applied := Apply(val, Red); applied != expected { t.Fatalf("failed: %v", applied) } } kubecolor-0.0.20/command/000077500000000000000000000000001404516252000151735ustar00rootroot00000000000000kubecolor-0.0.20/command/config.go000066400000000000000000000022711404516252000167710ustar00rootroot00000000000000package command import "os" type KubecolorConfig struct { Plain bool DarkBackground bool ForceColor bool ShowKubecolorVersion bool KubectlCmd string } func ResolveConfig(args []string) ([]string, *KubecolorConfig) { args, plainFlagFound := findAndRemoveBoolFlagIfExists(args, "--plain") args, lightBackgroundFlagFound := findAndRemoveBoolFlagIfExists(args, "--light-background") args, forceColorFlagFound := findAndRemoveBoolFlagIfExists(args, "--force-colors") args, kubecolorVersionFlagFound := findAndRemoveBoolFlagIfExists(args, "--kubecolor-version") darkBackground := !lightBackgroundFlagFound kubectlCmd := "kubectl" if kc := os.Getenv("KUBECTL_COMMAND"); kc != "" { kubectlCmd = kc } return args, &KubecolorConfig{ Plain: plainFlagFound, DarkBackground: darkBackground, ForceColor: forceColorFlagFound, ShowKubecolorVersion: kubecolorVersionFlagFound, KubectlCmd: kubectlCmd, } } func findAndRemoveBoolFlagIfExists(args []string, key string) ([]string, bool) { for i, arg := range args { if arg == key { return append(args[:i], args[i+1:]...), true } } return args, false } kubecolor-0.0.20/command/config_test.go000066400000000000000000000030741404516252000200320ustar00rootroot00000000000000package command import ( "os" "testing" "github.com/dty1er/kubecolor/testutil" ) func Test_ResolveConfig(t *testing.T) { tests := []struct { name string args []string kubectlCommand string expectedArgs []string expectedConf *KubecolorConfig }{ { name: "no config", args: []string{"get", "pods"}, expectedArgs: []string{"get", "pods"}, expectedConf: &KubecolorConfig{ Plain: false, DarkBackground: true, ForceColor: false, KubectlCmd: "kubectl", }, }, { name: "plain, dark, force", args: []string{"get", "pods", "--plain", "--light-background", "--force-colors"}, expectedArgs: []string{"get", "pods"}, expectedConf: &KubecolorConfig{ Plain: true, DarkBackground: false, ForceColor: true, KubectlCmd: "kubectl", }, }, { name: "KUBECTL_COMMAND exists", args: []string{"get", "pods", "--plain"}, kubectlCommand: "kubectl.1.19", expectedArgs: []string{"get", "pods"}, expectedConf: &KubecolorConfig{ Plain: true, DarkBackground: true, ForceColor: false, KubectlCmd: "kubectl.1.19", }, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { if tt.kubectlCommand != "" { os.Setenv("KUBECTL_COMMAND", tt.kubectlCommand) defer os.Unsetenv("KUBECTL_COMMAND") } args, conf := ResolveConfig(tt.args) testutil.MustEqual(t, tt.expectedArgs, args) testutil.MustEqual(t, tt.expectedConf, conf) }) } } kubecolor-0.0.20/command/errors.go000066400000000000000000000002541404516252000170370ustar00rootroot00000000000000package command import "fmt" type KubectlError struct { ExitCode int } func (ke *KubectlError) Error() string { return fmt.Sprintf("kubectl error: %d", ke.ExitCode) } kubecolor-0.0.20/command/errors_test.go000066400000000000000000000002521404516252000200740ustar00rootroot00000000000000package command import "testing" func TestKubectlError(t *testing.T) { e := &KubectlError{1} if e.Error() != "kubectl error: 1" { t.Errorf("unexpected error") } } kubecolor-0.0.20/command/runner.go000066400000000000000000000053521404516252000170400ustar00rootroot00000000000000package command import ( "bytes" "fmt" "io" "os" "os/exec" "strings" "sync" "github.com/dty1er/kubecolor/color" "github.com/dty1er/kubecolor/kubectl" "github.com/dty1er/kubecolor/printer" "github.com/mattn/go-colorable" ) var ( Stdout = colorable.NewColorableStdout() Stderr = colorable.NewColorableStderr() ) type Printers struct { FullColoredPrinter printer.Printer ErrorPrinter printer.Printer } // This is defined here to be replaced in test var getPrinters = func(subcommandInfo *kubectl.SubcommandInfo, darkBackground bool) *Printers { return &Printers{ FullColoredPrinter: &printer.KubectlOutputColoredPrinter{ SubcommandInfo: subcommandInfo, DarkBackground: darkBackground, Recursive: subcommandInfo.Recursive, }, ErrorPrinter: &printer.WithFuncPrinter{ Fn: func(line string) color.Color { if strings.HasPrefix(strings.ToLower(line), "error") { return color.Red } return color.Yellow }, }, } } func Run(args []string, version string) error { args, config := ResolveConfig(args) shouldColorize, subcommandInfo := ResolveSubcommand(args, config) if config.ShowKubecolorVersion { fmt.Fprintf(os.Stdout, "%s\n", version) return nil } cmd := exec.Command(config.KubectlCmd, args...) cmd.Stdin = os.Stdin // when should not colorize, just run command and return // TODO: right now, krew is unsupported by kubecolor but it should be. if !shouldColorize { cmd.Stdout = Stdout cmd.Stderr = Stderr if err := cmd.Start(); err != nil { return err } // inherit the kubectl exit code if err := cmd.Wait(); err != nil { return fmt.Errorf("%w", &KubectlError{ExitCode: cmd.ProcessState.ExitCode()}) } return nil } // when colorize, capture stdout and err then colorize it cmdOut, err := cmd.StdoutPipe() if err != nil { return err } cmdErr, err := cmd.StderrPipe() if err != nil { return err } // make buffer to be used in defer recover() buff := new(bytes.Buffer) outReader := io.TeeReader(cmdOut, buff) errReader := io.TeeReader(cmdErr, buff) if err := cmd.Start(); err != nil { return err } printers := getPrinters(subcommandInfo, config.DarkBackground) wg := &sync.WaitGroup{} wg.Add(1) go func() { defer wg.Done() defer func() { if r := recover(); r != nil { fmt.Fprintf(os.Stdout, buff.String()) } }() // This can panic when kubecolor has bug, so recover in defer printers.FullColoredPrinter.Print(outReader, Stdout) }() wg.Add(1) go func() { defer wg.Done() // This will unlikely panic printers.ErrorPrinter.Print(errReader, Stderr) }() wg.Wait() // inherit the kubectl exit code if err := cmd.Wait(); err != nil { return fmt.Errorf("%w", &KubectlError{ExitCode: cmd.ProcessState.ExitCode()}) } return nil } kubecolor-0.0.20/command/subcommand.go000066400000000000000000000035131404516252000176540ustar00rootroot00000000000000package command import ( "os" "github.com/dty1er/kubecolor/kubectl" "github.com/mattn/go-isatty" ) // mocked in unit tests var isOutputTerminal = func() bool { return isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) } func ResolveSubcommand(args []string, config *KubecolorConfig) (bool, *kubectl.SubcommandInfo) { // subcommandFound becomes false when subcommand is not found; e.g. "kubecolor --help" subcommandInfo, subcommandFound := kubectl.InspectSubcommandInfo(args) // if --plain found, it does not colorize if config.Plain { return false, subcommandInfo } // if subcommand is not found (e.g. kubecolor --help or just "kubecolor"), // it is treated as help because kubectl shows help for such input if !subcommandFound { subcommandInfo.Help = true return true, subcommandInfo } // when the command output is not tty, shouldColorize depends on --force-colors flag. // For example, if the command is run in a shellscript, it should not colorize. (e.g. in "kubectl completion bash") // However, if user wants colored output even if the out is not tty (e.g. kubecolor get xx | grep yy) // it colorizes the output based on --force-colors. if !isOutputTerminal() { return config.ForceColor, subcommandInfo } // else, when the given subcommand is supported, then we colorize it return subcommandFound && isColoringSupported(subcommandInfo.Subcommand), subcommandInfo } func isColoringSupported(sc kubectl.Subcommand) bool { // when you add something here, it won't be colorized unsupported := []kubectl.Subcommand{ kubectl.Create, kubectl.Delete, kubectl.Edit, kubectl.Attach, kubectl.Replace, kubectl.Completion, kubectl.Exec, kubectl.Proxy, kubectl.Plugin, kubectl.Wait, kubectl.Run, } for _, u := range unsupported { if sc == u { return false } } return true } kubecolor-0.0.20/command/subcommand_test.go000066400000000000000000000077121404516252000207200ustar00rootroot00000000000000package command import ( "testing" "github.com/dty1er/kubecolor/kubectl" "github.com/dty1er/kubecolor/testutil" ) func Test_ResolveSubcommand(t *testing.T) { tests := []struct { name string args []string conf *KubecolorConfig isOutputTerminal func() bool expectedShouldColorize bool expectedInfo *kubectl.SubcommandInfo }{ { name: "basic case", args: []string{"get", "pods"}, isOutputTerminal: func() bool { return true }, conf: &KubecolorConfig{ Plain: false, DarkBackground: true, ForceColor: false, KubectlCmd: "kubectl", }, expectedShouldColorize: true, expectedInfo: &kubectl.SubcommandInfo{Subcommand: kubectl.Get}, }, { name: "when plain, it won't colorize", args: []string{"get", "pods"}, isOutputTerminal: func() bool { return true }, conf: &KubecolorConfig{ Plain: true, DarkBackground: true, ForceColor: false, KubectlCmd: "kubectl", }, expectedShouldColorize: false, expectedInfo: &kubectl.SubcommandInfo{Subcommand: kubectl.Get}, }, { name: "when help, it will colorize", args: []string{"get", "pods", "-h"}, isOutputTerminal: func() bool { return true }, conf: &KubecolorConfig{ Plain: false, DarkBackground: true, ForceColor: false, KubectlCmd: "kubectl", }, expectedShouldColorize: true, expectedInfo: &kubectl.SubcommandInfo{Subcommand: kubectl.Get, Help: true}, }, { name: "when both plain and force, plain is chosen", args: []string{"get", "pods"}, isOutputTerminal: func() bool { return true }, conf: &KubecolorConfig{ Plain: true, DarkBackground: true, ForceColor: true, KubectlCmd: "kubectl", }, expectedShouldColorize: false, expectedInfo: &kubectl.SubcommandInfo{Subcommand: kubectl.Get}, }, { name: "when no subcommand is found, it becomes help", args: []string{}, isOutputTerminal: func() bool { return true }, conf: &KubecolorConfig{ Plain: false, DarkBackground: true, ForceColor: false, KubectlCmd: "kubectl", }, expectedShouldColorize: true, expectedInfo: &kubectl.SubcommandInfo{Help: true}, }, { name: "when not tty, it won't colorize", args: []string{"get", "pods"}, isOutputTerminal: func() bool { return false }, conf: &KubecolorConfig{ Plain: false, DarkBackground: true, ForceColor: false, KubectlCmd: "kubectl", }, expectedShouldColorize: false, expectedInfo: &kubectl.SubcommandInfo{Subcommand: kubectl.Get}, }, { name: "even if not tty, if force, it colorizes", args: []string{"get", "pods"}, isOutputTerminal: func() bool { return false }, conf: &KubecolorConfig{ Plain: false, DarkBackground: true, ForceColor: true, KubectlCmd: "kubectl", }, expectedShouldColorize: true, expectedInfo: &kubectl.SubcommandInfo{Subcommand: kubectl.Get}, }, { name: "when the subcommand is unsupported, it won't colorize", args: []string{"-h"}, isOutputTerminal: func() bool { return true }, conf: &KubecolorConfig{ Plain: false, DarkBackground: true, ForceColor: false, KubectlCmd: "kubectl", }, expectedShouldColorize: true, expectedInfo: &kubectl.SubcommandInfo{Help: true}, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { isOutputTerminal = tt.isOutputTerminal shouldColorize, info := ResolveSubcommand(tt.args, tt.conf) testutil.MustEqual(t, tt.expectedShouldColorize, shouldColorize) testutil.MustEqual(t, tt.expectedInfo, info) }) } } kubecolor-0.0.20/go.mod000066400000000000000000000004141404516252000146620ustar00rootroot00000000000000module github.com/dty1er/kubecolor go 1.14 require ( github.com/MakeNowJust/heredoc v1.0.0 github.com/google/go-cmp v0.5.2 github.com/mattn/go-colorable v0.1.8 github.com/mattn/go-isatty v0.0.12 golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061 // indirect ) kubecolor-0.0.20/go.sum000066400000000000000000000024641404516252000147160ustar00rootroot00000000000000github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061 h1:DQmQoKxQWtyybCtX/3dIuDBcAhFszqq8YiNeS6sNu1c= golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= kubecolor-0.0.20/kubectl/000077500000000000000000000000001404516252000152065ustar00rootroot00000000000000kubecolor-0.0.20/kubectl/subcommand.go000066400000000000000000000101471404516252000176700ustar00rootroot00000000000000package kubectl import ( "strings" ) type SubcommandInfo struct { Subcommand Subcommand FormatOption FormatOption NoHeader bool Watch bool Help bool Recursive bool Short bool IsKrew bool } type FormatOption int const ( None FormatOption = iota Wide Json Yaml ) type Subcommand int const ( Create Subcommand = iota + 1 Expose Run Set Explain Get Edit Delete Rollout Scale Autoscale Certificate ClusterInfo Top Cordon Uncordon Drain Taint Describe Logs Attach Exec PortForward Proxy Cp Auth Diff Apply Patch Replace Wait Convert Kustomize Label Annotate Completion APIResources APIVersions Config Plugin Version Options ) var strToSubcommand = map[string]Subcommand{ "create": Create, "expose": Expose, "run": Run, "set": Set, "explain": Explain, "get": Get, "edit": Edit, "delete": Delete, "rollout": Rollout, "scale": Scale, "autoscale": Autoscale, "certificate": Certificate, "cluster-info": ClusterInfo, "top": Top, "cordon": Cordon, "uncordon": Uncordon, "drain": Drain, "taint": Taint, "describe": Describe, "logs": Logs, "attach": Attach, "exec": Exec, "port-forward": PortForward, "proxy": Proxy, "cp": Cp, "auth": Auth, "diff": Diff, "apply": Apply, "patch": Patch, "replace": Replace, "wait": Wait, "convert": Convert, "kustomize": Kustomize, "label": Label, "annotate": Annotate, "completion": Completion, "api-resources": APIResources, "api-versions": APIVersions, "config": Config, "plugin": Plugin, "version": Version, "options": Options, } func InspectSubcommand(command string) (Subcommand, bool) { sc, ok := strToSubcommand[command] return sc, ok } func CollectCommandlineOptions(args []string, info *SubcommandInfo) { for i := range args { if strings.HasPrefix(args[i], "--output") { switch args[i] { case "--output=json": info.FormatOption = Json case "--output=yaml": info.FormatOption = Yaml case "--output=wide": info.FormatOption = Wide default: if len(args)-1 > i { formatOption := args[i+1] switch formatOption { case "json": info.FormatOption = Json case "yaml": info.FormatOption = Yaml case "wide": info.FormatOption = Wide default: // custom-columns, go-template, etc are currently not supported } } } } else if strings.HasPrefix(args[i], "-o") { switch args[i] { // both '-ojson' and '-o=json' works case "-ojson", "-o=json": info.FormatOption = Json case "-oyaml", "-o=yaml": info.FormatOption = Yaml case "-owide", "-o=wide": info.FormatOption = Wide default: // otherwise, look for next arg because '-o json' also works if len(args)-1 > i { formatOption := args[i+1] switch formatOption { case "json": info.FormatOption = Json case "yaml": info.FormatOption = Yaml case "wide": info.FormatOption = Wide default: // custom-columns, go-template, etc are currently not supported } } } } else if strings.HasPrefix(args[i], "--short") { switch args[i] { case "--short=true": info.Short = true case "--short=false": info.Short = false default: info.Short = true } } else if args[i] == "--no-headers" { info.NoHeader = true } else if args[i] == "-w" || args[i] == "--watch" { info.Watch = true } else if args[i] == "--recursive=true" { info.Recursive = true } else if args[i] == "-h" || args[i] == "--help" { info.Help = true } } } // TODO: return shouldColorize = false when the given args is for plugin func InspectSubcommandInfo(args []string) (*SubcommandInfo, bool) { ret := &SubcommandInfo{} CollectCommandlineOptions(args, ret) for i := range args { cmd, ok := InspectSubcommand(args[i]) if !ok { continue } ret.Subcommand = cmd return ret, true } return ret, false } kubecolor-0.0.20/kubectl/subcommand_test.go000066400000000000000000000065741404516252000207400ustar00rootroot00000000000000package kubectl import ( "strings" "testing" "github.com/google/go-cmp/cmp" ) // func TestInspectSubcommandInfo(args []string) (*SubcommandInfo, bool) { func TestInspectSubcommandInfo(t *testing.T) { tests := []struct { args string expected *SubcommandInfo expectedOK bool }{ {"get pods", &SubcommandInfo{Subcommand: Get}, true}, {"get pod", &SubcommandInfo{Subcommand: Get}, true}, {"get po", &SubcommandInfo{Subcommand: Get}, true}, {"get pod -o wide", &SubcommandInfo{Subcommand: Get, FormatOption: Wide}, true}, {"get pod -o=wide", &SubcommandInfo{Subcommand: Get, FormatOption: Wide}, true}, {"get pod -owide", &SubcommandInfo{Subcommand: Get, FormatOption: Wide}, true}, {"get pod -o json", &SubcommandInfo{Subcommand: Get, FormatOption: Json}, true}, {"get pod -o=json", &SubcommandInfo{Subcommand: Get, FormatOption: Json}, true}, {"get pod -ojson", &SubcommandInfo{Subcommand: Get, FormatOption: Json}, true}, {"get pod -o yaml", &SubcommandInfo{Subcommand: Get, FormatOption: Yaml}, true}, {"get pod -o=yaml", &SubcommandInfo{Subcommand: Get, FormatOption: Yaml}, true}, {"get pod -oyaml", &SubcommandInfo{Subcommand: Get, FormatOption: Yaml}, true}, {"get pod --output json", &SubcommandInfo{Subcommand: Get, FormatOption: Json}, true}, {"get pod --output=json", &SubcommandInfo{Subcommand: Get, FormatOption: Json}, true}, {"get pod --output yaml", &SubcommandInfo{Subcommand: Get, FormatOption: Yaml}, true}, {"get pod --output=yaml", &SubcommandInfo{Subcommand: Get, FormatOption: Yaml}, true}, {"get pod --output wide", &SubcommandInfo{Subcommand: Get, FormatOption: Wide}, true}, {"get pod --output=wide", &SubcommandInfo{Subcommand: Get, FormatOption: Wide}, true}, {"get pod --no-headers", &SubcommandInfo{Subcommand: Get, NoHeader: true}, true}, {"get pod -w", &SubcommandInfo{Subcommand: Get, Watch: true}, true}, {"get pod --watch", &SubcommandInfo{Subcommand: Get, Watch: true}, true}, {"get pod -h", &SubcommandInfo{Subcommand: Get, Help: true}, true}, {"get pod --help", &SubcommandInfo{Subcommand: Get, Help: true}, true}, {"describe pod pod-aaa", &SubcommandInfo{Subcommand: Describe}, true}, {"top pod", &SubcommandInfo{Subcommand: Top}, true}, {"top pods", &SubcommandInfo{Subcommand: Top}, true}, {"api-versions", &SubcommandInfo{Subcommand: APIVersions}, true}, {"explain pod", &SubcommandInfo{Subcommand: Explain}, true}, {"explain pod --recursive=true", &SubcommandInfo{Subcommand: Explain, Recursive: true}, true}, // "--recursive true" is not acceptable by kubectl explain {"explain pod --recursive true", &SubcommandInfo{Subcommand: Explain}, true}, {"version", &SubcommandInfo{Subcommand: Version}, true}, {"version --client", &SubcommandInfo{Subcommand: Version}, true}, {"version --short", &SubcommandInfo{Subcommand: Version, Short: true}, true}, {"version -o json", &SubcommandInfo{Subcommand: Version, FormatOption: Json}, true}, {"version -o yaml", &SubcommandInfo{Subcommand: Version, FormatOption: Yaml}, true}, {"apply", &SubcommandInfo{Subcommand: Apply}, true}, {"", &SubcommandInfo{}, false}, } for _, tt := range tests { tt := tt t.Run(tt.args, func(t *testing.T) { t.Parallel() s, ok := InspectSubcommandInfo(strings.Split(tt.args, " ")) if tt.expectedOK != ok { t.Error("failed") } if diff := cmp.Diff(s, tt.expected); diff != "" { t.Errorf(diff) } }) } } kubecolor-0.0.20/manifests/000077500000000000000000000000001404516252000155465ustar00rootroot00000000000000kubecolor-0.0.20/manifests/deployment.yaml000066400000000000000000000005421404516252000206130ustar00rootroot00000000000000apiVersion: apps/v1 kind: Deployment metadata: name: nginx namespace: kubecolor spec: replicas: 3 selector: matchLabels: app: nginx strategy: rollingUpdate: maxSurge: 50% maxUnavailable: 0% template: metadata: labels: app: nginx spec: containers: - image: nginx name: nginx kubecolor-0.0.20/manifests/namespace.yaml000066400000000000000000000001311404516252000203610ustar00rootroot00000000000000kind: Namespace apiVersion: v1 metadata: name: kubecolor labels: name: kubecolor kubecolor-0.0.20/manifests/pod.yaml000066400000000000000000000002521404516252000172130ustar00rootroot00000000000000apiVersion: v1 kind: Pod metadata: name: test-pod namespace: kubecolor spec: containers: - name: main image: remrain/tiny-hello args: - -test kubecolor-0.0.20/manifests/replicaset.yaml000066400000000000000000000004271404516252000205700ustar00rootroot00000000000000apiVersion: apps/v1 kind: ReplicaSet metadata: name: nginx namespace: kubecolor spec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - image: nginx name: nginx kubecolor-0.0.20/manifests/resource_quota.yaml000066400000000000000000000003021404516252000214650ustar00rootroot00000000000000apiVersion: v1 kind: ResourceQuota metadata: name: mem-cpu-quota namespace: kubecolor spec: hard: requests.cpu: "1" requests.memory: 1Gi limits.cpu: "2" limits.memory: 2Gi kubecolor-0.0.20/printer/000077500000000000000000000000001404516252000152405ustar00rootroot00000000000000kubecolor-0.0.20/printer/colors_definition.go000066400000000000000000000021211404516252000212740ustar00rootroot00000000000000package printer import "github.com/dty1er/kubecolor/color" var ( // Preset of colors for background // Please use them when you just need random colors colorsForDarkBackground = []color.Color{ color.Cyan, color.Green, color.Magenta, color.White, color.Yellow, } colorsForLightBackground = []color.Color{ color.Cyan, color.Green, color.Magenta, color.Black, color.Yellow, color.Blue, } // colors to be recommended to be used for some context // e.g. Json, Yaml, kubectl-describe format etc. // colors which look good in dark-backgrounded environment KeyColorForDark = color.White StringColorForDark = color.Cyan BoolColorForDark = color.Green NumberColorForDark = color.Magenta NullColorForDark = color.Yellow HeaderColorForDark = color.White // for plain table // colors which look good in light-backgrounded environment KeyColorForLight = color.Black StringColorForLight = color.Blue BoolColorForLight = color.Green NumberColorForLight = color.Magenta NullColorForLight = color.Yellow HeaderColorForLight = color.Black // for plain table ) kubecolor-0.0.20/printer/format.go000066400000000000000000000035111404516252000170570ustar00rootroot00000000000000package printer import ( "strconv" "strings" "github.com/dty1er/kubecolor/color" ) // toSpaces returns repeated spaces whose length is n. func toSpaces(n int) string { return strings.Repeat(" ", n) } // getColorByKeyIndent returns a color based on the given indent. // When you want to change key color based on indent depth (e.g. Json, Yaml), use this function func getColorByKeyIndent(indent int, basicIndentWidth int, dark bool) color.Color { switch indent / basicIndentWidth % 2 { case 1: if dark { return color.White } return color.Black default: return color.Yellow } } // getColorByValueType returns a color by value. // This is intended to be used to colorize any structured data e.g. Json, Yaml. func getColorByValueType(val string, dark bool) color.Color { if val == "null" || val == "" || val == "" { if dark { return NullColorForDark } return NullColorForLight } if val == "true" || val == "false" { if dark { return BoolColorForDark } return BoolColorForLight } if _, err := strconv.Atoi(val); err == nil { if dark { return NumberColorForDark } return NumberColorForLight } if dark { return StringColorForDark } return StringColorForLight } // getColorsByBackground returns a preset of colors depending on given background color func getColorsByBackground(dark bool) []color.Color { if dark { return colorsForDarkBackground } return colorsForLightBackground } // getHeaderColorByBackground returns a defined color for Header (not actual data) by the background color func getHeaderColorByBackground(dark bool) color.Color { if dark { return HeaderColorForDark } return HeaderColorForLight } // findIndent returns a length of indent (spaces at left) in the given line func findIndent(line string) int { return len(line) - len(strings.TrimLeft(line, " ")) } kubecolor-0.0.20/printer/format_test.go000066400000000000000000000056231404516252000201240ustar00rootroot00000000000000package printer import ( "testing" "github.com/dty1er/kubecolor/color" "github.com/google/go-cmp/cmp" ) func Test_toSpaces(t *testing.T) { if toSpaces(3) != " " { t.Fatalf("fail") } } func Test_getColorByKeyIndent(t *testing.T) { tests := []struct { name string dark bool indent int basicIndentWidth int expected color.Color }{ {"dark depth: 1", true, 2, 2, color.White}, {"light depth: 1", false, 2, 2, color.Black}, {"dark depth: 2", true, 4, 2, color.Yellow}, {"light depth: 2", false, 4, 2, color.Yellow}, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() got := getColorByKeyIndent(tt.indent, tt.basicIndentWidth, tt.dark) if got != tt.expected { t.Errorf("fail: got: %v, expected: %v", got, tt.expected) } }) } } func Test_getColorByValueType(t *testing.T) { tests := []struct { name string dark bool val string expected color.Color }{ {"dark null", true, "null", NullColorForDark}, {"light null", false, "", NullColorForLight}, {"dark bool", true, "true", BoolColorForDark}, {"light bool", false, "false", BoolColorForLight}, {"dark number", true, "123", NumberColorForDark}, {"light number", false, "456", NumberColorForLight}, {"dark string", true, "aaa", StringColorForDark}, {"light string", false, "12345a", StringColorForLight}, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() got := getColorByValueType(tt.val, tt.dark) if got != tt.expected { t.Errorf("fail: got: %v, expected: %v", got, tt.expected) } }) } } func Test_getColorsByBackground(t *testing.T) { tests := []struct { name string dark bool expected []color.Color }{ {"dark", true, colorsForDarkBackground}, {"light", false, colorsForLightBackground}, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() got := getColorsByBackground(tt.dark) if diff := cmp.Diff(got, tt.expected); diff != "" { t.Errorf("fail: %v", diff) } }) } } func Test_getHeaderColorByBackground(t *testing.T) { tests := []struct { name string dark bool expected color.Color }{ {"dark", true, HeaderColorForDark}, {"light", false, HeaderColorForLight}, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() got := getHeaderColorByBackground(tt.dark) if got != tt.expected { t.Errorf("fail: got: %v, expected: %v", got, tt.expected) } }) } } func Test_findIndent(t *testing.T) { tests := []struct { line string expected int }{ {"no indent", 0}, {" 2 indent", 2}, } for _, tt := range tests { tt := tt t.Run(tt.line, func(t *testing.T) { t.Parallel() got := findIndent(tt.line) if got != tt.expected { t.Errorf("fail: got: %v, expected: %v", got, tt.expected) } }) } } kubecolor-0.0.20/printer/json.go000066400000000000000000000060211404516252000165370ustar00rootroot00000000000000package printer import ( "bufio" "fmt" "io" "strings" "github.com/dty1er/kubecolor/color" ) type JsonPrinter struct { DarkBackground bool } func (jp *JsonPrinter) Print(r io.Reader, w io.Writer) { scanner := bufio.NewScanner(r) for scanner.Scan() { line := scanner.Text() printLineAsJsonFormat(line, w, jp.DarkBackground) } } func printLineAsJsonFormat(line string, w io.Writer, dark bool) { indentCnt := findIndent(line) indent := toSpaces(indentCnt) trimmedLine := strings.TrimLeft(line, " ") if strings.HasPrefix(trimmedLine, "{") || strings.HasPrefix(trimmedLine, "}") || strings.HasPrefix(trimmedLine, "]") { // when coming here, it must not be starting with key. // that patterns are: // { // } // }, // ] // ], // note: it must not be "[" because it will be always after key // in this case, just write it without color // fmt.Fprintf(w, "%s", toSpaces(indentCnt)) fmt.Fprintf(w, "%s%s", indent, trimmedLine) fmt.Fprintf(w, "\n") return } // when coming here, the line must be one of: // "key": { // "key": [ // "key": value // "key": value, // value, // value splitted := strings.SplitN(trimmedLine, ": ", 2) // if key contains ": " this works in a wrong way but it's unlikely to happen if len(splitted) == 1 { // when coming here, it will be a value in an array fmt.Fprintf(w, "%s%s\n", indent, toColorizedJsonValue(splitted[0], dark)) return } key := splitted[0] val := splitted[1] fmt.Fprintf(w, "%s%s: %s\n", indent, toColorizedJsonKey(key, indentCnt, 4, dark), toColorizedJsonValue(val, dark)) } // toColorizedJsonKey returns colored json key func toColorizedJsonKey(key string, indentCnt, basicWidth int, dark bool) string { hasColon := strings.HasSuffix(key, ":") // remove colon and double quotations although they might not exist actually key = strings.TrimRight(key, ":") doubleQuoteTrimmed := strings.TrimRight(strings.TrimLeft(key, `"`), `"`) format := `"%s"` if hasColon { format += ":" } return fmt.Sprintf(format, color.Apply(doubleQuoteTrimmed, getColorByKeyIndent(indentCnt, basicWidth, dark))) } // toColorizedJsonValue returns colored json value. // This function checks it trailing comma and double quotation exist // then colorize the given value considering them. func toColorizedJsonValue(value string, dark bool) string { if value == "{" { return "{" } if value == "[" { return "[" } if value == "{}," { return "{}," } if value == "{}" { return "{}" } hasComma := strings.HasSuffix(value, ",") // remove comma and double quotations although they might not exist actually value = strings.TrimRight(value, ",") isString := strings.HasPrefix(value, `"`) && strings.HasSuffix(value, `"`) doubleQuoteTrimmedValue := strings.TrimRight(strings.TrimLeft(value, `"`), `"`) var format string switch { case hasComma && isString: format = `"%s",` case hasComma: format = `%s,` case isString: format = `"%s"` default: format = `%s` } return fmt.Sprintf(format, color.Apply(doubleQuoteTrimmedValue, getColorByValueType(value, dark))) } kubecolor-0.0.20/printer/json_test.go000066400000000000000000000046001404516252000175770ustar00rootroot00000000000000package printer import ( "bytes" "strings" "testing" "github.com/dty1er/kubecolor/testutil" ) func Test_JsonPrinter_Print(t *testing.T) { tests := []struct { name string darkBackground bool input string expected string }{ { name: "values can be colored by its type", darkBackground: true, input: testutil.NewHereDoc(` { "apiVersion": "v1", "kind": "Pod", "num": 598, "bool": true, "null": null }`), expected: testutil.NewHereDoc(` { "apiVersion": "v1", "kind": "Pod", "num": 598, "bool": true, "null": null } `), }, { name: "keys can be colored by its indentation level", darkBackground: true, input: testutil.NewHereDoc(` { "k1": "v1", "k2": { "k3": "v3", "k4": { "k5": "v5" }, "k6": "v6" } }`), expected: testutil.NewHereDoc(` { "k1": "v1", "k2": { "k3": "v3", "k4": { "k5": "v5" }, "k6": "v6" } } `), }, { name: "{} and [] are not colorized", darkBackground: true, input: testutil.NewHereDoc(` { "apiVersion": "v1", "kind": { "k2": [ "a", "b", "c" ], "k3": { "k4": "val" }, "k5": {} } }`), expected: testutil.NewHereDoc(` { "apiVersion": "v1", "kind": { "k2": [ "a", "b", "c" ], "k3": { "k4": "val" }, "k5": {} } } `), }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() r := strings.NewReader(tt.input) var w bytes.Buffer printer := JsonPrinter{DarkBackground: tt.darkBackground} printer.Print(r, &w) testutil.MustEqual(t, tt.expected, w.String()) }) } } kubecolor-0.0.20/printer/kubectl_apply.go000066400000000000000000000046761404516252000204420ustar00rootroot00000000000000package printer import ( "bufio" "fmt" "io" "strings" "github.com/dty1er/kubecolor/color" ) type ApplyPrinter struct { DarkBackground bool } // kubectl apply // deployment.apps/foo unchanged // deployment.apps/bar created // deployment.apps/quux configured func (ap *ApplyPrinter) Print(r io.Reader, w io.Writer) { const ( applyActionCreated = "created" applyActionConfigured = "configured" applyActionUnchanged = "unchanged" dryRunStr = "(dry run)" ) darkColors := map[string]color.Color{ applyActionCreated: color.Green, applyActionConfigured: color.Yellow, applyActionUnchanged: color.Magenta, dryRunStr: color.Cyan, } lightColors := map[string]color.Color{ applyActionCreated: color.Green, applyActionConfigured: color.Yellow, applyActionUnchanged: color.Magenta, dryRunStr: color.Blue, } colors := func(action string, dark bool) color.Color { if dark { return darkColors[action] } return lightColors[action] } colorize := func(line, action string, dryRun bool, wr io.Writer) { if dryRun { arg := strings.TrimSuffix(line, fmt.Sprintf(" %s %s", action, dryRunStr)) fmt.Fprintf(w, "%s %s %s\n", arg, color.Apply(action, colors(action, ap.DarkBackground)), color.Apply(dryRunStr, colors(dryRunStr, ap.DarkBackground)), ) return } arg := strings.TrimSuffix(line, " "+action) fmt.Fprintf(w, "%s %s\n", arg, color.Apply(action, colors(action, ap.DarkBackground))) } scanner := bufio.NewScanner(r) for scanner.Scan() { line := scanner.Text() switch { // on dry run cases, it shows "xxx created (dry run)" case strings.HasSuffix(line, fmt.Sprintf(" %s %s", applyActionCreated, dryRunStr)): colorize(line, applyActionCreated, true, w) case strings.HasSuffix(line, fmt.Sprintf(" %s %s", applyActionConfigured, dryRunStr)): colorize(line, applyActionConfigured, true, w) case strings.HasSuffix(line, fmt.Sprintf(" %s %s", applyActionUnchanged, dryRunStr)): colorize(line, applyActionUnchanged, true, w) // not dry run cases, it shows "xxx created" case strings.HasSuffix(line, " "+applyActionCreated): colorize(line, applyActionCreated, false, w) case strings.HasSuffix(line, " "+applyActionConfigured): colorize(line, applyActionConfigured, false, w) case strings.HasSuffix(line, " "+applyActionUnchanged): colorize(line, applyActionUnchanged, false, w) default: fmt.Fprintf(w, "%s\n", color.Apply(line, color.Green)) } } } kubecolor-0.0.20/printer/kubectl_apply_test.go000066400000000000000000000033541404516252000214710ustar00rootroot00000000000000package printer import ( "bytes" "strings" "testing" "github.com/dty1er/kubecolor/testutil" ) func Test_ApplyPrinter_Print(t *testing.T) { tests := []struct { name string darkBackground bool tablePrinter *TablePrinter input string expected string }{ { name: "created", darkBackground: true, input: testutil.NewHereDoc(` deployment.apps/foo created`), expected: testutil.NewHereDoc(` deployment.apps/foo created `), }, { name: "configured", darkBackground: true, input: testutil.NewHereDoc(` deployment.apps/foo configured`), expected: testutil.NewHereDoc(` deployment.apps/foo configured `), }, { name: "unchanged", darkBackground: true, input: testutil.NewHereDoc(` deployment.apps/foo unchanged`), expected: testutil.NewHereDoc(` deployment.apps/foo unchanged `), }, { name: "dry run", darkBackground: true, input: testutil.NewHereDoc(` deployment.apps/foo unchanged (dry run)`), expected: testutil.NewHereDoc(` deployment.apps/foo unchanged (dry run) `), }, { name: "something else. This likely won't happen but fallbacks here just in case.", darkBackground: true, input: testutil.NewHereDoc(` deployment.apps/foo bar`), expected: testutil.NewHereDoc(` deployment.apps/foo bar `), }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() r := strings.NewReader(tt.input) var w bytes.Buffer printer := ApplyPrinter{DarkBackground: tt.darkBackground} printer.Print(r, &w) testutil.MustEqual(t, tt.expected, w.String()) }) } } kubecolor-0.0.20/printer/kubectl_describe.go000066400000000000000000000075631404516252000210730ustar00rootroot00000000000000package printer import ( "bufio" "fmt" "io" "strings" "github.com/dty1er/kubecolor/color" ) // DescribePrinter is a specific printer to print kubectl describe format. type DescribePrinter struct { DarkBackground bool TablePrinter *TablePrinter } func (dp *DescribePrinter) Print(r io.Reader, w io.Writer) { basicIndentWidth := 2 // according to kubectl describe format scanner := bufio.NewScanner(r) for scanner.Scan() { line := scanner.Text() if line == "" { fmt.Fprintln(w) continue } // Split a line by spaces to colorize and render them // For example: // e.g. 1----------------- // Status: Running // ----------------------- // spacesIndices: [[7, 15]] // <- where spaces locate // columns: ["Status:", "Running"] // // e.g. 2-------------------------------------------- // Ports: 10001/TCP, 5000/TCP, 18000/TCP // -------------------------------------------------- // spacesIndices: [[0, 3], [10, 19]] // <- where spaces locate // columns: ["Ports:", "10001/TCP, 5000/TCP, 18000/TCP"] // // So now, we know where to render which column. spacesIndices := spaces.FindAllStringIndex(line, -1) columns := spaces.Split(line, -1) // when the line has indent (spaces on left), the first item will be // just a "" and we don't need it so remove if len(columns) > 0 { if columns[0] == "" { columns = columns[1:] } } // First, identify if there is an indent indentCnt := findIndent(line) indent := toSpaces(indentCnt) if indentCnt > 0 { // TODO: Remove this condition for workaround // Basically, kubectl describe output has its indentation level // with **2** spaces, but "Resource Quota" section in // `kubectl describe ns` output has only 1 space at the head. // Because of it, indentCnt is still 1, but the indent space is not in `spacesIndices` (see regex definition of `spaces`) // So it must be checked here // https://github.com/dty1er/kubecolor/issues/36 // When https://github.com/kubernetes/kubectl/issues/1005#issuecomment-758385759 is fixed // this is not needed anymore. if indentCnt > 1 { // when an indent exists, removes it because it's already captured by "indent" var spacesIndices = spacesIndices[1:] } } // when there are multiple columns, treat is as table format if len(columns) > 2 { dp.TablePrinter.printLineAsTableFormat(w, line, getColorsByBackground(dp.DarkBackground)) continue } // First, write the first value assuming it's a key keyColor := getColorByKeyIndent(indentCnt, basicIndentWidth, dp.DarkBackground) valColor := getColorByValueType(columns[0], dp.DarkBackground) // TODO: Remove this if statement for workaround // Basically, kubectl describe output has its indentation level // with **2** spaces, but "Resource Quota" section in // `kubectl describe ns` output has only 1 space at the head. // Because of it, `spaces.Split` doesn't trim the head space (see the regex definition of `spaces`) // So it must be trimmed here // https://github.com/dty1er/kubecolor/issues/36 // When https://github.com/kubernetes/kubectl/issues/1005#issuecomment-758385759 is fixed // this is not needed anymore. if strings.HasPrefix(columns[0], " ") { columns[0] = strings.TrimLeft(columns[0], " ") } if strings.HasSuffix(columns[0], ":") { // trailing ":" should not be colorized fmt.Fprintf(w, "%s%s:", indent, color.Apply(strings.TrimRight(columns[0], ":"), keyColor)) } else if len(columns) == 1 { fmt.Fprintf(w, "%s%s", indent, color.Apply(columns[0], valColor)) } else { fmt.Fprintf(w, "%s%s", indent, color.Apply(columns[0], keyColor)) } if len(columns) == 1 { fmt.Fprint(w, "\n") continue } spacesPos := spacesIndices[0] spacesCnt := spacesPos[1] - spacesPos[0] fmt.Fprintf(w, "%s%s\n", toSpaces(spacesCnt), color.Apply(columns[1], getColorByValueType(columns[1], dp.DarkBackground))) } } kubecolor-0.0.20/printer/kubectl_describe_test.go000066400000000000000000000224301404516252000221200ustar00rootroot00000000000000package printer import ( "bytes" "strings" "testing" "github.com/dty1er/kubecolor/testutil" ) func Test_DescribePrinter_Print(t *testing.T) { tests := []struct { name string darkBackground bool tablePrinter *TablePrinter input string expected string }{ { name: "values can be colored by its type", darkBackground: true, tablePrinter: nil, input: testutil.NewHereDoc(` Name: nginx-lpv5x Namespace: default Priority: 0 Node: minikube/172.17.0.3 Ready: true Start Time: Sat, 10 Oct 2020 14:07:17 +0900 Labels: app=nginx Annotations: `), expected: testutil.NewHereDoc(` Name: nginx-lpv5x Namespace: default Priority: 0 Node: minikube/172.17.0.3 Ready: true Start Time: Sat, 10 Oct 2020 14:07:17 +0900 Labels: app=nginx Annotations:  `), }, { name: "key color changes based on its indentation", darkBackground: true, tablePrinter: nil, input: testutil.NewHereDoc(` IP: 172.18.0.7 IPs: IP: 172.18.0.7 Controlled By: ReplicaSet/nginx Containers: nginx: Container ID: docker://2885230a30908c8a6bda5a5366619c730b25b994eea61c931bba08ef4a8c8593 Started: Sat, 10 Oct 2020 14:07:44 +0900`), expected: testutil.NewHereDoc(` IP: 172.18.0.7 IPs: IP: 172.18.0.7 Controlled By: ReplicaSet/nginx Containers: nginx: Container ID: docker://2885230a30908c8a6bda5a5366619c730b25b994eea61c931bba08ef4a8c8593 Started: Sat, 10 Oct 2020 14:07:44 +0900 `), }, { name: "table format in kubectl describe can be colored by describe", darkBackground: true, tablePrinter: NewTablePrinter(false, true, nil), input: testutil.NewHereDoc(` Conditions: Type Status LastHeartbeatTime LastTransitionTime Reason Message ---- ------ ----------------- ------------------ ------ ------- MemoryPressure False Sun, 18 Oct 2020 12:00:54 +0900 Wed, 14 Oct 2020 09:28:18 +0900 KubeletHasSufficientMemory kubelet has sufficient memory available DiskPressure False Sun, 18 Oct 2020 12:00:54 +0900 Wed, 14 Oct 2020 09:28:18 +0900 KubeletHasNoDiskPressure kubelet has no disk pressure Addresses: InternalIP: 172.17.0.3 Hostname: minikube Capacity: cpu: 6 memory: 2036900Ki pods: 110 Allocatable: cpu: 6 memory: 2036900Ki pods: 110 System Info: Machine ID: 55d2ccaefc9847c9a69356e7f3bd23f4 System UUID: fe312784-2364-4bba-a55e-f56051539c21 Non-terminated Pods: (14 in total) Namespace Name CPU Requests CPU Limits Memory Requests Memory Limits AGE --------- ---- ------------ ---------- --------------- ------------- --- default nginx-6799fc88d8-dnmv5 0 (0%) 0 (0%) 0 (0%) 0 (0%) 7d21h default nginx-6799fc88d8-m8pbc 0 (0%) 0 (0%) 0 (0%) 0 (0%) 7d21h default nginx-6799fc88d8-qdf9b 0 (0%) 0 (0%) 0 (0%) 0 (0%) 7d21h Allocated resources: (Total limits may be over 100 percent, i.e., overcommitted.) Resource Requests Limits -------- -------- ------ cpu 650m (10%) 0 (0%) memory 70Mi (3%) 170Mi (8%) Events: `), expected: testutil.NewHereDoc(` Conditions:  Type Status LastHeartbeatTime LastTransitionTime Reason Message  ---- ------ ----------------- ------------------ ------ -------  MemoryPressure False Sun, 18 Oct 2020 12:00:54 +0900 Wed, 14 Oct 2020 09:28:18 +0900 KubeletHasSufficientMemory kubelet has sufficient memory available  DiskPressure False Sun, 18 Oct 2020 12:00:54 +0900 Wed, 14 Oct 2020 09:28:18 +0900 KubeletHasNoDiskPressure kubelet has no disk pressure Addresses: InternalIP: 172.17.0.3 Hostname: minikube Capacity: cpu: 6 memory: 2036900Ki pods: 110 Allocatable: cpu: 6 memory: 2036900Ki pods: 110 System Info: Machine ID: 55d2ccaefc9847c9a69356e7f3bd23f4 System UUID: fe312784-2364-4bba-a55e-f56051539c21 Non-terminated Pods: (14 in total)  Namespace Name CPU Requests CPU Limits Memory Requests Memory Limits AGE  --------- ---- ------------ ---------- --------------- ------------- ---  default nginx-6799fc88d8-dnmv5 0 (0%) 0 (0%) 0 (0%) 0 (0%) 7d21h  default nginx-6799fc88d8-m8pbc 0 (0%) 0 (0%) 0 (0%) 0 (0%) 7d21h  default nginx-6799fc88d8-qdf9b 0 (0%) 0 (0%) 0 (0%) 0 (0%) 7d21h Allocated resources: (Total limits may be over 100 percent, i.e., overcommitted.)  Resource Requests Limits  -------- -------- ------  cpu 650m (10%) 0 (0%)  memory 70Mi (3%) 170Mi (8%) Events:  `), }, { // This test input is invalid because contents in `Resource Quotas` have only 1 space as its indentation. // This is the bug of kubectl 1.19.3, and because of this // kubecolor was crashing. // A workaround PR is created for this and this test is making sure if it works. // So, this test should be removed after the kubectl fix // https://github.com/kubernetes/kubectl/issues/1005 // For more details, see the PR description on GitHub. name: "invalid test for the workaround", darkBackground: true, tablePrinter: NewTablePrinter(false, true, nil), input: testutil.NewHereDoc(` Name: default Labels: Annotations: Status: Active Resource Quotas Name: mem-cpu-quota Resource Used Hard -------- --- --- limits.cpu 0 2 limits.memory 0 2Gi requests.cpu 0 1 requests.memory 0 1Gi No LimitRange resource.`), expected: testutil.NewHereDoc(` Name: default Labels:  Annotations:  Status: Active Resource Quotas Name: mem-cpu-quota  Resource Used Hard  -------- --- ---  limits.cpu 0 2  limits.memory 0 2Gi  requests.cpu 0 1  requests.memory 0 1Gi No LimitRange resource. `), }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() r := strings.NewReader(tt.input) var w bytes.Buffer printer := DescribePrinter{DarkBackground: tt.darkBackground, TablePrinter: tt.tablePrinter} printer.Print(r, &w) testutil.MustEqual(t, tt.expected, w.String()) }) } } kubecolor-0.0.20/printer/kubectl_explain.go000066400000000000000000000051351404516252000207440ustar00rootroot00000000000000package printer import ( "bufio" "fmt" "io" "strings" "github.com/dty1er/kubecolor/color" ) // ExplainPrinter is a specific printer to print kubectl explain format. type ExplainPrinter struct { DarkBackground bool Recursive bool renderingFields bool } func (ep *ExplainPrinter) Print(r io.Reader, w io.Writer) { // https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/kubectl/pkg/explain/model_printer.go#L24-L30 descriptionIndentLevel := 5 scanner := bufio.NewScanner(r) for scanner.Scan() { line := scanner.Text() if line == "" { fmt.Fprintln(w) continue } if ep.renderingFields { ep.printField(line, w) continue } indentCnt := findIndent(line) switch indentCnt { case 0: ep.printKeyVal(line, w) case descriptionIndentLevel: ep.printDescription(line, w) } if line == "FIELDS:" { ep.renderingFields = true } } } func (ep *ExplainPrinter) printKeyVal(line string, w io.Writer) { var key, val string keyAndVal := spaces.Split(line, 2) if len(keyAndVal) == 1 { key = line } else { key, val = keyAndVal[0], keyAndVal[1] } key = strings.TrimRight(key, ":") key = color.Apply(key, getColorByKeyIndent(0, 2, ep.DarkBackground)) if val != "" { val = color.Apply(val, getColorByValueType(val, ep.DarkBackground)) } spacesIndices := spaces.FindAllStringIndex(line, -1) spacesBetweenKeyAndVal := "" if len(spacesIndices) > 0 { spacesBetweenKeyAndVal = toSpaces(spacesIndices[0][1] - spacesIndices[0][0]) } fmt.Fprintf(w, "%s:%s%s\n", key, spacesBetweenKeyAndVal, val) } func (ep *ExplainPrinter) printDescription(line string, w io.Writer) { fmt.Fprintf(w, "%s%s\n", toSpaces(5), color.Apply(strings.TrimLeft(line, " "), getColorByValueType(line, ep.DarkBackground))) } func (ep *ExplainPrinter) printField(line string, w io.Writer) { if ep.Recursive { ep.printKeyAndType(line, w) return } indentCnt := findIndent(line) if indentCnt == 3 { ep.printKeyAndType(line, w) return } ep.printDescription(line, w) } func (ep *ExplainPrinter) printKeyAndType(line string, w io.Writer) { indentCnt := findIndent(line) line = strings.TrimLeft(line, " ") keyAndVal := singleOrMultipleSpaces.Split(line, 2) // spaces between key and type can be only 1 space key, val := keyAndVal[0], keyAndVal[1] val = strings.TrimLeft(strings.TrimRight(val, ">"), "<") key = color.Apply(key, getColorByKeyIndent(indentCnt, 2, ep.DarkBackground)) val = color.Apply(val, getColorByValueType(line, ep.DarkBackground)) // I don't know why but kubectl explain uses \t as delimiter fmt.Fprintf(w, "%s%s\t<%s>\n", toSpaces(indentCnt), key, val) } kubecolor-0.0.20/printer/kubectl_explain_test.go000066400000000000000000000147161404516252000220100ustar00rootroot00000000000000package printer import ( "bytes" "strings" "testing" "github.com/dty1er/kubecolor/testutil" ) func Test_ExplainPrinter_Print(t *testing.T) { tests := []struct { name string darkBackground bool recursive bool input string expected string }{ { name: "kind, version, description, fields can be colorized with recursive=false", darkBackground: true, recursive: false, input: testutil.NewHereDoc(` KIND: Node VERSION: v1 DESCRIPTION: Node is a worker node in Kubernetes. Each node will have a unique identifier in the cache (i.e. in etcd). FIELDS: apiVersion APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources kind Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds metadata Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata spec Spec defines the behavior of a node. https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status status Most recently observed status of the node. Populated by the system. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status`), expected: testutil.NewHereDoc(` KIND: Node VERSION: v1 DESCRIPTION: Node is a worker node in Kubernetes. Each node will have a unique identifier in the cache (i.e. in etcd). FIELDS: apiVersion <string> APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources kind <string> Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds metadata <Object> Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata spec <Object> Spec defines the behavior of a node. https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status status <Object> Most recently observed status of the node. Populated by the system. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status `), }, { name: "kind, version, description, fields can be colorized with recursive=true", darkBackground: true, recursive: true, input: testutil.NewHereDoc(` KIND: Node VERSION: v1 DESCRIPTION: Node is a worker node in Kubernetes. Each node will have a unique identifier in the cache (i.e. in etcd). FIELDS: apiVersion kind metadata annotations clusterName creationTimestamp deletionGracePeriodSeconds deletionTimestamp finalizers <[]string> generateName generation labels managedFields <[]Object> apiVersion fieldsType fieldsV1 manager operation time name namespace `), expected: testutil.NewHereDoc(` KIND: Node VERSION: v1 DESCRIPTION: Node is a worker node in Kubernetes. Each node will have a unique identifier in the cache (i.e. in etcd). FIELDS: apiVersion <string> kind <string> metadata <Object> annotations <map[string]string> clusterName <string> creationTimestamp <string> deletionGracePeriodSeconds <integer> deletionTimestamp <string> finalizers <[]string> generateName <string> generation <integer> labels <map[string]string> managedFields <[]Object> apiVersion <string> fieldsType <string> fieldsV1 <map[string]> manager <string> operation <string> time <string> name <string> namespace <string> `), }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() r := strings.NewReader(tt.input) var w bytes.Buffer printer := ExplainPrinter{DarkBackground: tt.darkBackground, Recursive: tt.recursive} printer.Print(r, &w) testutil.MustEqual(t, tt.expected, w.String()) }) } } kubecolor-0.0.20/printer/kubectl_options.go000066400000000000000000000017611404516252000210000ustar00rootroot00000000000000package printer import ( "bufio" "fmt" "io" "strings" "github.com/dty1er/kubecolor/color" ) type OptionsPrinter struct { DarkBackground bool } func (op *OptionsPrinter) Print(r io.Reader, w io.Writer) { scanner := bufio.NewScanner(r) isFirstLine := true for scanner.Scan() { line := scanner.Text() if line == "" { fmt.Fprintln(w) continue } if isFirstLine { fmt.Fprintf(w, "%s\n", color.Apply(line, op.firstLineColor())) isFirstLine = false continue } indentCnt := findIndent(line) indent := toSpaces(indentCnt) trimmedLine := strings.TrimLeft(line, " ") splitted := strings.SplitN(trimmedLine, ": ", 2) key, val := splitted[0], splitted[1] fmt.Fprintf(w, "%s%s: %s\n", indent, color.Apply(key, getColorByKeyIndent(0, 2, op.DarkBackground)), color.Apply(val, getColorByValueType(val, op.DarkBackground))) } } func (op *OptionsPrinter) firstLineColor() color.Color { if op.DarkBackground { return StringColorForDark } return StringColorForLight } kubecolor-0.0.20/printer/kubectl_options_test.go000066400000000000000000000055731404516252000220440ustar00rootroot00000000000000package printer import ( "bytes" "strings" "testing" "github.com/dty1er/kubecolor/testutil" ) func Test_OptionsPrinter_Print(t *testing.T) { tests := []struct { name string darkBackground bool input string expected string }{ { name: "successful", darkBackground: true, input: testutil.NewHereDoc(` The following options can be passed to any command: --add-dir-header=false: If true, adds the file directory to the header of the log messages --alsologtostderr=false: log to standard error as well as files --as='': Username to impersonate for the operation --as-group=[]: Group to impersonate for the operation, this flag can be repeated to specify multiple groups. --cache-dir='/home/dtyler/.kube/cache': Default cache directory --certificate-authority='': Path to a cert file for the certificate authority --client-certificate='': Path to a client certificate file for TLS --client-key='': Path to a client key file for TLS --cluster='': The name of the kubeconfig cluster to use --context='': The name of the kubeconfig context to use --insecure-skip-tls-verify=false: If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure `), expected: testutil.NewHereDoc(` The following options can be passed to any command: --add-dir-header=false: If true, adds the file directory to the header of the log messages --alsologtostderr=false: log to standard error as well as files --as='': Username to impersonate for the operation --as-group=[]: Group to impersonate for the operation, this flag can be repeated to specify multiple groups. --cache-dir='/home/dtyler/.kube/cache': Default cache directory --certificate-authority='': Path to a cert file for the certificate authority --client-certificate='': Path to a client certificate file for TLS --client-key='': Path to a client key file for TLS --cluster='': The name of the kubeconfig cluster to use --context='': The name of the kubeconfig context to use --insecure-skip-tls-verify=false: If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure `), }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() r := strings.NewReader(tt.input) var w bytes.Buffer printer := OptionsPrinter{DarkBackground: tt.darkBackground} printer.Print(r, &w) testutil.MustEqual(t, tt.expected, w.String()) }) } } kubecolor-0.0.20/printer/kubectl_output_colored_printer.go000066400000000000000000000062751404516252000241240ustar00rootroot00000000000000package printer import ( "io" "strconv" "strings" "github.com/dty1er/kubecolor/color" "github.com/dty1er/kubecolor/kubectl" ) // KubectlOutputColoredPrinter is a printer to print data depending on // which kubectl subcommand is executed. type KubectlOutputColoredPrinter struct { SubcommandInfo *kubectl.SubcommandInfo DarkBackground bool Recursive bool } // Print reads r then write it to w, its format is based on kubectl subcommand. // If given subcommand is not supported by the printer, it prints data in Green. func (kp *KubectlOutputColoredPrinter) Print(r io.Reader, w io.Writer) { withHeader := !kp.SubcommandInfo.NoHeader var printer Printer = &SingleColoredPrinter{Color: color.Green} // default in green switch kp.SubcommandInfo.Subcommand { case kubectl.Top, kubectl.APIResources: printer = NewTablePrinter(withHeader, kp.DarkBackground, nil) case kubectl.APIVersions: printer = NewTablePrinter(false, kp.DarkBackground, nil) // api-versions always doesn't have header case kubectl.Get: switch { case kp.SubcommandInfo.FormatOption == kubectl.None, kp.SubcommandInfo.FormatOption == kubectl.Wide: printer = NewTablePrinter( withHeader, kp.DarkBackground, func(_ int, column string) (color.Color, bool) { if column == "CrashLoopBackOff" { return color.Red, true } // When Readiness is "n/m" then yellow if strings.Count(column, "/") == 1 { if arr := strings.Split(column, "/"); arr[0] != arr[1] { _, e1 := strconv.Atoi(arr[0]) _, e2 := strconv.Atoi(arr[1]) if e1 == nil && e2 == nil { // check both is number return color.Yellow, true } } } return 0, false }, ) case kp.SubcommandInfo.FormatOption == kubectl.Json: printer = &JsonPrinter{DarkBackground: kp.DarkBackground} case kp.SubcommandInfo.FormatOption == kubectl.Yaml: printer = &YamlPrinter{DarkBackground: kp.DarkBackground} } case kubectl.Describe: printer = &DescribePrinter{ DarkBackground: kp.DarkBackground, TablePrinter: NewTablePrinter(false, kp.DarkBackground, nil), } case kubectl.Explain: printer = &ExplainPrinter{ DarkBackground: kp.DarkBackground, Recursive: kp.Recursive, } case kubectl.Version: switch { case kp.SubcommandInfo.FormatOption == kubectl.Json: printer = &JsonPrinter{DarkBackground: kp.DarkBackground} case kp.SubcommandInfo.FormatOption == kubectl.Yaml: printer = &YamlPrinter{DarkBackground: kp.DarkBackground} case kp.SubcommandInfo.Short: printer = &VersionShortPrinter{ DarkBackground: kp.DarkBackground, } default: printer = &VersionPrinter{ DarkBackground: kp.DarkBackground, } } case kubectl.Options: printer = &OptionsPrinter{ DarkBackground: kp.DarkBackground, } case kubectl.Apply: switch { case kp.SubcommandInfo.FormatOption == kubectl.Json: printer = &JsonPrinter{DarkBackground: kp.DarkBackground} case kp.SubcommandInfo.FormatOption == kubectl.Yaml: printer = &YamlPrinter{DarkBackground: kp.DarkBackground} default: printer = &ApplyPrinter{DarkBackground: kp.DarkBackground} } } if kp.SubcommandInfo.Help { printer = &SingleColoredPrinter{Color: color.Yellow} } printer.Print(r, w) } kubecolor-0.0.20/printer/kubectl_output_colored_printer_test.go000066400000000000000000000667001404516252000251620ustar00rootroot00000000000000package printer import ( "bytes" "strings" "testing" "github.com/dty1er/kubecolor/kubectl" "github.com/dty1er/kubecolor/testutil" ) func Test_KubectlOutputColoredPrinter_Print(t *testing.T) { tests := []struct { name string darkBackground bool subcommandInfo *kubectl.SubcommandInfo input string expected string }{ { name: "kubectl top pod", darkBackground: true, subcommandInfo: &kubectl.SubcommandInfo{ Subcommand: kubectl.Top, }, input: testutil.NewHereDoc(` NAME CPU(cores) MEMORY(bytes) app-29twd 779m 221Mi app-2hhr6 1036m 220Mi app-52mbv 881m 137Mi`), expected: testutil.NewHereDoc(` NAME CPU(cores) MEMORY(bytes) app-29twd 779m 221Mi app-2hhr6 1036m 220Mi app-52mbv 881m 137Mi `), }, { name: "kubectl top pod --no-headers", darkBackground: true, subcommandInfo: &kubectl.SubcommandInfo{ Subcommand: kubectl.Top, NoHeader: true, }, input: testutil.NewHereDoc(` app-29twd 779m 221Mi app-2hhr6 1036m 220Mi app-52mbv 881m 137Mi`), expected: testutil.NewHereDoc(` app-29twd 779m 221Mi app-2hhr6 1036m 220Mi app-52mbv 881m 137Mi `), }, { name: "kubectl api-resources", darkBackground: true, subcommandInfo: &kubectl.SubcommandInfo{ Subcommand: kubectl.APIResources, }, input: testutil.NewHereDoc(` NAME SHORTNAMES APIGROUP NAMESPACED KIND bindings true Binding componentstatuses cs false ComponentStatus pods po true Pod podtemplates true PodTemplate replicationcontrollers rc true ReplicationController resourcequotas quota true ResourceQuota secrets true Secret serviceaccounts sa true ServiceAccount services svc true Service mutatingwebhookconfigurations admissionregistration.k8s.io false MutatingWebhookConfiguration customresourcedefinitions crd,crds apiextensions.k8s.io false CustomResourceDefinition controllerrevisions apps true ControllerRevision daemonsets ds apps true DaemonSet statefulsets sts apps true StatefulSet tokenreviews authentication.k8s.io false TokenReview `), expected: testutil.NewHereDoc(` NAME SHORTNAMES APIGROUP NAMESPACED KIND bindings true Binding componentstatuses cs false ComponentStatus pods po true Pod podtemplates true PodTemplate replicationcontrollers rc true ReplicationController resourcequotas quota true ResourceQuota secrets true Secret serviceaccounts sa true ServiceAccount services svc true Service mutatingwebhookconfigurations admissionregistration.k8s.io false MutatingWebhookConfiguration customresourcedefinitions crd,crds apiextensions.k8s.io false CustomResourceDefinition controllerrevisions apps true ControllerRevision daemonsets ds apps true DaemonSet statefulsets sts apps true StatefulSet tokenreviews authentication.k8s.io false TokenReview `), }, { name: "kubectl api-resources --no-headers", darkBackground: true, subcommandInfo: &kubectl.SubcommandInfo{ Subcommand: kubectl.APIResources, NoHeader: true, }, input: testutil.NewHereDoc(` bindings true Binding componentstatuses cs false ComponentStatus pods po true Pod podtemplates true PodTemplate replicationcontrollers rc true ReplicationController resourcequotas quota true ResourceQuota secrets true Secret serviceaccounts sa true ServiceAccount services svc true Service mutatingwebhookconfigurations admissionregistration.k8s.io false MutatingWebhookConfiguration customresourcedefinitions crd,crds apiextensions.k8s.io false CustomResourceDefinition controllerrevisions apps true ControllerRevision daemonsets ds apps true DaemonSet statefulsets sts apps true StatefulSet tokenreviews authentication.k8s.io false TokenReview `), expected: testutil.NewHereDoc(` bindings true Binding componentstatuses cs false ComponentStatus pods po true Pod podtemplates true PodTemplate replicationcontrollers rc true ReplicationController resourcequotas quota true ResourceQuota secrets true Secret serviceaccounts sa true ServiceAccount services svc true Service mutatingwebhookconfigurations admissionregistration.k8s.io false MutatingWebhookConfiguration customresourcedefinitions crd,crds apiextensions.k8s.io false CustomResourceDefinition controllerrevisions apps true ControllerRevision daemonsets ds apps true DaemonSet statefulsets sts apps true StatefulSet tokenreviews authentication.k8s.io false TokenReview `), }, { name: "kubectl get pod", darkBackground: true, subcommandInfo: &kubectl.SubcommandInfo{ Subcommand: kubectl.Get, }, input: testutil.NewHereDoc(` NAME READY STATUS RESTARTS AGE nginx-dnmv5 1/1 Running 0 6d6h nginx-m8pbc 1/1 Running 0 6d6h nginx-qdf9b 1/1 Running 0 6d6h`), expected: testutil.NewHereDoc(` NAME READY STATUS RESTARTS AGE nginx-dnmv5 1/1 Running 0 6d6h nginx-m8pbc 1/1 Running 0 6d6h nginx-qdf9b 1/1 Running 0 6d6h `), }, { name: "kubectl get pod", darkBackground: true, subcommandInfo: &kubectl.SubcommandInfo{ Subcommand: kubectl.Get, }, input: testutil.NewHereDoc(` NAME READY STATUS RESTARTS AGE nginx-dnmv5 1/1 CrashLoopBackOff 0 6d6h nginx-m8pbc 1/1 Running 0 6d6h nginx-qdf9b 0/1 Running 0 6d6h`), expected: testutil.NewHereDoc(` NAME READY STATUS RESTARTS AGE nginx-dnmv5 1/1 CrashLoopBackOff 0 6d6h nginx-m8pbc 1/1 Running 0 6d6h nginx-qdf9b 0/1 Running 0 6d6h `), }, { name: "kubectl get pod --no-headers", darkBackground: true, subcommandInfo: &kubectl.SubcommandInfo{ Subcommand: kubectl.Get, NoHeader: true, }, input: testutil.NewHereDoc(` nginx-dnmv5 1/1 Running 0 6d6h nginx-m8pbc 1/1 Running 0 6d6h nginx-qdf9b 1/1 Running 0 6d6h`), expected: testutil.NewHereDoc(` nginx-dnmv5 1/1 Running 0 6d6h nginx-m8pbc 1/1 Running 0 6d6h nginx-qdf9b 1/1 Running 0 6d6h `), }, { name: "kubectl get pod -o wide", darkBackground: true, subcommandInfo: &kubectl.SubcommandInfo{ Subcommand: kubectl.Get, FormatOption: kubectl.Wide, }, input: testutil.NewHereDoc(` NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES nginx-6799fc88d8-dnmv5 1/1 Running 0 7d10h 172.18.0.5 minikube nginx-6799fc88d8-m8pbc 1/1 Running 0 7d10h 172.18.0.4 minikube nginx-6799fc88d8-qdf9b 1/1 Running 0 7d10h 172.18.0.3 minikube `), expected: testutil.NewHereDoc(` NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES nginx-6799fc88d8-dnmv5 1/1 Running 0 7d10h 172.18.0.5 minikube   nginx-6799fc88d8-m8pbc 1/1 Running 0 7d10h 172.18.0.4 minikube   nginx-6799fc88d8-qdf9b 1/1 Running 0 7d10h 172.18.0.3 minikube   `), }, { name: "kubectl get pod -o json", darkBackground: true, subcommandInfo: &kubectl.SubcommandInfo{ Subcommand: kubectl.Get, FormatOption: kubectl.Json, }, input: testutil.NewHereDoc(` { "apiVersion": "v1", "kind": "Pod", "num": 598, "bool": true, "null": null }`), expected: testutil.NewHereDoc(` { "apiVersion": "v1", "kind": "Pod", "num": 598, "bool": true, "null": null } `), }, { name: "kubectl get pod -o yaml", darkBackground: true, subcommandInfo: &kubectl.SubcommandInfo{ Subcommand: kubectl.Get, FormatOption: kubectl.Yaml, }, input: testutil.NewHereDoc(` apiVersion: v1 kind: "Pod" num: 415 unknown: none: bool: true`), expected: testutil.NewHereDoc(` apiVersion: v1 kind: "Pod" num: 415 unknown:  none:  bool: true `), }, { name: "kubectl describe pod", darkBackground: true, subcommandInfo: &kubectl.SubcommandInfo{ Subcommand: kubectl.Describe, }, input: testutil.NewHereDoc(` Name: nginx-lpv5x Namespace: default Priority: 0 Node: minikube/172.17.0.3 Ready: true Start Time: Sat, 10 Oct 2020 14:07:17 +0900 Labels: app=nginx Annotations: `), expected: testutil.NewHereDoc(` Name: nginx-lpv5x Namespace: default Priority: 0 Node: minikube/172.17.0.3 Ready: true Start Time: Sat, 10 Oct 2020 14:07:17 +0900 Labels: app=nginx Annotations:  `), }, { name: "kubectl api-versions", darkBackground: true, subcommandInfo: &kubectl.SubcommandInfo{ Subcommand: kubectl.APIVersions, }, input: testutil.NewHereDoc(` acme.cert-manager.io/v1alpha2 admissionregistration.k8s.io/v1beta1 apiextensions.k8s.io/v1beta1 apiregistration.k8s.io/v1 apiregistration.k8s.io/v1beta1 apps/v1 apps/v1beta1 apps/v1beta2 authentication.k8s.io/v1 authentication.k8s.io/v1beta1 authorization.k8s.io/v1 authorization.k8s.io/v1beta1 autoscaling/v1 autoscaling/v2beta1 autoscaling/v2beta2 batch/v1 batch/v1beta1`), expected: testutil.NewHereDoc(` acme.cert-manager.io/v1alpha2 admissionregistration.k8s.io/v1beta1 apiextensions.k8s.io/v1beta1 apiregistration.k8s.io/v1 apiregistration.k8s.io/v1beta1 apps/v1 apps/v1beta1 apps/v1beta2 authentication.k8s.io/v1 authentication.k8s.io/v1beta1 authorization.k8s.io/v1 authorization.k8s.io/v1beta1 autoscaling/v1 autoscaling/v2beta1 autoscaling/v2beta2 batch/v1 batch/v1beta1 `), }, { name: "kubectl explain", darkBackground: true, subcommandInfo: &kubectl.SubcommandInfo{ Subcommand: kubectl.Explain, }, input: testutil.NewHereDoc(` KIND: Node VERSION: v1 DESCRIPTION: Node is a worker node in Kubernetes. Each node will have a unique identifier in the cache (i.e. in etcd). FIELDS: apiVersion APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources `), expected: testutil.NewHereDoc(` KIND: Node VERSION: v1 DESCRIPTION: Node is a worker node in Kubernetes. Each node will have a unique identifier in the cache (i.e. in etcd). FIELDS: apiVersion <string> APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources `), }, { name: "kubectl version", darkBackground: true, subcommandInfo: &kubectl.SubcommandInfo{ Subcommand: kubectl.Version, }, input: testutil.NewHereDoc(` Client Version: version.Info{Major:"1", Minor:"19", GitVersion:"v1.19.3", GitCommit:"1e11e4a2108024935ecfcb2912226cedeafd99df", GitTreeState:"clean", BuildDate:"2020-10-14T18:49:28Z", GoVersion:"go1.15.2", Compiler:"gc", Platform:"darwin/amd64"} Server Version: version.Info{Major:"1", Minor:"19", GitVersion:"v1.19.2", GitCommit:"f5743093fd1c663cb0cbc89748f730662345d44d", GitTreeState:"clean", BuildDate:"2020-09-16T13:32:58Z", GoVersion:"go1.15", Compiler:"gc", Platform:"linux/amd64"}`), expected: testutil.NewHereDoc(` Client Version: version.Info{Major:"1", Minor:"19", GitVersion:"v1.19.3", GitCommit:"1e11e4a2108024935ecfcb2912226cedeafd99df", GitTreeState:"clean", BuildDate:"2020-10-14T18:49:28Z", GoVersion:"go1.15.2", Compiler:"gc", Platform:"darwin/amd64"} Server Version: version.Info{Major:"1", Minor:"19", GitVersion:"v1.19.2", GitCommit:"f5743093fd1c663cb0cbc89748f730662345d44d", GitTreeState:"clean", BuildDate:"2020-09-16T13:32:58Z", GoVersion:"go1.15", Compiler:"gc", Platform:"linux/amd64"} `), }, { name: "kubectl version --client", darkBackground: true, subcommandInfo: &kubectl.SubcommandInfo{ Subcommand: kubectl.Version, }, input: testutil.NewHereDoc(` Client Version: version.Info{Major:"1", Minor:"19", GitVersion:"v1.19.3", GitCommit:"1e11e4a2108024935ecfcb2912226cedeafd99df", GitTreeState:"clean", BuildDate:"2020-10-14T18:49:28Z", GoVersion:"go1.15.2", Compiler:"gc", Platform:"darwin/amd64"}`), expected: testutil.NewHereDoc(` Client Version: version.Info{Major:"1", Minor:"19", GitVersion:"v1.19.3", GitCommit:"1e11e4a2108024935ecfcb2912226cedeafd99df", GitTreeState:"clean", BuildDate:"2020-10-14T18:49:28Z", GoVersion:"go1.15.2", Compiler:"gc", Platform:"darwin/amd64"} `), }, { name: "kubectl version --short", darkBackground: true, subcommandInfo: &kubectl.SubcommandInfo{ Subcommand: kubectl.Version, Short: true, }, input: testutil.NewHereDoc(` Client Version: v1.19.3 Server Version: v1.19.2`), expected: testutil.NewHereDoc(` Client Version: v1.19.3 Server Version: v1.19.2 `), }, { name: "kubectl version --short --client", darkBackground: true, subcommandInfo: &kubectl.SubcommandInfo{ Subcommand: kubectl.Version, Short: true, }, input: testutil.NewHereDoc(` Client Version: v1.19.3`), expected: testutil.NewHereDoc(` Client Version: v1.19.3 `), }, { name: "kubectl options", darkBackground: true, subcommandInfo: &kubectl.SubcommandInfo{ Subcommand: kubectl.Options, }, input: testutil.NewHereDoc(` The following options can be passed to any command: --add-dir-header=false: If true, adds the file directory to the header of the log messages --alsologtostderr=false: log to standard error as well as files --as='': Username to impersonate for the operation --as-group=[]: Group to impersonate for the operation, this flag can be repeated to specify multiple groups. --cache-dir='/home/dtyler/.kube/cache': Default cache directory --certificate-authority='': Path to a cert file for the certificate authority --client-certificate='': Path to a client certificate file for TLS --client-key='': Path to a client key file for TLS --cluster='': The name of the kubeconfig cluster to use --context='': The name of the kubeconfig context to use --insecure-skip-tls-verify=false: If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure `), expected: testutil.NewHereDoc(` The following options can be passed to any command: --add-dir-header=false: If true, adds the file directory to the header of the log messages --alsologtostderr=false: log to standard error as well as files --as='': Username to impersonate for the operation --as-group=[]: Group to impersonate for the operation, this flag can be repeated to specify multiple groups. --cache-dir='/home/dtyler/.kube/cache': Default cache directory --certificate-authority='': Path to a cert file for the certificate authority --client-certificate='': Path to a client certificate file for TLS --client-key='': Path to a client key file for TLS --cluster='': The name of the kubeconfig cluster to use --context='': The name of the kubeconfig context to use --insecure-skip-tls-verify=false: If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure `), }, { name: "kubectl apply -o json", darkBackground: true, subcommandInfo: &kubectl.SubcommandInfo{ Subcommand: kubectl.Apply, FormatOption: kubectl.Json, }, input: testutil.NewHereDoc(` { "apiVersion": "apps/v1", "kind": "Deployment", "metadata": { "annotations": { "deployment.kubernetes.io/revision": "1", "test": "false" }, "creationTimestamp": "2020-11-04T13:14:07Z", "generation": 3 } }`), expected: testutil.NewHereDoc(` { "apiVersion": "apps/v1", "kind": "Deployment", "metadata": { "annotations": { "deployment.kubernetes.io/revision": "1", "test": "false" }, "creationTimestamp": "2020-11-04T13:14:07Z", "generation": 3 } } `), }, { name: "kubectl apply -o yaml", darkBackground: true, subcommandInfo: &kubectl.SubcommandInfo{ Subcommand: kubectl.Apply, FormatOption: kubectl.Yaml, }, input: testutil.NewHereDoc(` apiVersion: apps/v1 kind: Deployment metadata: annotations: deployment.kubernetes.io/revision: "1" test: "false" creationTimestamp: "2020-11-04T13:14:07Z" generation: 3 status: availableReplicas: 3 conditions: - lastTransitionTime: "2020-11-04T13:14:07Z" lastUpdateTime: "2020-11-04T13:14:27Z" message: ReplicaSet "nginx-f89759699" has successfully progressed. reason: NewReplicaSetAvailable status: "True" type: Progressing - lastTransitionTime: "2020-12-27T04:41:49Z" lastUpdateTime: "2020-12-27T04:41:49Z" message: Deployment has minimum availability. reason: MinimumReplicasAvailable status: "True" type: Available observedGeneration: 3 readyReplicas: 3 replicas: 3 updatedReplicas: 3 `), expected: testutil.NewHereDoc(` apiVersion: apps/v1 kind: Deployment metadata: annotations: deployment.kubernetes.io/revision: "1" test: "false" creationTimestamp: "2020-11-04T13:14:07Z" generation: 3 status: availableReplicas: 3 conditions: - lastTransitionTime: "2020-11-04T13:14:07Z" lastUpdateTime: "2020-11-04T13:14:27Z" message: ReplicaSet "nginx-f89759699" has successfully progressed. reason: NewReplicaSetAvailable status: "True" type: Progressing - lastTransitionTime: "2020-12-27T04:41:49Z" lastUpdateTime: "2020-12-27T04:41:49Z" message: Deployment has minimum availability. reason: MinimumReplicasAvailable status: "True" type: Available observedGeneration: 3 readyReplicas: 3 replicas: 3 updatedReplicas: 3 `), }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() r := strings.NewReader(tt.input) var w bytes.Buffer printer := KubectlOutputColoredPrinter{ SubcommandInfo: tt.subcommandInfo, DarkBackground: tt.darkBackground, } printer.Print(r, &w) testutil.MustEqual(t, tt.expected, w.String()) }) } } kubecolor-0.0.20/printer/kubectl_version.go000066400000000000000000000042731404516252000207730ustar00rootroot00000000000000package printer import ( "bufio" "fmt" "io" "strings" "github.com/dty1er/kubecolor/color" ) type VersionShortPrinter struct { DarkBackground bool } // kubectl version --short format // Client Version: v1.19.3 // Server Version: v1.19.2 func (vsp *VersionShortPrinter) Print(r io.Reader, w io.Writer) { scanner := bufio.NewScanner(r) for scanner.Scan() { line := scanner.Text() splitted := strings.Split(line, ": ") key, val := splitted[0], splitted[1] fmt.Fprintf(w, "%s: %s\n", color.Apply(key, getColorByKeyIndent(0, 2, vsp.DarkBackground)), color.Apply(val, getColorByValueType(val, vsp.DarkBackground)), ) } } type VersionPrinter struct { DarkBackground bool } func (vp *VersionPrinter) Print(r io.Reader, w io.Writer) { scanner := bufio.NewScanner(r) for scanner.Scan() { line := scanner.Text() splitted := strings.SplitN(line, ": ", 2) key, val := splitted[0], splitted[1] key = color.Apply(key, getColorByKeyIndent(0, 2, vp.DarkBackground)) // val is go struct like // version.Info{Major:"1", Minor:"19", GitVersion:"v1.19.2", GitCommit:"f5743093fd1c663cb0cbc89748f730662345d44d", GitTreeState:"clean", BuildDate:"2020-09-16T13:32:58Z", GoVersion:"go1.15", Compiler:"gc", Platform:"linux/amd64"} val = strings.TrimRight(val, "}") pkgAndValues := strings.SplitN(val, "{", 2) packageName := pkgAndValues[0] values := strings.Split(pkgAndValues[1], ", ") coloredValues := make([]string, len(values)) fmt.Fprintf(w, "%s: %s{", key, color.Apply(packageName, getColorByKeyIndent(2, 2, vp.DarkBackground))) for i, value := range values { kv := strings.SplitN(value, ":", 2) coloredKey := color.Apply(kv[0], getColorByKeyIndent(0, 2, vp.DarkBackground)) isValDoubleQuotationSurrounded := strings.HasPrefix(kv[1], `"`) && strings.HasSuffix(kv[1], `"`) val := strings.TrimRight(strings.TrimLeft(kv[1], `"`), `"`) coloredVal := color.Apply(val, getColorByValueType(kv[1], vp.DarkBackground)) if isValDoubleQuotationSurrounded { coloredValues[i] = fmt.Sprintf(`%s:"%s"`, coloredKey, coloredVal) } else { coloredValues[i] = fmt.Sprintf(`%s:%s`, coloredKey, coloredVal) } } fmt.Fprintf(w, "%s}\n", strings.Join(coloredValues, ", ")) } } kubecolor-0.0.20/printer/kubectl_version_test.go000066400000000000000000000055171404516252000220340ustar00rootroot00000000000000package printer import ( "bytes" "strings" "testing" "github.com/dty1er/kubecolor/testutil" ) func Test_VersionPrinter_Print(t *testing.T) { tests := []struct { name string darkBackground bool recursive bool input string expected string }{ { name: "go struct dump can be colorized", darkBackground: true, input: testutil.NewHereDoc(` Client Version: version.Info{Major:"1", Minor:"19", GitVersion:"v1.19.3", GitCommit:"1e11e4a2108024935ecfcb2912226cedeafd99df", GitTreeState:"clean", BuildDate:"2020-10-14T18:49:28Z", GoVersion:"go1.15.2", Compiler:"gc", Platform:"darwin/amd64"} Server Version: version.Info{Major:"1", Minor:"19", GitVersion:"v1.19.2", GitCommit:"f5743093fd1c663cb0cbc89748f730662345d44d", GitTreeState:"clean", BuildDate:"2020-09-16T13:32:58Z", GoVersion:"go1.15", Compiler:"gc", Platform:"linux/amd64"}`), expected: testutil.NewHereDoc(` Client Version: version.Info{Major:"1", Minor:"19", GitVersion:"v1.19.3", GitCommit:"1e11e4a2108024935ecfcb2912226cedeafd99df", GitTreeState:"clean", BuildDate:"2020-10-14T18:49:28Z", GoVersion:"go1.15.2", Compiler:"gc", Platform:"darwin/amd64"} Server Version: version.Info{Major:"1", Minor:"19", GitVersion:"v1.19.2", GitCommit:"f5743093fd1c663cb0cbc89748f730662345d44d", GitTreeState:"clean", BuildDate:"2020-09-16T13:32:58Z", GoVersion:"go1.15", Compiler:"gc", Platform:"linux/amd64"} `), }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() r := strings.NewReader(tt.input) var w bytes.Buffer printer := VersionPrinter{DarkBackground: tt.darkBackground} printer.Print(r, &w) testutil.MustEqual(t, tt.expected, w.String()) }) } } func Test_VersionShortPrinter_Print(t *testing.T) { tests := []struct { name string darkBackground bool input string expected string }{ { name: "--short can be colorized", darkBackground: true, input: testutil.NewHereDoc(` Client Version: v1.19.3 Server Version: v1.19.2`), expected: testutil.NewHereDoc(` Client Version: v1.19.3 Server Version: v1.19.2 `), }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() r := strings.NewReader(tt.input) var w bytes.Buffer printer := VersionShortPrinter{DarkBackground: tt.darkBackground} printer.Print(r, &w) testutil.MustEqual(t, tt.expected, w.String()) }) } } kubecolor-0.0.20/printer/printer.go000066400000000000000000000004371404516252000172560ustar00rootroot00000000000000package printer import ( "io" "regexp" ) var singleOrMultipleSpaces = regexp.MustCompile("\\s{1,}") var spaces = regexp.MustCompile("\\s{2,}") // Printer can print something. // It reads data from r, then write them in w. type Printer interface { Print(r io.Reader, w io.Writer) } kubecolor-0.0.20/printer/single_colored_printer.go000066400000000000000000000007201404516252000223210ustar00rootroot00000000000000package printer import ( "bufio" "fmt" "io" "github.com/dty1er/kubecolor/color" ) // SingleColoredPrinter is a printer to print something in pre-cofigured color. type SingleColoredPrinter struct { Color color.Color } // Print reads r then writes it in w in sp.Color func (sp *SingleColoredPrinter) Print(r io.Reader, w io.Writer) { scanner := bufio.NewScanner(r) for scanner.Scan() { fmt.Fprintf(w, "%s\n", color.Apply(scanner.Text(), sp.Color)) } } kubecolor-0.0.20/printer/single_colored_printer_test.go000066400000000000000000000022131404516252000233570ustar00rootroot00000000000000package printer import ( "bytes" "strings" "testing" "github.com/dty1er/kubecolor/color" "github.com/dty1er/kubecolor/testutil" ) func Test_SingleColoredPrinter_Print(t *testing.T) { tests := []struct { name string color color.Color input string expected string }{ { name: "colored in white", color: color.White, input: testutil.NewHereDoc(` test test2 test3`), expected: testutil.NewHereDocf(` %s %s %s `, color.Apply("test", color.White), color.Apply("test2", color.White), color.Apply("test3", color.White)), }, { name: "colored in red", color: color.Red, input: testutil.NewHereDoc(` test test2 test3`), expected: testutil.NewHereDocf(` %s %s %s `, color.Apply("test", color.Red), color.Apply("test2", color.Red), color.Apply("test3", color.Red)), }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() r := strings.NewReader(tt.input) var w bytes.Buffer printer := SingleColoredPrinter{Color: tt.color} printer.Print(r, &w) testutil.MustEqual(t, tt.expected, w.String()) }) } } kubecolor-0.0.20/printer/table.go000066400000000000000000000107001404516252000166540ustar00rootroot00000000000000package printer import ( "bufio" "fmt" "io" "strings" "github.com/dty1er/kubecolor/color" ) type TablePrinter struct { WithHeader bool DarkBackground bool ColorDeciderFn func(index int, column string) (color.Color, bool) isFirstLine bool indexColorMap map[int]color.Color tempColors []color.Color } func NewTablePrinter(withHeader, darkBackground bool, colorDeciderFn func(index int, column string) (color.Color, bool)) *TablePrinter { return &TablePrinter{ WithHeader: withHeader, DarkBackground: darkBackground, ColorDeciderFn: colorDeciderFn, indexColorMap: map[int]color.Color{}, tempColors: []color.Color{}, } } func (tp *TablePrinter) Print(r io.Reader, w io.Writer) { tp.isFirstLine = true scanner := bufio.NewScanner(r) for scanner.Scan() { line := scanner.Text() if tp.isHeader(line) { fmt.Fprintf(w, "%s\n", color.Apply(line, getHeaderColorByBackground(tp.DarkBackground))) tp.isFirstLine = false continue } tp.printLineAsTableFormat(w, line, getColorsByBackground(tp.DarkBackground)) } } func (tp *TablePrinter) isHeader(line string) bool { // If every character is upper case, probably it's a header line. // e.g. // kubecolor get pod,rs // NAME READY STATUS RESTARTS AGE // pod/nginx-8spn9 1/1 Running 1 19d // pod/nginx-dplns 1/1 Running 1 19d // pod/nginx-lpv5x 1/1 Running 1 19d // NAME DESIRED CURRENT READY AGE <- this // replicaset.apps/nginx 3 3 3 19d // replicaset.apps/nginx-6799fc88d8 3 3 3 19d isEveryCharacterUpper := strings.ToUpper(line) == line return (tp.WithHeader && tp.isFirstLine) || isEveryCharacterUpper } // printTableFormat prints a line to w in kubectl "table" Format. // Table format is something like: // --------------------------------------------------------- // NAME READY STATUS RESTARTS AGE // nginx-6799fc88d8-dnmv5 1/1 Running 0 31h // nginx-6799fc88d8-m8pbc 1/1 Running 0 31h // nginx-6799fc88d8-qdf9b 1/1 Running 0 31h // nginx-8spn9 1/1 Running 0 31h // nginx-dplns 1/1 Running 0 31h // nginx-lpv5x 1/1 Running 0 31h // --------------------------------------------------------- // This function requires a line and tries to colorize it by each column. // If dark is true, use colors which are readable in dark-backgrounded environment, else, // it uses colors for light-backgrounded env. // This function doesn't respect if the line is "header", so // if you want to specify a special color for header, you must not pass the line // to this function. // deciderFn is a function to return context-specific color to be used to decorate a column. // If the function returned ok=true, then returned color will be used for the column. // If it returned ok=false, then default configurated color will be used. // If deciderFn is null, then this function uses the default configurated color. func (tp *TablePrinter) printLineAsTableFormat(w io.Writer, line string, colorsPreset []color.Color) { columns := spaces.Split(line, -1) spacesIndices := spaces.FindAllStringIndex(line, -1) if len(columns) == len(spacesIndices)-1 { // It should not come here. panic("kubecolor: unexpected format as table. this must be a bug of kubecolor") } for i, column := range columns { index := 0 if i != 0 { index = spacesIndices[i-1][1] + 1 } c := tp.decideColorForTable(index, colorsPreset) if tp.ColorDeciderFn != nil { if cc, ok := tp.ColorDeciderFn(i, column); ok { c = cc // prior injected deciderFn result } } // Write colored column fmt.Fprintf(w, "%s", color.Apply(column, c)) // Write spaces based on actual output // When writing the most left column, no extra spaces needed. if i <= len(spacesIndices)-1 { spacesIndex := spacesIndices[i] fmt.Fprintf(w, "%s", toSpaces(spacesIndex[1]-spacesIndex[0])) } } fmt.Fprintf(w, "\n") } func (tp *TablePrinter) decideColorForTable(index int, colors []color.Color) color.Color { if len(tp.tempColors) == 0 { tp.tempColors = make([]color.Color, len(colors)) copy(tp.tempColors, colors) } if c, ok := tp.indexColorMap[index]; ok { return c } c := tp.tempColors[0] tp.indexColorMap[index] = c tp.tempColors = tp.tempColors[1:] return c } kubecolor-0.0.20/printer/table_test.go000066400000000000000000000235061404516252000177230ustar00rootroot00000000000000package printer import ( "bytes" "strconv" "strings" "testing" "github.com/dty1er/kubecolor/color" "github.com/dty1er/kubecolor/testutil" ) func Test_TablePrinter_Print(t *testing.T) { tests := []struct { name string colorDeciderFn func(index int, column string) (color.Color, bool) withHeader bool darkBackground bool input string expected string }{ { name: "header is not colored - dark", colorDeciderFn: nil, withHeader: true, darkBackground: true, input: testutil.NewHereDoc(` NAME READY STATUS RESTARTS AGE nginx-dnmv5 1/1 Running 0 6d6h nginx-m8pbc 1/1 Running 0 6d6h nginx-qdf9b 1/1 Running 0 6d6h`), expected: testutil.NewHereDoc(` NAME READY STATUS RESTARTS AGE nginx-dnmv5 1/1 Running 0 6d6h nginx-m8pbc 1/1 Running 0 6d6h nginx-qdf9b 1/1 Running 0 6d6h `), }, { name: "multiple headers", colorDeciderFn: nil, withHeader: true, darkBackground: true, input: testutil.NewHereDoc(` NAME READY STATUS RESTARTS AGE pod/nginx-8spn9 1/1 Running 1 19d pod/nginx-dplns 1/1 Running 1 19d pod/nginx-lpv5x 1/1 Running 1 19d NAME DESIRED CURRENT READY AGE replicaset.apps/nginx 3 3 3 19d replicaset.apps/nginx-6799fc88d8 3 3 3 19d `), expected: testutil.NewHereDoc(` NAME READY STATUS RESTARTS AGE pod/nginx-8spn9 1/1 Running 1 19d pod/nginx-dplns 1/1 Running 1 19d pod/nginx-lpv5x 1/1 Running 1 19d  NAME DESIRED CURRENT READY AGE replicaset.apps/nginx 3 3 3 19d replicaset.apps/nginx-6799fc88d8 3 3 3 19d `), }, { name: "withheader=false, 1st line is not colored in header color but colored as a content of table", colorDeciderFn: nil, withHeader: false, darkBackground: true, input: testutil.NewHereDoc(` nginx-dnmv5 1/1 Running 0 6d6h nginx-m8pbc 1/1 Running 0 6d6h nginx-qdf9b 1/1 Running 0 6d6h`), expected: testutil.NewHereDoc(` nginx-dnmv5 1/1 Running 0 6d6h nginx-m8pbc 1/1 Running 0 6d6h nginx-qdf9b 1/1 Running 0 6d6h `), }, { name: "when darkBackground=false, color preset for light is used", colorDeciderFn: nil, withHeader: true, darkBackground: false, input: testutil.NewHereDoc(` NAME READY STATUS RESTARTS AGE nginx-dnmv5 1/1 Running 0 6d6h nginx-m8pbc 1/1 Running 0 6d6h nginx-qdf9b 1/1 Running 0 6d6h`), expected: testutil.NewHereDoc(` NAME READY STATUS RESTARTS AGE nginx-dnmv5 1/1 Running 0 6d6h nginx-m8pbc 1/1 Running 0 6d6h nginx-qdf9b 1/1 Running 0 6d6h `), }, { name: "colorDeciderFn works", colorDeciderFn: func(_ int, column string) (color.Color, bool) { if column == "CrashLoopBackOff" { return color.Red, true } // When Readiness is "n/m" then yellow if strings.Count(column, "/") == 1 { if arr := strings.Split(column, "/"); arr[0] != arr[1] { _, e1 := strconv.Atoi(arr[0]) _, e2 := strconv.Atoi(arr[1]) if e1 == nil && e2 == nil { // check both is number return color.Yellow, true } } } return 0, false }, withHeader: true, darkBackground: true, // "CrashLoopBackOff" will be red, "0/1" will be yellow input: testutil.NewHereDoc(` NAME READY STATUS RESTARTS AGE nginx-dnmv5 1/1 CrashLoopBackOff 0 6d6h nginx-m8pbc 1/1 Running 0 6d6h nginx-qdf9b 0/1 Running 0 6d6h`), expected: testutil.NewHereDoc(` NAME READY STATUS RESTARTS AGE nginx-dnmv5 1/1 CrashLoopBackOff 0 6d6h nginx-m8pbc 1/1 Running 0 6d6h nginx-qdf9b 0/1 Running 0 6d6h `), }, { name: "a table whose some parts are missing can be handled", colorDeciderFn: nil, withHeader: true, darkBackground: true, input: testutil.NewHereDoc(` NAME SHORTNAMES APIGROUP NAMESPACED KIND bindings true Binding componentstatuses cs false ComponentStatus pods po true Pod podtemplates true PodTemplate replicationcontrollers rc true ReplicationController resourcequotas quota true ResourceQuota secrets true Secret serviceaccounts sa true ServiceAccount services svc true Service mutatingwebhookconfigurations admissionregistration.k8s.io false MutatingWebhookConfiguration customresourcedefinitions crd,crds apiextensions.k8s.io false CustomResourceDefinition controllerrevisions apps true ControllerRevision daemonsets ds apps true DaemonSet statefulsets sts apps true StatefulSet tokenreviews authentication.k8s.io false TokenReview `), expected: testutil.NewHereDoc(` NAME SHORTNAMES APIGROUP NAMESPACED KIND bindings true Binding componentstatuses cs false ComponentStatus pods po true Pod podtemplates true PodTemplate replicationcontrollers rc true ReplicationController resourcequotas quota true ResourceQuota secrets true Secret serviceaccounts sa true ServiceAccount services svc true Service mutatingwebhookconfigurations admissionregistration.k8s.io false MutatingWebhookConfiguration customresourcedefinitions crd,crds apiextensions.k8s.io false CustomResourceDefinition controllerrevisions apps true ControllerRevision daemonsets ds apps true DaemonSet statefulsets sts apps true StatefulSet tokenreviews authentication.k8s.io false TokenReview `), }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() r := strings.NewReader(tt.input) var w bytes.Buffer printer := NewTablePrinter(tt.withHeader, tt.darkBackground, tt.colorDeciderFn) printer.Print(r, &w) testutil.MustEqual(t, tt.expected, w.String()) }) } } kubecolor-0.0.20/printer/with_func_printer.go000066400000000000000000000011141404516252000213150ustar00rootroot00000000000000package printer import ( "bufio" "fmt" "io" "github.com/dty1er/kubecolor/color" ) // WithFuncPrinter is a printer to print something based on injected logic. type WithFuncPrinter struct { Fn func(line string) color.Color } // Print reads r then writes it in w but its color is decided by // pre-injected function. // The function must not be nil, otherwise it panics. func (wp *WithFuncPrinter) Print(r io.Reader, w io.Writer) { scanner := bufio.NewScanner(r) for scanner.Scan() { line := scanner.Text() c := wp.Fn(line) fmt.Fprintf(w, "%s\n", color.Apply(line, c)) } } kubecolor-0.0.20/printer/with_func_printer_test.go000066400000000000000000000024441404516252000223630ustar00rootroot00000000000000package printer import ( "bytes" "strings" "testing" "github.com/dty1er/kubecolor/color" "github.com/dty1er/kubecolor/testutil" ) func Test_WithFuncPrinter_Print(t *testing.T) { tests := []struct { name string fn func(line string) color.Color input string expected string }{ { name: "colored in white", fn: func(_ string) color.Color { return color.White }, input: testutil.NewHereDoc(` test test2 test3`), expected: testutil.NewHereDocf(` %s %s %s `, color.Apply("test", color.White), color.Apply("test2", color.White), color.Apply("test3", color.White)), }, { name: "color changes by line", fn: func(line string) color.Color { if line == "test2" { return color.Red } return color.White }, input: testutil.NewHereDoc(` test test2 test3`), expected: testutil.NewHereDocf(` %s %s %s `, color.Apply("test", color.White), color.Apply("test2", color.Red), color.Apply("test3", color.White)), }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() r := strings.NewReader(tt.input) var w bytes.Buffer printer := WithFuncPrinter{Fn: tt.fn} printer.Print(r, &w) testutil.MustEqual(t, tt.expected, w.String()) }) } } kubecolor-0.0.20/printer/yaml.go000066400000000000000000000066141404516252000165400ustar00rootroot00000000000000package printer import ( "bufio" "fmt" "io" "strings" "github.com/dty1er/kubecolor/color" ) type YamlPrinter struct { DarkBackground bool inString bool } func (yp *YamlPrinter) Print(r io.Reader, w io.Writer) { scanner := bufio.NewScanner(r) for scanner.Scan() { line := scanner.Text() yp.printLineAsYamlFormat(line, w, yp.DarkBackground) } } func (yp *YamlPrinter) printLineAsYamlFormat(line string, w io.Writer, dark bool) { indentCnt := findIndent(line) // can be 0 indent := toSpaces(indentCnt) // so, can be empty trimmedLine := strings.TrimLeft(line, " ") if yp.inString { // if inString is true, the line must be a part of a string which is broken into several lines fmt.Fprintf(w, "%s%s\n", indent, yp.toColorizedStringValue(trimmedLine, dark)) yp.inString = !yp.isStringClosed(trimmedLine) return } splitted := strings.SplitN(trimmedLine, ": ", 2) // assuming key does not contain ": " while value might do if len(splitted) == 2 { // key: value key, val := splitted[0], splitted[1] fmt.Fprintf(w, "%s%s: %s\n", indent, yp.toColorizedYamlKey(key, indentCnt, 2, dark), yp.toColorizedYamlValue(val, dark)) yp.inString = yp.isStringOpenedButNotClosed(val) return } // when coming here, the line is just a "key:" or an element of an array if strings.HasSuffix(splitted[0], ":") { // key: fmt.Fprintf(w, "%s%s\n", indent, yp.toColorizedYamlKey(splitted[0], indentCnt, 2, dark)) return } fmt.Fprintf(w, "%s%s\n", indent, yp.toColorizedYamlValue(splitted[0], dark)) } func (yp *YamlPrinter) toColorizedYamlKey(key string, indentCnt, basicWidth int, dark bool) string { hasColon := strings.HasSuffix(key, ":") hasLeadingDash := strings.HasPrefix(key, "- ") key = strings.TrimSuffix(key, ":") key = strings.TrimPrefix(key, "- ") format := "%s" if hasColon { format += ":" } if hasLeadingDash { format = "- " + format indentCnt += 2 } return fmt.Sprintf(format, color.Apply(key, getColorByKeyIndent(indentCnt, basicWidth, dark))) } func (yp *YamlPrinter) toColorizedYamlValue(value string, dark bool) string { if value == "{}" { return "{}" } hasLeadingDash := strings.HasPrefix(value, "- ") value = strings.TrimPrefix(value, "- ") isDoubleQuoted := strings.HasPrefix(value, `"`) && strings.HasSuffix(value, `"`) trimmedValue := strings.TrimSuffix(strings.TrimPrefix(value, `"`), `"`) var format string switch { case hasLeadingDash && isDoubleQuoted: format = `- "%s"` case hasLeadingDash: format = "- %s" case isDoubleQuoted: format = `"%s"` default: format = "%s" } return fmt.Sprintf(format, color.Apply(trimmedValue, getColorByValueType(value, dark))) } func (yp *YamlPrinter) toColorizedStringValue(value string, dark bool) string { c := StringColorForLight if dark { c = StringColorForDark } isDoubleQuoted := strings.HasPrefix(value, `"`) && strings.HasSuffix(value, `"`) trimmedValue := strings.TrimRight(strings.TrimLeft(value, `"`), `"`) var format string switch { case isDoubleQuoted: format = `"%s"` default: format = "%s" } return fmt.Sprintf(format, color.Apply(trimmedValue, c)) } func (yp *YamlPrinter) isStringClosed(line string) bool { return strings.HasSuffix(line, "'") || strings.HasSuffix(line, `"`) } func (yp *YamlPrinter) isStringOpenedButNotClosed(line string) bool { return (strings.HasPrefix(line, "'") && !strings.HasSuffix(line, "'")) || (strings.HasPrefix(line, `"`) && !strings.HasSuffix(line, `"`)) } kubecolor-0.0.20/printer/yaml_test.go000066400000000000000000000100571404516252000175730ustar00rootroot00000000000000package printer import ( "bytes" "strings" "testing" "github.com/dty1er/kubecolor/testutil" ) func Test_YamlPrinter_Print(t *testing.T) { tests := []struct { name string darkBackground bool input string expected string }{ { name: "values can be colored by its type", darkBackground: true, input: testutil.NewHereDoc(` apiVersion: v1 kind: "Pod" num: 415 unknown: none: bool: true`), expected: testutil.NewHereDoc(` apiVersion: v1 kind: "Pod" num: 415 unknown:  none:  bool: true `), }, { name: "key color changes based on its indentation", darkBackground: true, input: testutil.NewHereDoc(` apiVersion: v1 items: - apiVersion: v1 key: - key2: 415 key3: true key4: key: val`), expected: testutil.NewHereDoc(` apiVersion: v1 items: - apiVersion: v1 key: - key2: 415 key3: true key4: key: val `), }, { name: "elements in an array can be colored", darkBackground: true, input: testutil.NewHereDoc(` lifecycle: preStop: exec: command: - sh - c - sleep 30`), expected: testutil.NewHereDoc(` lifecycle: preStop: exec: command: - sh - c - sleep 30 `), }, { name: "a value contains dash", darkBackground: true, input: testutil.NewHereDoc(` apiVersion: v1 items: - apiVersion: v1 key: - key2: 415 key3: true key4: key: -val`), expected: testutil.NewHereDoc(` apiVersion: v1 items: - apiVersion: v1 key: - key2: 415 key3: true key4: key: -val `), }, { name: "a long string which is broken into several lines can be colored", darkBackground: true, input: testutil.NewHereDoc(` - apiVersion: v1 kind: Pod metadata: annotations: annotation.long.1: 'Sometimes, you may want to specify what to command to use as kubectl. For example, when you want to use a versioned-kubectl kubectl.1.17, you can do that by an environment variable.' annotation.long.2: kubecolor colorizes your kubectl command output and does nothing else. kubecolor internally calls kubectl command and try to colorizes the output so you can use kubecolor as a complete alternative of kubectl annotation.short.1: normal length annotation`), expected: testutil.NewHereDoc(` - apiVersion: v1 kind: Pod metadata: annotations: annotation.long.1: 'Sometimes, you may want to specify what to command to use as kubectl. For example, when you want to use a versioned-kubectl kubectl.1.17, you can do that by an environment variable.' annotation.long.2: kubecolor colorizes your kubectl command output and does nothing else. kubecolor internally calls kubectl command and try to colorizes the output so you can use kubecolor as a complete alternative of kubectl annotation.short.1: normal length annotation `), }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() r := strings.NewReader(tt.input) var w bytes.Buffer printer := YamlPrinter{DarkBackground: tt.darkBackground} printer.Print(r, &w) testutil.MustEqual(t, tt.expected, w.String()) }) } } kubecolor-0.0.20/testutil/000077500000000000000000000000001404516252000154325ustar00rootroot00000000000000kubecolor-0.0.20/testutil/assert.go000066400000000000000000000005321404516252000172620ustar00rootroot00000000000000// testutil package is a utility for testing. // This package is inspired by morikuni/failure testutil_test.go package testutil import ( "testing" "github.com/google/go-cmp/cmp" ) func MustEqual(t testing.TB, want, got interface{}) { t.Helper() if diff := cmp.Diff(want, got); diff != "" { t.Errorf("diff (-want +got):\n%s", diff) } } kubecolor-0.0.20/testutil/heredoc.go000066400000000000000000000003241404516252000173710ustar00rootroot00000000000000package testutil import "github.com/MakeNowJust/heredoc" func NewHereDoc(s string) string { return heredoc.Doc(s) } func NewHereDocf(s string, args ...interface{}) string { return heredoc.Docf(s, args...) }