├── .gitignore ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── test.yaml ├── renovate.json ├── examples ├── kube-prometheus │ └── kustomization.yaml ├── hello-world │ └── kustomization.yaml ├── kube-prometheus-transformer │ ├── kustomization.yaml │ └── transformer.yaml └── transformer │ └── transformer.yaml ├── Dockerfile ├── Makefile ├── LICENSE ├── pkg ├── fixtures │ ├── fixtures.go │ └── hello-world.yaml └── parser │ ├── parser.go │ └── parser_test.go ├── go.mod ├── cmd └── kustomize-dot │ ├── main.go │ ├── utils.go │ ├── generate.go │ └── plugin.go ├── images ├── hello-world.svg ├── kube-prometheus-2.svg ├── kube-prometheus-1.svg └── kube-prometheus-3.svg ├── go.sum └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | bin/ 3 | coverage.txt 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | --- 2 | github: [dnaeon] 3 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ], 6 | "postUpdateOptions": [ 7 | "gomodTidy", 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | - package-ecosystem: docker 8 | directory: / 9 | schedule: 10 | interval: weekly 11 | -------------------------------------------------------------------------------- /examples/kube-prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: kustomize.config.k8s.io/v1beta1 3 | kind: Kustomization 4 | metadata: 5 | name: kube-prometheus 6 | 7 | buildMetadata: 8 | - originAnnotations 9 | 10 | resources: 11 | - https://github.com/prometheus-operator/kube-prometheus// 12 | -------------------------------------------------------------------------------- /examples/hello-world/kustomization.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: kustomize.config.k8s.io/v1beta1 3 | kind: Kustomization 4 | metadata: 5 | name: hello-world 6 | 7 | namespace: default 8 | 9 | buildMetadata: 10 | - originAnnotations 11 | 12 | resources: 13 | - https://github.com/kubernetes-sigs/kustomize//examples/helloWorld?ref=v1.0.6 14 | -------------------------------------------------------------------------------- /examples/kube-prometheus-transformer/kustomization.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: kustomize.config.k8s.io/v1beta1 3 | kind: Kustomization 4 | metadata: 5 | name: kube-prometheus 6 | 7 | buildMetadata: 8 | - originAnnotations 9 | 10 | resources: 11 | - https://github.com/prometheus-operator/kube-prometheus// 12 | 13 | transformers: 14 | - transformer.yaml 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.25-alpine as builder 2 | ENV CGO_ENABLED=0 3 | WORKDIR /go/src/ 4 | COPY go.mod go.sum ./ 5 | RUN go mod download 6 | COPY . . 7 | RUN go build -ldflags '-w -s' -v -o /usr/local/bin/kustomize-dot ./cmd/kustomize-dot 8 | 9 | FROM alpine:latest 10 | COPY --from=builder /usr/local/bin/kustomize-dot /usr/local/bin/kustomize-dot 11 | ENTRYPOINT ["kustomize-dot", "plugin"] 12 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | # .github/workflows/test.yaml 2 | on: [push, pull_request] 3 | name: test 4 | jobs: 5 | test: 6 | strategy: 7 | matrix: 8 | go-version: [1.24.x, 1.25.x] 9 | os: [ubuntu-latest] 10 | runs-on: ${{ matrix.os }} 11 | steps: 12 | - uses: actions/setup-go@v6 13 | with: 14 | go-version: ${{ matrix.go-version }} 15 | - uses: actions/checkout@v6 16 | - run: make test-cover 17 | - name: Upload coverage reports to Codecov 18 | uses: codecov/codecov-action@v5.5.2 19 | with: 20 | token: ${{ secrets.CODECOV_TOKEN }} 21 | slug: dnaeon/kustomize-dot 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := build 2 | LOCAL_BIN ?= $(shell pwd)/bin 3 | 4 | BINARY ?= $(LOCAL_BIN)/kustomize-dot 5 | 6 | $(LOCAL_BIN): 7 | mkdir -p $(LOCAL_BIN) 8 | 9 | $(BINARY): $(LOCAL_BIN) 10 | go build -o $(BINARY) ./cmd/kustomize-dot/ 11 | 12 | build: $(BINARY) 13 | 14 | tidy: 15 | go mod tidy 16 | 17 | test: 18 | go test -v -race $(shell go list ./... | grep -E -v 'cmd|fixtures') 19 | 20 | test-cover: 21 | go test -v -race -coverprofile=coverage.txt -covermode=atomic $(shell go list ./... | grep -E -v 'cmd|fixtures') 22 | 23 | docker-build: 24 | docker build -t dnaeon/kustomize-dot:latest . 25 | 26 | .PHONY: build tidy test test-cover docker-build 27 | -------------------------------------------------------------------------------- /examples/transformer/transformer.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: dnaeon.github.io/v1 3 | kind: KustomizeDot 4 | metadata: 5 | name: kustomize-dot 6 | annotations: 7 | config.kubernetes.io/function: | 8 | container: 9 | image: dnaeon/kustomize-dot:latest 10 | spec: 11 | # Graph layout direction - TB, BT, LR or RL 12 | layout: LR 13 | 14 | # Highlight resources of a given kind with the specified color 15 | highlightKinds: 16 | Deployment: green 17 | Service: yellow 18 | Role: pink 19 | 20 | # Highlight all resources from a given namespace with the specified color 21 | highlightNamespaces: 22 | default: orange 23 | kube-system: pink 24 | 25 | # Drop specified resources from the graph 26 | dropKinds: 27 | # - ConfigMap 28 | # - RoleBinding 29 | 30 | # Drop all resources from the specified namespaces 31 | dropNamespaces: 32 | - foo 33 | - bar 34 | 35 | # Keep the specified resources only and drop anything else 36 | keepKinds: 37 | # - baz 38 | # - qux 39 | 40 | # Keep the resources from the specified namespaces only, and drop anything 41 | # else. 42 | keepNamespaces: 43 | # - monitoring 44 | -------------------------------------------------------------------------------- /examples/kube-prometheus-transformer/transformer.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: dnaeon.github.io/v1 3 | kind: KustomizeDot 4 | metadata: 5 | name: kustomize-dot 6 | annotations: 7 | config.kubernetes.io/function: | 8 | container: 9 | image: dnaeon/kustomize-dot:latest 10 | spec: 11 | # Graph layout direction - TB, BT, LR or RL 12 | layout: LR 13 | 14 | # Highlight resources of a given kind with the specified color 15 | highlightKinds: 16 | Deployment: green 17 | Service: yellow 18 | Role: pink 19 | 20 | # Highlight all resources from a given namespace with the specified color 21 | highlightNamespaces: 22 | default: orange 23 | kube-system: pink 24 | 25 | # Drop specified resources from the graph 26 | dropKinds: 27 | # - ConfigMap 28 | # - RoleBinding 29 | 30 | # Drop all resources from the specified namespaces 31 | dropNamespaces: 32 | - foo 33 | - bar 34 | 35 | # Keep the specified resources only and drop anything else 36 | keepKinds: 37 | # - baz 38 | # - qux 39 | 40 | # Keep the resources from the specified namespaces only, and drop anything 41 | # else. 42 | keepNamespaces: 43 | # - monitoring 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 Marin Atanasov Nikolov 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 18 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 19 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 20 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 21 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 22 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 23 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /pkg/fixtures/fixtures.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Marin Atanasov Nikolov 2 | // All rights reserved. 3 | // 4 | // Redistribution and use in source and binary forms, with or without 5 | // modification, are permitted provided that the following conditions 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer. 10 | // 2. Redistributions in binary form must reproduce the above copyright 11 | // notice, this list of conditions and the following disclaimer in the 12 | // documentation and/or other materials provided with the distribution. 13 | // 14 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 18 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 19 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 20 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 21 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 22 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 23 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | // POSSIBILITY OF SUCH DAMAGE. 25 | 26 | // Package fixtures provides sample test fixtures. 27 | package fixtures 28 | 29 | import _ "embed" 30 | 31 | //go:embed hello-world.yaml 32 | var HelloWorld string 33 | 34 | //go:embed kube-prometheus.yaml 35 | var KubePrometheus string 36 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dnaeon/kustomize-dot 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/urfave/cli/v2 v2.27.7 7 | gopkg.in/dnaeon/go-graph.v1 v1.0.2 8 | sigs.k8s.io/kustomize/api v0.21.0 9 | sigs.k8s.io/kustomize/kyaml v0.21.0 10 | ) 11 | 12 | require ( 13 | github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect 14 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 15 | github.com/go-echarts/go-echarts/v2 v2.5.4 // indirect 16 | github.com/go-errors/errors v1.4.2 // indirect 17 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 18 | github.com/go-openapi/jsonreference v0.20.2 // indirect 19 | github.com/go-openapi/swag v0.23.0 // indirect 20 | github.com/google/gnostic-models v0.6.9 // indirect 21 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 22 | github.com/josharian/intern v1.0.0 // indirect 23 | github.com/mailru/easyjson v0.7.7 // indirect 24 | github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect 25 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 26 | github.com/spf13/cobra v1.9.1 // indirect 27 | github.com/spf13/pflag v1.0.6 // indirect 28 | github.com/xlab/treeprint v1.2.0 // indirect 29 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect 30 | go.yaml.in/yaml/v2 v2.4.2 // indirect 31 | go.yaml.in/yaml/v3 v3.0.3 // indirect 32 | golang.org/x/sys v0.35.0 // indirect 33 | google.golang.org/protobuf v1.36.5 // indirect 34 | gopkg.in/dnaeon/go-deque.v1 v1.0.0-20250203064611-7d48f7299755 // indirect 35 | gopkg.in/dnaeon/go-priorityqueue.v1 v1.1.1 // indirect 36 | gopkg.in/yaml.v3 v3.0.1 // indirect 37 | k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 // indirect 38 | k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect 39 | sigs.k8s.io/yaml v1.5.0 // indirect 40 | ) 41 | -------------------------------------------------------------------------------- /pkg/fixtures/hello-world.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | altGreeting: Good Morning! 4 | enableRisky: "false" 5 | kind: ConfigMap 6 | metadata: 7 | annotations: 8 | config.kubernetes.io/origin: | 9 | path: examples/helloWorld/configMap.yaml 10 | repo: https://github.com/kubernetes-sigs/kustomize 11 | ref: v1.0.6 12 | labels: 13 | app: hello 14 | name: the-map 15 | namespace: default 16 | --- 17 | apiVersion: v1 18 | kind: Service 19 | metadata: 20 | annotations: 21 | config.kubernetes.io/origin: | 22 | path: examples/helloWorld/service.yaml 23 | repo: https://github.com/kubernetes-sigs/kustomize 24 | ref: v1.0.6 25 | labels: 26 | app: hello 27 | name: the-service 28 | namespace: default 29 | spec: 30 | ports: 31 | - port: 8666 32 | protocol: TCP 33 | targetPort: 8080 34 | selector: 35 | app: hello 36 | deployment: hello 37 | type: LoadBalancer 38 | --- 39 | apiVersion: apps/v1 40 | kind: Deployment 41 | metadata: 42 | annotations: 43 | config.kubernetes.io/origin: | 44 | path: examples/helloWorld/deployment.yaml 45 | repo: https://github.com/kubernetes-sigs/kustomize 46 | ref: v1.0.6 47 | labels: 48 | app: hello 49 | name: the-deployment 50 | namespace: default 51 | spec: 52 | replicas: 3 53 | selector: 54 | matchLabels: 55 | app: hello 56 | template: 57 | metadata: 58 | labels: 59 | app: hello 60 | deployment: hello 61 | spec: 62 | containers: 63 | - command: 64 | - /hello 65 | - --port=8080 66 | - --enableRiskyFeature=$(ENABLE_RISKY) 67 | env: 68 | - name: ALT_GREETING 69 | valueFrom: 70 | configMapKeyRef: 71 | key: altGreeting 72 | name: the-map 73 | - name: ENABLE_RISKY 74 | valueFrom: 75 | configMapKeyRef: 76 | key: enableRisky 77 | name: the-map 78 | image: monopole/hello:1 79 | name: the-container 80 | ports: 81 | - containerPort: 8080 82 | -------------------------------------------------------------------------------- /cmd/kustomize-dot/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Marin Atanasov Nikolov 2 | // All rights reserved. 3 | // 4 | // Redistribution and use in source and binary forms, with or without 5 | // modification, are permitted provided that the following conditions 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer. 10 | // 2. Redistributions in binary form must reproduce the above copyright 11 | // notice, this list of conditions and the following disclaimer in the 12 | // documentation and/or other materials provided with the distribution. 13 | // 14 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 18 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 19 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 20 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 21 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 22 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 23 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | // POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package main 27 | 28 | import ( 29 | "fmt" 30 | "os" 31 | 32 | "github.com/urfave/cli/v2" 33 | ) 34 | 35 | func main() { 36 | app := &cli.App{ 37 | Name: "kustomize-dot", 38 | Version: "0.1.0", 39 | EnableBashCompletion: true, 40 | Suggest: true, 41 | Usage: "tool for generating graphs from kustomize resources", 42 | Authors: []*cli.Author{ 43 | { 44 | Name: "Marin Atanasov Nikolov", 45 | Email: "dnaeon@gmail.com", 46 | }, 47 | }, 48 | Commands: []*cli.Command{ 49 | newGenerateCommand(), 50 | newPluginCommand(), 51 | }, 52 | } 53 | 54 | if err := app.Run(os.Args); err != nil { 55 | fmt.Fprintf(os.Stderr, "%s\n", err) 56 | os.Exit(1) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /cmd/kustomize-dot/utils.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Marin Atanasov Nikolov 2 | // All rights reserved. 3 | // 4 | // Redistribution and use in source and binary forms, with or without 5 | // modification, are permitted provided that the following conditions 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer. 10 | // 2. Redistributions in binary form must reproduce the above copyright 11 | // notice, this list of conditions and the following disclaimer in the 12 | // documentation and/or other materials provided with the distribution. 13 | // 14 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 18 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 19 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 20 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 21 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 22 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 23 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | // POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package main 27 | 28 | import ( 29 | "errors" 30 | "fmt" 31 | "slices" 32 | "strings" 33 | 34 | "github.com/dnaeon/kustomize-dot/pkg/parser" 35 | "github.com/urfave/cli/v2" 36 | ) 37 | 38 | // errUnsupportedLayout is returned when the app was called with invalid layout 39 | // direction. 40 | var errUnsupportedLayout = errors.New("unsupported graph layout") 41 | 42 | // errInvalidKV is an error which is returned when attempting to parse an 43 | // invalid key/value pair. 44 | var errInvalidKV = errors.New("invalid key/value pair") 45 | 46 | // kvSeparator is the separator used to parse key/value pairs from a string, 47 | // e.g. foo=bar, bar=baz, etc. 48 | const kvSeparator = "=" 49 | 50 | // getLayoutDirection returns the graph layout direction from the CLI context 51 | func getLayoutDirection(ctx *cli.Context) (parser.LayoutDirection, error) { 52 | supportedLayouts := []parser.LayoutDirection{ 53 | parser.LayoutDirectionBT, 54 | parser.LayoutDirectionTB, 55 | parser.LayoutDirectionLR, 56 | parser.LayoutDirectionRL, 57 | } 58 | 59 | layout := parser.LayoutDirection(ctx.String("layout")) 60 | if !slices.Contains(supportedLayouts, layout) { 61 | return parser.LayoutDirection(""), fmt.Errorf("%w: %s", errUnsupportedLayout, layout) 62 | } 63 | 64 | return layout, nil 65 | } 66 | 67 | // kv represents a key/value pair. 68 | type kv struct { 69 | key string 70 | val string 71 | } 72 | 73 | // parseKV parses a key/value pairs. The key/value pairs are expected to be in 74 | // the form of foo=bar, bar=baz, etc. 75 | func parseKV(values ...string) ([]*kv, error) { 76 | pairs := make([]*kv, 0) 77 | for _, val := range values { 78 | parts := strings.Split(val, kvSeparator) 79 | if len(parts) != 2 { 80 | return nil, fmt.Errorf("%w: %s", errInvalidKV, val) 81 | } 82 | pair := &kv{key: parts[0], val: parts[1]} 83 | pairs = append(pairs, pair) 84 | } 85 | 86 | return pairs, nil 87 | } 88 | -------------------------------------------------------------------------------- /images/hello-world.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 1374393678912 14 | 15 | configmap/the-map 16 | 17 | 18 | 19 | 1374393679040 20 | 21 | examples/helloWorld/configMap.yaml 22 | 23 | 24 | 25 | 1374393678912->1374393679040 26 | 27 | 28 | https://github.com/kubernetes-sigs/kustomize (ref v1.0.6) 29 | 30 | 31 | 32 | 1374393679104 33 | 34 | service/the-service 35 | 36 | 37 | 38 | 1374393679232 39 | 40 | examples/helloWorld/service.yaml 41 | 42 | 43 | 44 | 1374393679104->1374393679232 45 | 46 | 47 | https://github.com/kubernetes-sigs/kustomize (ref v1.0.6) 48 | 49 | 50 | 51 | 1374393679296 52 | 53 | deployment/the-deployment 54 | 55 | 56 | 57 | 1374393679424 58 | 59 | examples/helloWorld/deployment.yaml 60 | 61 | 62 | 63 | 1374393679296->1374393679424 64 | 65 | 66 | https://github.com/kubernetes-sigs/kustomize (ref v1.0.6) 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /cmd/kustomize-dot/generate.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Marin Atanasov Nikolov 2 | // All rights reserved. 3 | // 4 | // Redistribution and use in source and binary forms, with or without 5 | // modification, are permitted provided that the following conditions 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer. 10 | // 2. Redistributions in binary form must reproduce the above copyright 11 | // notice, this list of conditions and the following disclaimer in the 12 | // documentation and/or other materials provided with the distribution. 13 | // 14 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 18 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 19 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 20 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 21 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 22 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 23 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | // POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package main 27 | 28 | import ( 29 | "os" 30 | 31 | "github.com/dnaeon/kustomize-dot/pkg/parser" 32 | "github.com/urfave/cli/v2" 33 | "gopkg.in/dnaeon/go-graph.v1" 34 | "sigs.k8s.io/kustomize/api/resource" 35 | ) 36 | 37 | // newGenerateCommand returns the command for generating dot representation of 38 | // the Kubernetes resources. 39 | func newGenerateCommand() *cli.Command { 40 | cmd := &cli.Command{ 41 | Name: "generate", 42 | Usage: "generate dot representation", 43 | Aliases: []string{"gen", "g"}, 44 | Action: execGenerateCommand, 45 | Flags: []cli.Flag{ 46 | &cli.StringFlag{ 47 | Name: "layout", 48 | Usage: "direction of graph layout", 49 | Value: "LR", 50 | Aliases: []string{"l"}, 51 | }, 52 | &cli.PathFlag{ 53 | Name: "file", 54 | Usage: "file containing the Kubernetes resources", 55 | Required: true, 56 | Aliases: []string{"f"}, 57 | }, 58 | &cli.StringSliceFlag{ 59 | Name: "highlight-kind", 60 | Usage: "highlight resources of a given kind with specified color", 61 | Aliases: []string{"kind-color", "hk"}, 62 | EnvVars: []string{"HIGHLIGHT_KIND", "KIND_COLOR"}, 63 | }, 64 | &cli.StringSliceFlag{ 65 | Name: "highlight-namespace", 66 | Usage: "highlight resources from a given namespace with specified color", 67 | Aliases: []string{"namespace-color", "hn"}, 68 | EnvVars: []string{"HIGHLIGHT_NAMESPACE", "NAMESPACE_COLOR"}, 69 | }, 70 | &cli.StringSliceFlag{ 71 | Name: "drop-kind", 72 | Usage: "drop resources of the given kind", 73 | Aliases: []string{"dk"}, 74 | EnvVars: []string{"DROP_KIND"}, 75 | }, 76 | &cli.StringSliceFlag{ 77 | Name: "drop-namespace", 78 | Usage: "drop all resources from the given namespace", 79 | Aliases: []string{"dn"}, 80 | EnvVars: []string{"DROP_NAMESPACE"}, 81 | }, 82 | &cli.StringSliceFlag{ 83 | Name: "keep-kind", 84 | Usage: "keep resources of the given kind only", 85 | Aliases: []string{"kk"}, 86 | EnvVars: []string{"KEEP_KIND"}, 87 | }, 88 | &cli.StringSliceFlag{ 89 | Name: "keep-namespace", 90 | Usage: "keep resources from the given namespace only", 91 | Aliases: []string{"kn"}, 92 | EnvVars: []string{"KEEP_NAMESPACE"}, 93 | }, 94 | }, 95 | } 96 | 97 | return cmd 98 | } 99 | 100 | // execGenerateCommand runs the command for generating dot representation of the 101 | // Kubernetes resources. 102 | func execGenerateCommand(ctx *cli.Context) error { 103 | layout, err := getLayoutDirection(ctx) 104 | if err != nil { 105 | return err 106 | } 107 | 108 | // graph layout direction 109 | opts := make([]parser.Option, 0) 110 | opts = append(opts, parser.WithLayoutDirection(layout)) 111 | 112 | // highlight-kind options 113 | hkValues := ctx.StringSlice("highlight-kind") 114 | hkPairs, err := parseKV(hkValues...) 115 | if err != nil { 116 | return err 117 | } 118 | for _, pair := range hkPairs { 119 | opts = append(opts, parser.WithHighlightKind(pair.key, pair.val)) 120 | } 121 | 122 | // highlight-namespace options 123 | hnValues := ctx.StringSlice("highlight-namespace") 124 | hnPairs, err := parseKV(hnValues...) 125 | if err != nil { 126 | return err 127 | } 128 | for _, pair := range hnPairs { 129 | opts = append(opts, parser.WithHighlightNamespace(pair.key, pair.val)) 130 | } 131 | 132 | // drop-kind options 133 | dkValues := ctx.StringSlice("drop-kind") 134 | for _, dk := range dkValues { 135 | opts = append(opts, parser.WithDropKind(dk)) 136 | } 137 | 138 | // drop-namespace options 139 | dnValues := ctx.StringSlice("drop-namespace") 140 | for _, dn := range dnValues { 141 | opts = append(opts, parser.WithDropNamespace(dn)) 142 | } 143 | 144 | // keep-kind options 145 | kkValues := ctx.StringSlice("keep-kind") 146 | for _, kk := range kkValues { 147 | opts = append(opts, parser.WithKeepKind(kk)) 148 | } 149 | 150 | // keep-namespace options 151 | knValues := ctx.StringSlice("keep-namespace") 152 | for _, kn := range knValues { 153 | opts = append(opts, parser.WithKeepNamespace(kn)) 154 | } 155 | 156 | // Read the resources and generate the graph 157 | var resources []*resource.Resource 158 | 159 | file := ctx.Path("file") 160 | if file == "-" { 161 | // Special case for resources passed on stdin 162 | resources, err = parser.ResourcesFromReader(os.Stdin) 163 | if err != nil { 164 | return err 165 | } 166 | } else { 167 | resources, err = parser.ResourcesFromPath(file) 168 | if err != nil { 169 | return err 170 | } 171 | } 172 | 173 | p := parser.New(opts...) 174 | g, err := p.Parse(resources) 175 | if err != nil { 176 | return err 177 | } 178 | 179 | return graph.WriteDot(g, os.Stdout) 180 | } 181 | -------------------------------------------------------------------------------- /cmd/kustomize-dot/plugin.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Marin Atanasov Nikolov 2 | // All rights reserved. 3 | // 4 | // Redistribution and use in source and binary forms, with or without 5 | // modification, are permitted provided that the following conditions 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer. 10 | // 2. Redistributions in binary form must reproduce the above copyright 11 | // notice, this list of conditions and the following disclaimer in the 12 | // documentation and/or other materials provided with the distribution. 13 | // 14 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 18 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 19 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 20 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 21 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 22 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 23 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | // POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package main 27 | 28 | import ( 29 | "bytes" 30 | "fmt" 31 | 32 | "github.com/dnaeon/kustomize-dot/pkg/parser" 33 | "github.com/urfave/cli/v2" 34 | "gopkg.in/dnaeon/go-graph.v1" 35 | "sigs.k8s.io/kustomize/kyaml/fn/framework" 36 | "sigs.k8s.io/kustomize/kyaml/fn/framework/command" 37 | "sigs.k8s.io/kustomize/kyaml/kio" 38 | "sigs.k8s.io/kustomize/kyaml/yaml" 39 | ) 40 | 41 | // pluginConfig contains the plugin configuration 42 | type pluginConfig struct { 43 | // Spec is the spec of the plugin 44 | Spec pluginSpec `yaml:"spec"` 45 | } 46 | 47 | // pluginSpec contains the config spec for the plugin. 48 | type pluginSpec struct { 49 | // Layout contains the layout direction 50 | Layout string `yaml:"layout"` 51 | 52 | // HighlightKinds contains the mapping between Kubernetes resource kind 53 | // and the color with which to paint it. 54 | HighlightKinds map[string]string `yaml:"highlightKinds"` 55 | 56 | // HighlightNamespace contains the mapping between Kubernetes namespace 57 | // and the color with which to paint all resources from the given 58 | // namespace. 59 | HighlightNamespaces map[string]string `yaml:"highlightNamespaces"` 60 | 61 | // DropKinds contains the resource kinds which will be dropped from the 62 | // graph. 63 | DropKinds []string `yaml:"dropKinds"` 64 | 65 | // DropNamespaces contains the list of namespaces to drop, along with 66 | // all resources from them. 67 | DropNamespaces []string `yaml:"dropNamespaces"` 68 | 69 | // KeepKinds contains the list of Kubernetes resources which will be 70 | // kept. Any other resource will be dropped. 71 | KeepKinds []string `yaml:"keepKinds"` 72 | 73 | // KeepNamespaces contains the list of namespaces to keep, along with 74 | // the resources from them. Anything else will be dropped. 75 | KeepNamespaces []string `yaml:"keepNamespace"` 76 | } 77 | 78 | // newPluginCommand returns the command for running kustomize-dot as KRM 79 | // Function plugin 80 | func newPluginCommand() *cli.Command { 81 | cmd := &cli.Command{ 82 | Name: "plugin", 83 | Usage: "run as KRM Function plugin", 84 | Aliases: []string{"p"}, 85 | Action: execPluginCommand, 86 | } 87 | 88 | return cmd 89 | } 90 | 91 | // execPluginCommand runs kustomize-dot as a KRM Function plugin. 92 | // 93 | // See [1] for more details about KRM Function plugins. 94 | // 95 | // [1]: https://kubectl.docs.kubernetes.io/guides/extending_kustomize/containerized_krm_functions/ 96 | func execPluginCommand(ctx *cli.Context) error { 97 | var config pluginConfig 98 | 99 | fn := func(items []*yaml.RNode) ([]*yaml.RNode, error) { 100 | opts := make([]parser.Option, 0) 101 | 102 | // Layout direction 103 | opts = append(opts, parser.WithLayoutDirection(parser.LayoutDirection(config.Spec.Layout))) 104 | 105 | // Highlight Resource Kinds 106 | for kind, color := range config.Spec.HighlightKinds { 107 | opts = append(opts, parser.WithHighlightKind(kind, color)) 108 | } 109 | 110 | // Highlight Namespaces 111 | for ns, color := range config.Spec.HighlightNamespaces { 112 | opts = append(opts, parser.WithHighlightNamespace(ns, color)) 113 | } 114 | 115 | // Drop Resource Kinds 116 | for _, kind := range config.Spec.DropKinds { 117 | opts = append(opts, parser.WithDropKind(kind)) 118 | } 119 | 120 | // Drop Namespaces 121 | for _, ns := range config.Spec.DropNamespaces { 122 | opts = append(opts, parser.WithDropNamespace(ns)) 123 | } 124 | 125 | // Keep Resource Kinds 126 | for _, kind := range config.Spec.KeepKinds { 127 | opts = append(opts, parser.WithKeepKind(kind)) 128 | } 129 | 130 | // Keep Namespaces 131 | for _, ns := range config.Spec.KeepNamespaces { 132 | opts = append(opts, parser.WithKeepNamespace(ns)) 133 | } 134 | 135 | // Parse resources and generate the graph 136 | resources, err := parser.ResourcesFromRNodes(items) 137 | if err != nil { 138 | return nil, fmt.Errorf("cannot parse resources: %w", err) 139 | } 140 | 141 | p := parser.New(opts...) 142 | g, err := p.Parse(resources) 143 | if err != nil { 144 | return nil, fmt.Errorf("cannot generate graph: %w", err) 145 | } 146 | 147 | var buf bytes.Buffer 148 | if err := graph.WriteDot(g, &buf); err != nil { 149 | return nil, err 150 | } 151 | 152 | // Return the transformed resources as a ConfigMap 153 | out, err := parser.NewResourceFactory().FromMapWithName( 154 | "kustomize-dot", 155 | map[string]any{ 156 | "apiVersion": "v1", 157 | "kind": "ConfigMap", 158 | "metadata": map[string]string{ 159 | "name": "kustomize-dot", 160 | "namespace": "default", 161 | }, 162 | "data": map[string]string{ 163 | "dot": buf.String(), 164 | }, 165 | }, 166 | ) 167 | if err != nil { 168 | return nil, err 169 | } 170 | 171 | return []*yaml.RNode{&out.RNode}, nil 172 | } 173 | 174 | processor := framework.SimpleProcessor{Config: &config, Filter: kio.FilterFunc(fn)} 175 | cmd := command.Build(processor, command.StandaloneDisabled, false) 176 | 177 | return cmd.Execute() 178 | } 179 | -------------------------------------------------------------------------------- /images/kube-prometheus-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 1374420488320 14 | 15 | default/role/prometheus-k8s 16 | 17 | 18 | 19 | 1374420488448 20 | 21 | manifests/prometheus-roleSpecificNamespaces.yaml 22 | 23 | 24 | 25 | 1374420488320->1374420488448 26 | 27 | 28 | https://github.com/prometheus-operator/kube-prometheus 29 | 30 | 31 | 32 | 1374420488512 33 | 34 | kube-system/role/prometheus-k8s 35 | 36 | 37 | 38 | 1374420488512->1374420488448 39 | 40 | 41 | https://github.com/prometheus-operator/kube-prometheus 42 | 43 | 44 | 45 | 1374420488576 46 | 47 | default/rolebinding/prometheus-k8s 48 | 49 | 50 | 51 | 1374420488640 52 | 53 | manifests/prometheus-roleBindingSpecificNamespaces.yaml 54 | 55 | 56 | 57 | 1374420488576->1374420488640 58 | 59 | 60 | https://github.com/prometheus-operator/kube-prometheus 61 | 62 | 63 | 64 | 1374420488704 65 | 66 | kube-system/rolebinding/prometheus-k8s 67 | 68 | 69 | 70 | 1374420488704->1374420488640 71 | 72 | 73 | https://github.com/prometheus-operator/kube-prometheus 74 | 75 | 76 | 77 | 1374420488768 78 | 79 | kube-system/rolebinding/resource-metrics-auth-reader 80 | 81 | 82 | 83 | 1374420488832 84 | 85 | manifests/prometheusAdapter-roleBindingAuthReader.yaml 86 | 87 | 88 | 89 | 1374420488768->1374420488832 90 | 91 | 92 | https://github.com/prometheus-operator/kube-prometheus 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /images/kube-prometheus-1.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 1374427654976 14 | 15 | manifests/prometheus-roleSpecificNamespaces.yaml 16 | 17 | 18 | 19 | 1374427655040 20 | 21 | kube-system/role/prometheus-k8s 22 | 23 | 24 | 25 | 1374427655040->1374427654976 26 | 27 | 28 | https://github.com/prometheus-operator/kube-prometheus 29 | 30 | 31 | 32 | 1374427655104 33 | 34 | default/rolebinding/prometheus-k8s 35 | 36 | 37 | 38 | 1374427655296 39 | 40 | manifests/prometheus-roleBindingSpecificNamespaces.yaml 41 | 42 | 43 | 44 | 1374427655104->1374427655296 45 | 46 | 47 | https://github.com/prometheus-operator/kube-prometheus 48 | 49 | 50 | 51 | 1374427655360 52 | 53 | kube-system/rolebinding/prometheus-k8s 54 | 55 | 56 | 57 | 1374427655360->1374427655296 58 | 59 | 60 | https://github.com/prometheus-operator/kube-prometheus 61 | 62 | 63 | 64 | 1374427655552 65 | 66 | kube-system/rolebinding/resource-metrics-auth-reader 67 | 68 | 69 | 70 | 1374427655616 71 | 72 | manifests/prometheusAdapter-roleBindingAuthReader.yaml 73 | 74 | 75 | 76 | 1374427655552->1374427655616 77 | 78 | 79 | https://github.com/prometheus-operator/kube-prometheus 80 | 81 | 82 | 83 | 1374427654848 84 | 85 | default/role/prometheus-k8s 86 | 87 | 88 | 89 | 1374427654848->1374427654976 90 | 91 | 92 | https://github.com/prometheus-operator/kube-prometheus 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 2 | github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= 3 | github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 4 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 8 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/go-echarts/go-echarts/v2 v2.5.4 h1:bw0REczgtgI/o7GPqae4AzsiJwwyJvyWwJ7vuM0G6tQ= 10 | github.com/go-echarts/go-echarts/v2 v2.5.4/go.mod h1:56YlvzhW/a+du15f3S2qUGNDfKnFOeJSThBIrVFHDtI= 11 | github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= 12 | github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= 13 | github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 14 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 15 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 16 | github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= 17 | github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= 18 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 19 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 20 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 21 | github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= 22 | github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= 23 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 24 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 25 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 26 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 27 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 28 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 29 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 30 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 31 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 32 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 33 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 34 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 35 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 36 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 37 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 38 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 39 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 40 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 41 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 42 | github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= 43 | github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= 44 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 45 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 46 | github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= 47 | github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 48 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 49 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 50 | github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= 51 | github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= 52 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 53 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 54 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 55 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 56 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 57 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 58 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 59 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 60 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 61 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 62 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 63 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 64 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 65 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 66 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 67 | github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= 68 | github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= 69 | github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= 70 | github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= 71 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= 72 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= 73 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 74 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 75 | go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= 76 | go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= 77 | go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE= 78 | go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI= 79 | golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= 80 | golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 81 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 82 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 83 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 84 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 85 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 86 | gopkg.in/dnaeon/go-deque.v1 v1.0.0-20250203064611-7d48f7299755 h1:wB8d7G8z0rrFgS/Wr16pBVL3MxFp8M+/fTJGcBAxn1k= 87 | gopkg.in/dnaeon/go-deque.v1 v1.0.0-20250203064611-7d48f7299755/go.mod h1:mAZ2p0oPgDAeRlUKq3IJOE9RvqVvA3BIA4WKxG/9zDM= 88 | gopkg.in/dnaeon/go-graph.v1 v1.0.2 h1:OHShBkXqG65QSQwU0/CQfcqMvJh/+XoR7gZiPprtEVc= 89 | gopkg.in/dnaeon/go-graph.v1 v1.0.2/go.mod h1:N9qN2W5CwdzCVmrqtLPmQ3HawuV81HBhuIyUJiMoXT0= 90 | gopkg.in/dnaeon/go-priorityqueue.v1 v1.1.1 h1:AS4IIyStH/47cZpPdBk/Db6QXg+UmcDjZvSOoDgGsbU= 91 | gopkg.in/dnaeon/go-priorityqueue.v1 v1.1.1/go.mod h1:JNUtwj2QQsBHhsIHNjxdDaSmLW4RZtvJHO8VYtsMkY4= 92 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 93 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 94 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 95 | k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 h1:hcha5B1kVACrLujCKLbr8XWMxCxzQx42DY8QKYJrDLg= 96 | k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7/go.mod h1:GewRfANuJ70iYzvn+i4lezLDAFzvjxZYK1gn1lWcfas= 97 | k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= 98 | k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 99 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= 100 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= 101 | sigs.k8s.io/kustomize/api v0.21.0 h1:I7nry5p8iDJbuRdYS7ez8MUvw7XVNPcIP5GkzzuXIIQ= 102 | sigs.k8s.io/kustomize/api v0.21.0/go.mod h1:XGVQuR5n2pXKWbzXHweZU683pALGw/AMVO4zU4iS8SE= 103 | sigs.k8s.io/kustomize/kyaml v0.21.0 h1:7mQAf3dUwf0wBerWJd8rXhVcnkk5Tvn/q91cGkaP6HQ= 104 | sigs.k8s.io/kustomize/kyaml v0.21.0/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ= 105 | sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ= 106 | sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4= 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kustomize-dot 2 | 3 | [![Build Status](https://github.com/dnaeon/kustomize-dot/actions/workflows/test.yaml/badge.svg)](https://github.com/dnaeon/kustomize-dot/actions/workflows/test.yaml/badge.svg) 4 | [![Go Reference](https://pkg.go.dev/badge/github.com/dnaeon/kustomize-dot.svg)](https://pkg.go.dev/github.com/dnaeon/kustomize-dot) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/dnaeon/kustomize-dot)](https://goreportcard.com/report/github.com/dnaeon/kustomize-dot) 6 | [![codecov](https://codecov.io/gh/dnaeon/kustomize-dot/branch/master/graph/badge.svg)](https://codecov.io/gh/dnaeon/kustomize-dot) 7 | 8 | `kustomize-dot` is a CLI and [kustomize](https://kustomize.io/) 9 | [KRM Function plugin](https://github.com/kubernetes-sigs/kustomize/blob/master/cmd/config/docs/api-conventions/functions-spec.md), 10 | which generates a graph of Kubernetes resources and their origin. 11 | 12 | ![Hello World](./images/hello-world.svg) 13 | 14 | # Requirements 15 | 16 | * Go version 1.22.x or later 17 | * Docker for local development 18 | * [kustomize](https://kustomize.io/) for building manifests 19 | 20 | # Installation 21 | 22 | There are multiple ways to install `kustomize-dot`. 23 | 24 | In order to build `kustomize-dot` from the latest repo sources execute the 25 | following command. 26 | 27 | ``` shell 28 | make build 29 | ``` 30 | 31 | If you prefer installing `kustomize-dot` using `go install` execute the 32 | following command instead. 33 | 34 | ``` shell 35 | go install github.com/dnaeon/kustomize-dot/cmd/kustomize-dot@latest 36 | ``` 37 | 38 | Build a Docker image of `kustomize-dot`. 39 | 40 | ``` shell 41 | make docker-build 42 | ``` 43 | 44 | # Usage 45 | 46 | `kustomize-dot` can operate in two modes - as a standalone CLI application, or 47 | as a 48 | [KRM Function plugin](https://kubectl.docs.kubernetes.io/guides/extending_kustomize/containerized_krm_functions/). 49 | 50 | In order to generate a graph of the Kubernetes resources and their origin when 51 | building a kustomization target we need to enable the `originAnnotations` build 52 | option in our `kustomization.yaml` file. 53 | 54 | ``` yaml 55 | buildMetadata: 56 | - originAnnotations 57 | ``` 58 | 59 | ## CLI 60 | 61 | The following section provides some examples on how to use `kustomize-dot` as a 62 | standalone CLI app. 63 | 64 | The following example is based on the official 65 | [kustomize helloWorld example](https://github.com/kubernetes-sigs/kustomize/tree/master/examples/helloWorld). 66 | 67 | ``` yaml 68 | --- 69 | apiVersion: kustomize.config.k8s.io/v1beta1 70 | kind: Kustomization 71 | metadata: 72 | name: hello-world 73 | 74 | buildMetadata: 75 | - originAnnotations 76 | 77 | resources: 78 | - https://github.com/kubernetes-sigs/kustomize//examples/helloWorld/?timeout=120 79 | ``` 80 | 81 | In order to generate the graph we can build the manifests using `kustomize 82 | build`, pipe the resources to `kustomize-dot` for generating the [Dot 83 | representation](https://graphviz.org/doc/info/lang.html), and finally pipe the 84 | result to `dot` for rendering the graph. 85 | 86 | The [fixtures package](./pkg/fixtures) contains ready to render resources, which 87 | have already been built using `kustomize build`. The following command will 88 | render the graph for the [kustomize helloWorld 89 | example](https://github.com/kubernetes-sigs/kustomize/tree/master/examples/helloWorld). 90 | 91 | ``` shell 92 | kustomize-dot generate -f pkg/fixtures/hello-world.yaml | \ 93 | dot -T svg -o graph.svg 94 | ``` 95 | 96 | Or you could execute the following command instead, which will generate the same 97 | graph. 98 | 99 | ``` shell 100 | kustomize build examples/hello-world | \ 101 | kustomize-dot generate -f - | \ 102 | dot -T svg -o graph.svg 103 | ``` 104 | 105 | The following example builds the graph of resources for 106 | [kube-prometheus operator](https://github.com/prometheus-operator/kube-prometheus). 107 | 108 | ``` shell 109 | kustomize-dot generate -f pkg/fixtures/kube-prometheus.yaml 110 | ``` 111 | 112 | The [resulting graph is big](./images/kube-prometheus-full.svg) enough to be 113 | confusing. 114 | 115 | `kustomize-dot` is flexible and supports filtering of resources, highlighting of 116 | resources or whole namespaces, setting graph layout direction, etc. This is 117 | useful when we want to get a more focused view of the resulting graph. 118 | 119 | For example the following graph will _keep_ only resources from the `default` 120 | and `kube-system` namespaces. 121 | 122 | ``` shell 123 | kustomize-dot generate -f pkg/fixtures/kube-prometheus.yaml \ 124 | --keep-namespace default \ 125 | --keep-namespace kube-system 126 | ``` 127 | 128 | The result looks like this. 129 | 130 | ![kube-prometheus-1](./images/kube-prometheus-1.svg) 131 | 132 | We can also highlight the resources from the different namespaces, e.g. 133 | 134 | ```shell 135 | kustomize-dot generate -f pkg/fixtures/kube-prometheus.yaml \ 136 | --keep-namespace default \ 137 | --keep-namespace kube-system \ 138 | --highlight-namespace default=pink \ 139 | --highlight-namespace kube-system=yellow 140 | ``` 141 | 142 | This is what the graph above looks like when we've applied highlighting to it. 143 | 144 | ![kube-prometheus-2](./images/kube-prometheus-2.svg) 145 | 146 | The following example will keep only the `ConfigMap` resources from the 147 | `monitoring` namespace. 148 | 149 | ```shell 150 | kustomize-dot generate -f pkg/fixtures/kube-prometheus.yaml \ 151 | --keep-namespace monitoring \ 152 | --keep-kind ConfigMap 153 | ``` 154 | 155 | And this is what the graph for the `ConfigMap` resources looks like. 156 | 157 | ![kube-prometheus-3](./images/kube-prometheus-3.svg) 158 | 159 | The `--keep-kind`, `--keep-namespace`, `--drop-kind`, `--drop-namespace`, 160 | `--highlight-kind` and `--highlight-namespace` options may be repeated any 161 | number of times, which allows the filters to be applied on many resource kinds 162 | and namespaces. 163 | 164 | This example keeps resources from the `monitoring` namespace only, but drops all 165 | `ConfigMap` resources from it, and then highlights various kinds with different 166 | colors. 167 | 168 | ``` shell 169 | kustomize-dot generate -f pkg/fixtures/kube-prometheus.yaml \ 170 | --keep-namespace monitoring \ 171 | --drop-kind ConfigMap \ 172 | --highlight-kind service=yellow \ 173 | --highlight-kind servicemonitor=orange \ 174 | --highlight-kind serviceaccount=lightgray \ 175 | --highlight-kind deployment=magenta \ 176 | --highlight-kind prometheusrule=lightgreen \ 177 | --highlight-kind networkpolicy=cyan 178 | ``` 179 | 180 | The resulting graph looks like this. 181 | 182 | ![kube-prometheus-4](./images/kube-prometheus-4.svg) 183 | 184 | ## KRM Function 185 | 186 | When `kustomize-dot` is invoked as a [KRM Function 187 | plugin](https://kubectl.docs.kubernetes.io/guides/extending_kustomize/containerized_krm_functions/) 188 | it acts as a transformer in accordance to the [KRM Function 189 | spec](https://github.com/kubernetes-sigs/kustomize/blob/master/cmd/config/docs/api-conventions/functions-spec.md), 190 | which accepts a `ResourceList` as input on `stdin` and outputs a single 191 | `ConfigMap` with the [Dot 192 | representation](https://graphviz.org/doc/info/lang.html) of the resources and 193 | their origin on `stdout`. 194 | 195 | The KRM Function supports the same features as the CLI application, allowing the 196 | user to filter out specific resources, set graph layout and highlight resources 197 | and namespaces. 198 | 199 | The following is an example configuration for the `kustomize-dot` KRM Function 200 | plugin. You can find this example in the 201 | [examples/kube-prometheus-transformer](./examples/kube-prometheus-transformer) 202 | directory of this repo. 203 | 204 | ``` yaml 205 | # transformer.yaml 206 | --- 207 | apiVersion: dnaeon.github.io/v1 208 | kind: KustomizeDot 209 | metadata: 210 | name: kustomize-dot 211 | annotations: 212 | config.kubernetes.io/function: | 213 | container: 214 | image: dnaeon/kustomize-dot:latest 215 | spec: 216 | # Graph layout direction - TB, BT, LR or RL 217 | layout: LR 218 | 219 | # Highlight resources of a given kind with the specified color 220 | highlightKinds: 221 | Deployment: green 222 | Service: yellow 223 | Role: pink 224 | 225 | # Highlight all resources from a given namespace with the specified color 226 | highlightNamespaces: 227 | default: orange 228 | kube-system: pink 229 | 230 | # Drop specified resources from the graph 231 | dropKinds: 232 | # - ConfigMap 233 | # - RoleBinding 234 | 235 | # Drop all resources from the specified namespaces 236 | dropNamespaces: 237 | - foo 238 | - bar 239 | 240 | # Keep the specified resources only and drop anything else 241 | keepKinds: 242 | # - baz 243 | # - qux 244 | 245 | # Keep the resources from the specified namespaces only, and drop anything 246 | # else. 247 | keepNamespaces: 248 | # - monitoring 249 | ``` 250 | 251 | And this is an example kustomization file, which uses our KRM Function plugin as 252 | a transformer. 253 | 254 | ``` yaml 255 | # kustomization.yaml 256 | --- 257 | apiVersion: kustomize.config.k8s.io/v1beta1 258 | kind: Kustomization 259 | metadata: 260 | name: kube-prometheus 261 | 262 | buildMetadata: 263 | - originAnnotations 264 | 265 | resources: 266 | - https://github.com/prometheus-operator/kube-prometheus// 267 | 268 | transformers: 269 | - transformer.yaml 270 | ``` 271 | 272 | The following command will build the manifests and then pass them to our plugin, 273 | which will generate the Dot representation of the resources. The output will 274 | contain a single `ConfigMap` named `kustomize-dot`, whose data is the actual 275 | `dot` representation of the graph. 276 | 277 | ``` shell 278 | kustomize build --enable-alpha-plugins examples/kube-prometheus-transformer 279 | ``` 280 | 281 | Or you can pipe the output directly to `dot(1)` and render the graph, e.g. 282 | 283 | ``` shell 284 | kustomize build --enable-alpha-plugins examples/kube-prometheus-transformer | \ 285 | yq '.data.dot' | \ 286 | dot -Tsvg -o graph.svg 287 | ``` 288 | 289 | # Tests 290 | 291 | Run the tests. 292 | 293 | ``` shell 294 | make test 295 | ``` 296 | 297 | Run test coverage. 298 | 299 | ``` shell 300 | make test-cover 301 | ``` 302 | 303 | # Contributing 304 | 305 | `kustomize-dot` is hosted on 306 | [Github](https://github.com/dnaeon/kustomize-dot). Please contribute by 307 | reporting issues, suggesting features or by sending patches using pull requests. 308 | 309 | # License 310 | 311 | `kustomize-dot` is Open Source and licensed under the [BSD 312 | License](http://opensource.org/licenses/BSD-2-Clause). 313 | -------------------------------------------------------------------------------- /pkg/parser/parser.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Marin Atanasov Nikolov 2 | // All rights reserved. 3 | // 4 | // Redistribution and use in source and binary forms, with or without 5 | // modification, are permitted provided that the following conditions 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer. 10 | // 2. Redistributions in binary form must reproduce the above copyright 11 | // notice, this list of conditions and the following disclaimer in the 12 | // documentation and/or other materials provided with the distribution. 13 | // 14 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 18 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 19 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 20 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 21 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 22 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 23 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | // POSSIBILITY OF SUCH DAMAGE. 25 | 26 | // Package parser provides utilities for generating dependency graphs by parsing 27 | // Kubernetes resources produced by kustomize build. 28 | package parser 29 | 30 | import ( 31 | "fmt" 32 | "io" 33 | "os" 34 | "strings" 35 | 36 | "gopkg.in/dnaeon/go-graph.v1" 37 | "sigs.k8s.io/kustomize/api/provider" 38 | "sigs.k8s.io/kustomize/api/resource" 39 | "sigs.k8s.io/kustomize/kyaml/yaml" 40 | ) 41 | 42 | // notClonedPrefix is the prefix added by kustomize for the origin annotation, 43 | // which will be stripped when we generate the graph. 44 | const notClonedPrefix = "notCloned/" 45 | 46 | // LayoutDirection is a type which represents the direction of the graph layout. 47 | type LayoutDirection string 48 | 49 | // String implements the [fmt.Stringer] interface 50 | func (ld LayoutDirection) String() string { 51 | return string(ld) 52 | } 53 | 54 | const ( 55 | // LayoutDirectionTB specifies Top-to-Botton layout 56 | LayoutDirectionTB LayoutDirection = "TB" 57 | 58 | // LayoutDirectionBT specifies Botton-to-Top layout 59 | LayoutDirectionBT LayoutDirection = "BT" 60 | 61 | // LayoutDirectionLR specifies Left-to-Right layout 62 | LayoutDirectionLR LayoutDirection = "LR" 63 | 64 | // LayoutDirectionRL specifies Right-to-Left layout 65 | LayoutDirectionRL LayoutDirection = "RL" 66 | ) 67 | 68 | // NewDepProvider creates a new [provider.DepProvider]. 69 | func NewDepProvider() *provider.DepProvider { 70 | return provider.NewDefaultDepProvider() 71 | } 72 | 73 | // NewResourceFactory creates a new [resource.Factory] 74 | func NewResourceFactory() *resource.Factory { 75 | return NewDepProvider().GetResourceFactory() 76 | } 77 | 78 | // ResourcesFromBytes returns the list of [resource.Resource] items contained 79 | // within the given data. 80 | func ResourcesFromBytes(data []byte) ([]*resource.Resource, error) { 81 | return NewResourceFactory().SliceFromBytes(data) 82 | } 83 | 84 | // ResourcesFromRNodes returns the list of [resource.Resource] items represented 85 | // by the given sequence of [yaml.RNode] items. 86 | func ResourcesFromRNodes(items []*yaml.RNode) ([]*resource.Resource, error) { 87 | return NewResourceFactory().ResourcesFromRNodes(items) 88 | } 89 | 90 | // ResourcesFromPath returns the list of [resource.Resource] items by parsing 91 | // the Kubernetes resources from the given path. 92 | func ResourcesFromPath(path string) ([]*resource.Resource, error) { 93 | data, err := os.ReadFile(path) 94 | if err != nil { 95 | return nil, err 96 | } 97 | 98 | return ResourcesFromBytes(data) 99 | } 100 | 101 | // ResourcesFromReader returns the list of [resource.Resource] items by parsing 102 | // the Kubernetes resource from the given [io.Reader]. 103 | func ResourcesFromReader(r io.Reader) ([]*resource.Resource, error) { 104 | data, err := io.ReadAll(r) 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | return ResourcesFromBytes(data) 110 | } 111 | 112 | // Parser knows how to parse a sequence of [resource.Resource] items and build a 113 | // dependency graph out of it. 114 | // 115 | // The vertices in the graph represent the [resource.Resource] instances, which 116 | // are connected via edges to their origins. 117 | type Parser struct { 118 | // highlightKindMap contains mappings between Kubernetes resource kinds 119 | // and the color with which to paint resources with the respective kind. 120 | highlightKindMap map[string]string 121 | 122 | // highlightNamespaceMap contains the mapping between Kubernetes 123 | // namespaces and the color with which to paint all resources from the 124 | // respective namespace. 125 | highlightNamespaceMap map[string]string 126 | 127 | // layoutDirection specifies the direction of graph layout. 128 | layoutDirection LayoutDirection 129 | 130 | // dropResourceKinds contains the list of resource kinds, which will be 131 | // dropped from the resulting graph. 132 | dropResourceKinds []string 133 | 134 | // dropNamespaces contains the list of namespaces, from which 135 | // any resource in the specified namespaces will be dropped from the 136 | // resulting graph. 137 | dropNamespaces []string 138 | 139 | // keepResourceKinds contains the list of resource kinds, which will be 140 | // kept. Any other resource kind will be dropped from the resulting 141 | // graph. 142 | keepResourceKinds []string 143 | 144 | // keepNamespaces contains the list of namespaces, from which resources 145 | // will be kept. Any resource, which is not in the specified namespaces 146 | // will be dropped. 147 | keepNamespaces []string 148 | } 149 | 150 | // New creates a new [Parser] and configures it using the specified options. 151 | func New(opts ...Option) *Parser { 152 | p := &Parser{ 153 | highlightKindMap: make(map[string]string), 154 | highlightNamespaceMap: make(map[string]string), 155 | layoutDirection: LayoutDirectionLR, 156 | dropResourceKinds: make([]string, 0), 157 | dropNamespaces: make([]string, 0), 158 | keepResourceKinds: make([]string, 0), 159 | keepNamespaces: make([]string, 0), 160 | } 161 | 162 | for _, opt := range opts { 163 | opt(p) 164 | } 165 | 166 | return p 167 | } 168 | 169 | // Option is a function which configures the [Parser]. 170 | type Option func(p *Parser) 171 | 172 | // WithHighlightKind is an [Option] which configures the [Parser] to paint resources 173 | // with the given Kubernetes Resource Kind with the specified color. 174 | func WithHighlightKind(kind string, color string) Option { 175 | opt := func(p *Parser) { 176 | p.highlightKindMap[strings.ToLower(kind)] = color 177 | } 178 | 179 | return opt 180 | } 181 | 182 | // WithHighlightNamespace is an [Option] which configures the [Parser] to paint 183 | // all resources from the given namespace with the specified color. 184 | func WithHighlightNamespace(namespace string, color string) Option { 185 | opt := func(p *Parser) { 186 | p.highlightNamespaceMap[strings.ToLower(namespace)] = color 187 | } 188 | 189 | return opt 190 | } 191 | 192 | // WithLayoutDirection is an [Option] which configures the [Parser] to generate 193 | // the graph with the specified direction. 194 | func WithLayoutDirection(layout LayoutDirection) Option { 195 | opt := func(p *Parser) { 196 | p.layoutDirection = layout 197 | } 198 | 199 | return opt 200 | } 201 | 202 | // WithDropKind is an [Option], which configures the [Parser] to drop the 203 | // specified Kubernetes resource kind from the resulting graph. 204 | func WithDropKind(kind string) Option { 205 | opt := func(p *Parser) { 206 | p.dropResourceKinds = append(p.dropResourceKinds, strings.ToLower(kind)) 207 | } 208 | 209 | return opt 210 | } 211 | 212 | // WithKeepKind is an [Option], which configures the [Parser] to keep only 213 | // resources of the given kind. Any other resource kind will be dropped from the 214 | // resulting graph. 215 | func WithKeepKind(kind string) Option { 216 | opt := func(p *Parser) { 217 | p.keepResourceKinds = append(p.keepResourceKinds, strings.ToLower(kind)) 218 | } 219 | 220 | return opt 221 | } 222 | 223 | // WithDropNamespace is an [Option], which configures the [Parser] to drop all 224 | // resources from the specified namespace. 225 | func WithDropNamespace(namespace string) Option { 226 | opt := func(p *Parser) { 227 | p.dropNamespaces = append(p.dropNamespaces, strings.ToLower(namespace)) 228 | } 229 | 230 | return opt 231 | } 232 | 233 | // WithKeepNamespace is an [Option], which configures the [Parser] to keep only 234 | // resources from the specified namespace. Any other resource will be dropped 235 | // from the resulting graph. 236 | func WithKeepNamespace(namespace string) Option { 237 | opt := func(p *Parser) { 238 | p.keepNamespaces = append(p.keepNamespaces, strings.ToLower(namespace)) 239 | } 240 | 241 | return opt 242 | } 243 | 244 | // Parse parses the given sequence of [resource.Resource] items in order to 245 | // generate a directed [graph.Graph]. 246 | func (p *Parser) Parse(resources []*resource.Resource) (graph.Graph[string], error) { 247 | g := graph.New[string](graph.KindDirected) 248 | 249 | for _, r := range resources { 250 | if p.shouldDropResource(r) { 251 | continue 252 | } 253 | 254 | // Add u to the graph, and paint the vertex 255 | uName := p.vertexNameFromResource(r) 256 | u := g.AddVertex(uName) 257 | p.applyHighlights(u, r) 258 | 259 | // Add v to the graph, which represents the resource origin 260 | origin, err := r.GetOrigin() 261 | if err != nil { 262 | return nil, err 263 | } 264 | 265 | // No origin metadata found, skip it 266 | if origin == nil { 267 | continue 268 | } 269 | 270 | vName := p.vertexNameFromOrigin(origin) 271 | g.AddVertex(vName) 272 | 273 | e := g.AddEdge(uName, vName) 274 | label := p.edgeLabelFromOrigin(origin) 275 | e.DotAttributes["label"] = label 276 | } 277 | 278 | // Set direction of graph layout and other graph-specific attributes 279 | graphAttrs := g.GetDotAttributes() 280 | graphAttrs["rankdir"] = p.layoutDirection.String() 281 | 282 | return g, nil 283 | } 284 | 285 | // shouldDropResource is a predicate, which returns true, if the resource is to 286 | // be dropped from the graph, and returns false otherwise. 287 | func (p *Parser) shouldDropResource(r *resource.Resource) bool { 288 | kind := strings.ToLower(r.GetKind()) 289 | namespace := strings.ToLower(r.GetNamespace()) 290 | gvk := r.GetGvk() 291 | 292 | // Drop resource, if it is part of any drop-namespaces 293 | for _, dn := range p.dropNamespaces { 294 | if namespace == dn { 295 | return true 296 | } 297 | } 298 | 299 | // Drop resource, if it is part of any drop-resource-kinds 300 | for _, drk := range p.dropResourceKinds { 301 | if kind == drk { 302 | return true 303 | } 304 | } 305 | 306 | // Drop resources, if they are outside of the configured keep-namespaces 307 | keepNamespaceIsSet := false 308 | keepKindIsSet := false 309 | foundKeepNamespace := false 310 | foundKeepKind := false 311 | if len(p.keepNamespaces) > 0 && !gvk.IsClusterScoped() { 312 | keepNamespaceIsSet = true 313 | for _, kn := range p.keepNamespaces { 314 | if namespace == kn { 315 | foundKeepNamespace = true 316 | break 317 | } 318 | } 319 | } 320 | 321 | // Drop resources, if they are not part of the configured 322 | // keep-resource-kinds. 323 | if len(p.keepResourceKinds) > 0 { 324 | keepKindIsSet = true 325 | for _, krk := range p.keepResourceKinds { 326 | if kind == krk { 327 | foundKeepKind = true 328 | break 329 | } 330 | } 331 | } 332 | 333 | switch { 334 | case keepNamespaceIsSet && !foundKeepNamespace: 335 | // Resource is not part of the keep-namespaces, so drop it. 336 | return true 337 | case keepNamespaceIsSet && keepKindIsSet && foundKeepNamespace && !foundKeepKind: 338 | // Resource is part of the keep-namespaces, but not part of the 339 | // keep-resource-kinds, so drop it. 340 | return true 341 | case keepKindIsSet && !foundKeepKind: 342 | // Resource is not part of the keep-resource-kinds, so drop it 343 | return true 344 | default: 345 | // Don't drop the resource 346 | return false 347 | } 348 | } 349 | 350 | // applyHighlights applies the highlight styles to the [graph.Vertex] u for 351 | // [resource.Resource] r. 352 | func (p *Parser) applyHighlights(u *graph.Vertex[string], r *resource.Resource) { 353 | // First we paint resources by namespace 354 | namespace := strings.ToLower(r.GetNamespace()) 355 | kind := strings.ToLower(r.GetKind()) 356 | 357 | namespaceColor, ok := p.highlightNamespaceMap[namespace] 358 | if ok { 359 | u.DotAttributes["color"] = namespaceColor 360 | u.DotAttributes["fillcolor"] = namespaceColor 361 | } 362 | 363 | // Then we paint resources by kind 364 | kindColor, ok := p.highlightKindMap[kind] 365 | if ok { 366 | u.DotAttributes["color"] = kindColor 367 | u.DotAttributes["fillcolor"] = kindColor 368 | } 369 | } 370 | 371 | // vertexNameFromResource returns a string representing the vertex name for the 372 | // given [resource.Resource]. 373 | func (p *Parser) vertexNameFromResource(r *resource.Resource) string { 374 | namespace := r.GetNamespace() 375 | name := r.GetName() 376 | gvk := r.GetGvk() 377 | kind := strings.ToLower(r.GetKind()) 378 | 379 | // Cluster-scoped resource 380 | if gvk.IsClusterScoped() { 381 | return fmt.Sprintf("%s/%s", kind, name) 382 | } 383 | 384 | // Namespace-scoped resource 385 | return fmt.Sprintf("%s/%s/%s", namespace, kind, name) 386 | } 387 | 388 | // vertexNameFromOrigin returns a string representing the vertex name for the 389 | // given [resource.Origin]. 390 | func (p *Parser) vertexNameFromOrigin(origin *resource.Origin) string { 391 | path := origin.Path 392 | path = strings.TrimPrefix(path, notClonedPrefix) 393 | switch { 394 | case origin.ConfiguredIn != "": 395 | // Generator or transformer created resource 396 | return origin.ConfiguredIn 397 | default: 398 | return path 399 | } 400 | } 401 | 402 | // edgeLabelFromOrigin returns a string to be used as an edge label. 403 | func (p *Parser) edgeLabelFromOrigin(origin *resource.Origin) string { 404 | switch { 405 | case origin.ConfiguredIn != "": 406 | // Generator or transformer created resource 407 | return fmt.Sprintf("%s/%s", origin.ConfiguredBy.APIVersion, origin.ConfiguredBy.Kind) 408 | case origin.Repo != "": 409 | // Remote resource 410 | if origin.Ref != "" { 411 | return fmt.Sprintf("%s (ref %s)", origin.Repo, origin.Ref) 412 | } 413 | return origin.Repo 414 | default: 415 | // Local resource 416 | return "" 417 | } 418 | } 419 | -------------------------------------------------------------------------------- /pkg/parser/parser_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Marin Atanasov Nikolov 2 | // All rights reserved. 3 | // 4 | // Redistribution and use in source and binary forms, with or without 5 | // modification, are permitted provided that the following conditions 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer. 10 | // 2. Redistributions in binary form must reproduce the above copyright 11 | // notice, this list of conditions and the following disclaimer in the 12 | // documentation and/or other materials provided with the distribution. 13 | // 14 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 18 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 19 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 20 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 21 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 22 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 23 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | // POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package parser 27 | 28 | import ( 29 | "errors" 30 | "fmt" 31 | "maps" 32 | "strings" 33 | "testing" 34 | 35 | "github.com/dnaeon/kustomize-dot/pkg/fixtures" 36 | 37 | "sigs.k8s.io/kustomize/api/resource" 38 | "sigs.k8s.io/kustomize/kyaml/yaml" 39 | ) 40 | 41 | func TestParseResources(t *testing.T) { 42 | type testCase struct { 43 | data string 44 | desc string 45 | wantResources int 46 | wantError error 47 | } 48 | 49 | testCases := []testCase{ 50 | { 51 | desc: "empty data", 52 | data: "", 53 | wantResources: 0, 54 | wantError: nil, 55 | }, 56 | { 57 | desc: "single resource data", 58 | data: ` 59 | apiVersion: v1 60 | data: 61 | altGreeting: Good Morning! 62 | enableRisky: "false" 63 | kind: ConfigMap 64 | metadata: 65 | annotations: 66 | config.kubernetes.io/origin: | 67 | path: examples/helloWorld/configMap.yaml 68 | repo: https://github.com/kubernetes-sigs/kustomize 69 | ref: v1.0.6 70 | labels: 71 | app: hello 72 | name: the-map 73 | `, 74 | wantError: nil, 75 | wantResources: 1, 76 | }, 77 | { 78 | desc: "hello world resources", 79 | data: fixtures.HelloWorld, 80 | wantResources: 3, 81 | wantError: nil, 82 | }, 83 | { 84 | desc: "kube prometheus resources", 85 | data: fixtures.KubePrometheus, 86 | wantResources: 124, 87 | wantError: nil, 88 | }, 89 | { 90 | desc: "bad data", 91 | data: "some bad data in here", 92 | wantResources: 0, 93 | wantError: yaml.ErrMissingMetadata, 94 | }, 95 | } 96 | 97 | for _, tc := range testCases { 98 | // Parse resources from bytes 99 | t.Run(fmt.Sprintf("ResourcesFromBytes with %s", tc.desc), func(t *testing.T) { 100 | gotResources, err := ResourcesFromBytes([]byte(tc.data)) 101 | if !errors.Is(err, tc.wantError) { 102 | t.Fatalf("want %v error, got %v", tc.wantError, err) 103 | } 104 | 105 | if len(gotResources) != tc.wantResources { 106 | t.Fatalf("got %d resource(s), want %d", len(gotResources), tc.wantResources) 107 | } 108 | }) 109 | 110 | t.Run(fmt.Sprintf("parser.ResourcesFromReader with %s", tc.desc), func(t *testing.T) { 111 | reader := strings.NewReader(tc.data) 112 | gotResources, err := ResourcesFromReader(reader) 113 | if !errors.Is(err, tc.wantError) { 114 | t.Fatalf("want %v error, got %v", tc.wantError, err) 115 | } 116 | 117 | if len(gotResources) != tc.wantResources { 118 | t.Fatalf("got %d resource(s), want %d", len(gotResources), tc.wantResources) 119 | } 120 | }) 121 | } 122 | } 123 | 124 | func TestVertexNameAndEdgeLabelFromOrigin(t *testing.T) { 125 | type testCase struct { 126 | desc string 127 | wantEdgeLabel string 128 | wantVertexName string 129 | origin *resource.Origin 130 | } 131 | 132 | testCases := []testCase{ 133 | { 134 | desc: "local resource", 135 | wantVertexName: "foo.yaml", 136 | wantEdgeLabel: "", 137 | origin: &resource.Origin{ 138 | Path: "foo.yaml", 139 | }, 140 | }, 141 | { 142 | desc: "generator / transformer created resource", 143 | wantVertexName: "foo", 144 | wantEdgeLabel: "v1/my-generator", 145 | origin: &resource.Origin{ 146 | Path: "foo.yaml", 147 | ConfiguredIn: "foo", 148 | ConfiguredBy: yaml.ResourceIdentifier{ 149 | TypeMeta: yaml.TypeMeta{ 150 | APIVersion: "v1", 151 | Kind: "my-generator", 152 | }, 153 | }, 154 | }, 155 | }, 156 | { 157 | desc: "remote resource without ref", 158 | wantVertexName: "foo.yaml", 159 | wantEdgeLabel: "github.com/dnaeon/kustomize-dot", 160 | origin: &resource.Origin{ 161 | Repo: "github.com/dnaeon/kustomize-dot", 162 | Path: "foo.yaml", 163 | }, 164 | }, 165 | { 166 | desc: "remote resource with ref", 167 | wantVertexName: "foo.yaml", 168 | wantEdgeLabel: "github.com/dnaeon/kustomize-dot (ref v1)", 169 | origin: &resource.Origin{ 170 | Repo: "github.com/dnaeon/kustomize-dot", 171 | Ref: "v1", 172 | Path: "foo.yaml", 173 | }, 174 | }, 175 | } 176 | 177 | p := New() 178 | for _, tc := range testCases { 179 | t.Run(tc.desc, func(t *testing.T) { 180 | gotVertexName := p.vertexNameFromOrigin(tc.origin) 181 | if gotVertexName != tc.wantVertexName { 182 | t.Fatalf("want vertex name %q, got name %q", tc.wantVertexName, gotVertexName) 183 | } 184 | 185 | gotEdgeLabel := p.edgeLabelFromOrigin(tc.origin) 186 | if gotEdgeLabel != tc.wantEdgeLabel { 187 | t.Fatalf("want edge label %q, got label %q", tc.wantEdgeLabel, gotEdgeLabel) 188 | } 189 | }) 190 | } 191 | } 192 | 193 | func TestVertexNameFromResource(t *testing.T) { 194 | configMap, err := NewResourceFactory().FromMapWithName( 195 | "kustomize-dot", 196 | map[string]any{ 197 | "apiVersion": "v1", 198 | "kind": "ConfigMap", 199 | "metadata": map[string]string{ 200 | "name": "kustomize-dot", 201 | "namespace": "default", 202 | }, 203 | "data": map[string]string{ 204 | "foo": "bar", 205 | }, 206 | }, 207 | ) 208 | if err != nil { 209 | t.Fatal("failed to create ConfigMap resource") 210 | } 211 | 212 | namespace, err := NewResourceFactory().FromMapWithName( 213 | "default", 214 | map[string]any{ 215 | "apiVersion": "v1", 216 | "kind": "Namespace", 217 | "metadata": map[string]string{ 218 | "name": "default", 219 | }, 220 | }, 221 | ) 222 | if err != nil { 223 | t.Fatal("failed to create Namespace resource") 224 | } 225 | 226 | type testCase struct { 227 | desc string 228 | wantName string 229 | resource *resource.Resource 230 | } 231 | testCases := []testCase{ 232 | { 233 | desc: "namespace resource", 234 | wantName: "namespace/default", 235 | resource: namespace, 236 | }, 237 | { 238 | desc: "ConfigMap resource", 239 | wantName: "default/configmap/kustomize-dot", 240 | resource: configMap, 241 | }, 242 | } 243 | 244 | p := New() 245 | for _, tc := range testCases { 246 | t.Run(tc.desc, func(t *testing.T) { 247 | gotName := p.vertexNameFromResource(tc.resource) 248 | if gotName != tc.wantName { 249 | t.Fatalf("want vertex name %q, got name %q", tc.wantName, gotName) 250 | } 251 | }) 252 | } 253 | } 254 | 255 | func TestWithKeepAndWithDropOptions(t *testing.T) { 256 | // Our test resources 257 | configMap, err := NewResourceFactory().FromMapWithName( 258 | "kustomize-dot", 259 | map[string]any{ 260 | "apiVersion": "v1", 261 | "kind": "ConfigMap", 262 | "metadata": map[string]string{ 263 | "name": "kustomize-dot", 264 | "namespace": "default", 265 | }, 266 | "data": map[string]string{ 267 | "foo": "bar", 268 | }, 269 | }, 270 | ) 271 | if err != nil { 272 | t.Fatal("failed to create ConfigMap resource") 273 | } 274 | 275 | namespace, err := NewResourceFactory().FromMapWithName( 276 | "default", 277 | map[string]any{ 278 | "apiVersion": "v1", 279 | "kind": "Namespace", 280 | "metadata": map[string]string{ 281 | "name": "default", 282 | }, 283 | }, 284 | ) 285 | if err != nil { 286 | t.Fatal("failed to create Namespace resource") 287 | } 288 | 289 | type testCase struct { 290 | desc string 291 | r *resource.Resource 292 | shouldDrop bool 293 | opts []Option 294 | } 295 | 296 | testCases := []testCase{ 297 | { 298 | desc: "empty opts - should persist resource", 299 | r: configMap, 300 | shouldDrop: false, 301 | opts: []Option{}, 302 | }, 303 | { 304 | desc: "WithDropNamespace - should drop", 305 | r: configMap, 306 | shouldDrop: true, 307 | opts: []Option{WithDropNamespace("default")}, 308 | }, 309 | { 310 | desc: "WithDropNamespace - should persist", 311 | r: configMap, 312 | shouldDrop: false, 313 | opts: []Option{WithDropNamespace("foobar")}, // Resource is in the default namespace 314 | }, 315 | { 316 | desc: "WithDropKind - should drop", 317 | r: namespace, 318 | shouldDrop: true, 319 | opts: []Option{WithDropKind("Namespace")}, 320 | }, 321 | { 322 | desc: "WithDropKind - should persist", 323 | r: configMap, 324 | shouldDrop: false, 325 | opts: []Option{WithDropKind("Secret")}, // Resource is a ConfigMap 326 | }, 327 | { 328 | desc: "WithKeepKind - should drop", 329 | r: configMap, 330 | shouldDrop: true, 331 | opts: []Option{WithKeepKind("Secret")}, // Resource is a ConfigMap 332 | }, 333 | { 334 | desc: "WithKeepKind - should persist", 335 | r: configMap, 336 | shouldDrop: false, 337 | opts: []Option{WithKeepKind("ConfigMap")}, 338 | }, 339 | { 340 | desc: "WithKeepNamespace - should persist", 341 | r: configMap, 342 | shouldDrop: false, 343 | opts: []Option{WithKeepNamespace("default")}, 344 | }, 345 | { 346 | desc: "WithKeepNamespace - should drop", 347 | r: configMap, 348 | shouldDrop: true, 349 | opts: []Option{WithKeepNamespace("foobar")}, // Resource is in the default namespace 350 | }, 351 | { 352 | desc: "WithKeepNamespace - should persist cluster scoped resource", 353 | r: namespace, 354 | shouldDrop: false, 355 | opts: []Option{WithKeepNamespace("foobar")}, // Resource is cluster-scoped 356 | }, 357 | { 358 | desc: "WithKeepNamespace and WithDropKind - should drop", 359 | r: configMap, 360 | shouldDrop: true, 361 | opts: []Option{WithKeepNamespace("default"), WithDropKind("ConfigMap")}, 362 | }, 363 | { 364 | desc: "WithKeepNamespace and WithKeepKind - should drop", 365 | r: configMap, 366 | shouldDrop: true, 367 | // Resource is not a Secret, so it should be dropped 368 | opts: []Option{WithKeepNamespace("default"), WithKeepKind("Secret")}, 369 | }, 370 | } 371 | 372 | for _, tc := range testCases { 373 | t.Run(tc.desc, func(t *testing.T) { 374 | p := New(tc.opts...) 375 | gotShouldDrop := p.shouldDropResource(tc.r) 376 | if gotShouldDrop != tc.shouldDrop { 377 | t.Fatalf("shouldDrop() returned %t, expected %t", gotShouldDrop, tc.shouldDrop) 378 | } 379 | }) 380 | } 381 | } 382 | 383 | func TestWithHighlightOptions(t *testing.T) { 384 | type testCase struct { 385 | desc string 386 | opts []Option 387 | wantHighlightKindMap map[string]string 388 | wantHighlightNamespaceMap map[string]string 389 | } 390 | 391 | testCases := []testCase{ 392 | { 393 | desc: "empty opts - no highlights", 394 | opts: []Option{}, 395 | wantHighlightKindMap: map[string]string{}, 396 | wantHighlightNamespaceMap: map[string]string{}, 397 | }, 398 | { 399 | desc: "WithHighlightKind - multiple highlights", 400 | opts: []Option{ 401 | WithHighlightKind("ConfigMap", "red"), 402 | WithHighlightKind("Secret", "green"), 403 | WithHighlightKind("Namespace", "blue"), 404 | }, 405 | wantHighlightKindMap: map[string]string{ 406 | "configmap": "red", 407 | "secret": "green", 408 | "namespace": "blue", 409 | }, 410 | wantHighlightNamespaceMap: map[string]string{}, 411 | }, 412 | { 413 | desc: "WithHighlightNamespace - multiple highlights", 414 | opts: []Option{ 415 | WithHighlightNamespace("foo", "red"), 416 | WithHighlightNamespace("bar", "green"), 417 | WithHighlightNamespace("baz", "blue"), 418 | }, 419 | wantHighlightKindMap: map[string]string{}, 420 | wantHighlightNamespaceMap: map[string]string{ 421 | "foo": "red", 422 | "bar": "green", 423 | "baz": "blue", 424 | }, 425 | }, 426 | } 427 | 428 | for _, tc := range testCases { 429 | t.Run(tc.desc, func(t *testing.T) { 430 | p := New(tc.opts...) 431 | if !maps.Equal(p.highlightKindMap, tc.wantHighlightKindMap) { 432 | t.Fatalf("want highlightKindMap %v, got %v", tc.wantHighlightKindMap, p.highlightKindMap) 433 | } 434 | if !maps.Equal(p.highlightNamespaceMap, tc.wantHighlightNamespaceMap) { 435 | t.Fatalf("want highlightNamespaceMap %v, got %v", tc.wantHighlightNamespaceMap, p.highlightNamespaceMap) 436 | } 437 | }) 438 | } 439 | } 440 | 441 | func TestWithLayoutDirection(t *testing.T) { 442 | type testCase struct { 443 | desc string 444 | opts []Option 445 | wantLayoutDirection LayoutDirection 446 | wantLayoutStr string 447 | } 448 | 449 | testCases := []testCase{ 450 | { 451 | desc: "no layout specified - defaults to LR", 452 | opts: []Option{}, 453 | wantLayoutDirection: LayoutDirectionLR, 454 | wantLayoutStr: "LR", 455 | }, 456 | { 457 | desc: "WithLayoutDirection specified", 458 | opts: []Option{WithLayoutDirection(LayoutDirectionBT)}, 459 | wantLayoutDirection: LayoutDirectionBT, 460 | wantLayoutStr: "BT", 461 | }, 462 | } 463 | 464 | for _, tc := range testCases { 465 | t.Run(tc.desc, func(t *testing.T) { 466 | p := New(tc.opts...) 467 | if p.layoutDirection != tc.wantLayoutDirection { 468 | t.Fatalf("want layout direction %v, got %v", tc.wantLayoutDirection, p.layoutDirection) 469 | } 470 | if p.layoutDirection.String() != tc.wantLayoutStr { 471 | t.Fatalf("want layout string %v, got %v", tc.wantLayoutStr, p.layoutDirection) 472 | } 473 | }) 474 | } 475 | } 476 | 477 | func TestParse(t *testing.T) { 478 | type testCase struct { 479 | desc string 480 | data string // data from which to get raw resources 481 | wantResources int // number of resources we get from the raw data 482 | wantVs int // number of expected vertices in the graph 483 | wantEs int // number of expected edges in the graph 484 | opts []Option // options with which to configure the parser 485 | } 486 | 487 | testCases := []testCase{ 488 | { 489 | desc: "empty data - no options", 490 | data: "", 491 | wantResources: 0, 492 | wantVs: 0, 493 | wantEs: 0, 494 | opts: []Option{}, 495 | }, 496 | { 497 | desc: "hello world resources - no options", 498 | data: fixtures.HelloWorld, 499 | wantResources: 3, 500 | wantVs: 6, // 3 resources + 3 origins 501 | wantEs: 3, 502 | opts: []Option{}, 503 | }, 504 | { 505 | desc: "hello world resources - WithDropKind", 506 | data: fixtures.HelloWorld, 507 | wantResources: 3, 508 | wantVs: 4, // 2 resources + 2 origins 509 | wantEs: 2, 510 | opts: []Option{WithDropKind("Service")}, 511 | }, 512 | { 513 | desc: "hello world resources - WithDropNamespace", 514 | data: fixtures.HelloWorld, 515 | wantResources: 3, 516 | wantVs: 0, 517 | wantEs: 0, 518 | opts: []Option{WithDropNamespace("default")}, 519 | }, 520 | { 521 | desc: "hello world resources - WithKeepNamespace", 522 | data: fixtures.HelloWorld, 523 | wantResources: 3, 524 | wantVs: 0, 525 | wantEs: 0, 526 | opts: []Option{WithKeepNamespace("foobar")}, // Resources are from default namespace 527 | }, 528 | } 529 | 530 | for _, tc := range testCases { 531 | t.Run(tc.desc, func(t *testing.T) { 532 | reader := strings.NewReader(tc.data) 533 | gotResources, err := ResourcesFromReader(reader) 534 | if err != nil { 535 | t.Fatalf("parsing resources failed: %s", err) 536 | } 537 | 538 | if len(gotResources) != tc.wantResources { 539 | t.Fatalf("got %d resource(s), want %d", len(gotResources), tc.wantResources) 540 | } 541 | 542 | p := New(tc.opts...) 543 | g, err := p.Parse(gotResources) 544 | if err != nil { 545 | t.Fatalf("failed to parse resources as graph: %s", err) 546 | } 547 | 548 | gotVs := g.GetVertices() 549 | if tc.wantVs != len(gotVs) { 550 | t.Fatalf("want |V|=%d, got |V|=%d", tc.wantVs, len(gotVs)) 551 | } 552 | 553 | gotEs := g.GetEdges() 554 | if tc.wantEs != len(gotEs) { 555 | t.Fatalf("want |E|=%d, got |E|=%d", tc.wantEs, len(gotEs)) 556 | } 557 | }) 558 | } 559 | } 560 | -------------------------------------------------------------------------------- /images/kube-prometheus-3.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 1374434824256 14 | 15 | monitoring/configmap/grafana-dashboard-nodes 16 | 17 | 18 | 19 | 1374434822976 20 | 21 | manifests/grafana-dashboardDefinitions.yaml 22 | 23 | 24 | 25 | 1374434824256->1374434822976 26 | 27 | 28 | https://github.com/prometheus-operator/kube-prometheus 29 | 30 | 31 | 32 | 1374434824320 33 | 34 | monitoring/configmap/grafana-dashboard-nodes-darwin 35 | 36 | 37 | 38 | 1374434824320->1374434822976 39 | 40 | 41 | https://github.com/prometheus-operator/kube-prometheus 42 | 43 | 44 | 45 | 1374434823040 46 | 47 | monitoring/configmap/grafana-dashboard-apiserver 48 | 49 | 50 | 51 | 1374434823040->1374434822976 52 | 53 | 54 | https://github.com/prometheus-operator/kube-prometheus 55 | 56 | 57 | 58 | 1374434823552 59 | 60 | monitoring/configmap/grafana-dashboard-k8s-resources-node 61 | 62 | 63 | 64 | 1374434823552->1374434822976 65 | 66 | 67 | https://github.com/prometheus-operator/kube-prometheus 68 | 69 | 70 | 71 | 1374434823680 72 | 73 | monitoring/configmap/grafana-dashboard-k8s-resources-workload 74 | 75 | 76 | 77 | 1374434823680->1374434822976 78 | 79 | 80 | https://github.com/prometheus-operator/kube-prometheus 81 | 82 | 83 | 84 | 1374434824896 85 | 86 | manifests/grafana-dashboardSources.yaml 87 | 88 | 89 | 90 | 1374434822528 91 | 92 | monitoring/configmap/adapter-config 93 | 94 | 95 | 96 | 1374434822656 97 | 98 | manifests/prometheusAdapter-configMap.yaml 99 | 100 | 101 | 102 | 1374434822528->1374434822656 103 | 104 | 105 | https://github.com/prometheus-operator/kube-prometheus 106 | 107 | 108 | 109 | 1374434822848 110 | 111 | manifests/blackboxExporter-configuration.yaml 112 | 113 | 114 | 115 | 1374434823744 116 | 117 | monitoring/configmap/grafana-dashboard-k8s-resources-workloads-namespace 118 | 119 | 120 | 121 | 1374434823744->1374434822976 122 | 123 | 124 | https://github.com/prometheus-operator/kube-prometheus 125 | 126 | 127 | 128 | 1374434823296 129 | 130 | monitoring/configmap/grafana-dashboard-grafana-overview 131 | 132 | 133 | 134 | 1374434823296->1374434822976 135 | 136 | 137 | https://github.com/prometheus-operator/kube-prometheus 138 | 139 | 140 | 141 | 1374434823424 142 | 143 | monitoring/configmap/grafana-dashboard-k8s-resources-multicluster 144 | 145 | 146 | 147 | 1374434823424->1374434822976 148 | 149 | 150 | https://github.com/prometheus-operator/kube-prometheus 151 | 152 | 153 | 154 | 1374434823936 155 | 156 | monitoring/configmap/grafana-dashboard-namespace-by-workload 157 | 158 | 159 | 160 | 1374434823936->1374434822976 161 | 162 | 163 | https://github.com/prometheus-operator/kube-prometheus 164 | 165 | 166 | 167 | 1374434824832 168 | 169 | monitoring/configmap/grafana-dashboards 170 | 171 | 172 | 173 | 1374434824832->1374434824896 174 | 175 | 176 | https://github.com/prometheus-operator/kube-prometheus 177 | 178 | 179 | 180 | 1374434823232 181 | 182 | monitoring/configmap/grafana-dashboard-controller-manager 183 | 184 | 185 | 186 | 1374434823232->1374434822976 187 | 188 | 189 | https://github.com/prometheus-operator/kube-prometheus 190 | 191 | 192 | 193 | 1374434823104 194 | 195 | monitoring/configmap/grafana-dashboard-cluster-total 196 | 197 | 198 | 199 | 1374434823104->1374434822976 200 | 201 | 202 | https://github.com/prometheus-operator/kube-prometheus 203 | 204 | 205 | 206 | 1374434823616 207 | 208 | monitoring/configmap/grafana-dashboard-k8s-resources-pod 209 | 210 | 211 | 212 | 1374434823616->1374434822976 213 | 214 | 215 | https://github.com/prometheus-operator/kube-prometheus 216 | 217 | 218 | 219 | 1374434824064 220 | 221 | monitoring/configmap/grafana-dashboard-node-cluster-rsrc-use 222 | 223 | 224 | 225 | 1374434824064->1374434822976 226 | 227 | 228 | https://github.com/prometheus-operator/kube-prometheus 229 | 230 | 231 | 232 | 1374434824768 233 | 234 | monitoring/configmap/grafana-dashboard-workload-total 235 | 236 | 237 | 238 | 1374434824768->1374434822976 239 | 240 | 241 | https://github.com/prometheus-operator/kube-prometheus 242 | 243 | 244 | 245 | 1374434824384 246 | 247 | monitoring/configmap/grafana-dashboard-persistentvolumesusage 248 | 249 | 250 | 251 | 1374434824384->1374434822976 252 | 253 | 254 | https://github.com/prometheus-operator/kube-prometheus 255 | 256 | 257 | 258 | 1374434824448 259 | 260 | monitoring/configmap/grafana-dashboard-pod-total 261 | 262 | 263 | 264 | 1374434824448->1374434822976 265 | 266 | 267 | https://github.com/prometheus-operator/kube-prometheus 268 | 269 | 270 | 271 | 1374434824704 272 | 273 | monitoring/configmap/grafana-dashboard-scheduler 274 | 275 | 276 | 277 | 1374434824704->1374434822976 278 | 279 | 280 | https://github.com/prometheus-operator/kube-prometheus 281 | 282 | 283 | 284 | 1374434824576 285 | 286 | monitoring/configmap/grafana-dashboard-prometheus-remote-write 287 | 288 | 289 | 290 | 1374434824576->1374434822976 291 | 292 | 293 | https://github.com/prometheus-operator/kube-prometheus 294 | 295 | 296 | 297 | 1374434824640 298 | 299 | monitoring/configmap/grafana-dashboard-proxy 300 | 301 | 302 | 303 | 1374434824640->1374434822976 304 | 305 | 306 | https://github.com/prometheus-operator/kube-prometheus 307 | 308 | 309 | 310 | 1374434822720 311 | 312 | monitoring/configmap/blackbox-exporter-configuration 313 | 314 | 315 | 316 | 1374434822720->1374434822848 317 | 318 | 319 | https://github.com/prometheus-operator/kube-prometheus 320 | 321 | 322 | 323 | 1374434823808 324 | 325 | monitoring/configmap/grafana-dashboard-kubelet 326 | 327 | 328 | 329 | 1374434823808->1374434822976 330 | 331 | 332 | https://github.com/prometheus-operator/kube-prometheus 333 | 334 | 335 | 336 | 1374434824512 337 | 338 | monitoring/configmap/grafana-dashboard-prometheus 339 | 340 | 341 | 342 | 1374434824512->1374434822976 343 | 344 | 345 | https://github.com/prometheus-operator/kube-prometheus 346 | 347 | 348 | 349 | 1374434823872 350 | 351 | monitoring/configmap/grafana-dashboard-namespace-by-pod 352 | 353 | 354 | 355 | 1374434823872->1374434822976 356 | 357 | 358 | https://github.com/prometheus-operator/kube-prometheus 359 | 360 | 361 | 362 | 1374434824128 363 | 364 | monitoring/configmap/grafana-dashboard-node-rsrc-use 365 | 366 | 367 | 368 | 1374434824128->1374434822976 369 | 370 | 371 | https://github.com/prometheus-operator/kube-prometheus 372 | 373 | 374 | 375 | 1374434822912 376 | 377 | monitoring/configmap/grafana-dashboard-alertmanager-overview 378 | 379 | 380 | 381 | 1374434822912->1374434822976 382 | 383 | 384 | https://github.com/prometheus-operator/kube-prometheus 385 | 386 | 387 | 388 | 1374434823360 389 | 390 | monitoring/configmap/grafana-dashboard-k8s-resources-cluster 391 | 392 | 393 | 394 | 1374434823360->1374434822976 395 | 396 | 397 | https://github.com/prometheus-operator/kube-prometheus 398 | 399 | 400 | 401 | 1374434823488 402 | 403 | monitoring/configmap/grafana-dashboard-k8s-resources-namespace 404 | 405 | 406 | 407 | 1374434823488->1374434822976 408 | 409 | 410 | https://github.com/prometheus-operator/kube-prometheus 411 | 412 | 413 | 414 | --------------------------------------------------------------------------------