├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md ├── stale.yml └── workflows │ ├── helm-release.yml │ ├── lint-pr.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yml ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── assets └── rbac-wizard-demo.gif ├── charts └── rbac-wizard │ ├── .helmignore │ ├── Chart.yaml │ ├── README.md │ ├── templates │ ├── _helpers.tpl │ ├── clusterrole.yaml │ ├── clusterrolebinding.yaml │ ├── deployment.yaml │ ├── hpa.yaml │ ├── ingress.yaml │ ├── service.yaml │ ├── serviceaccount.yaml │ └── tests │ │ └── test-connection.yaml │ └── values.yaml ├── cmd ├── root.go ├── serve.go └── version.go ├── dev └── kind.yaml ├── go.mod ├── go.sum ├── internal ├── bindings.go ├── client.go ├── logger │ └── logger.go ├── middlewares.go ├── statik │ └── statik.go ├── types.go └── whatIf.go ├── main.go ├── rbac-wizard.rb └── ui ├── .eslintrc.json ├── .npmrc ├── LICENSE ├── app ├── error.tsx ├── layout.tsx ├── page.tsx ├── providers.tsx └── what-if │ └── page.tsx ├── components ├── graph.tsx ├── icons.tsx ├── navbar.tsx ├── primitives.ts ├── table.tsx └── theme-switch.tsx ├── config ├── fonts.ts └── site.ts ├── next-env.d.ts ├── next.config.js ├── package.json ├── postcss.config.js ├── public ├── rbac-wizard-logo-embedded.svg └── rbac-wizard-logo.png ├── styles └── globals.css ├── tailwind.config.js ├── tsconfig.json └── types └── index.ts /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behaviour: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behaviour** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Post a question about the project 4 | title: '' 5 | labels: question 6 | assignees: '' 7 | --- 8 | 9 | **Your question** 10 | A clear and concise question. 11 | 12 | **Additional context** 13 | Add any other context about your question here. -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Number of days of inactivity before an issue becomes stale 3 | daysUntilStale: 90 4 | 5 | # Number of days of inactivity before an stale issue is closed 6 | daysUntilClose: 30 7 | 8 | # Label to use when marking an issue as stale 9 | staleLabel: stale 10 | 11 | # Limit to only `issues` or `pulls` 12 | only: issues 13 | 14 | issues: 15 | 16 | # Comment to post when marking an issue as stale. 17 | markComment: > 18 | This issue has been automatically marked as stale because it has not had 19 | recent activity. It will be closed if no further activity occurs. Thank you 20 | for your contributions. 21 | 22 | # Comment to post when closing a stale issue. 23 | closeComment: > 24 | This issue has been automatically closed because it has not had recent 25 | activity since being marked as stale. 26 | 27 | pulls: 28 | 29 | # Comment to post when marking a PR as stale. 30 | markComment: > 31 | This PR has been automatically marked as stale because it has not had 32 | recent activity. It will be closed if no further activity occurs. Thank you 33 | for your contributions. 34 | 35 | To track this PR (even if closed), please open a corresponding issue if one 36 | does not already exist. 37 | 38 | # Comment to post when closing a stale PR. 39 | closeComment: > 40 | This PR has been automatically closed because it has not had recent 41 | activity since being marked as stale. 42 | 43 | Please reopen when work resumes. 44 | -------------------------------------------------------------------------------- /.github/workflows/helm-release.yml: -------------------------------------------------------------------------------- 1 | name: release helm chart 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - charts/** 9 | 10 | permissions: 11 | contents: write 12 | packages: write 13 | issues: write 14 | 15 | jobs: 16 | release: 17 | permissions: 18 | contents: write 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | with: 24 | fetch-depth: 0 25 | 26 | - name: Configure Git 27 | run: | 28 | git config user.name "$GITHUB_ACTOR" 29 | git config user.email "$GITHUB_ACTOR@users.noreply.github.com" 30 | 31 | - name: Install Helm 32 | uses: azure/setup-helm@v4 33 | env: 34 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 35 | 36 | - name: Run chart-releaser 37 | uses: helm/chart-releaser-action@v1.6.0 38 | with: 39 | skip_existing: true 40 | packages_with_index: true 41 | charts_dir: ./charts 42 | mark_as_latest: false 43 | env: 44 | CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 45 | -------------------------------------------------------------------------------- /.github/workflows/lint-pr.yml: -------------------------------------------------------------------------------- 1 | name: "Lint PR" 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | permissions: 11 | pull-requests: write 12 | 13 | jobs: 14 | main: 15 | name: Validate PR title 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: lint_pr_title 19 | id: lint_pr_title 20 | uses: amannn/action-semantic-pull-request@v5 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | - uses: marocchino/sticky-pull-request-comment@v2 24 | if: always() && (steps.lint_pr_title.outputs.error_message != null) 25 | with: 26 | header: pr-title-lint-error 27 | message: | 28 | Hey there and thank you for opening this pull request! 👋🏼 29 | 30 | We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted. 31 | 32 | Details: 33 | 34 | ``` 35 | ${{ steps.lint_pr_title.outputs.error_message }} 36 | ``` 37 | # Delete a previous comment when the issue has been resolved 38 | - if: ${{ steps.lint_pr_title.outputs.error_message == null }} 39 | uses: marocchino/sticky-pull-request-comment@v2 40 | with: 41 | header: pr-title-lint-error 42 | delete: true 43 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: ['v*.*.*'] 6 | workflow_dispatch: 7 | inputs: 8 | version: 9 | description: 'Version to release' 10 | required: true 11 | default: 'v0.0.0' 12 | 13 | permissions: 14 | contents: write 15 | packages: write 16 | issues: write 17 | 18 | jobs: 19 | goreleaser: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | with: 24 | fetch-depth: 0 25 | - run: git fetch --force --tags 26 | - uses: actions/setup-go@v5 27 | with: 28 | go-version: '1.23' 29 | - uses: goreleaser/goreleaser-action@v5 30 | with: 31 | distribution: goreleaser 32 | version: latest 33 | args: release --clean 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | containerize: 37 | runs-on: ubuntu-latest 38 | needs: goreleaser 39 | steps: 40 | - name: Checkout 41 | uses: actions/checkout@v4 42 | - name: Setup QEMU 43 | uses: docker/setup-qemu-action@v3 44 | - name: Set up Buildx 45 | id: buildx 46 | uses: docker/setup-buildx-action@v3 47 | - name: Login to GHCR 48 | if: github.event_name != 'pull_request' 49 | uses: docker/login-action@v3 50 | with: 51 | registry: ghcr.io 52 | username: ${{ github.repository_owner }} 53 | password: ${{ secrets.GITHUB_TOKEN }} 54 | - name: Docker meta 55 | id: rbac-wizard 56 | uses: docker/metadata-action@v5 57 | with: 58 | images: | 59 | ghcr.io/${{ github.repository }} 60 | tags: | 61 | type=schedule 62 | type=semver,pattern={{version}} 63 | type=sha,prefix= 64 | - name: Build and push 65 | uses: docker/build-push-action@v6 66 | with: 67 | context: . 68 | platforms: linux/amd64,linux/arm64 69 | push: ${{ github.event_name != 'pull_request' }} 70 | tags: ${{ steps.rbac-wizard.outputs.tags }} 71 | labels: ${{ steps.rbac-wizard.outputs.labels }} 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything 2 | * 3 | 4 | # But not these files... 5 | !/.gitignore 6 | 7 | !*.go 8 | !go.sum 9 | !go.mod 10 | 11 | !README.md 12 | !LICENSE 13 | 14 | !ui/app/* 15 | !ui/what-if/* 16 | !ui/components/* 17 | !ui/config/* 18 | !ui/public/* 19 | !ui/styles/* 20 | !ui/types/* 21 | !ui/.eslintrc.json 22 | !ui/.npmrc 23 | !ui/LICENSE 24 | !ui/next.config.js 25 | !ui/next-env.d.ts 26 | !ui/package.json 27 | !ui/postcss.config.js 28 | !ui/tailwind.config.js 29 | !ui/tsconfig.json 30 | 31 | !charts/** 32 | !index.yaml 33 | !*.tgz 34 | 35 | !.github/* 36 | !.github/workflows/* 37 | !.github/ISSUE_TEMPLATE/* 38 | 39 | !.goreleaser.yml 40 | 41 | !Makefile 42 | !Dockerfile 43 | 44 | !assets/* 45 | !rbac-wizard.rb 46 | 47 | !dev/* 48 | 49 | # ...even if they are in subdirectories 50 | !*/ 51 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: rbac-wizard 2 | 3 | changelog: 4 | sort: desc 5 | filters: 6 | exclude: 7 | - '^Merge pull request' 8 | groups: 9 | - title: "Features" 10 | regexp: '^.*?feat(\([[:word:]]+\))??!?:.+$' 11 | order: 0 12 | - title: "Bug fixes" 13 | regexp: '^.*?bug(\([[:word:]]+\))??!?:.+$' 14 | order: 1 15 | - title: "Documentation Updates" 16 | regexp: '^.*?docs(\([[:word:]]+\))??!?:.+$' 17 | order: 2 18 | - title: "Other Changes" 19 | regexp: "^(ci|build|misc|perf|deps):" 20 | order: 3 21 | - title: "Miscellaneous" 22 | regexp: ".*" 23 | order: 4 24 | 25 | before: 26 | hooks: 27 | - go fmt ./... 28 | - go mod tidy 29 | - go mod download 30 | 31 | builds: 32 | - id: rbac-wizard 33 | main: . 34 | binary: rbac-wizard 35 | env: 36 | - CGO_ENABLED=0 37 | ldflags: -s -w -X github.com/pehlicd/rbac-wizard/cmd.versionString={{ .Tag }} -X github.com/pehlicd/rbac-wizard/cmd.buildDate={{ .Date }} -X github.com/pehlicd/rbac-wizard/cmd.buildCommit={{ .Commit }} 38 | goos: 39 | - linux 40 | - darwin 41 | - windows 42 | goarch: 43 | - amd64 44 | - arm64 45 | ignore: 46 | - goos: windows 47 | goarch: arm64 48 | 49 | archives: 50 | - builds: 51 | - rbac-wizard 52 | name_template: "{{ .ProjectName }}_{{ .Tag }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" 53 | wrap_in_directory: false 54 | format: tar.gz 55 | files: 56 | - LICENSE 57 | 58 | release: 59 | github: 60 | name: rbac-wizard 61 | owner: pehlicd 62 | draft: false -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22 AS frontend-builder 2 | 3 | WORKDIR /app/ui 4 | 5 | COPY ui /app/ui 6 | 7 | RUN npm install -g npm@latest && \ 8 | npm install --force 9 | RUN npm run build 10 | 11 | FROM golang:1.23 as backend-builder 12 | 13 | WORKDIR /app 14 | 15 | COPY . . 16 | 17 | RUN go install github.com/rakyll/statik@latest 18 | 19 | COPY --from=frontend-builder /app/ui/dist /app/ui/dist 20 | 21 | RUN statik -src=./ui/dist/ -dest=./internal/ -f 22 | RUN go build -o rbac-wizard 23 | 24 | FROM alpine:3.20.2 25 | 26 | COPY --from=backend-builder /app/rbac-wizard /usr/local/bin/rbac-wizard 27 | 28 | RUN apk add libc6-compat 29 | 30 | EXPOSE 8080 31 | 32 | ENTRYPOINT ["rbac-wizard"] 33 | 34 | CMD ["serve"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2024 Furkan Pehlivan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # ---------------------------------------------------------------------------- # 2 | # ______ ______ ___ _____ _ _ _____ ______ ___ ______ ______ # 3 | # | ___ \| ___ \ / _ \ / __ \ | | | ||_ _||___ / / _ \ | ___ \| _ \ # 4 | # | |_/ /| |_/ // /_\ \| / \/ | | | | | | / / / /_\ \| |_/ /| | | | # 5 | # | / | ___ \| _ || | | |/\| | | | / / | _ || / | | | | # 6 | # | |\ \ | |_/ /| | | || \__/\ \ /\ / _| |_ ./ /___| | | || |\ \ | |/ / # 7 | # \_| \_|\____/ \_| |_/ \____/ \/ \/ \___/ \_____/\_| |_/\_| \_||___/ # 8 | # # 9 | # ---------------------------------------------------------------------------- # 10 | 11 | # Color Definitions 12 | NO_COLOR = \033[0m 13 | OK_COLOR = \033[32;01m 14 | ERROR_COLOR = \033[31;01m 15 | WARN_COLOR = \033[33;01m 16 | 17 | # Directories 18 | APP_NAME = rbac-wizard 19 | BIN_DIR = ./bin 20 | GO_BUILD = $(BIN_DIR)/$(APP_NAME) 21 | 22 | # Main Targets 23 | .PHONY: run serve run-ui build-ui build-ui-and-embed build-backend fmt create-cluster delete-cluster deploy-ingress-nginx clean 24 | 25 | ## Run the application in serve mode 26 | run: 27 | @echo "$(OK_COLOR)==> Running the application...$(NO_COLOR)" 28 | go run . serve 29 | 30 | ## Serve the application (build UI and backend first) 31 | serve: build-ui-and-embed build-backend 32 | @echo "$(OK_COLOR)==> Starting the application...$(NO_COLOR)" 33 | go run . serve 34 | 35 | ## Run UI in development mode 36 | run-ui: 37 | @echo "$(OK_COLOR)==> Running UI in development mode...$(NO_COLOR)" 38 | cd ui && npm run dev 39 | 40 | ## Build the UI 41 | build-ui: 42 | @echo "$(OK_COLOR)==> Building UI...$(NO_COLOR)" 43 | cd ui && npm run build 44 | 45 | ## Build UI, embed it into Go application, and clean up artifacts 46 | build-ui-and-embed: build-ui 47 | @echo "$(OK_COLOR)==> Embedding UI files into Go application...$(NO_COLOR)" 48 | statik -src=./ui/dist/ -dest=./internal/ -f 49 | @echo "$(OK_COLOR)==> Cleaning up UI build artifacts...$(NO_COLOR)" 50 | rm -rf ./ui/dist 51 | 52 | ## Build the Go backend and place the binary in bin directory 53 | build-backend: 54 | @echo "$(OK_COLOR)==> Building Go backend...$(NO_COLOR)" 55 | mkdir -p $(BIN_DIR) 56 | go build -o $(GO_BUILD) 57 | 58 | ## Format Go code and tidy modules 59 | fmt: 60 | @echo "$(OK_COLOR)==> Formatting Go code and tidying modules...$(NO_COLOR)" 61 | go fmt ./... 62 | go mod tidy 63 | 64 | # Kubernetes Cluster Management 65 | ## Create a Kubernetes cluster using Kind 66 | create-k8s-cluster: 67 | @echo "$(OK_COLOR)==> Creating Kubernetes cluster...$(NO_COLOR)" 68 | kind create cluster --config dev/kind.yaml 69 | 70 | ## Delete the Kubernetes cluster 71 | delete-k8s-cluster: 72 | @echo "$(OK_COLOR)==> Deleting Kubernetes cluster...$(NO_COLOR)" 73 | kind delete cluster -n 'rbac-wizard-dev' 74 | 75 | ## Deploy NGINX Ingress controller to the cluster 76 | deploy-ingress-nginx: 77 | @echo "$(OK_COLOR)==> Deploying ingress-nginx...$(NO_COLOR)" 78 | kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml 79 | @echo "$(OK_COLOR)==> Waiting for ingress-nginx to be ready...$(NO_COLOR)" 80 | kubectl wait --namespace ingress-nginx \ 81 | --for=condition=ready pod \ 82 | --selector=app.kubernetes.io/component=controller \ 83 | --timeout=180s 84 | 85 | # Cleanup 86 | ## Remove built binaries and cleanup 87 | clean: 88 | @echo "$(OK_COLOR)==> Cleaning up build artifacts...$(NO_COLOR)" 89 | rm -rf $(BIN_DIR) 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | # RBAC Wizard 6 | 7 | ![go version](https://img.shields.io/github/go-mod/go-version/pehlicd/rbac-wizard) 8 | ![release](https://img.shields.io/github/v/release/pehlicd/rbac-wizard?filter=v*) 9 | ![helm release](https://img.shields.io/github/v/release/pehlicd/rbac-wizard?filter=rbac-wizard*&logo=helm) 10 | ![license](https://img.shields.io/github/license/pehlicd/rbac-wizard) 11 | [![go report](https://goreportcard.com/badge/github.com/pehlicd/rbac-wizard)](https://goreportcard.com/report/github.com/pehlicd/rbac-wizard) 12 | 13 | RBAC Wizard is a tool that helps you visualize and analyze the RBAC configurations of your Kubernetes cluster. It provides a graphical representation of the Kubernetes RBAC objects. 14 | 15 |
16 | 17 | 18 | | Demo | 19 | |--------------------------------------------| 20 | | | 21 | 22 |
23 | 24 | ## How to install 25 | 26 | ### Helm 27 | 28 | Since rbac-wizard is capable of getting kubernetes clientset from the cluster ease free, you can also install it on your cluster using Helm with 3 simple steps! 29 | 30 | ```bash 31 | # to add the Helm repository 32 | helm repo add rbac-wizard https://rbac-wizard.pehli.dev 33 | # to pull the latest Helm charts 34 | helm pull rbac-wizard/rbac-wizard 35 | # to install the Helm charts with the default values 36 | helm install rbac-wizard rbac-wizard/rbac-wizard --namespace rbac-wizard --create-namespace 37 | ``` 38 | 39 | ### Homebrew 40 | 41 | ```bash 42 | brew tap pehlicd/rbac-wizard https://github.com/pehlicd/rbac-wizard 43 | brew install rbac-wizard 44 | ``` 45 | 46 | ### Using go install 47 | 48 | ```bash 49 | go install github.com/pehlicd/rbac-wizard@latest 50 | ``` 51 | 52 | ## How to use 53 | 54 | Using RBAC Wizard is super simple. Just run the following command: 55 | 56 | ```bash 57 | rbac-wizard serve 58 | ``` 59 | 60 | ## How to contribute 61 | 62 | If you'd like to contribute to RBAC Wizard, feel free to submit pull requests or open issues on the [GitHub repository](https://github.com/pehlicd/rbac-wizard). Your feedback and contributions are highly appreciated! 63 | 64 | ## License 65 | 66 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 67 | 68 | --- 69 | 70 | Developed by [Furkan Pehlivan](https://github.com/pehlicd) - [Project Repository](https://github.com/pehlicd/rbac-wizard) -------------------------------------------------------------------------------- /assets/rbac-wizard-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pehlicd/rbac-wizard/24720e39cfdaa6ef18f41503dce069ebeba0a354/assets/rbac-wizard-demo.gif -------------------------------------------------------------------------------- /charts/rbac-wizard/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /charts/rbac-wizard/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: rbac-wizard 3 | home: https://rbac-wizard.pehli.dev 4 | description: A Helm chart for deploying RBAC Wizard to the Kubernetes 5 | maintainers: 6 | - name: pehlicd 7 | email: furkanpehlivan34@gmail.com 8 | url: https://github.com/pehlicd 9 | 10 | type: application 11 | version: 0.1.0 12 | appVersion: "0.0.6" 13 | -------------------------------------------------------------------------------- /charts/rbac-wizard/README.md: -------------------------------------------------------------------------------- 1 | # rbac-wizard 2 | 3 | ![Version: 0.1.0](https://img.shields.io/badge/Version-0.1.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.0.6](https://img.shields.io/badge/AppVersion-0.0.6-informational?style=flat-square) 4 | 5 | A Helm chart for deploying RBAC Wizard to the Kubernetes 6 | 7 | **Homepage:** 8 | 9 | ## Maintainers 10 | 11 | | Name | Email | Url | 12 | | ---- | ------ | --- | 13 | | pehlicd | | | 14 | 15 | ## Values 16 | 17 | | Key | Type | Default | Description | 18 | |-----|------|---------|-------------| 19 | | affinity | object | `{}` | | 20 | | autoscaling.enabled | bool | `false` | | 21 | | autoscaling.maxReplicas | int | `100` | | 22 | | autoscaling.minReplicas | int | `1` | | 23 | | autoscaling.targetCPUUtilizationPercentage | int | `80` | | 24 | | clusterRole.annotations | object | `{}` | | 25 | | clusterRole.create | bool | `true` | | 26 | | clusterRole.name | string | `""` | | 27 | | clusterRoleBinding.annotations | object | `{}` | | 28 | | clusterRoleBinding.create | bool | `true` | | 29 | | clusterRoleBinding.name | string | `""` | | 30 | | fullnameOverride | string | `""` | | 31 | | image.pullPolicy | string | `"IfNotPresent"` | | 32 | | image.repository | string | `"ghcr.io/pehlicd/rbac-wizard"` | | 33 | | image.tag | string | `"latest"` | | 34 | | imagePullSecrets | list | `[]` | | 35 | | ingress.annotations | object | `{}` | | 36 | | ingress.className | string | `""` | | 37 | | ingress.enabled | bool | `true` | | 38 | | ingress.hosts[0].host | string | `"rbac-wizard.local"` | | 39 | | ingress.hosts[0].paths[0].path | string | `"/"` | | 40 | | ingress.hosts[0].paths[0].pathType | string | `"ImplementationSpecific"` | | 41 | | ingress.tls | list | `[]` | | 42 | | livenessProbe.httpGet.path | string | `"/"` | | 43 | | livenessProbe.httpGet.port | string | `"http"` | | 44 | | nameOverride | string | `""` | | 45 | | nodeSelector | object | `{}` | | 46 | | podAnnotations | object | `{}` | | 47 | | podLabels | object | `{}` | | 48 | | podSecurityContext | object | `{}` | | 49 | | readinessProbe.httpGet.path | string | `"/"` | | 50 | | readinessProbe.httpGet.port | string | `"http"` | | 51 | | replicaCount | int | `1` | | 52 | | resources | object | `{}` | | 53 | | securityContext | object | `{}` | | 54 | | service.port | int | `8080` | | 55 | | service.type | string | `"ClusterIP"` | | 56 | | serviceAccount.annotations | object | `{}` | | 57 | | serviceAccount.automount | bool | `true` | | 58 | | serviceAccount.create | bool | `true` | | 59 | | serviceAccount.name | string | `""` | | 60 | | tolerations | list | `[]` | | 61 | | volumeMounts | list | `[]` | | 62 | | volumes | list | `[]` | | 63 | -------------------------------------------------------------------------------- /charts/rbac-wizard/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "rbac-wizard.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "rbac-wizard.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "rbac-wizard.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "rbac-wizard.labels" -}} 37 | helm.sh/chart: {{ include "rbac-wizard.chart" . }} 38 | {{ include "rbac-wizard.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "rbac-wizard.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "rbac-wizard.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "rbac-wizard.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "rbac-wizard.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | 64 | {{- define "rbac-wizard.clusterRoleName" -}} 65 | {{- if .Values.clusterRole.create }} 66 | {{- default (include "rbac-wizard.fullname" .) .Values.clusterRole.name }} 67 | {{- else }} 68 | {{- default "default" .Values.clusterRole.name }} 69 | {{- end }} 70 | {{- end }} 71 | 72 | {{- define "rbac-wizard.clusterRoleBindingName" -}} 73 | {{- if .Values.clusterRoleBinding.create }} 74 | {{- default (include "rbac-wizard.fullname" .) .Values.clusterRoleBinding.name }} 75 | {{- else }} 76 | {{- default "default" .Values.clusterRoleBinding.name }} 77 | {{- end }} 78 | {{- end }} 79 | -------------------------------------------------------------------------------- /charts/rbac-wizard/templates/clusterrole.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.clusterRole.create -}} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: {{ include "rbac-wizard.clusterRoleName" . }} 6 | labels: 7 | {{- include "rbac-wizard.labels" . | nindent 4 }} 8 | {{- with .Values.clusterRole.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | rules: 13 | - apiGroups: ["rbac.authorization.k8s.io"] 14 | resources: 15 | - roles 16 | - rolebindings 17 | - clusterroles 18 | - clusterrolebindings 19 | verbs: ["list", "get", "watch"] 20 | - apiGroups: [""] 21 | resources: 22 | - serviceaccounts 23 | verbs: ["list", "get", "watch"] 24 | {{- end }} 25 | -------------------------------------------------------------------------------- /charts/rbac-wizard/templates/clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.clusterRoleBinding.create -}} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRoleBinding 4 | metadata: 5 | name: {{ include "rbac-wizard.clusterRoleBindingName" . }} 6 | labels: 7 | {{- include "rbac-wizard.labels" . | nindent 4 }} 8 | {{- with .Values.clusterRoleBinding.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | subjects: 13 | - kind: ServiceAccount 14 | name: {{ include "rbac-wizard.serviceAccountName" . }} 15 | namespace: {{ .Release.Namespace }} 16 | roleRef: 17 | kind: ClusterRole 18 | name: {{ include "rbac-wizard.clusterRoleName" . }} 19 | apiGroup: rbac.authorization.k8s.io 20 | {{- end }} 21 | -------------------------------------------------------------------------------- /charts/rbac-wizard/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "rbac-wizard.fullname" . }} 5 | labels: 6 | {{- include "rbac-wizard.labels" . | nindent 4 }} 7 | spec: 8 | {{- if not .Values.autoscaling.enabled }} 9 | replicas: {{ .Values.replicaCount }} 10 | {{- end }} 11 | selector: 12 | matchLabels: 13 | {{- include "rbac-wizard.selectorLabels" . | nindent 6 }} 14 | template: 15 | metadata: 16 | {{- with .Values.podAnnotations }} 17 | annotations: 18 | {{- toYaml . | nindent 8 }} 19 | {{- end }} 20 | labels: 21 | {{- include "rbac-wizard.labels" . | nindent 8 }} 22 | {{- with .Values.podLabels }} 23 | {{- toYaml . | nindent 8 }} 24 | {{- end }} 25 | spec: 26 | {{- with .Values.imagePullSecrets }} 27 | imagePullSecrets: 28 | {{- toYaml . | nindent 8 }} 29 | {{- end }} 30 | serviceAccountName: {{ include "rbac-wizard.serviceAccountName" . }} 31 | securityContext: 32 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 33 | containers: 34 | - name: {{ .Chart.Name }} 35 | securityContext: 36 | {{- toYaml .Values.securityContext | nindent 12 }} 37 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 38 | imagePullPolicy: {{ .Values.image.pullPolicy }} 39 | ports: 40 | - name: http 41 | containerPort: {{ .Values.service.port }} 42 | protocol: TCP 43 | livenessProbe: 44 | {{- toYaml .Values.livenessProbe | nindent 12 }} 45 | readinessProbe: 46 | {{- toYaml .Values.readinessProbe | nindent 12 }} 47 | resources: 48 | {{- toYaml .Values.resources | nindent 12 }} 49 | {{- with .Values.volumeMounts }} 50 | volumeMounts: 51 | {{- toYaml . | nindent 12 }} 52 | {{- end }} 53 | {{- with .Values.volumes }} 54 | volumes: 55 | {{- toYaml . | nindent 8 }} 56 | {{- end }} 57 | {{- with .Values.nodeSelector }} 58 | nodeSelector: 59 | {{- toYaml . | nindent 8 }} 60 | {{- end }} 61 | {{- with .Values.affinity }} 62 | affinity: 63 | {{- toYaml . | nindent 8 }} 64 | {{- end }} 65 | {{- with .Values.tolerations }} 66 | tolerations: 67 | {{- toYaml . | nindent 8 }} 68 | {{- end }} 69 | -------------------------------------------------------------------------------- /charts/rbac-wizard/templates/hpa.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.autoscaling.enabled }} 2 | apiVersion: autoscaling/v2 3 | kind: HorizontalPodAutoscaler 4 | metadata: 5 | name: {{ include "rbac-wizard.fullname" . }} 6 | labels: 7 | {{- include "rbac-wizard.labels" . | nindent 4 }} 8 | spec: 9 | scaleTargetRef: 10 | apiVersion: apps/v1 11 | kind: Deployment 12 | name: {{ include "rbac-wizard.fullname" . }} 13 | minReplicas: {{ .Values.autoscaling.minReplicas }} 14 | maxReplicas: {{ .Values.autoscaling.maxReplicas }} 15 | metrics: 16 | {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} 17 | - type: Resource 18 | resource: 19 | name: cpu 20 | target: 21 | type: Utilization 22 | averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} 23 | {{- end }} 24 | {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} 25 | - type: Resource 26 | resource: 27 | name: memory 28 | target: 29 | type: Utilization 30 | averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} 31 | {{- end }} 32 | {{- end }} 33 | -------------------------------------------------------------------------------- /charts/rbac-wizard/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $fullName := include "rbac-wizard.fullname" . -}} 3 | {{- $svcPort := .Values.service.port -}} 4 | {{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} 5 | {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} 6 | {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} 7 | {{- end }} 8 | {{- end }} 9 | {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} 10 | apiVersion: networking.k8s.io/v1 11 | {{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} 12 | apiVersion: networking.k8s.io/v1beta1 13 | {{- else -}} 14 | apiVersion: extensions/v1beta1 15 | {{- end }} 16 | kind: Ingress 17 | metadata: 18 | name: {{ $fullName }} 19 | labels: 20 | {{- include "rbac-wizard.labels" . | nindent 4 }} 21 | {{- with .Values.ingress.annotations }} 22 | annotations: 23 | {{- toYaml . | nindent 4 }} 24 | {{- end }} 25 | spec: 26 | {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} 27 | ingressClassName: {{ .Values.ingress.className }} 28 | {{- end }} 29 | {{- if .Values.ingress.tls }} 30 | tls: 31 | {{- range .Values.ingress.tls }} 32 | - hosts: 33 | {{- range .hosts }} 34 | - {{ . | quote }} 35 | {{- end }} 36 | secretName: {{ .secretName }} 37 | {{- end }} 38 | {{- end }} 39 | rules: 40 | {{- range .Values.ingress.hosts }} 41 | - host: {{ .host | quote }} 42 | http: 43 | paths: 44 | {{- range .paths }} 45 | - path: {{ .path }} 46 | {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} 47 | pathType: {{ .pathType }} 48 | {{- end }} 49 | backend: 50 | {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} 51 | service: 52 | name: {{ $fullName }} 53 | port: 54 | number: {{ $svcPort }} 55 | {{- else }} 56 | serviceName: {{ $fullName }} 57 | servicePort: {{ $svcPort }} 58 | {{- end }} 59 | {{- end }} 60 | {{- end }} 61 | {{- end }} 62 | -------------------------------------------------------------------------------- /charts/rbac-wizard/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "rbac-wizard.fullname" . }} 5 | labels: 6 | {{- include "rbac-wizard.labels" . | nindent 4 }} 7 | spec: 8 | type: {{ .Values.service.type }} 9 | ports: 10 | - port: {{ .Values.service.port }} 11 | targetPort: http 12 | protocol: TCP 13 | name: http 14 | selector: 15 | {{- include "rbac-wizard.selectorLabels" . | nindent 4 }} 16 | -------------------------------------------------------------------------------- /charts/rbac-wizard/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "rbac-wizard.serviceAccountName" . }} 6 | labels: 7 | {{- include "rbac-wizard.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | automountServiceAccountToken: {{ .Values.serviceAccount.automount }} 13 | {{- end }} 14 | -------------------------------------------------------------------------------- /charts/rbac-wizard/templates/tests/test-connection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: "{{ include "rbac-wizard.fullname" . }}-test-connection" 5 | labels: 6 | {{- include "rbac-wizard.labels" . | nindent 4 }} 7 | annotations: 8 | "helm.sh/hook": test 9 | spec: 10 | containers: 11 | - name: wget 12 | image: busybox 13 | command: ['wget'] 14 | args: ['{{ include "rbac-wizard.fullname" . }}:{{ .Values.service.port }}'] 15 | restartPolicy: Never 16 | -------------------------------------------------------------------------------- /charts/rbac-wizard/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for rbac-wizard. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | image: 8 | repository: ghcr.io/pehlicd/rbac-wizard 9 | pullPolicy: IfNotPresent 10 | tag: latest 11 | 12 | imagePullSecrets: [] 13 | nameOverride: "" 14 | fullnameOverride: "" 15 | 16 | serviceAccount: 17 | create: true 18 | automount: true 19 | annotations: {} 20 | name: "" 21 | 22 | clusterRole: 23 | create: true 24 | annotations: {} 25 | name: "" 26 | 27 | clusterRoleBinding: 28 | create: true 29 | annotations: {} 30 | name: "" 31 | 32 | podAnnotations: {} 33 | podLabels: {} 34 | 35 | podSecurityContext: 36 | {} 37 | # fsGroup: 2000 38 | 39 | securityContext: 40 | {} 41 | # capabilities: 42 | # drop: 43 | # - ALL 44 | # readOnlyRootFilesystem: true 45 | # runAsNonRoot: true 46 | # runAsUser: 1000 47 | 48 | service: 49 | type: ClusterIP 50 | port: 8080 51 | 52 | ingress: 53 | enabled: true 54 | className: "" 55 | annotations: 56 | {} 57 | # kubernetes.io/ingress.class: nginx 58 | # kubernetes.io/tls-acme: "true" 59 | hosts: 60 | - host: rbac-wizard.local 61 | paths: 62 | - path: / 63 | pathType: ImplementationSpecific 64 | tls: [] 65 | # - secretName: rbac-wizard-tls 66 | # hosts: 67 | # - rbac-wizard.local 68 | 69 | resources: 70 | {} 71 | # limits: 72 | # cpu: 100m 73 | # memory: 128Mi 74 | # requests: 75 | # cpu: 100m 76 | # memory: 128Mi 77 | 78 | livenessProbe: 79 | httpGet: 80 | path: / 81 | port: http 82 | readinessProbe: 83 | httpGet: 84 | path: / 85 | port: http 86 | 87 | autoscaling: 88 | enabled: false 89 | minReplicas: 1 90 | maxReplicas: 100 91 | targetCPUUtilizationPercentage: 80 92 | # targetMemoryUtilizationPercentage: 80 93 | 94 | volumes: [] 95 | # - name: foo 96 | # secret: 97 | # secretName: mysecret 98 | # optional: false 99 | 100 | volumeMounts: [] 101 | # - name: foo 102 | # mountPath: "/etc/foo" 103 | # readOnly: true 104 | 105 | nodeSelector: {} 106 | 107 | tolerations: [] 108 | 109 | affinity: {} 110 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2024 Furkan Pehlivan 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package cmd 24 | 25 | import ( 26 | "os" 27 | 28 | "github.com/spf13/cobra" 29 | ) 30 | 31 | // rootCmd represents the base command when called without any subcommands 32 | var rootCmd = &cobra.Command{ 33 | Use: "rbac-wizard", 34 | Short: "RBAC Wizard is a tool to visualize Kubernetes RBAC resources.", 35 | Long: `RBAC Wizard is a tool to visualize Kubernetes RBAC resources. It can be used to view and check the permissions relationships between RBAC objects.`, 36 | } 37 | 38 | func Execute() { 39 | err := rootCmd.Execute() 40 | if err != nil { 41 | os.Exit(1) 42 | } 43 | } 44 | 45 | func init() { 46 | rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 47 | } 48 | -------------------------------------------------------------------------------- /cmd/serve.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2024 Furkan Pehlivan 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package cmd 24 | 25 | import ( 26 | "encoding/json" 27 | "fmt" 28 | "io" 29 | "net/http" 30 | 31 | "github.com/rakyll/statik/fs" 32 | "github.com/rs/cors" 33 | "github.com/spf13/cobra" 34 | "gopkg.in/yaml.v2" 35 | v1 "k8s.io/api/rbac/v1" 36 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 37 | "k8s.io/apimachinery/pkg/runtime" 38 | kyaml "k8s.io/apimachinery/pkg/runtime/serializer/yaml" 39 | 40 | "github.com/pehlicd/rbac-wizard/internal" 41 | "github.com/pehlicd/rbac-wizard/internal/logger" 42 | _ "github.com/pehlicd/rbac-wizard/internal/statik" 43 | ) 44 | 45 | // serveCmd represents the serve command 46 | var serveCmd = &cobra.Command{ 47 | Use: "serve", 48 | Short: "Start the server for the rbac-wizard", 49 | Long: `Start the server for the rbac-wizard. This will start the server on the specified port and serve the frontend.`, 50 | Run: func(cmd *cobra.Command, args []string) { 51 | port, _ := cmd.Flags().GetString("port") 52 | enableLogging, _ := cmd.Flags().GetBool("logging") 53 | logLevel, _ := cmd.Flags().GetString("log-level") 54 | logFormat, _ := cmd.Flags().GetString("log-format") 55 | serve(port, enableLogging, logLevel, logFormat) 56 | }, 57 | } 58 | 59 | var app internal.App 60 | 61 | type Serve struct { 62 | App internal.App 63 | } 64 | 65 | func init() { 66 | rootCmd.AddCommand(serveCmd) 67 | 68 | serveCmd.Flags().StringP("port", "p", "8080", "Port to run the server on") 69 | serveCmd.Flags().BoolP("logging", "g", false, "Enable logging") 70 | serveCmd.Flags().StringP("log-level", "l", "info", "Log level") 71 | serveCmd.Flags().StringP("log-format", "f", "text", "Log format default is text [text, json]") 72 | } 73 | 74 | func serve(port string, logging bool, logLevel string, logFormat string) { 75 | // Set up logger if logging is enabled 76 | if logging { 77 | l := logger.New(logLevel, logFormat) 78 | app.Logger = l 79 | } else { 80 | l := logger.New("off", logFormat) 81 | app.Logger = l 82 | } 83 | 84 | kubeClient, err := internal.GetClientset() 85 | if err != nil { 86 | app.Logger.Fatal().Err(err).Msg("Failed to create Kubernetes client") 87 | } 88 | 89 | app.KubeClient = kubeClient 90 | 91 | serve := Serve{ 92 | app, 93 | } 94 | 95 | // Set up statik filesystem 96 | statikFS, err := fs.New() 97 | if err != nil { 98 | app.Logger.Fatal().Err(err).Msg("Failed to create statik filesystem") 99 | } 100 | 101 | // Set up CORS 102 | c := setupCors(port) 103 | 104 | // Create a new serve mux 105 | mux := http.NewServeMux() 106 | 107 | // Set up handlers 108 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 109 | serveStaticFiles(statikFS, w, r, "index.html") 110 | }) 111 | mux.HandleFunc("/what-if", func(w http.ResponseWriter, r *http.Request) { 112 | serveStaticFiles(statikFS, w, r, "what-if.html") 113 | }) 114 | mux.HandleFunc("/api/data", serve.dataHandler) 115 | mux.HandleFunc("/api/what-if", serve.whatIfHandler) 116 | 117 | handler := c.Handler(serve.App.LoggerMiddleware(mux)) 118 | 119 | // Start the server 120 | startupMessage := fmt.Sprintf("Starting rbac-wizard on %s", fmt.Sprintf("http://localhost:%s", port)) 121 | fmt.Println(startupMessage) 122 | if err := http.ListenAndServe(":"+port, handler); err != nil { 123 | fmt.Printf("Failed to start server: %v\n", err) 124 | } 125 | } 126 | 127 | func setupCors(port string) *cors.Cors { 128 | return cors.New(cors.Options{ 129 | AllowOriginVaryRequestFunc: func(r *http.Request, origin string) (bool, []string) { 130 | // Implement your dynamic origin check here 131 | host := r.Host // Extract the host from the request 132 | allowedOrigins := []string{"http://localhost:" + port, "https://" + host, "http://localhost:3000"} 133 | for _, allowedOrigin := range allowedOrigins { 134 | if origin == allowedOrigin { 135 | return true, []string{"Origin"} 136 | } 137 | } 138 | return false, nil 139 | }, 140 | AllowedMethods: []string{"GET", "POST", "OPTIONS"}, 141 | AllowedHeaders: []string{"Accept", "Content-Type", "X-CSRF-Token"}, 142 | AllowCredentials: true, 143 | }) 144 | } 145 | 146 | func serveStaticFiles(statikFS http.FileSystem, w http.ResponseWriter, r *http.Request, defaultFile string) { 147 | // Set cache control headers 148 | cacheControllers(w) 149 | 150 | path := r.URL.Path 151 | if path == "/" { 152 | path = "/" + defaultFile 153 | } 154 | 155 | file, err := statikFS.Open(path) 156 | if err != nil { 157 | // If the file is not found, serve the default file (index.html) 158 | file, err = statikFS.Open("/" + defaultFile) 159 | if err != nil { 160 | http.NotFound(w, r) 161 | return 162 | } 163 | } 164 | defer file.Close() 165 | 166 | // Get the file information 167 | fileInfo, err := file.Stat() 168 | if err != nil { 169 | http.NotFound(w, r) 170 | return 171 | } 172 | 173 | http.ServeContent(w, r, path, fileInfo.ModTime(), file) 174 | } 175 | 176 | func (s *Serve) dataHandler(w http.ResponseWriter, _ *http.Request) { 177 | // Set cache control headers 178 | cacheControllers(w) 179 | 180 | // Get the bindings 181 | bindings, err := internal.Generator(app).GetBindings() 182 | if err != nil { 183 | s.App.Logger.Error().Err(err).Msg("Failed to get bindings") 184 | http.Error(w, "Failed to get bindings", http.StatusInternalServerError) 185 | return 186 | } 187 | 188 | data := internal.GenerateData(bindings) 189 | byteData, err := json.Marshal(data) 190 | if err != nil { 191 | s.App.Logger.Error().Err(err).Msg("Failed to marshal data") 192 | http.Error(w, "Failed to marshal data", http.StatusInternalServerError) 193 | return 194 | } 195 | 196 | // Write the bindings to the response 197 | w.Header().Set("Content-Type", "application/json") 198 | _, err = w.Write(byteData) 199 | if err != nil { 200 | s.App.Logger.Error().Err(err).Msg("Failed to write data") 201 | http.Error(w, "Failed to write data", http.StatusInternalServerError) 202 | return 203 | } 204 | } 205 | 206 | func (s *Serve) whatIfHandler(w http.ResponseWriter, r *http.Request) { 207 | cacheControllers(w) 208 | 209 | if r.Method != http.MethodPost { 210 | s.App.Logger.Error().Msg("Invalid request method") 211 | http.Error(w, "Invalid request method", http.StatusMethodNotAllowed) 212 | return 213 | } 214 | 215 | body, err := io.ReadAll(r.Body) 216 | if err != nil { 217 | s.App.Logger.Error().Err(err).Msg("Failed to read request body") 218 | http.Error(w, "Failed to read request body", http.StatusInternalServerError) 219 | return 220 | } 221 | 222 | var input struct { 223 | Yaml string `json:"yaml"` 224 | } 225 | 226 | if err := json.Unmarshal(body, &input); err != nil { 227 | s.App.Logger.Error().Err(err).Msg("Failed to parse JSON") 228 | http.Error(w, "Failed to parse JSON", http.StatusBadRequest) 229 | return 230 | } 231 | 232 | var obj interface{} 233 | if err := yaml.Unmarshal([]byte(input.Yaml), &obj); err != nil { 234 | s.App.Logger.Error().Err(err).Msg("Invalid YAML format") 235 | http.Error(w, "Invalid YAML format", http.StatusBadRequest) 236 | return 237 | } 238 | 239 | var responseData struct { 240 | Nodes []internal.Node `json:"nodes"` 241 | Links []internal.Link `json:"links"` 242 | } 243 | 244 | if obj == nil { 245 | s.App.Logger.Error().Msg("Empty object") 246 | http.Error(w, "Empty object", http.StatusBadRequest) 247 | return 248 | } 249 | 250 | decode := kyaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) 251 | uObj := &unstructured.Unstructured{} 252 | _, _, err = decode.Decode([]byte(input.Yaml), nil, uObj) 253 | if err != nil { 254 | s.App.Logger.Error().Err(err).Msg("Failed to parse object") 255 | http.Error(w, "Failed to parse object", http.StatusBadRequest) 256 | return 257 | } 258 | 259 | switch obj.(map[interface{}]interface{})["kind"] { 260 | case "ClusterRoleBinding": 261 | crb := &v1.ClusterRoleBinding{} 262 | err = runtime.DefaultUnstructuredConverter.FromUnstructured(uObj.UnstructuredContent(), crb) 263 | if err != nil { 264 | s.App.Logger.Error().Err(err).Msg("Failed to convert to ClusterRoleBinding") 265 | http.Error(w, "Failed to convert to ClusterRoleBinding", http.StatusBadRequest) 266 | return 267 | } 268 | 269 | responseData = internal.WhatIfGenerator(app).ProcessClusterRoleBinding(crb) 270 | case "RoleBinding": 271 | rb := &v1.RoleBinding{} 272 | err = runtime.DefaultUnstructuredConverter.FromUnstructured(uObj.UnstructuredContent(), rb) 273 | if err != nil { 274 | s.App.Logger.Error().Err(err).Msg("Failed to convert to RoleBinding") 275 | http.Error(w, "Failed to convert to ClusterRoleBinding", http.StatusBadRequest) 276 | return 277 | } 278 | responseData = internal.WhatIfGenerator(app).ProcessRoleBinding(rb) 279 | default: 280 | s.App.Logger.Error().Msg("Unsupported resource type") 281 | http.Error(w, "Unsupported resource type", http.StatusBadRequest) 282 | return 283 | } 284 | 285 | respData, err := json.Marshal(responseData) 286 | if err != nil { 287 | s.App.Logger.Error().Err(err).Msg("Failed to marshal response data") 288 | http.Error(w, "Failed to marshal response data", http.StatusInternalServerError) 289 | return 290 | } 291 | 292 | w.Header().Set("Content-Type", "application/json") 293 | _, err = w.Write(respData) 294 | if err != nil { 295 | s.App.Logger.Error().Err(err).Msg("Failed to write response data") 296 | http.Error(w, "Failed to write response data", http.StatusInternalServerError) 297 | return 298 | } 299 | } 300 | 301 | func cacheControllers(w http.ResponseWriter) { 302 | // Set cache control headers 303 | w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") 304 | w.Header().Set("Pragma", "no-cache") 305 | w.Header().Set("Expires", "0") 306 | } 307 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2024 Furkan Pehlivan 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package cmd 24 | 25 | import ( 26 | "fmt" 27 | "os" 28 | 29 | "github.com/spf13/cobra" 30 | ) 31 | 32 | var ( 33 | versionString string 34 | buildDate string 35 | buildCommit string 36 | ) 37 | 38 | // versionCmd represents the version command 39 | var versionCmd = &cobra.Command{ 40 | Use: "version", 41 | Short: "Print the version information of rbac-wizard", 42 | Long: `This command will print the version information of rbac-wizard and exit.`, 43 | Run: func(cmd *cobra.Command, args []string) { 44 | fmt.Printf("RBAC Wizard version: %s\n", versionString) 45 | fmt.Printf("Build date: %s\n", buildDate) 46 | fmt.Printf("Build commit: %s\n", buildCommit) 47 | os.Exit(0) 48 | }, 49 | } 50 | 51 | func init() { 52 | rootCmd.AddCommand(versionCmd) 53 | } 54 | -------------------------------------------------------------------------------- /dev/kind.yaml: -------------------------------------------------------------------------------- 1 | kind: Cluster 2 | apiVersion: kind.x-k8s.io/v1alpha4 3 | name: rbac-wizard-dev 4 | nodes: 5 | - role: control-plane 6 | kubeadmConfigPatches: 7 | - | 8 | kind: InitConfiguration 9 | nodeRegistration: 10 | kubeletExtraArgs: 11 | node-labels: "ingress-ready=true" 12 | extraPortMappings: 13 | - containerPort: 80 14 | hostPort: 80 15 | protocol: TCP 16 | - containerPort: 443 17 | hostPort: 443 18 | protocol: TCP 19 | - role: worker 20 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/pehlicd/rbac-wizard 2 | 3 | go 1.23.3 4 | 5 | require ( 6 | github.com/rakyll/statik v0.1.7 7 | github.com/rs/cors v1.11.0 8 | github.com/spf13/cobra v1.8.0 9 | gopkg.in/yaml.v2 v2.4.0 10 | k8s.io/api v0.30.0 11 | k8s.io/apimachinery v0.30.0 12 | k8s.io/client-go v0.30.0 13 | ) 14 | 15 | require ( 16 | github.com/mattn/go-colorable v0.1.13 // indirect 17 | github.com/mattn/go-isatty v0.0.19 // indirect 18 | ) 19 | 20 | require ( 21 | github.com/davecgh/go-spew v1.1.1 // indirect 22 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 23 | github.com/go-logr/logr v1.4.1 // indirect 24 | github.com/go-openapi/jsonpointer v0.19.6 // indirect 25 | github.com/go-openapi/jsonreference v0.20.2 // indirect 26 | github.com/go-openapi/swag v0.22.3 // indirect 27 | github.com/gogo/protobuf v1.3.2 // indirect 28 | github.com/golang/protobuf v1.5.4 // indirect 29 | github.com/google/gnostic-models v0.6.8 // indirect 30 | github.com/google/gofuzz v1.2.0 // indirect 31 | github.com/google/uuid v1.3.0 // indirect 32 | github.com/imdario/mergo v0.3.6 // indirect 33 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 34 | github.com/josharian/intern v1.0.0 // indirect 35 | github.com/json-iterator/go v1.1.12 // indirect 36 | github.com/mailru/easyjson v0.7.7 // indirect 37 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 38 | github.com/modern-go/reflect2 v1.0.2 // indirect 39 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 40 | github.com/rs/zerolog v1.33.0 41 | github.com/spf13/pflag v1.0.5 // indirect 42 | golang.org/x/net v0.23.0 // indirect 43 | golang.org/x/oauth2 v0.10.0 // indirect 44 | golang.org/x/sys v0.18.0 // indirect 45 | golang.org/x/term v0.18.0 // indirect 46 | golang.org/x/text v0.14.0 // indirect 47 | golang.org/x/time v0.3.0 // indirect 48 | google.golang.org/appengine v1.6.7 // indirect 49 | google.golang.org/protobuf v1.33.0 // indirect 50 | gopkg.in/inf.v0 v0.9.1 // indirect 51 | gopkg.in/yaml.v3 v3.0.1 // indirect 52 | k8s.io/klog/v2 v2.120.1 // indirect 53 | k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect 54 | k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect 55 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 56 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect 57 | sigs.k8s.io/yaml v1.3.0 // indirect 58 | ) 59 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 2 | github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 3 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= 8 | github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 9 | github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= 10 | github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 11 | github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= 12 | github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 13 | github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= 14 | github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= 15 | github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= 16 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 17 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= 18 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= 19 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 20 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 21 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 22 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 23 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 24 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 25 | github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= 26 | github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= 27 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 28 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 29 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 30 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 31 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 32 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 33 | github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= 34 | github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 35 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 36 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 37 | github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= 38 | github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 39 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 40 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 41 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 42 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 43 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 44 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 45 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 46 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 47 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 48 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 49 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 50 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 51 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 52 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 53 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 54 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 55 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 56 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 57 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 58 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 59 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 60 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 61 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 62 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 63 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 64 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 65 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 66 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 67 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 68 | github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY= 69 | github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM= 70 | github.com/onsi/gomega v1.31.0 h1:54UJxxj6cPInHS3a35wm6BK/F9nHYueZ1NVujHDrnXE= 71 | github.com/onsi/gomega v1.31.0/go.mod h1:DW9aCi7U6Yi40wNVAvT6kzFnEVEI5n3DloYBiKiT6zk= 72 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 73 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 74 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 75 | github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ= 76 | github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc= 77 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 78 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 79 | github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= 80 | github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= 81 | github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 82 | github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= 83 | github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= 84 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 85 | github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= 86 | github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= 87 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 88 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 89 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 90 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 91 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 92 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 93 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 94 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 95 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 96 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 97 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 98 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 99 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 100 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 101 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 102 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 103 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 104 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 105 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 106 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 107 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 108 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 109 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 110 | golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= 111 | golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 112 | golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8= 113 | golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI= 114 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 115 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 116 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 117 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 118 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 119 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 120 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 121 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 122 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 123 | golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= 124 | golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 125 | golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= 126 | golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= 127 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 128 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 129 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 130 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 131 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 132 | golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= 133 | golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 134 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 135 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 136 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 137 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 138 | golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= 139 | golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= 140 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 141 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 142 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 143 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 144 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= 145 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 146 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 147 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 148 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 149 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 150 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 151 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 152 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 153 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 154 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 155 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 156 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 157 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 158 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 159 | k8s.io/api v0.30.0 h1:siWhRq7cNjy2iHssOB9SCGNCl2spiF1dO3dABqZ8niA= 160 | k8s.io/api v0.30.0/go.mod h1:OPlaYhoHs8EQ1ql0R/TsUgaRPhpKNxIMrKQfWUp8QSE= 161 | k8s.io/apimachinery v0.30.0 h1:qxVPsyDM5XS96NIh9Oj6LavoVFYff/Pon9cZeDIkHHA= 162 | k8s.io/apimachinery v0.30.0/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= 163 | k8s.io/client-go v0.30.0 h1:sB1AGGlhY/o7KCyCEQ0bPWzYDL0pwOZO4vAtTSh/gJQ= 164 | k8s.io/client-go v0.30.0/go.mod h1:g7li5O5256qe6TYdAMyX/otJqMhIiGgTapdLchhmOaY= 165 | k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= 166 | k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 167 | k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= 168 | k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= 169 | k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= 170 | k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 171 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= 172 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= 173 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= 174 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= 175 | sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= 176 | sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= 177 | -------------------------------------------------------------------------------- /internal/bindings.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2024 Furkan Pehlivan 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package internal 24 | 25 | import ( 26 | "context" 27 | "fmt" 28 | 29 | "gopkg.in/yaml.v2" 30 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 | "k8s.io/apimachinery/pkg/runtime" 32 | "k8s.io/apimachinery/pkg/runtime/serializer/json" 33 | "k8s.io/client-go/kubernetes/scheme" 34 | ) 35 | 36 | const ( 37 | ClusterRoleBindingKind = "ClusterRoleBinding" 38 | RoleBindingKind = "RoleBinding" 39 | ClusterRoleBindingAPIVersion = "rbac.authorization.k8s.io/v1" 40 | RoleBindingAPIVersion = "rbac.authorization.k8s.io/v1" 41 | ) 42 | 43 | func (app App) GetBindings() (*Bindings, error) { 44 | clientset := app.KubeClient 45 | 46 | crbs, err := clientset.RbacV1().ClusterRoleBindings().List(context.TODO(), metav1.ListOptions{}) 47 | if err != nil { 48 | fmt.Println("Error listing cluster role bindings:", err) 49 | return nil, err 50 | } 51 | 52 | rbs, err := clientset.RbacV1().RoleBindings("").List(context.TODO(), metav1.ListOptions{}) 53 | if err != nil { 54 | fmt.Println("Error listing role bindings:", err) 55 | return nil, err 56 | } 57 | 58 | return &Bindings{ 59 | ClusterRoleBindings: crbs, 60 | RoleBindings: rbs, 61 | }, nil 62 | } 63 | 64 | func GenerateData(bindings *Bindings) []Data { 65 | var data []Data 66 | var i int 67 | 68 | for _, crb := range bindings.ClusterRoleBindings.Items { 69 | crb.ManagedFields = nil 70 | 71 | data = append(data, Data{ 72 | Name: crb.Name, 73 | Id: i, 74 | Kind: ClusterRoleBindingKind, 75 | Subjects: crb.Subjects, 76 | RoleRef: crb.RoleRef, 77 | Raw: yamlParser(&crb, ClusterRoleBindingKind, ClusterRoleBindingAPIVersion), 78 | }) 79 | i++ 80 | } 81 | 82 | for _, rb := range bindings.RoleBindings.Items { 83 | rb.ManagedFields = nil 84 | data = append(data, Data{ 85 | Name: rb.Name, 86 | Id: i, 87 | Kind: RoleBindingKind, 88 | Subjects: rb.Subjects, 89 | RoleRef: rb.RoleRef, 90 | Raw: yamlParser(&rb, RoleBindingKind, RoleBindingAPIVersion), 91 | }) 92 | i++ 93 | } 94 | 95 | return data 96 | } 97 | 98 | func yamlParser(obj runtime.Object, kind string, apiVersion string) string { 99 | // Convert the object to YAML 100 | s := json.NewSerializerWithOptions(json.DefaultMetaFactory, scheme.Scheme, scheme.Scheme, json.SerializerOptions{Yaml: true, Pretty: true}) 101 | o, err := runtime.Encode(s, obj) 102 | if err != nil { 103 | return "" 104 | } 105 | 106 | if len(o) == 0 { 107 | return "Could not parse the object" 108 | } 109 | 110 | // Prepend the kind and apiVersion to the YAML 111 | o = []byte(fmt.Sprintf("kind: %s\napiVersion: %s\n%s", kind, apiVersion, o)) 112 | 113 | // Unmarshal the JSON into a generic map 114 | var yamlObj map[string]interface{} 115 | err = yaml.Unmarshal(o, &yamlObj) 116 | if err != nil { 117 | return err.Error() 118 | } 119 | 120 | // Marshal the map back into YAML 121 | yamlData, err := yaml.Marshal(yamlObj) 122 | if err != nil { 123 | return err.Error() 124 | } 125 | 126 | return string(yamlData) 127 | } 128 | -------------------------------------------------------------------------------- /internal/client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2024 Furkan Pehlivan 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package internal 24 | 25 | import ( 26 | "fmt" 27 | "os" 28 | "path/filepath" 29 | 30 | "k8s.io/client-go/kubernetes" 31 | "k8s.io/client-go/rest" 32 | "k8s.io/client-go/tools/clientcmd" 33 | "k8s.io/client-go/util/homedir" 34 | ) 35 | 36 | // GetClientset Creates a new clientset for the kubernetes 37 | func GetClientset() (*kubernetes.Clientset, error) { 38 | var config *rest.Config 39 | 40 | // First try to use the in-cluster configuration 41 | config, err := rest.InClusterConfig() 42 | if err != nil { 43 | // Fallback to kubeconfig 44 | var kubeconfig string 45 | if kc := os.Getenv("KUBECONFIG"); kc != "" { 46 | kubeconfig = kc 47 | } else { 48 | kubeconfig = filepath.Join(homedir.HomeDir(), ".kube", "config") 49 | } 50 | 51 | config, err = clientcmd.BuildConfigFromFlags("", kubeconfig) 52 | if err != nil { 53 | return nil, fmt.Errorf("failed to build config from kubeconfig path %s: %v", kubeconfig, err) 54 | } 55 | } 56 | 57 | // Create and store the clientset 58 | clientset, err := kubernetes.NewForConfig(config) 59 | if err != nil { 60 | return nil, fmt.Errorf("failed to create clientset: %v", err) 61 | } 62 | 63 | return clientset, nil 64 | } 65 | -------------------------------------------------------------------------------- /internal/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/rs/zerolog" 7 | ) 8 | 9 | func New(level string, format string) *zerolog.Logger { 10 | logger := zerolog.New(os.Stdout).Level(levelFromString(level)).With().Timestamp().Logger() 11 | if format == "json" { 12 | logger = logger.Output(zerolog.ConsoleWriter{Out: os.Stdout}) 13 | } else if format == "text" { 14 | logger = logger.Output(zerolog.ConsoleWriter{Out: os.Stdout, NoColor: true}) 15 | } 16 | 17 | return &logger 18 | } 19 | 20 | func levelFromString(level string) zerolog.Level { 21 | switch level { 22 | case "debug": 23 | return zerolog.DebugLevel 24 | case "info": 25 | return zerolog.InfoLevel 26 | case "warn": 27 | return zerolog.WarnLevel 28 | case "error": 29 | return zerolog.ErrorLevel 30 | case "fatal": 31 | return zerolog.FatalLevel 32 | case "panic": 33 | return zerolog.PanicLevel 34 | case "off": 35 | return zerolog.Disabled 36 | default: 37 | return zerolog.InfoLevel 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /internal/middlewares.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/rs/zerolog" 8 | ) 9 | 10 | func (l App) LoggerMiddleware(next http.Handler) http.Handler { 11 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 12 | start := time.Now() 13 | ww := &responseWriter{w: w, status: http.StatusOK} 14 | 15 | next.ServeHTTP(ww, r) 16 | 17 | duration := time.Since(start) 18 | 19 | level := zerolog.InfoLevel 20 | if ww.status >= 500 { 21 | level = zerolog.ErrorLevel 22 | } else if ww.status >= 400 { 23 | level = zerolog.WarnLevel 24 | } 25 | 26 | l.Logger.WithLevel(level). 27 | Str("method", r.Method). 28 | Str("path", r.URL.Path). 29 | Int("status", ww.status). 30 | Str("remote_addr", r.RemoteAddr). 31 | Dur("duration", duration). 32 | Msg("") 33 | }) 34 | } 35 | 36 | type responseWriter struct { 37 | w http.ResponseWriter 38 | status int 39 | wroteHeader bool 40 | } 41 | 42 | func (rw *responseWriter) Header() http.Header { 43 | return rw.w.Header() 44 | } 45 | 46 | func (rw *responseWriter) Write(b []byte) (int, error) { 47 | if !rw.wroteHeader { 48 | rw.WriteHeader(http.StatusOK) 49 | } 50 | return rw.w.Write(b) 51 | } 52 | 53 | func (rw *responseWriter) WriteHeader(statusCode int) { 54 | if !rw.wroteHeader { 55 | rw.status = statusCode 56 | rw.w.WriteHeader(statusCode) 57 | rw.wroteHeader = true 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /internal/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2024 Furkan Pehlivan 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package internal 24 | 25 | import ( 26 | "github.com/rs/zerolog" 27 | v1 "k8s.io/api/rbac/v1" 28 | "k8s.io/client-go/kubernetes" 29 | ) 30 | 31 | type App struct { 32 | KubeClient *kubernetes.Clientset 33 | Logger *zerolog.Logger 34 | } 35 | 36 | type Generator interface { 37 | GetBindings() (*Bindings, error) 38 | } 39 | 40 | type WhatIfGenerator interface { 41 | ProcessClusterRoleBinding(crb *v1.ClusterRoleBinding) struct { 42 | Nodes []Node `json:"nodes"` 43 | Links []Link `json:"links"` 44 | } 45 | ProcessRoleBinding(rb *v1.RoleBinding) struct { 46 | Nodes []Node `json:"nodes"` 47 | Links []Link `json:"links"` 48 | } 49 | } 50 | 51 | type Bindings struct { 52 | ClusterRoleBindings *v1.ClusterRoleBindingList `json:"clusterRoleBindings"` 53 | RoleBindings *v1.RoleBindingList `json:"roleBindings"` 54 | } 55 | 56 | type Data struct { 57 | Id int `json:"id"` 58 | Name string `json:"name"` 59 | Kind string `json:"kind"` 60 | Subjects []v1.Subject `json:"subjects"` 61 | RoleRef v1.RoleRef `json:"roleRef"` 62 | Raw string `json:"raw"` 63 | } 64 | -------------------------------------------------------------------------------- /internal/whatIf.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2024 Furkan Pehlivan 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package internal 24 | 25 | import ( 26 | "context" 27 | "fmt" 28 | 29 | v1 "k8s.io/api/rbac/v1" 30 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 | "k8s.io/client-go/kubernetes" 32 | ) 33 | 34 | type Node struct { 35 | ID string `json:"id"` 36 | Kind string `json:"kind"` 37 | ApiGroup string `json:"apiGroup"` 38 | Label string `json:"label"` 39 | } 40 | 41 | type Link struct { 42 | Source string `json:"source"` 43 | Target string `json:"target"` 44 | } 45 | 46 | func (app App) ProcessClusterRoleBinding(crb *v1.ClusterRoleBinding) (data struct { 47 | Nodes []Node `json:"nodes"` 48 | Links []Link `json:"links"` 49 | }) { 50 | crbNodeID := crb.Kind + "-" + crb.Name 51 | data.Nodes = append(data.Nodes, Node{ 52 | ID: crbNodeID, 53 | Kind: crb.Kind, 54 | ApiGroup: crb.APIVersion, 55 | Label: crbNodeID, 56 | }) 57 | 58 | for _, subject := range crb.Subjects { 59 | subjectInfo := fetchSubjectDetails(app.KubeClient, subject) 60 | if subjectInfo != nil { 61 | data.Nodes = append(data.Nodes, *subjectInfo) 62 | data.Links = append(data.Links, Link{ 63 | Source: crbNodeID, 64 | Target: subjectInfo.ID, 65 | }) 66 | } 67 | } 68 | 69 | roleRefInfo := fetchRoleRefDetails(app.KubeClient, crb.RoleRef) 70 | if roleRefInfo != nil { 71 | data.Nodes = append(data.Nodes, *roleRefInfo) 72 | data.Links = append(data.Links, Link{ 73 | Source: crbNodeID, 74 | Target: roleRefInfo.ID, 75 | }) 76 | } 77 | 78 | return data 79 | } 80 | 81 | func (app App) ProcessRoleBinding(rb *v1.RoleBinding) (data struct { 82 | Nodes []Node `json:"nodes"` 83 | Links []Link `json:"links"` 84 | }) { 85 | rbNodeID := rb.Kind + "-" + rb.Name 86 | data.Nodes = append(data.Nodes, Node{ 87 | ID: rbNodeID, 88 | Kind: rb.Kind, 89 | ApiGroup: rb.APIVersion, 90 | Label: rbNodeID, 91 | }) 92 | 93 | for _, subject := range rb.Subjects { 94 | subjectInfo := fetchSubjectDetails(app.KubeClient, subject) 95 | if subjectInfo != nil { 96 | data.Nodes = append(data.Nodes, *subjectInfo) 97 | data.Links = append(data.Links, Link{ 98 | Source: rbNodeID, 99 | Target: subjectInfo.ID, 100 | }) 101 | } 102 | } 103 | 104 | roleRefInfo := fetchRoleRefDetails(app.KubeClient, rb.RoleRef) 105 | if roleRefInfo != nil { 106 | data.Nodes = append(data.Nodes, *roleRefInfo) 107 | data.Links = append(data.Links, Link{ 108 | Source: rbNodeID, 109 | Target: roleRefInfo.ID, 110 | }) 111 | } 112 | 113 | return data 114 | } 115 | 116 | func fetchSubjectDetails(client *kubernetes.Clientset, subject v1.Subject) *Node { 117 | if subject.Kind == "ServiceAccount" { 118 | _, err := client.CoreV1().ServiceAccounts(subject.Namespace).Get(context.TODO(), subject.Name, metav1.GetOptions{}) 119 | if err != nil { 120 | return nil 121 | } 122 | } 123 | return &Node{ 124 | ID: subject.Kind + "-" + subject.Name, 125 | Kind: subject.Kind, 126 | ApiGroup: subject.APIGroup, 127 | Label: subject.Kind + "-" + subject.Name, 128 | } 129 | } 130 | 131 | func fetchRoleRefDetails(client *kubernetes.Clientset, roleRef v1.RoleRef) *Node { 132 | switch roleRef.Kind { 133 | case "ClusterRole": 134 | _, err := client.RbacV1().ClusterRoles().Get(context.TODO(), roleRef.Name, metav1.GetOptions{}) 135 | if err != nil { 136 | fmt.Printf("Error fetching cluster role: %v\n", err) 137 | return nil 138 | } 139 | return &Node{ 140 | ID: roleRef.Kind + "-" + roleRef.Name, 141 | Kind: roleRef.Kind, 142 | ApiGroup: roleRef.APIGroup, 143 | Label: roleRef.Kind + "-" + roleRef.Name, 144 | } 145 | case "Role": 146 | return &Node{ 147 | ID: roleRef.Kind + "-" + roleRef.Name, 148 | Kind: roleRef.Kind, 149 | ApiGroup: roleRef.APIGroup, 150 | Label: roleRef.Kind + "-" + roleRef.Name, 151 | } 152 | } 153 | 154 | return nil 155 | } 156 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2024 Furkan Pehlivan 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | package main 23 | 24 | import "github.com/pehlicd/rbac-wizard/cmd" 25 | 26 | func main() { 27 | cmd.Execute() 28 | } 29 | -------------------------------------------------------------------------------- /rbac-wizard.rb: -------------------------------------------------------------------------------- 1 | class RbacWizard < Formula 2 | desc "RBAC Wizard is a tool that helps you visualize and analyze the RBAC configurations of your Kubernetes cluster. It provides a graphical representation of the Kubernetes RBAC objects." 3 | homepage "https://github.com/pehlicd/rbac-wizard" 4 | url "https://github.com/pehlicd/rbac-wizard.git", 5 | tag: "v0.0.6", 6 | revision: "2aacf572eb466586d4dee921eedb5c61adfe86f5" 7 | license "MIT" 8 | head "https://github.com/pehlicd/rbac-wizard.git", branch: "main" 9 | 10 | depends_on "go" => :build 11 | 12 | def install 13 | project = "github.com/pehlicd/rbac-wizard" 14 | ldflags = %W[ 15 | -s -w 16 | -X #{project}/cmd.versionString=#{version} 17 | -X #{project}/cmd.buildCommit=#{Utils.git_head} 18 | -X #{project}/cmd.buildDate=#{time.iso8601} 19 | ] 20 | system "go", "build", *std_go_args(ldflags: ldflags) 21 | end 22 | 23 | test do 24 | assert_match version.to_s, "#{bin}/rbac-wizard version" 25 | end 26 | end -------------------------------------------------------------------------------- /ui/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /ui/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /ui/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2024 Furkan Pehlivan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /ui/app/error.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useEffect } from 'react' 4 | 5 | export default function Error({ 6 | error, 7 | reset, 8 | }: { 9 | error: Error 10 | reset: () => void 11 | }) { 12 | useEffect(() => { 13 | // Log the error to an error reporting service 14 | console.error(error) 15 | }, [error]) 16 | 17 | return ( 18 |
19 |

Something went wrong!

20 | 28 |
29 | ) 30 | } -------------------------------------------------------------------------------- /ui/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "@/styles/globals.css"; 2 | import { Metadata, Viewport } from "next"; 3 | import { siteConfig } from "@/config/site"; 4 | import { fontSans } from "@/config/fonts"; 5 | import { Providers } from "./providers"; 6 | import { Navbar } from "@/components/navbar"; 7 | import clsx from "clsx"; 8 | 9 | export const metadata: Metadata = { 10 | title: { 11 | default: siteConfig.name, 12 | template: `%s - ${siteConfig.name}`, 13 | }, 14 | description: siteConfig.description, 15 | icons: { 16 | icon: "/rbac-wizard-logo.png", 17 | }, 18 | }; 19 | 20 | export const viewport: Viewport = { 21 | themeColor: [ 22 | { media: "(prefers-color-scheme: light)", color: "white" }, 23 | { media: "(prefers-color-scheme: dark)", color: "black" }, 24 | ], 25 | } 26 | 27 | export default function RootLayout({ 28 | children, 29 | }: { 30 | children: React.ReactNode; 31 | }) { 32 | return ( 33 | 34 | RBAC Wizard 35 | 41 | 42 |
43 | 44 |
45 | {children} 46 |
47 |
48 |
49 | 50 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /ui/app/page.tsx: -------------------------------------------------------------------------------- 1 | import MainTable from "@/components/table"; 2 | import {Card, CardBody, CardHeader} from "@nextui-org/card"; 3 | import DisjointGraph from "@/components/graph"; 4 | 5 | export default function Home() { 6 | return ( 7 |
8 |
9 | 10 |
11 |
12 |
13 | 14 | 15 |

RBAC Map

16 |
17 | 18 | 19 | 20 |
21 |
22 |
23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /ui/app/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { NextUIProvider } from "@nextui-org/system"; 5 | import { useRouter } from 'next/navigation' 6 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 7 | import { ThemeProviderProps } from "next-themes/dist/types"; 8 | 9 | export interface ProvidersProps { 10 | children: React.ReactNode; 11 | themeProps?: ThemeProviderProps; 12 | } 13 | 14 | export function Providers({ children, themeProps }: ProvidersProps) { 15 | const router = useRouter(); 16 | 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /ui/app/what-if/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Editor from '@monaco-editor/react'; 3 | import { Card, CardBody, CardHeader } from "@nextui-org/card"; 4 | import { Badge, Button } from "@nextui-org/react"; 5 | import { useTheme } from 'next-themes'; 6 | import { useState } from 'react'; 7 | import axios from 'axios'; 8 | import DisjointGraph from '@/components/graph'; 9 | import { IoInformationCircle } from "react-icons/io5"; 10 | import {Tooltip} from "@nextui-org/tooltip"; 11 | 12 | export default function WhatIfPage() { 13 | const { theme } = useTheme(); 14 | const isDarkMode = theme === 'dark'; 15 | const editorTheme = isDarkMode ? 'vs-dark' : 'light'; 16 | 17 | const [yamlContent, setYamlContent] = useState(''); 18 | const [graphData, setGraphData] = useState<{ nodes: any[]; links: any[] } | null>(null); 19 | 20 | const handleEditorChange = (value: string | undefined) => { 21 | setYamlContent(value || ''); 22 | }; 23 | 24 | const handleGenerateClick = async () => { 25 | try { 26 | const response = await axios.post('/api/what-if', { yaml: yamlContent }); 27 | setGraphData(response.data); 28 | } catch (error) { 29 | console.error('Error generating graph:', error); 30 | } 31 | }; 32 | 33 | return ( 34 |
35 |
36 | 37 | 38 | 39 |

Editor

40 | 45 |
What is `What If?`
46 |
`What If?` helps you easily add one of your ClusterRoleBinding or RoleBinding manifests. When you click the `Generate` button, it visualizes your binding in a map format.
47 |
48 |
⚠️Please note that this feature is still in beta. If you encounter any issues, please report them on our GitHub page.
49 |
50 | } 51 | > 52 | 55 | 56 | 57 | 58 | 59 | 67 | 68 | 69 | 70 | 71 |

Graph

72 |
73 | 74 | {graphData && graphData.nodes && graphData.links && ( 75 | 76 | )} 77 | 78 |
79 | 80 | 81 |
82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /ui/components/graph.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useEffect, useRef, useState, useCallback } from 'react'; 3 | import * as d3 from 'd3'; 4 | import { useTheme } from 'next-themes'; 5 | import debounce from 'lodash.debounce'; 6 | import axios from "axios"; 7 | import { Select, SelectItem, Button } from '@nextui-org/react'; 8 | 9 | interface Node extends d3.SimulationNodeDatum { 10 | id: string; 11 | kind?: string; 12 | label: string; 13 | x?: number; 14 | y?: number; 15 | } 16 | 17 | interface Link extends d3.SimulationLinkDatum { 18 | source: string | Node; 19 | target: string | Node; 20 | } 21 | 22 | type Subject = { 23 | kind: string; 24 | apiGroup: string; 25 | name: string; 26 | }; 27 | 28 | type RoleRef = { 29 | kind: string; 30 | apiGroup: string; 31 | name: string; 32 | }; 33 | 34 | type BindingData = { 35 | id: number; 36 | name: string; 37 | kind: string; 38 | subjects: Subject[]; 39 | roleRef: RoleRef; 40 | details?: string; 41 | }; 42 | 43 | const Tooltip = ({ node, isDarkMode }: { node: Node; isDarkMode: boolean }) => ( 44 |
56 | {node.label} 57 |
58 | ); 59 | 60 | const DisjointGraph = ({ data, disable }: { data?: { nodes: Node[], links: Link[] }, disable?: boolean }) => { 61 | const svgRef = useRef(null); 62 | const [hoveredNode, setHoveredNode] = useState(null); 63 | const [selectedNodes, setSelectedNodes] = useState>(new Set()); 64 | const [allNodes, setAllNodes] = useState([]); 65 | const [allLinks, setAllLinks] = useState([]); 66 | const [bindingData, setBindingData] = useState([]); 67 | const { theme } = useTheme(); 68 | const isDarkMode = theme === 'dark'; 69 | 70 | const processGraphData = (data: BindingData[]) => { 71 | const nodes: Node[] = []; 72 | const links: Link[] = []; 73 | 74 | data.forEach(binding => { 75 | if (!binding.name || !binding.kind || !binding.subjects || !binding.roleRef) { 76 | console.error('Invalid binding data:', binding); 77 | return; 78 | } 79 | 80 | nodes.push({ id: binding.name, kind: binding.kind, label: binding.name }); 81 | 82 | binding.subjects.forEach(subject => { 83 | if (!subject.kind || !subject.apiGroup || !subject.name) { 84 | console.error('Invalid subject data:', subject); 85 | return; 86 | } 87 | const subjectId = `${subject.kind}-${subject.name}`; 88 | if (!nodes.find(n => n.id === subjectId)) { 89 | nodes.push({ id: subjectId, label: `${subject.kind} - ${subject.name}` }); 90 | } 91 | links.push({ source: binding.name, target: subjectId }); 92 | }); 93 | 94 | const roleRefId = `${binding.roleRef.kind}-${binding.roleRef.name}`; 95 | if (!nodes.find(n => n.id === roleRefId)) { 96 | nodes.push({ id: roleRefId, label: `${binding.roleRef.kind} - ${binding.roleRef.name}` }); 97 | } 98 | links.push({ source: binding.name, target: roleRefId }); 99 | }); 100 | 101 | return { nodes, links }; 102 | }; 103 | 104 | const renderGraph = useCallback((nodes: Node[], links: Link[], selectedIds: Set) => { 105 | const svg = d3.select(svgRef.current); 106 | svg.selectAll("*").remove(); // Clear previous elements 107 | 108 | const width = svg.node()?.clientWidth || 800; 109 | const height = svg.node()?.clientHeight || 600; 110 | 111 | const filteredNodes = new Set(); 112 | const filteredLinks = new Set(); 113 | 114 | if (selectedIds.size > 0) { 115 | selectedIds.forEach(id => { 116 | const mainNode = nodes.find(node => node.id === id); 117 | if (mainNode) { 118 | filteredNodes.add(mainNode); 119 | 120 | links.forEach(link => { 121 | if (link.source === id || link.target === id) { 122 | filteredLinks.add(link); 123 | filteredNodes.add(nodes.find(node => node.id === link.source) as Node); 124 | filteredNodes.add(nodes.find(node => node.id === link.target) as Node); 125 | } 126 | }); 127 | } 128 | }); 129 | } else { 130 | nodes.forEach(node => filteredNodes.add(node)); 131 | links.forEach(link => filteredLinks.add(link)); 132 | } 133 | 134 | const nodesToRender = Array.from(filteredNodes); 135 | const linksToRender = Array.from(filteredLinks); 136 | 137 | const g = svg.append('g'); 138 | 139 | const zoom = d3.zoom() 140 | .scaleExtent([0.1, 2]) 141 | .on('zoom', (event) => { 142 | g.attr('transform', event.transform.toString()); 143 | }); 144 | 145 | svg.call(zoom as any); 146 | 147 | const simulation = d3.forceSimulation(nodesToRender) 148 | .force('link', d3.forceLink(linksToRender).id((d: any) => d.id).distance(100)) 149 | .force('charge', d3.forceManyBody().strength(-100)) 150 | .force('center', d3.forceCenter(width / 2, height / 2)); 151 | 152 | const link = g.selectAll('.link') 153 | .data(linksToRender) 154 | .enter().append('line') 155 | .attr('class', 'link') 156 | .attr('stroke', '#666') 157 | .attr('stroke-width', 2); 158 | 159 | const node = g.selectAll('.node') 160 | .data(nodesToRender) 161 | .enter().append('circle') 162 | .attr('class', 'node') 163 | .attr('r', 10) 164 | .attr('fill', d => d.kind === 'ClusterRoleBinding' ? 'orange' : d.kind === 'RoleBinding' ? 'green' : 'pink') 165 | .call(drag(simulation) as any) 166 | .on('mouseover', debounce((_event, d) => setHoveredNode(d), 50)) 167 | .on('mouseout', debounce(() => setHoveredNode(null), 50)); 168 | 169 | const text = g.selectAll('.label') 170 | .data(nodesToRender) 171 | .enter().append('text') 172 | .attr('class', 'label') 173 | .attr('x', d => d.x!) 174 | .attr('y', d => d.y!) 175 | .text(d => d.label) 176 | .style('font-size', '13px') 177 | .style('fill', isDarkMode ? '#FFF' : '#000') 178 | .style('text-anchor', '-moz-initial') 179 | .style('font-weight', 'bold'); 180 | 181 | simulation.on('tick', () => { 182 | link 183 | .attr('x1', d => (d.source as Node).x!) 184 | .attr('y1', d => (d.source as Node).y!) 185 | .attr('x2', d => (d.target as Node).x!) 186 | .attr('y2', d => (d.target as Node).y!); 187 | 188 | node 189 | .attr('cx', d => d.x!) 190 | .attr('cy', d => d.y!); 191 | 192 | text 193 | .attr('x', d => d.x!) 194 | .attr('y', d => d.y!); 195 | }); 196 | 197 | // Legend 198 | const legend = svg.append('g') 199 | .attr('class', 'legend') 200 | .attr('transform', 'translate(20,20)'); 201 | 202 | const legendData = [ 203 | { label: 'ClusterRoleBinding', color: 'orange' }, 204 | { label: 'RoleBinding', color: 'green' }, 205 | { label: 'Other', color: 'pink' } 206 | ]; 207 | 208 | const legendItem = legend.selectAll('.legend-item') 209 | .data(legendData) 210 | .enter().append('g') 211 | .attr('class', 'legend-item') 212 | .attr('transform', (_d, i) => `translate(0, ${i * 20})`); 213 | 214 | legendItem.append('rect') 215 | .attr('width', 18) 216 | .attr('height', 18) 217 | .attr('fill', d => d.color); 218 | 219 | legendItem.append('text') 220 | .attr('x', 24) 221 | .attr('y', 9) 222 | .attr('dy', '0.35em') 223 | .text(d => d.label) 224 | .style('font-size', '12px') 225 | .style('fill', isDarkMode ? '#FFF' : '#333'); 226 | 227 | return () => { 228 | simulation.stop(); 229 | }; 230 | }, [isDarkMode]); 231 | 232 | useEffect(() => { 233 | const fetchData = async () => { 234 | try { 235 | const response = await axios.get('/api/data'); 236 | const data: BindingData[] | null = response.data; 237 | if (!data) { 238 | console.error(new Error('Data is null or undefined')); 239 | return; 240 | } 241 | const { nodes, links } = processGraphData(data); 242 | setAllNodes(nodes); 243 | setAllLinks(links); 244 | setBindingData(data); 245 | renderGraph(nodes, links, new Set()); 246 | } catch (error) { 247 | console.error('Error fetching data:', error); 248 | } 249 | }; 250 | 251 | if (!data) { 252 | fetchData().finally(() => { console.log('Data fetching completed'); }); 253 | } else { 254 | renderGraph(data.nodes, data.links, new Set()); 255 | } 256 | }, [isDarkMode, renderGraph, data]); 257 | 258 | useEffect(() => { 259 | const text = d3.selectAll('.label'); 260 | text.style('fill', isDarkMode ? '#FFF' : '#000'); 261 | 262 | const legendText = d3.selectAll('.legend-item text'); 263 | legendText.style('fill', isDarkMode ? '#FFF' : '#333'); 264 | }, [isDarkMode]); 265 | 266 | const drag = (simulation: d3.Simulation) => { 267 | const dragStarted = (event: d3.D3DragEvent, d: Node) => { 268 | if (!event.active) simulation.alphaTarget(0.3).restart(); 269 | d.fx = d.x; 270 | d.fy = d.y; 271 | }; 272 | 273 | const dragged = (event: d3.D3DragEvent, d: Node) => { 274 | d.fx = event.x; 275 | d.fy = event.y; 276 | }; 277 | 278 | const dragEnded = (event: d3.D3DragEvent, d: Node) => { 279 | if (!event.active) simulation.alphaTarget(0); 280 | d.fx = null; 281 | d.fy = null; 282 | }; 283 | 284 | return d3.drag() 285 | .on('start', dragStarted) 286 | .on('drag', dragged) 287 | .on('end', dragEnded); 288 | }; 289 | 290 | const handleSelectionChange = (keys: Set | any) => { 291 | const newSelectedNodes = new Set(Array.from(keys) as string[]); 292 | setSelectedNodes(newSelectedNodes); 293 | 294 | const selectedData = bindingData.filter(binding => newSelectedNodes.has(binding.name)); 295 | const { nodes, links } = processGraphData(selectedData); 296 | renderGraph(nodes, links, newSelectedNodes); 297 | }; 298 | 299 | const handleResetSelection = () => { 300 | setSelectedNodes(new Set()); 301 | if (selectedNodes.size === 0) return; // Nothing to reset 302 | renderGraph(allNodes, allLinks, new Set()); 303 | }; 304 | 305 | return ( 306 |
307 | {!disable && ( 308 |
309 | 322 | 323 |
324 | )} 325 | 326 | {hoveredNode && ( 327 | 328 | )} 329 |
330 | ); 331 | }; 332 | 333 | export default DisjointGraph; -------------------------------------------------------------------------------- /ui/components/icons.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { IconSvgProps } from "@/types"; 3 | import { IoMdRefreshCircle } from "react-icons/io"; 4 | 5 | export const Logo: React.FC = ({ 6 | size = 100, 7 | width, 8 | height, 9 | ...props 10 | }) => { 11 | return ( 12 | 20 | {/* SVG path of the wizard hat */} 21 | 22 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | {/* Add more details to the hat as needed */} 79 | 80 | ); 81 | }; 82 | 83 | export const RefreshIcon: React.FC = ({ 84 | size = 24, 85 | width, 86 | height, 87 | ...props 88 | }) => ( 89 | 90 | ); 91 | 92 | export const DiscordIcon: React.FC = ({ 93 | size = 24, 94 | width, 95 | height, 96 | ...props 97 | }) => { 98 | return ( 99 | 105 | 109 | 110 | ); 111 | }; 112 | 113 | export const TwitterIcon: React.FC = ({ 114 | size = 24, 115 | width, 116 | height, 117 | ...props 118 | }) => { 119 | return ( 120 | 126 | 130 | 131 | ); 132 | }; 133 | 134 | export const GithubIcon: React.FC = ({ 135 | size = 24, 136 | width, 137 | height, 138 | ...props 139 | }) => { 140 | return ( 141 | 147 | 153 | 154 | ); 155 | }; 156 | 157 | export const MoonFilledIcon = ({ 158 | size = 24, 159 | width, 160 | height, 161 | ...props 162 | }: IconSvgProps) => ( 163 | 177 | ); 178 | 179 | export const SunFilledIcon = ({ 180 | size = 24, 181 | width, 182 | height, 183 | ...props 184 | }: IconSvgProps) => ( 185 | 200 | ); 201 | 202 | export const HeartFilledIcon = ({ 203 | size = 24, 204 | width, 205 | height, 206 | ...props 207 | }: IconSvgProps) => ( 208 | 225 | ); 226 | 227 | export const SearchIcon = (props: IconSvgProps) => ( 228 | 253 | ); 254 | 255 | export const ChevronDownIcon = ({strokeWidth = 1.5, ...otherProps}: IconSvgProps) => ( 256 | 275 | ); 276 | 277 | export const VerticalDotsIcon = ({size = 24, width, height, ...props}: IconSvgProps) => ( 278 | 293 | ); 294 | 295 | export const PlusIcon = ({size = 24, width, height, ...props}: IconSvgProps) => ( 296 | 317 | ); 318 | 319 | export const CopyIcon = ({ size, height, width, ...props }: IconSvgProps) => { 320 | return ( 321 | 333 | 334 | 335 | ); 336 | }; -------------------------------------------------------------------------------- /ui/components/navbar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { 3 | Navbar as NextUINavbar, 4 | NavbarContent, 5 | NavbarMenu, 6 | NavbarMenuToggle, 7 | NavbarBrand, 8 | NavbarItem, 9 | NavbarMenuItem, 10 | } from "@nextui-org/navbar"; 11 | import { Link } from "@nextui-org/link"; 12 | 13 | import { link as linkStyles } from "@nextui-org/theme"; 14 | 15 | import { siteConfig } from "@/config/site"; 16 | import NextLink from "next/link"; 17 | import clsx from "clsx"; 18 | 19 | import { ThemeSwitch } from "@/components/theme-switch"; 20 | import { 21 | GithubIcon, 22 | Logo, 23 | } from "@/components/icons"; 24 | 25 | import React from "react"; 26 | 27 | export function Navbar() { 28 | const [isMenuOpen, setIsMenuOpen] = React.useState(false); 29 | return ( 30 | <> 31 | 51 | 52 | 53 | 54 | 55 |

RBAC Wizard

56 |
57 |
58 |
    59 | {siteConfig.navItems.map((item) => ( 60 | 61 | setIsMenuOpen(false)} 69 | > 70 | {item.label} 71 | 72 | 73 | ))} 74 |
75 |
76 | 77 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 98 | 99 | 100 | 101 |
102 | {siteConfig.navMenuItems.map((item, index) => ( 103 | 104 | setIsMenuOpen(false)} 115 | onPress={() => setIsMenuOpen(false)} 116 | > 117 | {item.label} 118 | 119 | 120 | ))} 121 |
122 |
123 |
124 | 125 | ); 126 | }; 127 | -------------------------------------------------------------------------------- /ui/components/primitives.ts: -------------------------------------------------------------------------------- 1 | import { tv } from "tailwind-variants"; 2 | 3 | export const title = tv({ 4 | base: "tracking-tight inline font-semibold", 5 | variants: { 6 | color: { 7 | violet: "from-[#FF1CF7] to-[#b249f8]", 8 | yellow: "from-[#FF705B] to-[#FFB457]", 9 | blue: "from-[#5EA2EF] to-[#0072F5]", 10 | cyan: "from-[#00b7fa] to-[#01cfea]", 11 | green: "from-[#6FEE8D] to-[#17c964]", 12 | pink: "from-[#FF72E1] to-[#F54C7A]", 13 | foreground: "dark:from-[#FFFFFF] dark:to-[#4B4B4B]", 14 | }, 15 | size: { 16 | sm: "text-3xl lg:text-4xl", 17 | md: "text-[2.3rem] lg:text-5xl leading-9", 18 | lg: "text-4xl lg:text-6xl", 19 | }, 20 | fullWidth: { 21 | true: "w-full block", 22 | }, 23 | }, 24 | defaultVariants: { 25 | size: "md", 26 | }, 27 | compoundVariants: [ 28 | { 29 | color: [ 30 | "violet", 31 | "yellow", 32 | "blue", 33 | "cyan", 34 | "green", 35 | "pink", 36 | "foreground", 37 | ], 38 | class: "bg-clip-text text-transparent bg-gradient-to-b", 39 | }, 40 | ], 41 | }); 42 | 43 | export const subtitle = tv({ 44 | base: "w-full md:w-1/2 my-2 text-lg lg:text-xl text-default-600 block max-w-full", 45 | variants: { 46 | fullWidth: { 47 | true: "!w-full", 48 | }, 49 | }, 50 | defaultVariants:{ 51 | fullWidth: true 52 | } 53 | }); 54 | -------------------------------------------------------------------------------- /ui/components/table.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useEffect, useState } from "react"; 3 | import { 4 | Table, 5 | TableHeader, 6 | TableColumn, 7 | TableBody, 8 | TableRow, 9 | TableCell, 10 | Input, 11 | Button, 12 | DropdownTrigger, 13 | Dropdown, 14 | DropdownMenu, 15 | DropdownItem, 16 | Chip, 17 | Pagination, 18 | Selection, 19 | ChipProps, 20 | SortDescriptor 21 | } from "@nextui-org/react"; 22 | import { SearchIcon, VerticalDotsIcon, ChevronDownIcon, RefreshIcon, CopyIcon } from "@/components/icons"; 23 | import { Modal, ModalBody, ModalContent } from "@nextui-org/modal"; 24 | import { Card, CardBody, CardHeader } from "@nextui-org/card"; 25 | import axios from "axios"; 26 | import { toast } from "react-toastify"; 27 | 28 | function capitalize(str: string) { 29 | return str.charAt(0).toUpperCase() + str.slice(1); 30 | } 31 | 32 | const kindColorMap: Record = { 33 | RoleBinding: "success", 34 | ClusterRoleBinding: "warning", 35 | }; 36 | 37 | const columns = [ 38 | { name: "NAME", uid: "name", sortable: true }, 39 | { name: "KIND", uid: "kind" }, 40 | { name: "SUBJECTS", uid: "subjects" }, 41 | { name: "ROLE REF", uid: "role_ref" }, 42 | { name: "DETAILS", uid: "details" }, 43 | ]; 44 | 45 | const kindOptions = [ 46 | { name: "ClusterRoleBinding", uid: "ClusterRoleBinding" }, 47 | { name: "RoleBinding", uid: "RoleBinding" }, 48 | ]; 49 | 50 | type Subject = { 51 | kind: string; 52 | apiGroup: string; 53 | name: string; 54 | }; 55 | 56 | type RoleRef = { 57 | kind: string; 58 | apiGroup: string; 59 | name: string; 60 | }; 61 | 62 | type BindingData = { 63 | id: number; 64 | name: string; 65 | kind: string; 66 | subjects: Subject[]; 67 | roleRef: RoleRef; 68 | raw?: string; 69 | }; 70 | 71 | export default function MainTable() { 72 | const [data, setData] = useState([]); 73 | const [filterValue, setFilterValue] = React.useState(""); 74 | const [selectedKeys, setSelectedKeys] = React.useState(new Set([])); 75 | const [visibleColumns, setVisibleColumns] = React.useState(new Set(columns.map(column => column.uid))); 76 | const [kindFilter, setKindFilter] = React.useState("all"); 77 | const [rowsPerPage, setRowsPerPage] = React.useState(5); 78 | const [sortDescriptor, setSortDescriptor] = React.useState({ 79 | column: "name", 80 | direction: "ascending", 81 | }); 82 | const [isModalOpen, setIsModalOpen] = React.useState(false); 83 | const [modalData, setModalData] = React.useState(null); 84 | const [page, setPage] = React.useState(1); 85 | const hasSearchFilter = Boolean(filterValue); 86 | 87 | useEffect(() => { 88 | axios.get('/api/data') 89 | .then(response => setData(response.data)) 90 | .catch(error => console.error('Error fetching data:', error)); 91 | }, []); 92 | 93 | useEffect(() => { 94 | const handleKeyDown = (event: KeyboardEvent) => { 95 | if (event.key === "Escape") { 96 | setIsModalOpen(false); 97 | } 98 | }; 99 | 100 | document.addEventListener("keydown", handleKeyDown); 101 | 102 | return () => { 103 | document.removeEventListener("keydown", handleKeyDown); 104 | }; 105 | }, []); 106 | 107 | const headerColumns = React.useMemo(() => { 108 | if (visibleColumns === "all") return columns; 109 | 110 | return columns.filter((column) => Array.from(visibleColumns).includes(column.uid)); 111 | }, [visibleColumns]); 112 | 113 | const filteredItems = React.useMemo(() => { 114 | let filteredUsers = [...data]; 115 | 116 | if (hasSearchFilter) { 117 | filteredUsers = filteredUsers.filter((user) => 118 | user.name.toLowerCase().includes(filterValue.toLowerCase()), 119 | ); 120 | } 121 | if (kindFilter !== "all" && Array.from(kindFilter).length !== kindOptions.length) { 122 | filteredUsers = filteredUsers.filter((user) => 123 | Array.from(kindFilter).includes(user.kind), 124 | ); 125 | } 126 | 127 | return filteredUsers; 128 | }, [data, filterValue, hasSearchFilter, kindFilter]); 129 | 130 | const pages = Math.ceil(filteredItems.length / rowsPerPage); 131 | 132 | const items = React.useMemo(() => { 133 | const start = (page - 1) * rowsPerPage; 134 | const end = start + rowsPerPage; 135 | 136 | return filteredItems.slice(start, end); 137 | }, [page, filteredItems, rowsPerPage]); 138 | 139 | const sortedItems = React.useMemo(() => { 140 | return [...items].sort((a: BindingData, b: BindingData) => { 141 | const first = a[sortDescriptor.column as keyof BindingData] as string | number; 142 | const second = b[sortDescriptor.column as keyof BindingData] as string | number; 143 | const cmp = first < second ? -1 : first > second ? 1 : 0; 144 | 145 | return sortDescriptor.direction === "descending" ? -cmp : cmp; 146 | }); 147 | }, [sortDescriptor, items]); 148 | 149 | const renderCell = React.useCallback((data: BindingData, columnKey: React.Key) => { 150 | const cellValue = data[columnKey as keyof BindingData]; 151 | 152 | switch (columnKey) { 153 | case "name": 154 | return ( 155 |
156 |

{cellValue?.toString()}

157 |

{data.name}

158 |
159 | ); 160 | case "kind": 161 | return ( 162 | 163 | {cellValue?.toString()} 164 | 165 | ); 166 | case "subjects": 167 | return ( 168 |
169 | {data.subjects?.map((subject: Subject, index) => ( 170 |

{subject.kind} - {subject.name}

171 | ))} 172 |
173 | ); 174 | case "role_ref": 175 | return ( 176 |
177 |

{data.roleRef?.kind} - {data.roleRef?.apiGroup} - {data.roleRef?.name}

178 |
179 | ); 180 | case "details": 181 | return ( 182 |
183 | 184 | 185 | 188 | 189 | 190 | { 192 | setModalData(data); // Set the binding data to the modalData state 193 | setIsModalOpen(true); // Open the modal 194 | } 195 | }>View 196 | 197 | 198 |
199 | ); 200 | default: 201 | return typeof cellValue === 'string' || typeof cellValue === 'number' ? cellValue : JSON.stringify(cellValue); 202 | } 203 | }, []); 204 | 205 | const onNextPage = React.useCallback(() => { 206 | if (page < pages) { 207 | setPage(page + 1); 208 | } 209 | }, [page, pages]); 210 | 211 | const onPreviousPage = React.useCallback(() => { 212 | if (page > 1) { 213 | setPage(page - 1); 214 | } 215 | }, [page]); 216 | 217 | const onRowsPerPageChange = React.useCallback((e: React.ChangeEvent) => { 218 | setRowsPerPage(Number(e.target.value)); 219 | setPage(1); 220 | }, []); 221 | 222 | const onSearchChange = React.useCallback((value?: string) => { 223 | if (value) { 224 | setFilterValue(value); 225 | setPage(1); 226 | } else { 227 | setFilterValue(""); 228 | } 229 | }, []); 230 | 231 | const onClear = React.useCallback(() => { 232 | setFilterValue("") 233 | setPage(1) 234 | }, []) 235 | 236 | const topContent = React.useMemo(() => { 237 | return ( 238 |
239 |
240 | } 245 | value={filterValue} 246 | onClear={() => onClear()} 247 | onValueChange={onSearchChange} 248 | /> 249 |
250 | 251 | 252 | 255 | 256 | 264 | {kindOptions.map((kind) => ( 265 | 266 | {capitalize(kind.name)} 267 | 268 | ))} 269 | 270 | 271 | 272 | 273 | 276 | 277 | 285 | {columns.map((column) => ( 286 | 287 | {capitalize(column.name)} 288 | 289 | ))} 290 | 291 | 292 | 295 |
296 |
297 |
298 | Total {data.length} bindings 299 | 310 |
311 |
312 | ); 313 | }, [filterValue, onSearchChange, kindFilter, visibleColumns, onRowsPerPageChange, onClear, data]); 314 | 315 | const bottomContent = React.useMemo(() => { 316 | return ( 317 |
318 | 327 |
328 | 331 | 334 |
335 |
336 | ); 337 | }, [page, pages, onPreviousPage, onNextPage]); 338 | 339 | const copyToClipboard = async () => { 340 | if (modalData && modalData.raw) { 341 | try { 342 | await navigator.clipboard.writeText(modalData.raw); 343 | toast.success("Successfully copied to the clipboard!"); 344 | } catch (err) { 345 | toast.error("Failed to copy to the clipboard!"); 346 | console.error("Failed to copy to the clipboard:", err) 347 | } 348 | } 349 | }; 350 | 351 | return ( 352 | <> 353 | 356 | 357 |

RBAC Table

358 |
359 | 360 | 377 | 378 | {(column) => ( 379 | 384 | {column.name} 385 | 386 | )} 387 | 388 | 389 | {(item) => ( 390 | 391 | {(columnKey) => {renderCell(item, columnKey)}} 392 | 393 | )} 394 | 395 |
396 | {/* Modal to display the data */} 397 | setIsModalOpen(false)} 422 | > 423 | 424 | 425 | 426 | 427 |
428 | 431 |
432 |
433 |                                                 {modalData && modalData.raw}
434 |                                             
435 |
436 |
437 |
438 |
439 |
440 |
441 |
442 | 443 | ); 444 | } -------------------------------------------------------------------------------- /ui/components/theme-switch.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { FC } from "react"; 4 | import { VisuallyHidden } from "@react-aria/visually-hidden"; 5 | import { SwitchProps, useSwitch } from "@nextui-org/switch"; 6 | import { useTheme } from "next-themes"; 7 | import {useIsSSR} from "@react-aria/ssr"; 8 | import clsx from "clsx"; 9 | 10 | import { SunFilledIcon, MoonFilledIcon } from "@/components/icons"; 11 | 12 | export interface ThemeSwitchProps { 13 | className?: string; 14 | classNames?: SwitchProps["classNames"]; 15 | } 16 | 17 | export const ThemeSwitch: FC = ({ 18 | className, 19 | classNames, 20 | }) => { 21 | const { theme, setTheme } = useTheme(); 22 | const isSSR = useIsSSR(); 23 | 24 | const onChange = () => { 25 | theme === "light" ? setTheme("dark") : setTheme("light"); 26 | }; 27 | 28 | const { 29 | Component, 30 | slots, 31 | isSelected, 32 | getBaseProps, 33 | getInputProps, 34 | getWrapperProps, 35 | } = useSwitch({ 36 | isSelected: theme === "light" || isSSR, 37 | "aria-label": `Switch to ${theme === "light" || isSSR ? "dark" : "light"} mode`, 38 | onChange, 39 | }); 40 | 41 | return ( 42 | 51 | 52 | 53 | 54 |
73 | {!isSelected || isSSR ? : } 74 |
75 |
76 | ); 77 | }; 78 | -------------------------------------------------------------------------------- /ui/config/fonts.ts: -------------------------------------------------------------------------------- 1 | import { Fira_Code as FontMono, Inter as FontSans } from "next/font/google" 2 | 3 | export const fontSans = FontSans({ 4 | subsets: ["latin"], 5 | variable: "--font-sans", 6 | }) 7 | 8 | export const fontMono = FontMono({ 9 | subsets: ["latin"], 10 | variable: "--font-mono", 11 | }) 12 | -------------------------------------------------------------------------------- /ui/config/site.ts: -------------------------------------------------------------------------------- 1 | export const siteConfig = { 2 | name: "RBAC Wizard", 3 | description: "Kubernetes RBAC Wizard", 4 | navItems: [ 5 | { 6 | label: "Dashboard", 7 | href: "/", 8 | }, 9 | { 10 | label: "What If?", 11 | href: "/what-if", 12 | }, 13 | ], 14 | navMenuItems: [ 15 | { 16 | label: "Dashboard", 17 | href: "/", 18 | }, 19 | { 20 | label: "What If?", 21 | href: "/what-if", 22 | }, 23 | ], 24 | links: { 25 | github: "https://github.com/pehlicd/rbac-wizard", 26 | docs: "https://github.com/pehlicd/rbac-wizard", 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /ui/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. 6 | -------------------------------------------------------------------------------- /ui/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | distDir: 'dist', 4 | output: 'export', 5 | } 6 | 7 | module.exports = nextConfig 8 | -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rbac-wizard", 3 | "version": "0.0.6", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@emotion/react": "^11.13.3", 13 | "@emotion/styled": "^11.13.0", 14 | "@monaco-editor/react": "^4.6.0", 15 | "@next/swc-linux-arm-gnueabihf": "^13.2.4", 16 | "@nextui-org/button": "^2.0.38", 17 | "@nextui-org/code": "^2.0.33", 18 | "@nextui-org/input": "^2.2.5", 19 | "@nextui-org/kbd": "^2.0.34", 20 | "@nextui-org/link": "^2.0.35", 21 | "@nextui-org/navbar": "^2.0.37", 22 | "@nextui-org/react": "^2.4.8", 23 | "@nextui-org/snippet": "^2.0.43", 24 | "@nextui-org/switch": "^2.0.34", 25 | "@nextui-org/system": "2.2.6", 26 | "@nextui-org/theme": "2.2.11", 27 | "@react-aria/ssr": "^3.9.6", 28 | "@react-aria/visually-hidden": "^3.8.16", 29 | "@types/node": "22.7.5", 30 | "@types/react": "18.3.11", 31 | "@types/react-dom": "18.3.1", 32 | "autoprefixer": "10.4.20", 33 | "axios": "^1.7.7", 34 | "clsx": "^2.1.1", 35 | "d3": "^7.9.0", 36 | "eslint": "9.12.0", 37 | "eslint-config-next": "^14.2.15", 38 | "framer-motion": "^11.11.8", 39 | "intl-messageformat": "^10.7.0", 40 | "lodash.debounce": "^4.0.8", 41 | "next": "^14.2.15", 42 | "next-themes": "^0.3.0", 43 | "postcss": "8.4.47", 44 | "react": "^18.3.1", 45 | "react-dom": "^18.3.1", 46 | "react-icons": "^5.3.0", 47 | "react-toastify": "^10.0.6", 48 | "styled-components": "^6.1.13", 49 | "tailwind-variants": "^0.2.1", 50 | "tailwindcss": "3.4.13", 51 | "typescript": "5.6.3" 52 | }, 53 | "devDependencies": { 54 | "@types/d3": "^7.4.3" 55 | } 56 | } -------------------------------------------------------------------------------- /ui/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /ui/public/rbac-wizard-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pehlicd/rbac-wizard/24720e39cfdaa6ef18f41503dce069ebeba0a354/ui/public/rbac-wizard-logo.png -------------------------------------------------------------------------------- /ui/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | @apply bg-background font-sans antialiased; 7 | } 8 | 9 | .card-header { 10 | @apply p-4; 11 | } 12 | 13 | .card-body { 14 | @apply p-4; 15 | } -------------------------------------------------------------------------------- /ui/tailwind.config.js: -------------------------------------------------------------------------------- 1 | import { nextui } from '@nextui-org/theme' 2 | import { Config } from 'tailwindcss' 3 | 4 | module.exports = { 5 | content: [ 6 | './components/**/*.{js,ts,jsx,tsx,mdx}', 7 | './app/**/*.{js,ts,jsx,tsx,mdx}', 8 | './node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}' 9 | ], 10 | theme: { 11 | extend: {}, 12 | }, 13 | darkMode: "class", 14 | plugins: [nextui()], 15 | } 16 | -------------------------------------------------------------------------------- /ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "incremental": true, 21 | "plugins": [ 22 | { 23 | "name": "next" 24 | } 25 | ], 26 | "paths": { 27 | "@/*": [ 28 | "./*" 29 | ] 30 | } 31 | }, 32 | "include": [ 33 | "next-env.d.ts", 34 | "**/*.ts", 35 | "**/*.tsx", 36 | ".next/types/**/*.ts", 37 | "build/types/**/*.ts", 38 | "dist/types/**/*.ts" 39 | ], 40 | "exclude": [ 41 | "node_modules" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /ui/types/index.ts: -------------------------------------------------------------------------------- 1 | import {SVGProps} from "react"; 2 | 3 | export type IconSvgProps = SVGProps & { 4 | size?: number; 5 | }; 6 | --------------------------------------------------------------------------------