├── .github
├── CODEOWNERS
├── pull_request_template.md
├── settings.yml
└── workflows
│ ├── build_container.yaml
│ ├── golangci_lint.yaml
│ ├── release.yaml
│ ├── semantic_pr.yaml
│ └── test.yaml
├── .gitignore
├── .goreleaser.yaml
├── .krew.yaml
├── .release-please-manifest.json
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── Makefile
├── README.md
├── SECURITY.md
├── SUPPORTED_MODELS.md
├── charts
└── k8sgpt
│ ├── Chart.yaml
│ ├── templates
│ ├── _helpers.tpl
│ ├── deployment.yaml
│ ├── role.yaml
│ ├── rolebinding.yaml
│ ├── sa.yaml
│ ├── secret.yaml
│ ├── service.yaml
│ └── serviceMonitor.yaml
│ └── values.yaml
├── cmd
├── analyze
│ └── analyze.go
├── auth
│ ├── add.go
│ ├── auth.go
│ ├── default.go
│ ├── list.go
│ ├── remove.go
│ └── update.go
├── cache
│ ├── add.go
│ ├── cache.go
│ ├── get.go
│ ├── list.go
│ ├── purge.go
│ └── remove.go
├── customAnalyzer
│ ├── add.go
│ ├── customAnalyzer.go
│ ├── list.go
│ └── remove.go
├── dump
│ └── dump.go
├── filters
│ ├── add.go
│ ├── filters.go
│ ├── list.go
│ └── remove.go
├── generate
│ └── generate.go
├── integration
│ ├── activate.go
│ ├── deactivate.go
│ ├── integration.go
│ └── list.go
├── root.go
├── root_test.go
├── serve
│ └── serve.go
└── version.go
├── container
├── Dockerfile
└── dashboards
│ └── k8sgpt-dashboard.json
├── go.mod
├── go.sum
├── images
├── banner-black.png
├── banner-white.png
├── banner-white.svg
├── demo.gif
├── demo1.gif
├── demo2.gif
├── demo3.png
├── demo4.gif
├── demo5.gif
├── image.png
├── landing.png
└── nodes.gif
├── main.go
├── pkg
├── ai
│ ├── amazonbedrock.go
│ ├── amazonbedrock_mock_test.go
│ ├── amazonbedrock_test.go
│ ├── amazonsagemaker.go
│ ├── azureopenai.go
│ ├── bedrock_interfaces.go
│ ├── bedrock_support
│ │ ├── completions.go
│ │ ├── completions_test.go
│ │ ├── model.go
│ │ ├── model_test.go
│ │ ├── responses.go
│ │ └── responses_test.go
│ ├── cohere.go
│ ├── customrest.go
│ ├── factory.go
│ ├── googlegenai.go
│ ├── googlevertexai.go
│ ├── huggingface.go
│ ├── iai.go
│ ├── interactive
│ │ └── interactive.go
│ ├── localai.go
│ ├── noopai.go
│ ├── ocigenai.go
│ ├── ollama.go
│ ├── openai.go
│ ├── openai_header_transport_test.go
│ ├── prompts.go
│ └── watsonxai.go
├── analysis
│ ├── analysis.go
│ ├── analysis_test.go
│ ├── output.go
│ └── output_test.go
├── analyzer
│ ├── analyzer.go
│ ├── configmap.go
│ ├── configmap_test.go
│ ├── cronjob.go
│ ├── cronjob_test.go
│ ├── deployment.go
│ ├── deployment_test.go
│ ├── events_test.go
│ ├── gateway.go
│ ├── gateway_test.go
│ ├── gatewayclass.go
│ ├── gatewayclass_test.go
│ ├── hpa.go
│ ├── hpa_test.go
│ ├── httproute.go
│ ├── httproute_test.go
│ ├── ingress.go
│ ├── ingress_test.go
│ ├── job.go
│ ├── job_test.go
│ ├── log.go
│ ├── log_test.go
│ ├── mutating_webhook.go
│ ├── mutating_webhook_test.go
│ ├── netpol.go
│ ├── netpol_test.go
│ ├── node.go
│ ├── node_test.go
│ ├── pdb.go
│ ├── pdb_test.go
│ ├── pod.go
│ ├── pod_test.go
│ ├── pvc.go
│ ├── pvc_test.go
│ ├── rs.go
│ ├── rs_test.go
│ ├── security.go
│ ├── security_test.go
│ ├── service.go
│ ├── service_test.go
│ ├── statefulset.go
│ ├── statefulset_test.go
│ ├── storage.go
│ ├── storage_test.go
│ ├── test_utils.go
│ ├── validating_webhook.go
│ └── validating_webhook_test.go
├── cache
│ ├── azuresa_based.go
│ ├── cache.go
│ ├── file_based.go
│ ├── gcs_based.go
│ ├── interplex_based.go
│ ├── interplex_based_test.go
│ ├── s3_based.go
│ └── types.go
├── common
│ └── types.go
├── custom
│ ├── client.go
│ └── types.go
├── custom_analyzer
│ └── customAnalyzer.go
├── integration
│ ├── aws
│ │ ├── aws.go
│ │ └── eks.go
│ ├── integration.go
│ ├── integration_test.go
│ ├── keda
│ │ ├── keda.go
│ │ └── scaledobject_analyzer.go
│ ├── kyverno
│ │ ├── analyzer.go
│ │ ├── analyzer_test.go
│ │ └── kyverno.go
│ └── prometheus
│ │ ├── config_analyzer.go
│ │ ├── prometheus.go
│ │ └── relabel_analyzer.go
├── kubernetes
│ ├── apireference.go
│ ├── apireference_test.go
│ ├── kubernetes.go
│ └── types.go
├── server
│ ├── README.md
│ ├── analyze
│ │ ├── analyze.go
│ │ └── handler.go
│ ├── client_example
│ │ ├── README.md
│ │ └── main.go
│ ├── config
│ │ ├── config.go
│ │ ├── handler.go
│ │ └── integration.go
│ ├── example
│ │ └── main.go
│ ├── log.go
│ ├── mcp.go
│ ├── query
│ │ ├── handler.go
│ │ ├── query.go
│ │ └── query_test.go
│ ├── server.go
│ └── server_test.go
└── util
│ ├── util.go
│ └── util_test.go
├── release-please-config.json
└── renovate.json
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # CODEOWNERS file indicates code owners for certain files
2 | #
3 | # Code owners will automatically be added as a reviewer for PRs that touch
4 | # the owned files.
5 | #
6 |
7 | # Default owners for everything in the repo
8 | #
9 | # Unless a later match takes precedence, these owners will be requested for
10 | # review when someone opens a pull request.
11 |
12 | /.github/settings.yml @k8sgpt-ai/maintainers
13 | * @k8sgpt-ai/maintainers @k8sgpt-ai/k8sgpt-maintainers @k8sgpt-ai/k8sgpt-approvers
14 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | <!--
2 | Thanks for creating this pull request 🤗
3 |
4 | Please make sure that the pull request is limited to one type (docs, feature, etc.) and keep it as small as possible. You can open multiple prs instead of opening a huge one.
5 | -->
6 |
7 | <!-- If this pull request closes an issue, please mention the issue number below -->
8 | Closes # <!-- Issue # here -->
9 |
10 | ## 📑 Description
11 | <!-- Add a brief description of the pr -->
12 |
13 | ## ✅ Checks
14 | <!-- Make sure your pr passes the CI checks and do check the following fields as needed - -->
15 | - [ ] My pull request adheres to the code style of this project
16 | - [ ] My code requires changes to the documentation
17 | - [ ] I have updated the documentation as required
18 | - [ ] All the tests have passed
19 |
20 | ## ℹ Additional Information
21 | <!-- Any additional information like breaking changes, dependencies added, screenshots, comparisons between new and old behavior, etc. -->
--------------------------------------------------------------------------------
/.github/settings.yml:
--------------------------------------------------------------------------------
1 | repository:
2 | name: "k8sgpt"
3 | description: "Giving Kubernetes SRE superpowers to everyone"
4 | homepage_url: "https://k8sgpt.ai"
5 | topics: kubernetes, devops, tooling, openai, sre
6 |
7 | default_branch: main
8 | allow_squash_merge: true
9 | allow_merge_commit: true
10 | allow_rebase_merge: true
11 |
12 | has_wiki: false
13 |
14 | teams:
15 | - name: "maintainers"
16 | permission: "admin"
17 | - name: "k8sgpt-maintainers"
18 | permission: "maintain"
19 | - name: "k8sgpt-approvers"
20 | permission: "push"
21 | - name: "contributors"
22 | permission: "push"
23 |
24 | branches:
25 | - name: main
26 | protection:
27 | required_pull_request_reviews:
28 | required_approving_review_count: 1
29 | dismiss_stale_reviews: true
30 | require_code_owner_reviews: true
31 | dismissal_restrictions: {}
32 | code_owner_approval: true
33 | required_conversation_resolution: true
34 |
35 | required_status_checks:
36 | strict: true
37 | contexts:
38 | - "DCO"
39 |
40 | enforce_admins: true
41 |
42 | required_linear_history: true
43 |
44 | restrictions:
45 | users: []
46 | apps: []
47 | teams: []
--------------------------------------------------------------------------------
/.github/workflows/golangci_lint.yaml:
--------------------------------------------------------------------------------
1 | name: Run golangci-lint
2 |
3 | on:
4 | pull_request:
5 | branches: [main]
6 |
7 | jobs:
8 | golangci-lint:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Check out code into the Go module directory
12 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
13 |
14 | - name: golangci-lint
15 | uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8
16 | with:
17 | version: v2.1.0
18 | only-new-issues: true
--------------------------------------------------------------------------------
/.github/workflows/semantic_pr.yaml:
--------------------------------------------------------------------------------
1 | name: Semantic PR Validation
2 | on:
3 | pull_request_target:
4 | types:
5 | - opened
6 | - edited
7 | - synchronize
8 | defaults:
9 | run:
10 | shell: bash
11 | jobs:
12 | validate:
13 | runs-on: ubuntu-latest
14 | permissions:
15 | contents: read # Needed for checking out the repository
16 | pull-requests: read # Needed for reading prs
17 | steps:
18 | - name: Validate Pull Request
19 | uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5.5.3
20 | env:
21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
22 | with:
23 | # Configure which types are allowed.
24 | # Default: https://github.com/commitizen/conventional-commit-types
25 | types: |
26 | feat
27 | fix
28 | build
29 | chore
30 | ci
31 | docs
32 | perf
33 | refactor
34 | revert
35 | style
36 | test
37 | deps
38 | scopes: |
39 | deps
40 | # Configure that a scope must always be provided.
41 | requireScope: false
42 | # When using "Squash and merge" on a PR with only one commit, GitHub
43 | # will suggest using that commit message instead of the PR title for the
44 | # merge commit, and it's easy to commit this by mistake. Enable this option
45 | # to also validate the commit message for one commit PRs.
46 | validateSingleCommit: true
47 | # Configure additional validation for the subject based on a regex.
48 | # This ensures the subject doesn't start with an uppercase character.
49 | subjectPattern: ^(?![A-Z]).+$
50 | # If `subjectPattern` is configured, you can use this property to override
51 | # the default error message that is shown when the pattern doesn't match.
52 | # The variables `subject` and `title` can be used within the message.
53 | subjectPatternError: |
54 | The subject "{subject}" found in the pull request title "{title}"
55 | didn't match the configured pattern. Please ensure that the subject
56 | doesn't start with an uppercase character.
--------------------------------------------------------------------------------
/.github/workflows/test.yaml:
--------------------------------------------------------------------------------
1 | name: Run tests
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | env:
12 | GO_VERSION: "~1.22"
13 |
14 | jobs:
15 | build:
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
19 |
20 | - name: Set up Go
21 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5
22 | with:
23 | go-version: ${{ env.GO_VERSION }}
24 |
25 | - name: Run test
26 | run: go test ./... -coverprofile=coverage.txt
27 | - name: Upload coverage to Codecov
28 | uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5
29 | env:
30 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | __debug*
3 | .DS_Store
4 | k8sgpt*
5 | !charts/k8sgpt
6 | *.vscode
7 | dist/
8 |
9 | bin/
10 | pkg/server/example/example
--------------------------------------------------------------------------------
/.goreleaser.yaml:
--------------------------------------------------------------------------------
1 | version: 2
2 | # This is an example .goreleaser.yml file with some sensible defaults.
3 | # Make sure to check the documentation at https://goreleaser.com
4 | before:
5 | hooks:
6 | # You may remove this if you don't use go modules.
7 | - go mod tidy
8 | # you may remove this if you don't need go generate
9 | - go generate ./...
10 | builds:
11 | - env:
12 | - CGO_ENABLED=0
13 | goos:
14 | - linux
15 | - windows
16 | - darwin
17 | ldflags:
18 | - -s -w
19 | - -X main.version={{.Version}}
20 | - -X main.commit={{.ShortCommit}}
21 | - -X main.Date={{.CommitDate}}
22 |
23 | nfpms:
24 | - file_name_template: "{{ .ProjectName }}_{{ .Arch }}"
25 | maintainer: "K8sGPT Maintainers <contact@k8sgpt.ai>"
26 | homepage: https://k8sgpt.ai
27 | description: >-
28 | K8sGPT is a tool for scanning your kubernetes clusters, diagnosing and triaging issues in simple english. It has SRE experience codified into it’s analyzers and helps to pull out the most relevant information to enrich it with AI.
29 | license: "Apache-2.0"
30 | formats:
31 | - deb
32 | - rpm
33 | - apk
34 | bindir: /usr/bin
35 | section: utils
36 | contents:
37 | - src: ./LICENSE
38 | dst: /usr/share/doc/k8sgpt/copyright
39 | file_info:
40 | mode: 0644
41 |
42 | sboms:
43 | - artifacts: archive
44 |
45 | archives:
46 | - format: tar.gz
47 | # this name template makes the OS and Arch compatible with the results of uname.
48 | name_template: >-
49 | {{ .ProjectName }}_
50 | {{- title .Os }}_
51 | {{- if eq .Arch "amd64" }}x86_64
52 | {{- else if eq .Arch "386" }}i386
53 | {{- else }}{{ .Arch }}{{ end }}
54 | {{- if .Arm }}v{{ .Arm }}{{ end }}
55 | # use zip for windows archives
56 | format_overrides:
57 | - goos: windows
58 | format: zip
59 |
60 | brews:
61 | - name: k8sgpt
62 | homepage: https://k8sgpt.ai
63 | repository:
64 | owner: k8sgpt-ai
65 | name: homebrew-k8sgpt
66 |
67 | checksum:
68 | name_template: "checksums.txt"
69 |
70 | snapshot:
71 | name_template: "{{ incpatch .Version }}-next"
72 |
73 | announce:
74 | slack:
75 | # Whether its enabled or not.
76 | #
77 | # Templates: allowed (since v2.6).
78 | enabled: true
79 |
80 | # Message template to use while publishing.
81 | #
82 | # Default: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}'.
83 | # Templates: allowed.
84 | message_template: "{{ .ProjectName }} release {{.Tag}} is out!"
85 |
86 | # The name of the channel that the user selected as a destination for webhook messages.
87 | channel: "#general"
88 |
89 | # Set your Webhook's user name.
90 | username: "K8sGPT"
91 |
92 | # Emoji to use as the icon for this message. Overrides icon_url.
93 | icon_emoji: ""
94 |
95 | # URL to an image to use as the icon for this message.
96 | icon_url: ""
97 |
98 |
--------------------------------------------------------------------------------
/.krew.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: krew.googlecontainertools.github.com/v1alpha2
2 | kind: Plugin
3 | metadata:
4 | name: gpt
5 | spec:
6 | version: {{ .TagName }}
7 | homepage: https://github.com/k8sgpt-ai/k8sgpt
8 | shortDescription: "Giving Kubernetes Superpowers to everyone"
9 | description: |
10 | A tool for scanning your Kubernetes clusters, diagnosing, and triaging issues in simple English.
11 | platforms:
12 | ##########
13 | # Darwin #
14 | ##########
15 | - selector:
16 | matchLabels:
17 | os: darwin
18 | arch: amd64
19 | {{addURIAndSha "https://github.com/k8sgpt-ai/k8sgpt/releases/download/{{ .TagName }}/k8sgpt_Darwin_x86_64.tar.gz" .TagName | indent 6 }}
20 | files:
21 | - from: "k8sgpt"
22 | to: "kubectl-gpt"
23 | - from: "LICENSE"
24 | to: "."
25 | bin: kubectl-gpt
26 | - selector:
27 | matchLabels:
28 | os: darwin
29 | arch: arm64
30 | {{addURIAndSha "https://github.com/k8sgpt-ai/k8sgpt/releases/download/{{ .TagName }}/k8sgpt_Darwin_arm64.tar.gz" .TagName | indent 6 }}
31 | files:
32 | - from: "k8sgpt"
33 | to: "kubectl-gpt"
34 | - from: "LICENSE"
35 | to: "."
36 | bin: kubectl-gpt
37 |
38 | #########
39 | # Linux #
40 | #########
41 | - selector:
42 | matchLabels:
43 | os: linux
44 | arch: amd64
45 | {{addURIAndSha "https://github.com/k8sgpt-ai/k8sgpt/releases/download/{{ .TagName }}/k8sgpt_Linux_x86_64.tar.gz" .TagName | indent 6 }}
46 | files:
47 | - from: "k8sgpt"
48 | to: "kubectl-gpt"
49 | - from: "LICENSE"
50 | to: "."
51 | bin: kubectl-gpt
52 | - selector:
53 | matchLabels:
54 | os: linux
55 | arch: arm64
56 | {{addURIAndSha "https://github.com/k8sgpt-ai/k8sgpt/releases/download/{{ .TagName }}/k8sgpt_Linux_arm64.tar.gz" .TagName | indent 6 }}
57 | files:
58 | - from: "k8sgpt"
59 | to: "kubectl-gpt"
60 | - from: "LICENSE"
61 | to: "."
62 | bin: kubectl-gpt
63 | - selector:
64 | matchLabels:
65 | os: linux
66 | arch: "386"
67 | {{addURIAndSha "https://github.com/k8sgpt-ai/k8sgpt/releases/download/{{ .TagName }}/k8sgpt_Linux_i386.tar.gz" .TagName | indent 6 }}
68 | files:
69 | - from: "k8sgpt"
70 | to: "kubectl-gpt"
71 | - from: "LICENSE"
72 | to: "."
73 | bin: kubectl-gpt
74 |
75 | ###########
76 | # Windows #
77 | ###########
78 | - selector:
79 | matchLabels:
80 | os: windows
81 | arch: amd64
82 | {{addURIAndSha "https://github.com/k8sgpt-ai/k8sgpt/releases/download/{{ .TagName }}/k8sgpt_Windows_x86_64.zip" .TagName | indent 6 }}
83 | files:
84 | - from: "k8sgpt"
85 | to: "kubectl-gpt"
86 | - from: "LICENSE"
87 | to: "."
88 | bin: kubectl-gpt
89 | - selector:
90 | matchLabels:
91 | os: windows
92 | arch: arm64
93 | {{addURIAndSha "https://github.com/k8sgpt-ai/k8sgpt/releases/download/{{ .TagName }}/k8sgpt_Windows_arm64.zip" .TagName | indent 6 }}
94 | files:
95 | - from: "k8sgpt"
96 | to: "kubectl-gpt"
97 | - from: "LICENSE"
98 | to: "."
99 | bin: kubectl-gpt
100 | - selector:
101 | matchLabels:
102 | os: windows
103 | arch: "386"
104 | {{addURIAndSha "https://github.com/k8sgpt-ai/k8sgpt/releases/download/{{ .TagName }}/k8sgpt_Windows_i386.zip" .TagName | indent 6 }}
105 | files:
106 | - from: "k8sgpt"
107 | to: "kubectl-gpt"
108 | - from: "LICENSE"
109 | to: "."
110 | bin: kubectl-gpt
111 |
--------------------------------------------------------------------------------
/.release-please-manifest.json:
--------------------------------------------------------------------------------
1 | {".":"0.4.21"}
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | We currently support the latest release for security patching and will deploy forward releases.
6 |
7 | For example if there is a vulnerability in release `0.1.0` we will fix that release in version `0.1.1-fix` or `0.1.1`
8 |
9 | ## Reporting a Vulnerability
10 |
11 | If you are aware of a vulnerability please feel free to disclose it responsibly to contact@k8sgpt.ai or to one of our maintainers in our Slack community.
12 |
--------------------------------------------------------------------------------
/SUPPORTED_MODELS.md:
--------------------------------------------------------------------------------
1 | # Supported AI Providers and Models in K8sGPT
2 |
3 | K8sGPT supports a variety of AI/LLM providers (backends). Some providers have a fixed set of supported models, while others allow you to specify any model supported by the provider.
4 |
5 | ---
6 |
7 | ## Providers and Supported Models
8 |
9 | ### OpenAI
10 | - **Model:** User-configurable (any model supported by OpenAI, e.g., `gpt-3.5-turbo`, `gpt-4`, etc.)
11 |
12 | ### Azure OpenAI
13 | - **Model:** User-configurable (any model deployed in your Azure OpenAI resource)
14 |
15 | ### LocalAI
16 | - **Model:** User-configurable (default: `llama3`)
17 |
18 | ### Ollama
19 | - **Model:** User-configurable (default: `llama3`, others can be specified)
20 |
21 | ### NoOpAI
22 | - **Model:** N/A (no real model, used for testing)
23 |
24 | ### Cohere
25 | - **Model:** User-configurable (any model supported by Cohere)
26 |
27 | ### Amazon Bedrock
28 | - **Supported Models:**
29 | - anthropic.claude-sonnet-4-20250514-v1:0
30 | - us.anthropic.claude-sonnet-4-20250514-v1:0
31 | - eu.anthropic.claude-sonnet-4-20250514-v1:0
32 | - apac.anthropic.claude-sonnet-4-20250514-v1:0
33 | - us.anthropic.claude-3-7-sonnet-20250219-v1:0
34 | - eu.anthropic.claude-3-7-sonnet-20250219-v1:0
35 | - apac.anthropic.claude-3-7-sonnet-20250219-v1:0
36 | - anthropic.claude-3-5-sonnet-20240620-v1:0
37 | - us.anthropic.claude-3-5-sonnet-20241022-v2:0
38 | - anthropic.claude-v2
39 | - anthropic.claude-v1
40 | - anthropic.claude-instant-v1
41 | - ai21.j2-ultra-v1
42 | - ai21.j2-jumbo-instruct
43 | - amazon.titan-text-express-v1
44 | - amazon.nova-pro-v1:0
45 | - eu.amazon.nova-pro-v1:0
46 | - us.amazon.nova-pro-v1:0
47 | - amazon.nova-lite-v1:0
48 | - eu.amazon.nova-lite-v1:0
49 | - us.amazon.nova-lite-v1:0
50 | - anthropic.claude-3-haiku-20240307-v1:0
51 |
52 | ### Amazon SageMaker
53 | - **Model:** User-configurable (any model deployed in your SageMaker endpoint)
54 |
55 | ### Google GenAI
56 | - **Model:** User-configurable (any model supported by Google GenAI, e.g., `gemini-pro`)
57 |
58 | ### Huggingface
59 | - **Model:** User-configurable (any model supported by Huggingface Inference API)
60 |
61 | ### Google VertexAI
62 | - **Supported Models:**
63 | - gemini-1.0-pro-001
64 |
65 | ### OCI GenAI
66 | - **Model:** User-configurable (any model supported by OCI GenAI)
67 |
68 | ### Custom REST
69 | - **Model:** User-configurable (any model your custom REST endpoint supports)
70 |
71 | ### IBM Watsonx
72 | - **Supported Models:**
73 | - ibm/granite-13b-chat-v2
74 |
75 | ---
76 |
77 | For more details on configuring each provider and model, refer to the official K8sGPT documentation and the provider's own documentation.
--------------------------------------------------------------------------------
/charts/k8sgpt/Chart.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v2
2 | appVersion: v0.3.0 #x-release-please-version
3 | description: A Helm chart for K8SGPT
4 | name: k8sgpt
5 | type: application
6 | version: 1.0.0
7 |
--------------------------------------------------------------------------------
/charts/k8sgpt/templates/_helpers.tpl:
--------------------------------------------------------------------------------
1 | {{/*
2 | Expand the name of the chart.
3 | */}}
4 | {{- define "k8sgpt.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 "k8sgpt.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 "k8sgpt.chart" -}}
30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
31 | {{- end }}
32 |
33 | {{/*
34 | Common labels
35 | */}}
36 | {{- define "k8sgpt.labels" -}}
37 | helm.sh/chart: {{ include "k8sgpt.chart" . }}
38 | app.kubernetes.io/name: {{ include "k8sgpt.name" . }}
39 | app.kubernetes.io/instance: {{ .Release.Name }}
40 | app.kubernetes.io/managed-by: {{ .Release.Service }}
41 | {{- if .Chart.AppVersion }}
42 | app.kubernetes.io/version: {{ .Chart.AppVersion }}
43 | {{- end }}
44 | {{- end }}
--------------------------------------------------------------------------------
/charts/k8sgpt/templates/deployment.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: {{ template "k8sgpt.fullname" . }}
5 | namespace: {{ .Release.Namespace | quote }}
6 | {{- if .Values.deployment.annotations }}
7 | annotations:
8 | {{- toYaml .Values.deployment.annotations | nindent 4 }}
9 | {{- end }}
10 | labels:
11 | {{- include "k8sgpt.labels" . | nindent 4 }}
12 | spec:
13 | replicas: 1
14 | selector:
15 | matchLabels:
16 | app.kubernetes.io/name: {{ include "k8sgpt.name" . }}
17 | app.kubernetes.io/instance: {{ .Release.Name }}
18 | template:
19 | metadata:
20 | labels:
21 | app.kubernetes.io/name: {{ include "k8sgpt.name" . }}
22 | app.kubernetes.io/instance: {{ .Release.Name }}
23 | spec:
24 | {{- if .Values.deployment.securityContext }}
25 | securityContext:
26 | {{- toYaml .Values.deployment.securityContext | nindent 8 }}
27 | {{- end }}
28 | serviceAccountName: {{ template "k8sgpt.fullname" . }}
29 | containers:
30 | - name: k8sgpt-container
31 | imagePullPolicy: {{ .Values.deployment.imagePullPolicy }}
32 | image: {{ .Values.deployment.image.repository }}:{{ .Values.deployment.image.tag | default .Chart.AppVersion }}
33 | ports:
34 | - containerPort: 8080
35 | args: ["serve"]
36 | {{- if .Values.deployment.resources }}
37 | resources:
38 | {{- toYaml .Values.deployment.resources | nindent 10 }}
39 | {{- end }}
40 | env:
41 | - name: K8SGPT_MODEL
42 | value: {{ .Values.deployment.env.model }}
43 | - name: K8SGPT_BACKEND
44 | value: {{ .Values.deployment.env.backend }}
45 | {{- if .Values.secret.secretKey }}
46 | - name: K8SGPT_PASSWORD
47 | valueFrom:
48 | secretKeyRef:
49 | name: ai-backend-secret
50 | key: secret-key
51 | {{- end }}
52 | - name: XDG_CONFIG_HOME
53 | value: /k8sgpt-config/
54 | - name: XDG_CACHE_HOME
55 | value: /k8sgpt-config/
56 | volumeMounts:
57 | - mountPath: /k8sgpt-config
58 | name: config
59 | volumes:
60 | - emptyDir: {}
61 | name: config
62 |
--------------------------------------------------------------------------------
/charts/k8sgpt/templates/role.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: rbac.authorization.k8s.io/v1
2 | kind: ClusterRole
3 | metadata:
4 | name: {{ template "k8sgpt.fullname" . }}
5 | namespace: {{ .Release.Namespace | quote }}
6 | labels:
7 | {{- include "k8sgpt.labels" . | nindent 4 }}
8 | rules:
9 | - apiGroups:
10 | - '*'
11 | resources:
12 | - '*'
13 | verbs:
14 | - get
15 | - list
16 | - watch
--------------------------------------------------------------------------------
/charts/k8sgpt/templates/rolebinding.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: rbac.authorization.k8s.io/v1
2 | kind: ClusterRoleBinding
3 | metadata:
4 | name: {{ template "k8sgpt.fullname" . }}
5 | namespace: {{ .Release.Namespace | quote }}
6 | labels:
7 | {{- include "k8sgpt.labels" . | nindent 4 }}
8 | subjects:
9 | - kind: ServiceAccount
10 | name: {{ template "k8sgpt.fullname" . }}
11 | namespace: {{ .Release.Namespace | quote }}
12 | roleRef:
13 | kind: ClusterRole
14 | name: {{ template "k8sgpt.fullname" . }}
15 | apiGroup: rbac.authorization.k8s.io
--------------------------------------------------------------------------------
/charts/k8sgpt/templates/sa.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: ServiceAccount
3 | metadata:
4 | name: {{ template "k8sgpt.fullname" . }}
5 | namespace: {{ .Release.Namespace | quote }}
6 | labels:
7 | {{- include "k8sgpt.labels" . | nindent 4 }}
--------------------------------------------------------------------------------
/charts/k8sgpt/templates/secret.yaml:
--------------------------------------------------------------------------------
1 | {{- if .Values.secret.secretKey }}
2 | apiVersion: v1
3 | data:
4 | secret-key: {{ .Values.secret.secretKey }}
5 | kind: Secret
6 | metadata:
7 | name: ai-backend-secret
8 | namespace: {{ .Release.Namespace | quote }}
9 | type: Opaque
10 | {{- end}}
--------------------------------------------------------------------------------
/charts/k8sgpt/templates/service.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | name: {{ template "k8sgpt.fullname" . }}
5 | namespace: {{ .Release.Namespace | quote }}
6 | labels:
7 | {{- include "k8sgpt.labels" . | nindent 4 }}
8 | {{- if .Values.service.annotations }}
9 | annotations:
10 | {{- toYaml .Values.service.annotations | nindent 4 }}
11 | {{- end }}
12 | spec:
13 | selector:
14 | app.kubernetes.io/name: {{ include "k8sgpt.name" . }}
15 | ports:
16 | - name: http
17 | port: 8080
18 | targetPort: 8080
19 | - name: metrics
20 | port: 8081
21 | targetPort: 8081
22 | type: {{ .Values.service.type }}
23 |
--------------------------------------------------------------------------------
/charts/k8sgpt/templates/serviceMonitor.yaml:
--------------------------------------------------------------------------------
1 | {{- if .Values.serviceMonitor.enabled }}
2 | apiVersion: monitoring.coreos.com/v1
3 | kind: ServiceMonitor
4 | metadata:
5 | name: {{ template "k8sgpt.fullname" . }}
6 | namespace: {{ .Release.Namespace | quote }}
7 | labels:
8 | {{- include "k8sgpt.labels" . | nindent 4 }}
9 | {{- if .Values.serviceMonitor.additionalLabels }}
10 | {{- toYaml .Values.serviceMonitor.additionalLabels | nindent 4 }}
11 | {{- end }}
12 | spec:
13 | endpoints:
14 | - honorLabels: true
15 | path: /metrics
16 | port: metrics
17 | selector:
18 | matchLabels:
19 | app.kubernetes.io/name: {{ include "k8sgpt.name" . }}
20 | app.kubernetes.io/instance: {{ .Release.Name }}
21 | {{- end }}
--------------------------------------------------------------------------------
/charts/k8sgpt/values.yaml:
--------------------------------------------------------------------------------
1 | deployment:
2 | image:
3 | repository: ghcr.io/k8sgpt-ai/k8sgpt
4 | tag: "" # defaults to Chart.appVersion if unspecified
5 | imagePullPolicy: Always
6 | annotations: {}
7 | env:
8 | model: "gpt-3.5-turbo"
9 | backend: "openai" # one of: [ openai | llama ]
10 | resources:
11 | limits:
12 | cpu: "1"
13 | memory: "512Mi"
14 | requests:
15 | cpu: "0.2"
16 | memory: "156Mi"
17 | securityContext: {}
18 | # Set securityContext.runAsUser/runAsGroup if necessary. Values below were taken from https://github.com/k8sgpt-ai/k8sgpt/blob/main/container/Dockerfile
19 | # runAsUser: 65532
20 | # runAsGroup: 65532
21 | secret:
22 | secretKey: "" # base64 encoded OpenAI token
23 |
24 | service:
25 | type: ClusterIP
26 | annotations: {}
27 |
28 | serviceMonitor:
29 | enabled: false
30 | additionalLabels: {}
31 |
--------------------------------------------------------------------------------
/cmd/auth/auth.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 | http://www.apache.org/licenses/LICENSE-2.0
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 | */
13 |
14 | package auth
15 |
16 | import (
17 | "github.com/k8sgpt-ai/k8sgpt/pkg/ai"
18 | "github.com/spf13/cobra"
19 | )
20 |
21 | var (
22 | backend string
23 | password string
24 | baseURL string
25 | endpointName string
26 | model string
27 | engine string
28 | temperature float32
29 | providerRegion string
30 | providerId string
31 | compartmentId string
32 | topP float32
33 | topK int32
34 | maxTokens int
35 | organizationId string
36 | )
37 |
38 | var configAI ai.AIConfiguration
39 |
40 | // authCmd represents the auth command
41 | var AuthCmd = &cobra.Command{
42 | Use: "auth",
43 | Short: "Authenticate with your chosen backend",
44 | Long: `Provide the necessary credentials to authenticate with your chosen backend.`,
45 | Run: func(cmd *cobra.Command, args []string) {
46 | if len(args) == 0 {
47 | _ = cmd.Help()
48 | return
49 | }
50 | },
51 | }
52 |
53 | func init() {
54 | // add subcommand to list backends
55 | AuthCmd.AddCommand(listCmd)
56 | // add subcommand to create new backend provider
57 | AuthCmd.AddCommand(addCmd)
58 | // add subcommand to remove new backend provider
59 | AuthCmd.AddCommand(removeCmd)
60 | // add subcommand to set default backend provider
61 | AuthCmd.AddCommand(defaultCmd)
62 | // add subcommand to update backend provider
63 | AuthCmd.AddCommand(updateCmd)
64 | }
65 |
--------------------------------------------------------------------------------
/cmd/auth/default.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 | http://www.apache.org/licenses/LICENSE-2.0
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 | */
13 |
14 | package auth
15 |
16 | import (
17 | "os"
18 | "strings"
19 |
20 | "github.com/fatih/color"
21 | "github.com/spf13/cobra"
22 | "github.com/spf13/viper"
23 | )
24 |
25 | var (
26 | providerName string
27 | )
28 |
29 | var defaultCmd = &cobra.Command{
30 | Use: "default",
31 | Short: "Set your default AI backend provider",
32 | Long: "The command to set your new default AI backend provider (default is openai)",
33 | Run: func(cmd *cobra.Command, args []string) {
34 | err := viper.UnmarshalKey("ai", &configAI)
35 | if err != nil {
36 | color.Red("Error: %v", err)
37 | os.Exit(1)
38 | }
39 | if providerName == "" {
40 | if configAI.DefaultProvider != "" {
41 | color.Yellow("Your default provider is %s", configAI.DefaultProvider)
42 | } else {
43 | color.Yellow("Your default provider is openai")
44 | }
45 | os.Exit(0)
46 | }
47 | // lowercase the provider name
48 | providerName = strings.ToLower(providerName)
49 |
50 | // Check if the provider is in the provider list
51 | providerExists := false
52 | for _, provider := range configAI.Providers {
53 | if provider.Name == providerName {
54 | providerExists = true
55 | }
56 | }
57 | if !providerExists {
58 | color.Red("Error: Provider %s does not exist", providerName)
59 | os.Exit(1)
60 | }
61 | // Set the default provider
62 | configAI.DefaultProvider = providerName
63 |
64 | viper.Set("ai", configAI)
65 | // Viper write config
66 | err = viper.WriteConfig()
67 | if err != nil {
68 | color.Red("Error: %v", err)
69 | os.Exit(1)
70 | }
71 | // Print acknowledgement
72 | color.Green("Default provider set to %s", providerName)
73 | },
74 | }
75 |
76 | func init() {
77 | // provider name flag
78 | defaultCmd.Flags().StringVarP(&providerName, "provider", "p", "", "The name of the provider to set as default")
79 | }
80 |
--------------------------------------------------------------------------------
/cmd/auth/list.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 | http://www.apache.org/licenses/LICENSE-2.0
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 | */
13 |
14 | package auth
15 |
16 | import (
17 | "fmt"
18 | "os"
19 |
20 | "github.com/fatih/color"
21 | "github.com/k8sgpt-ai/k8sgpt/pkg/ai"
22 | "github.com/spf13/cobra"
23 | "github.com/spf13/viper"
24 | )
25 |
26 | var details bool
27 |
28 | var listCmd = &cobra.Command{
29 | Use: "list",
30 | Short: "List configured providers",
31 | Long: "The list command displays a list of configured providers",
32 | Run: func(cmd *cobra.Command, args []string) {
33 |
34 | // get ai configuration
35 | err := viper.UnmarshalKey("ai", &configAI)
36 | if err != nil {
37 | color.Red("Error: %v", err)
38 | os.Exit(1)
39 | }
40 |
41 | // Print the default if it is set
42 | fmt.Print(color.YellowString("Default: \n"))
43 | if configAI.DefaultProvider != "" {
44 | fmt.Printf("> %s\n", color.BlueString(configAI.DefaultProvider))
45 | } else {
46 | fmt.Printf("> %s\n", color.BlueString("openai"))
47 | }
48 |
49 | // Get list of all AI Backends and only print them if they are not in the provider list
50 | fmt.Print(color.YellowString("Active: \n"))
51 | for _, aiBackend := range ai.Backends {
52 | providerExists := false
53 | for _, provider := range configAI.Providers {
54 | if provider.Name == aiBackend {
55 | providerExists = true
56 | }
57 | }
58 | if providerExists {
59 | fmt.Printf("> %s\n", color.GreenString(aiBackend))
60 | if details {
61 | for _, provider := range configAI.Providers {
62 | if provider.Name == aiBackend {
63 | printDetails(provider)
64 | }
65 | }
66 | }
67 | }
68 | }
69 | fmt.Print(color.YellowString("Unused: \n"))
70 | for _, aiBackend := range ai.Backends {
71 | providerExists := false
72 | for _, provider := range configAI.Providers {
73 | if provider.Name == aiBackend {
74 | providerExists = true
75 | }
76 | }
77 | if !providerExists {
78 | fmt.Printf("> %s\n", color.RedString(aiBackend))
79 | }
80 | }
81 | },
82 | }
83 |
84 | func init() {
85 | listCmd.Flags().BoolVar(&details, "details", false, "Print active provider configuration details")
86 | }
87 |
88 | func printDetails(provider ai.AIProvider) {
89 | if provider.Model != "" {
90 | fmt.Printf(" - Model: %s\n", provider.Model)
91 | }
92 | if provider.Engine != "" {
93 | fmt.Printf(" - Engine: %s\n", provider.Engine)
94 | }
95 | if provider.BaseURL != "" {
96 | fmt.Printf(" - BaseURL: %s\n", provider.BaseURL)
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/cmd/auth/remove.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 | http://www.apache.org/licenses/LICENSE-2.0
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 | */
13 |
14 | package auth
15 |
16 | import (
17 | "os"
18 | "strings"
19 |
20 | "github.com/fatih/color"
21 | "github.com/spf13/cobra"
22 | "github.com/spf13/viper"
23 | )
24 |
25 | var removeCmd = &cobra.Command{
26 | Use: "remove",
27 | Short: "Remove provider(s)",
28 | Long: "The command to remove AI backend provider(s)",
29 | PreRun: func(cmd *cobra.Command, args []string) {
30 | _ = cmd.MarkFlagRequired("backends")
31 | },
32 | Run: func(cmd *cobra.Command, args []string) {
33 | if backend == "" {
34 | color.Red("Error: backends must be set.")
35 | _ = cmd.Help()
36 | return
37 | }
38 | inputBackends := strings.Split(backend, ",")
39 |
40 | err := viper.UnmarshalKey("ai", &configAI)
41 | if err != nil {
42 | color.Red("Error: %v", err)
43 | os.Exit(1)
44 | }
45 |
46 | for _, b := range inputBackends {
47 | foundBackend := false
48 | for i, provider := range configAI.Providers {
49 | if b == provider.Name {
50 | foundBackend = true
51 | configAI.Providers = append(configAI.Providers[:i], configAI.Providers[i+1:]...)
52 | if configAI.DefaultProvider == b {
53 | configAI.DefaultProvider = "openai"
54 | }
55 | color.Green("%s deleted from the AI backend provider list", b)
56 | break
57 | }
58 | }
59 | if !foundBackend {
60 | color.Red("Error: %s does not exist in configuration file. Please use k8sgpt auth new.", b)
61 | os.Exit(1)
62 | }
63 | }
64 |
65 | viper.Set("ai", configAI)
66 | if err := viper.WriteConfig(); err != nil {
67 | color.Red("Error writing config file: %s", err.Error())
68 | os.Exit(1)
69 | }
70 |
71 | },
72 | }
73 |
74 | func init() {
75 | // add flag for backends
76 | removeCmd.Flags().StringVarP(&backend, "backends", "b", "", "Backend AI providers to remove (separated by a comma)")
77 | }
78 |
--------------------------------------------------------------------------------
/cmd/cache/add.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 | */
15 | package cache
16 |
17 | import (
18 | "fmt"
19 | "os"
20 | "strings"
21 |
22 | "github.com/fatih/color"
23 | "github.com/k8sgpt-ai/k8sgpt/pkg/cache"
24 | "github.com/spf13/cobra"
25 | )
26 |
27 | var (
28 | region string
29 | //nolint:unused
30 | bucketName string
31 | storageAccount string
32 | containerName string
33 | projectId string
34 | endpoint string
35 | insecure bool
36 | )
37 |
38 | // addCmd represents the add command
39 | var addCmd = &cobra.Command{
40 | Use: "add [cache type]",
41 | Short: "Add a remote cache",
42 | Long: `This command allows you to add a remote cache to store the results of an analysis.
43 | The supported cache types are:
44 | - Azure Blob storage (e.g., k8sgpt cache add azure)
45 | - Google Cloud storage (e.g., k8sgpt cache add gcs)
46 | - S3 (e.g., k8sgpt cache add s3)
47 | - Interplex (e.g., k8sgpt cache add interplex)`,
48 | Run: func(cmd *cobra.Command, args []string) {
49 | if len(args) == 0 {
50 | color.Red("Error: Please provide a value for cache types. Run k8sgpt cache add --help")
51 | os.Exit(1)
52 | }
53 | fmt.Println(color.YellowString("Adding remote based cache"))
54 | cacheType := args[0]
55 | remoteCache, err := cache.NewCacheProvider(strings.ToLower(cacheType), bucketName, region, endpoint, storageAccount, containerName, projectId, insecure)
56 | if err != nil {
57 | color.Red("Error: %v", err)
58 | os.Exit(1)
59 | }
60 | err = cache.AddRemoteCache(remoteCache)
61 | if err != nil {
62 | color.Red("Error: %v", err)
63 | os.Exit(1)
64 | }
65 | },
66 | }
67 |
68 | func init() {
69 | CacheCmd.AddCommand(addCmd)
70 | addCmd.Flags().StringVarP(®ion, "region", "r", "us-east-1", "The region to use for the AWS S3 or GCS cache")
71 | addCmd.Flags().StringVarP(&endpoint, "endpoint", "e", "", "The S3 or minio endpoint")
72 | addCmd.Flags().BoolVarP(&insecure, "insecure", "i", false, "Skip TLS verification for S3/Minio custom endpoint")
73 | addCmd.Flags().StringVarP(&bucketName, "bucket", "b", "", "The name of the AWS S3 bucket to use for the cache")
74 | addCmd.Flags().StringVarP(&projectId, "projectid", "p", "", "The GCP project ID")
75 | addCmd.Flags().StringVarP(&storageAccount, "storageacc", "s", "", "The Azure storage account name of the container")
76 | addCmd.Flags().StringVarP(&containerName, "container", "c", "", "The Azure container name to use for the cache")
77 | addCmd.MarkFlagsRequiredTogether("storageacc", "container")
78 | // Tedious check to ensure we don't include arguments from different providers
79 | addCmd.MarkFlagsMutuallyExclusive("region", "storageacc")
80 | addCmd.MarkFlagsMutuallyExclusive("region", "container")
81 | addCmd.MarkFlagsMutuallyExclusive("bucket", "storageacc")
82 | addCmd.MarkFlagsMutuallyExclusive("bucket", "container")
83 | addCmd.MarkFlagsMutuallyExclusive("projectid", "storageacc")
84 | addCmd.MarkFlagsMutuallyExclusive("projectid", "container")
85 | }
86 |
--------------------------------------------------------------------------------
/cmd/cache/cache.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 | */
15 | package cache
16 |
17 | import (
18 | "github.com/spf13/cobra"
19 | )
20 |
21 | // cacheCmd represents the cache command
22 | var CacheCmd = &cobra.Command{
23 | Use: "cache",
24 | Short: "For working with the cache the results of an analysis",
25 | Long: `Cache commands allow you to add a remote cache, list the contents of the cache, and remove items from the cache.`,
26 | Run: func(cmd *cobra.Command, args []string) {
27 | err := cmd.Help()
28 | if err != nil {
29 | panic(err)
30 | }
31 | },
32 | }
33 |
34 | func init() {
35 | }
36 |
--------------------------------------------------------------------------------
/cmd/cache/get.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 | */
15 | package cache
16 |
17 | import (
18 | "fmt"
19 | "github.com/fatih/color"
20 | "github.com/k8sgpt-ai/k8sgpt/pkg/cache"
21 | "github.com/spf13/cobra"
22 | "os"
23 | )
24 |
25 | // listCmd represents the list command
26 | var getCmd = &cobra.Command{
27 | Use: "get",
28 | Short: "Get the current cache",
29 | Long: `Returns the current remote cache being used`,
30 | Run: func(cmd *cobra.Command, args []string) {
31 |
32 | // load remote cache if it is configured
33 | c, err := cache.GetCacheConfiguration()
34 | if err != nil {
35 | color.Red("Error: %v", err)
36 | os.Exit(1)
37 | }
38 | fmt.Printf("Current remote cache is: %s", c.GetName())
39 | },
40 | }
41 |
42 | func init() {
43 | CacheCmd.AddCommand(getCmd)
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/cmd/cache/list.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 | */
15 | package cache
16 |
17 | import (
18 | "os"
19 | "reflect"
20 |
21 | "github.com/fatih/color"
22 | "github.com/k8sgpt-ai/k8sgpt/pkg/cache"
23 | "github.com/olekukonko/tablewriter"
24 | "github.com/spf13/cobra"
25 | )
26 |
27 | // listCmd represents the list command
28 | var listCmd = &cobra.Command{
29 | Use: "list",
30 | Short: "List the contents of the cache",
31 | Long: `This command allows you to list the contents of the cache.`,
32 | Run: func(cmd *cobra.Command, args []string) {
33 |
34 | // load remote cache if it is configured
35 | c, err := cache.GetCacheConfiguration()
36 | if err != nil {
37 | color.Red("Error: %v", err)
38 | os.Exit(1)
39 | }
40 | names, err := c.List()
41 | if err != nil {
42 | color.Red("Error: %v", err)
43 | os.Exit(1)
44 | }
45 |
46 | var headers []string
47 | obj := cache.CacheObjectDetails{}
48 | objType := reflect.TypeOf(obj)
49 | for i := 0; i < objType.NumField(); i++ {
50 | field := objType.Field(i)
51 | headers = append(headers, field.Name)
52 | }
53 |
54 | table := tablewriter.NewWriter(os.Stdout)
55 | table.SetHeader(headers)
56 |
57 | for _, v := range names {
58 | table.Append([]string{v.Name, v.UpdatedAt.String()})
59 | }
60 | table.Render()
61 | },
62 | }
63 |
64 | func init() {
65 | CacheCmd.AddCommand(listCmd)
66 |
67 | }
68 |
--------------------------------------------------------------------------------
/cmd/cache/purge.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 | */
15 | package cache
16 |
17 | import (
18 | "fmt"
19 | "os"
20 |
21 | "github.com/fatih/color"
22 | "github.com/k8sgpt-ai/k8sgpt/pkg/cache"
23 | "github.com/spf13/cobra"
24 | )
25 |
26 | var all bool
27 |
28 | var purgeCmd = &cobra.Command{
29 | Use: "purge [object name]",
30 | Short: "Purge a remote cache",
31 | Long: "This command allows you to delete/purge one object from the cache or all objects with --all flag.",
32 | Run: func(cmd *cobra.Command, args []string) {
33 | c, err := cache.GetCacheConfiguration()
34 | if err != nil {
35 | color.Red("Error: %v", err)
36 | os.Exit(1)
37 | }
38 |
39 | if all {
40 | fmt.Println(color.YellowString("Purging all objects from the remote cache."))
41 | names, err := c.List()
42 | if err != nil {
43 | color.Red("Error listing cache objects: %v", err)
44 | os.Exit(1)
45 | }
46 | if len(names) == 0 {
47 | fmt.Println(color.GreenString("No objects to delete."))
48 | return
49 | }
50 | var failed []string
51 | for _, obj := range names {
52 | err := c.Remove(obj.Name)
53 | if err != nil {
54 | failed = append(failed, obj.Name)
55 | }
56 | }
57 | if len(failed) > 0 {
58 | color.Red("Failed to delete: %v", failed)
59 | os.Exit(1)
60 | }
61 | fmt.Println(color.GreenString("All objects deleted."))
62 | return
63 | }
64 |
65 | if len(args) == 0 {
66 | color.Red("Error: Please provide a value for object name or use --all. Run k8sgpt cache purge --help")
67 | os.Exit(1)
68 | }
69 | objectKey := args[0]
70 | fmt.Println(color.YellowString("Purging a remote cache."))
71 | err = c.Remove(objectKey)
72 | if err != nil {
73 | color.Red("Error: %v", err)
74 | os.Exit(1)
75 | }
76 | fmt.Println(color.GreenString("Object deleted."))
77 | },
78 | }
79 |
80 | func init() {
81 | purgeCmd.Flags().BoolVar(&all, "all", false, "Purge all objects in the cache")
82 | CacheCmd.AddCommand(purgeCmd)
83 | }
84 |
--------------------------------------------------------------------------------
/cmd/cache/remove.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 | */
15 | package cache
16 |
17 | import (
18 | "os"
19 |
20 | "github.com/fatih/color"
21 | "github.com/k8sgpt-ai/k8sgpt/pkg/cache"
22 | "github.com/spf13/cobra"
23 | )
24 |
25 | // removeCmd represents the remove command
26 | var removeCmd = &cobra.Command{
27 | Use: "remove",
28 | Short: "Remove the remote cache",
29 | Long: `This command allows you to remove the remote cache and use the default filecache.`,
30 | Run: func(cmd *cobra.Command, args []string) {
31 |
32 | err := cache.RemoveRemoteCache()
33 | if err != nil {
34 | color.Red("Error: %v", err)
35 | os.Exit(1)
36 | }
37 | color.Green("Successfully removed the remote cache")
38 | },
39 | }
40 |
41 | func init() {
42 | CacheCmd.AddCommand(removeCmd)
43 | }
44 |
--------------------------------------------------------------------------------
/cmd/customAnalyzer/add.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 | http://www.apache.org/licenses/LICENSE-2.0
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 | */
13 |
14 | package customanalyzer
15 |
16 | import (
17 | "os"
18 |
19 | "github.com/fatih/color"
20 | customAnalyzer "github.com/k8sgpt-ai/k8sgpt/pkg/custom_analyzer"
21 | "github.com/spf13/cobra"
22 | "github.com/spf13/viper"
23 | )
24 |
25 | var (
26 | name string
27 | url string
28 | port int
29 | )
30 |
31 | var addCmd = &cobra.Command{
32 | Use: "add",
33 | Aliases: []string{"add"},
34 | Short: "This command will add a custom analyzer from source",
35 | Long: "This command allows you to add/remote/list an existing custom analyzer.",
36 | Run: func(cmd *cobra.Command, args []string) {
37 | err := viper.UnmarshalKey("custom_analyzers", &configCustomAnalyzer)
38 | if err != nil {
39 | color.Red("Error: %v", err)
40 | os.Exit(1)
41 | }
42 | analyzer := customAnalyzer.NewCustomAnalyzer()
43 |
44 | // Check if configuration is valid
45 | err = analyzer.Check(configCustomAnalyzer, name, url, port)
46 | if err != nil {
47 | color.Red("Error adding custom analyzer: %s", err.Error())
48 | os.Exit(1)
49 | }
50 |
51 | configCustomAnalyzer = append(configCustomAnalyzer, customAnalyzer.CustomAnalyzerConfiguration{
52 | Name: name,
53 | Connection: customAnalyzer.Connection{
54 | Url: url,
55 | Port: port,
56 | },
57 | })
58 |
59 | viper.Set("custom_analyzers", configCustomAnalyzer)
60 | if err := viper.WriteConfig(); err != nil {
61 | color.Red("Error writing config file: %s", err.Error())
62 | os.Exit(1)
63 | }
64 | color.Green("%s added to the custom analyzers config list", name)
65 |
66 | },
67 | }
68 |
69 | func init() {
70 | addCmd.Flags().StringVarP(&name, "name", "n", "my-custom-analyzer", "Name of the custom analyzer.")
71 | addCmd.Flags().StringVarP(&url, "url", "u", "localhost", "URL for the custom analyzer connection.")
72 | addCmd.Flags().IntVarP(&port, "port", "r", 8085, "Port for the custom analyzer connection.")
73 | }
74 |
--------------------------------------------------------------------------------
/cmd/customAnalyzer/customAnalyzer.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 | http://www.apache.org/licenses/LICENSE-2.0
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 | */
13 |
14 | package customanalyzer
15 |
16 | import (
17 | customAnalyzer "github.com/k8sgpt-ai/k8sgpt/pkg/custom_analyzer"
18 | "github.com/spf13/cobra"
19 | )
20 |
21 | var configCustomAnalyzer []customAnalyzer.CustomAnalyzerConfiguration
22 |
23 | // authCmd represents the auth command
24 | var CustomAnalyzerCmd = &cobra.Command{
25 | Use: "custom-analyzer",
26 | Short: "Manage a custom analyzer",
27 | Long: `This command allows you to manage custom analyzers, including adding, removing, and listing them.`,
28 | Run: func(cmd *cobra.Command, args []string) {
29 | if len(args) == 0 {
30 | _ = cmd.Help()
31 | return
32 | }
33 | },
34 | }
35 |
36 | func init() {
37 | // add subcommand to add custom analyzer
38 | CustomAnalyzerCmd.AddCommand(addCmd)
39 | // remove subcomment to remove custom analyzer
40 | CustomAnalyzerCmd.AddCommand(removeCmd)
41 | // list subcomment to list custom analyzer
42 | CustomAnalyzerCmd.AddCommand(listCmd)
43 | }
44 |
--------------------------------------------------------------------------------
/cmd/customAnalyzer/list.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 | http://www.apache.org/licenses/LICENSE-2.0
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 | */
13 |
14 | package customanalyzer
15 |
16 | import (
17 | "fmt"
18 | "os"
19 |
20 | "github.com/fatih/color"
21 | customAnalyzer "github.com/k8sgpt-ai/k8sgpt/pkg/custom_analyzer"
22 | "github.com/spf13/cobra"
23 | "github.com/spf13/viper"
24 | )
25 |
26 | var details bool
27 |
28 | var listCmd = &cobra.Command{
29 | Use: "list",
30 | Short: "List configured custom analyzers",
31 | Long: "The list command displays a list of configured custom analyzers",
32 | Run: func(cmd *cobra.Command, args []string) {
33 |
34 | // get custom_analyzers configuration
35 | err := viper.UnmarshalKey("custom_analyzers", &configCustomAnalyzer)
36 | if err != nil {
37 | color.Red("Error: %v", err)
38 | os.Exit(1)
39 | }
40 |
41 | // Get list of all Custom Analyers configured
42 | fmt.Print(color.YellowString("Active: \n"))
43 | for _, analyzer := range configCustomAnalyzer {
44 | fmt.Printf("> %s\n", color.GreenString(analyzer.Name))
45 | if details {
46 | printDetails(analyzer)
47 | }
48 | }
49 | },
50 | }
51 |
52 | func init() {
53 | listCmd.Flags().BoolVar(&details, "details", false, "Print custom analyzers configuration details")
54 | }
55 |
56 | func printDetails(analyzer customAnalyzer.CustomAnalyzerConfiguration) {
57 | fmt.Printf(" - Url: %s\n", analyzer.Connection.Url)
58 | fmt.Printf(" - Port: %d\n", analyzer.Connection.Port)
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/cmd/customAnalyzer/remove.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 | http://www.apache.org/licenses/LICENSE-2.0
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 | */
13 |
14 | package customanalyzer
15 |
16 | import (
17 | "os"
18 | "strings"
19 |
20 | "github.com/fatih/color"
21 | "github.com/spf13/cobra"
22 | "github.com/spf13/viper"
23 | )
24 |
25 | var (
26 | names string
27 | )
28 |
29 | var removeCmd = &cobra.Command{
30 | Use: "remove",
31 | Short: "Remove custom analyzer(s)",
32 | Long: "The command to remove custom analyzer(s)",
33 | PreRun: func(cmd *cobra.Command, args []string) {
34 | // Ensure that the "names" flag is provided before running the command
35 | _ = cmd.MarkFlagRequired("names")
36 | },
37 | Run: func(cmd *cobra.Command, args []string) {
38 | if names == "" {
39 | // Display an error message and show command help if "names" is not set
40 | color.Red("Error: names must be set.")
41 | _ = cmd.Help()
42 | return
43 | }
44 | // Split the provided names by comma
45 | inputCustomAnalyzers := strings.Split(names, ",")
46 |
47 | // Load the custom analyzers from the configuration file
48 | err := viper.UnmarshalKey("custom_analyzers", &configCustomAnalyzer)
49 | if err != nil {
50 | // Display an error message if the configuration cannot be loaded
51 | color.Red("Error: %v", err)
52 | os.Exit(1)
53 | }
54 |
55 | // Iterate over each input analyzer name
56 | for _, inputAnalyzer := range inputCustomAnalyzers {
57 | foundAnalyzer := false
58 | // Search for the analyzer in the current configuration
59 | for i, analyzer := range configCustomAnalyzer {
60 | if analyzer.Name == inputAnalyzer {
61 | foundAnalyzer = true
62 |
63 | // Remove the analyzer from the configuration list
64 | configCustomAnalyzer = append(configCustomAnalyzer[:i], configCustomAnalyzer[i+1:]...)
65 | color.Green("%s deleted from the custom analyzer list", analyzer.Name)
66 | break
67 | }
68 | }
69 | if !foundAnalyzer {
70 | // Display an error if the analyzer is not found in the configuration
71 | color.Red("Error: %s does not exist in configuration file. Please use k8sgpt custom-analyzer add.", inputAnalyzer)
72 | os.Exit(1)
73 | }
74 | }
75 |
76 | // Save the updated configuration back to the file
77 | viper.Set("custom_analyzers", configCustomAnalyzer)
78 | if err := viper.WriteConfig(); err != nil {
79 | // Display an error if the configuration cannot be written
80 | color.Red("Error writing config file: %s", err.Error())
81 | os.Exit(1)
82 | }
83 |
84 | },
85 | }
86 |
87 | func init() {
88 | // add flag for names
89 | removeCmd.Flags().StringVarP(&names, "names", "n", "", "Custom analyzers to remove (separated by a comma)")
90 | }
91 |
--------------------------------------------------------------------------------
/cmd/dump/dump.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 | http://www.apache.org/licenses/LICENSE-2.0
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 | */
13 |
14 | package dump
15 |
16 | import (
17 | "encoding/json"
18 | "fmt"
19 | "net/http"
20 | "os"
21 | "time"
22 |
23 | "github.com/fatih/color"
24 | "github.com/k8sgpt-ai/k8sgpt/pkg/ai"
25 | "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
26 | "github.com/spf13/cobra"
27 | "github.com/spf13/viper"
28 | "k8s.io/apimachinery/pkg/version"
29 | )
30 |
31 | type K8sGPTInfo struct {
32 | Version string
33 | Commit string
34 | Date string
35 | }
36 | type DumpOut struct {
37 | AIConfiguration ai.AIConfiguration
38 | ActiveFilters []string
39 | KubenetesServerVersion *version.Info
40 | K8sGPTInfo K8sGPTInfo
41 | }
42 |
43 | var DumpCmd = &cobra.Command{
44 | Use: "dump",
45 | Short: "Creates a dumpfile for debugging issues with K8sGPT",
46 | Long: `The dump command will create a dump.*.json which will contain K8sGPT non-sensitive configuration information.`,
47 | Run: func(cmd *cobra.Command, args []string) {
48 |
49 | // Fetch the configuration object(s)
50 | // get ai configuration
51 | var configAI ai.AIConfiguration
52 | err := viper.UnmarshalKey("ai", &configAI)
53 | if err != nil {
54 | color.Red("Error: %v", err)
55 | os.Exit(1)
56 | }
57 |
58 | var newProvider []ai.AIProvider
59 | for _, config := range configAI.Providers {
60 | // we blank out the custom headers for data protection reasons
61 | config.CustomHeaders = make([]http.Header, 0)
62 | // blank out the password
63 | if len(config.Password) > 4 {
64 | config.Password = config.Password[:4] + "***"
65 | } else {
66 | // If the password is shorter than 4 characters
67 | config.Password = "***"
68 | }
69 | newProvider = append(newProvider, config)
70 | }
71 | configAI.Providers = newProvider
72 | activeFilters := viper.GetStringSlice("active_filters")
73 | kubecontext := viper.GetString("kubecontext")
74 | kubeconfig := viper.GetString("kubeconfig")
75 | client, err := kubernetes.NewClient(kubecontext, kubeconfig)
76 | if err != nil {
77 | color.Red("Error: %v", err)
78 | os.Exit(1)
79 | }
80 | v, err := client.Client.Discovery().ServerVersion()
81 | if err != nil {
82 | color.Yellow("Could not find kubernetes server version")
83 | }
84 | var dumpOut DumpOut = DumpOut{
85 | AIConfiguration: configAI,
86 | ActiveFilters: activeFilters,
87 | KubenetesServerVersion: v,
88 | K8sGPTInfo: K8sGPTInfo{
89 | Version: viper.GetString("Version"),
90 | Commit: viper.GetString("Commit"),
91 | Date: viper.GetString("Date"),
92 | },
93 | }
94 | // Serialize dumpOut to JSON
95 | jsonData, err := json.MarshalIndent(dumpOut, "", " ")
96 | if err != nil {
97 | color.Red("Error: %v", err)
98 | os.Exit(1)
99 | }
100 | // Write JSON data to file
101 | f := fmt.Sprintf("dump_%s.json", time.Now().Format("20060102150405"))
102 | err = os.WriteFile(f, jsonData, 0644)
103 | if err != nil {
104 | color.Red("Error: %v", err)
105 | os.Exit(1)
106 | }
107 | color.Green("Dump created successfully: %s", f)
108 | },
109 | }
110 |
111 | func init() {
112 |
113 | }
114 |
--------------------------------------------------------------------------------
/cmd/filters/add.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 | http://www.apache.org/licenses/LICENSE-2.0
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 | */
13 |
14 | package filters
15 |
16 | import (
17 | "os"
18 | "strings"
19 |
20 | "github.com/fatih/color"
21 | "github.com/k8sgpt-ai/k8sgpt/pkg/analyzer"
22 | "github.com/k8sgpt-ai/k8sgpt/pkg/util"
23 | "github.com/spf13/cobra"
24 | "github.com/spf13/viper"
25 | )
26 |
27 | var addCmd = &cobra.Command{
28 | Use: "add [filter(s)]",
29 | Short: "Adds one or more new filters.",
30 | Long: `The add command adds one or more new filters to the default set of filters used by the analyze.`,
31 | Args: cobra.ExactArgs(1),
32 | Run: func(cmd *cobra.Command, args []string) {
33 | inputFilters := strings.Split(args[0], ",")
34 | coreFilters, additionalFilters, integrationFilters := analyzer.ListFilters()
35 |
36 | availableFilters := append(append(coreFilters, additionalFilters...), integrationFilters...)
37 | // Verify filter exist
38 | invalidFilters := []string{}
39 | for _, f := range inputFilters {
40 | if f == "" {
41 | color.Red("Filter cannot be empty. Please use correct syntax.")
42 | os.Exit(1)
43 | }
44 | foundFilter := false
45 | for _, filter := range availableFilters {
46 | if filter == f {
47 | foundFilter = true
48 |
49 | // WARNING: This is to enable users correctly understand implications
50 | // of enabling logs
51 | if filter == "Log" {
52 | color.Yellow("Warning: by enabling logs, you will be sending potentially sensitive data to the AI backend.")
53 | }
54 |
55 | break
56 | }
57 | }
58 | if !foundFilter {
59 | invalidFilters = append(invalidFilters, f)
60 | }
61 | }
62 |
63 | if len(invalidFilters) != 0 {
64 | color.Red("Filter %s does not exist. Please use k8sgpt filters list", strings.Join(invalidFilters, ", "))
65 | os.Exit(1)
66 | }
67 |
68 | // Get defined active_filters
69 | activeFilters := viper.GetStringSlice("active_filters")
70 | if len(activeFilters) == 0 {
71 | activeFilters = coreFilters
72 | }
73 |
74 | mergedFilters := append(activeFilters, inputFilters...)
75 |
76 | uniqueFilters, dupplicatedFilters := util.RemoveDuplicates(mergedFilters)
77 |
78 | // Verify dupplicate
79 | if len(dupplicatedFilters) != 0 {
80 | color.Red("Duplicate filters found: %s", strings.Join(dupplicatedFilters, ", "))
81 | os.Exit(1)
82 | }
83 |
84 | viper.Set("active_filters", uniqueFilters)
85 |
86 | if err := viper.WriteConfig(); err != nil {
87 | color.Red("Error writing config file: %s", err.Error())
88 | os.Exit(1)
89 | }
90 | color.Green("Filter %s added", strings.Join(inputFilters, ", "))
91 | },
92 | }
93 |
--------------------------------------------------------------------------------
/cmd/filters/filters.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 | http://www.apache.org/licenses/LICENSE-2.0
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 | */
13 |
14 | package filters
15 |
16 | import (
17 | "github.com/spf13/cobra"
18 | )
19 |
20 | var FiltersCmd = &cobra.Command{
21 | Use: "filters",
22 | Aliases: []string{"filter"},
23 | Short: "Manage filters for analyzing Kubernetes resources",
24 | Long: `The filters command allows you to manage filters that are used to analyze Kubernetes resources.
25 | You can list available filters to analyze resources.`,
26 | Run: func(cmd *cobra.Command, args []string) {
27 | if len(args) == 0 {
28 | _ = cmd.Help()
29 | return
30 | }
31 | },
32 | }
33 |
34 | func init() {
35 | FiltersCmd.AddCommand(listCmd)
36 | FiltersCmd.AddCommand(addCmd)
37 | FiltersCmd.AddCommand(removeCmd)
38 | }
39 |
--------------------------------------------------------------------------------
/cmd/filters/list.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 | http://www.apache.org/licenses/LICENSE-2.0
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 | */
13 |
14 | package filters
15 |
16 | import (
17 | "fmt"
18 | "slices"
19 |
20 | "github.com/fatih/color"
21 | "github.com/k8sgpt-ai/k8sgpt/pkg/analyzer"
22 | "github.com/k8sgpt-ai/k8sgpt/pkg/integration"
23 | "github.com/k8sgpt-ai/k8sgpt/pkg/util"
24 | "github.com/spf13/cobra"
25 | "github.com/spf13/viper"
26 | )
27 |
28 | var listCmd = &cobra.Command{
29 | Use: "list",
30 | Short: "List available filters",
31 | Long: `The list command displays a list of available filters that can be used to analyze Kubernetes resources.`,
32 | Run: func(cmd *cobra.Command, args []string) {
33 | activeFilters := viper.GetStringSlice("active_filters")
34 | coreFilters, additionalFilters, integrationFilters := analyzer.ListFilters()
35 | integration := integration.NewIntegration()
36 | availableFilters := append(append(coreFilters, additionalFilters...), integrationFilters...)
37 |
38 | if len(activeFilters) == 0 {
39 | activeFilters = coreFilters
40 | }
41 | inactiveFilters := util.SliceDiff(availableFilters, activeFilters)
42 | fmt.Print(color.YellowString("Active: \n"))
43 | for _, filter := range activeFilters {
44 | // if the filter is an integration, mark this differently
45 | // but if the integration is inactive, remove
46 | if slices.Contains(integrationFilters, filter) {
47 | fmt.Printf("> %s\n", color.BlueString("%s (integration)", filter))
48 | } else {
49 | // This strange bit of logic will loop through every integration via
50 | // OwnsAnalyzer subcommand to check the filter and as the integrationFilters...
51 | // was no match, we know this isn't part of an active integration
52 | if _, err := integration.AnalyzerByIntegration(filter); err != nil {
53 | fmt.Printf("> %s\n", color.GreenString(filter))
54 | }
55 | }
56 | }
57 |
58 | // display inactive filters
59 | if len(inactiveFilters) != 0 {
60 | fmt.Print(color.YellowString("Unused: \n"))
61 | for _, filter := range inactiveFilters {
62 | // if the filter is an integration, mark this differently
63 | if slices.Contains(integrationFilters, filter) {
64 | fmt.Printf("> %s\n", color.BlueString("%s (integration)", filter))
65 | } else {
66 | fmt.Printf("> %s\n", color.RedString(filter))
67 | }
68 | }
69 | }
70 | },
71 | }
72 |
--------------------------------------------------------------------------------
/cmd/filters/remove.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 | http://www.apache.org/licenses/LICENSE-2.0
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 | */
13 |
14 | package filters
15 |
16 | import (
17 | "os"
18 | "strings"
19 |
20 | "github.com/fatih/color"
21 | "github.com/k8sgpt-ai/k8sgpt/pkg/analyzer"
22 | "github.com/k8sgpt-ai/k8sgpt/pkg/util"
23 | "github.com/spf13/cobra"
24 | "github.com/spf13/viper"
25 | )
26 |
27 | var removeCmd = &cobra.Command{
28 | Use: "remove [filter(s)]",
29 | Short: "Remove one or more filters.",
30 | Long: `The add command remove one or more filters to the default set of filters used by the analyze.`,
31 | Args: cobra.ExactArgs(1),
32 | Run: func(cmd *cobra.Command, args []string) {
33 | inputFilters := strings.Split(args[0], ",")
34 |
35 | // Get defined active_filters
36 | activeFilters := viper.GetStringSlice("active_filters")
37 | coreFilters, _, _ := analyzer.ListFilters()
38 |
39 | if len(activeFilters) == 0 {
40 | activeFilters = coreFilters
41 | }
42 |
43 | // Check if input input filters is not empty
44 | for _, f := range inputFilters {
45 | if f == "" {
46 | color.Red("Filter cannot be empty. Please use correct syntax.")
47 | os.Exit(1)
48 | }
49 | }
50 |
51 | // verify dupplicate filters example: k8sgpt filters remove Pod Pod
52 | uniqueFilters, dupplicatedFilters := util.RemoveDuplicates(inputFilters)
53 | if len(dupplicatedFilters) != 0 {
54 | color.Red("Duplicate filters found: %s", strings.Join(dupplicatedFilters, ", "))
55 | os.Exit(1)
56 | }
57 |
58 | // Verify if filter exist in config file and update default_filter
59 | filterNotFound := []string{}
60 | for _, filter := range uniqueFilters {
61 | foundFilter := false
62 | for i, f := range activeFilters {
63 | if f == filter {
64 | foundFilter = true
65 | activeFilters = append(activeFilters[:i], activeFilters[i+1:]...)
66 | break
67 | }
68 | }
69 | if !foundFilter {
70 | filterNotFound = append(filterNotFound, filter)
71 | }
72 | }
73 |
74 | if len(filterNotFound) != 0 {
75 | color.Red("Filter(s) %s does not exist in configuration file. Please use k8sgpt filters add.", strings.Join(filterNotFound, ", "))
76 | os.Exit(1)
77 | }
78 |
79 | viper.Set("active_filters", activeFilters)
80 |
81 | if err := viper.WriteConfig(); err != nil {
82 | color.Red("Error writing config file: %s", err.Error())
83 | os.Exit(1)
84 | }
85 | color.Green("Filter(s) %s removed", strings.Join(inputFilters, ", "))
86 | },
87 | }
88 |
--------------------------------------------------------------------------------
/cmd/generate/generate.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 | http://www.apache.org/licenses/LICENSE-2.0
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 | */
13 |
14 | package generate
15 |
16 | import (
17 | "fmt"
18 | "os/exec"
19 | "runtime"
20 |
21 | "github.com/fatih/color"
22 | "github.com/spf13/cobra"
23 | "github.com/spf13/viper"
24 | )
25 |
26 | var (
27 | backend string
28 | backendType string
29 | )
30 |
31 | // generateCmd represents the auth command
32 | var GenerateCmd = &cobra.Command{
33 | Use: "generate",
34 | Short: "Generate Key for your chosen backend (opens browser)",
35 | Long: `Opens your browser to generate a key for your chosen backend.`,
36 | Run: func(cmd *cobra.Command, args []string) {
37 |
38 | backendType = viper.GetString("backend_type")
39 | if backendType == "" {
40 | // Set the default backend
41 | backend = "openai"
42 | }
43 | // override the default backend if a flag is provided
44 | if backend != "" {
45 | backendType = backend
46 | }
47 | fmt.Println("")
48 | openbrowser("https://platform.openai.com/api-keys")
49 | },
50 | }
51 |
52 | func init() {
53 | // add flag for backend
54 | GenerateCmd.Flags().StringVarP(&backend, "backend", "b", "openai", "Backend AI provider")
55 | }
56 |
57 | func openbrowser(url string) {
58 | var err error
59 | isGui := true
60 | switch runtime.GOOS {
61 | case "linux":
62 | _, err = exec.LookPath("xdg-open")
63 | if err != nil {
64 | isGui = false
65 | } else {
66 | err = exec.Command("xdg-open", url).Start()
67 | }
68 | case "windows":
69 | err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
70 | case "darwin":
71 | err = exec.Command("open", url).Start()
72 | default:
73 | err = fmt.Errorf("unsupported platform")
74 | }
75 | printInstructions(isGui, backend)
76 | if err != nil {
77 | fmt.Println(err)
78 | }
79 | }
80 |
81 | func printInstructions(isGui bool, backendType string) {
82 | fmt.Println("")
83 | if isGui {
84 | color.Green("Opening: https://platform.openai.com/api-keys to generate a key for %s", backendType)
85 | fmt.Println("")
86 | } else {
87 | color.Green("Please open: https://platform.openai.com/api-keys to generate a key for %s", backendType)
88 | fmt.Println("")
89 | }
90 | color.Green("Please copy the generated key and run `k8sgpt auth add` to add it to your config file")
91 | fmt.Println("")
92 | }
93 |
--------------------------------------------------------------------------------
/cmd/integration/activate.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 | http://www.apache.org/licenses/LICENSE-2.0
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 | */
13 |
14 | package integration
15 |
16 | import (
17 | "github.com/fatih/color"
18 | "github.com/k8sgpt-ai/k8sgpt/pkg/analyzer"
19 | "github.com/k8sgpt-ai/k8sgpt/pkg/integration"
20 | "github.com/spf13/cobra"
21 | "github.com/spf13/viper"
22 | )
23 |
24 | var skipInstall bool
25 |
26 | // activateCmd represents the activate command
27 | var activateCmd = &cobra.Command{
28 | Use: "activate [integration]",
29 | Short: "Activate an integration",
30 | Long: ``,
31 | Args: cobra.ExactArgs(1),
32 | Run: func(cmd *cobra.Command, args []string) {
33 | integrationName := args[0]
34 | coreFilters, _, _ := analyzer.ListFilters()
35 |
36 | // Update filters
37 | activeFilters := viper.GetStringSlice("active_filters")
38 | if len(activeFilters) == 0 {
39 | activeFilters = coreFilters
40 | }
41 |
42 | integration := integration.NewIntegration()
43 | // Check if the integation exists
44 | err := integration.Activate(integrationName, namespace, activeFilters, skipInstall)
45 | if err != nil {
46 | color.Red("Error: %v", err)
47 | return
48 | }
49 |
50 | color.Green("Activated integration %s", integrationName)
51 | },
52 | }
53 |
54 | func init() {
55 | IntegrationCmd.AddCommand(activateCmd)
56 | activateCmd.Flags().BoolVarP(&skipInstall, "no-install", "s", false, "Only activate the integration filter without installing the filter (for example, if that filter plugin is already deployed in cluster, we do not need to re-install it again)")
57 | }
58 |
--------------------------------------------------------------------------------
/cmd/integration/deactivate.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 | http://www.apache.org/licenses/LICENSE-2.0
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 | */
13 |
14 | package integration
15 |
16 | import (
17 | "github.com/fatih/color"
18 | "github.com/k8sgpt-ai/k8sgpt/pkg/integration"
19 | "github.com/spf13/cobra"
20 | )
21 |
22 | // deactivateCmd represents the deactivate command
23 | var deactivateCmd = &cobra.Command{
24 | Use: "deactivate [integration]",
25 | Short: "Deactivate an integration",
26 | Args: cobra.ExactArgs(1),
27 | Long: `For example e.g. k8sgpt integration deactivate prometheus`,
28 | Run: func(cmd *cobra.Command, args []string) {
29 | integrationName := args[0]
30 |
31 | integration := integration.NewIntegration()
32 |
33 | if err := integration.Deactivate(integrationName, namespace); err != nil {
34 | color.Red("Error: %v", err)
35 | return
36 | }
37 |
38 | color.Green("Deactivated integration %s", integrationName)
39 |
40 | },
41 | }
42 |
43 | func init() {
44 | IntegrationCmd.AddCommand(deactivateCmd)
45 | }
46 |
--------------------------------------------------------------------------------
/cmd/integration/integration.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 | http://www.apache.org/licenses/LICENSE-2.0
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 | */
13 |
14 | package integration
15 |
16 | import (
17 | "github.com/spf13/cobra"
18 | )
19 |
20 | var (
21 | namespace string
22 | )
23 |
24 | // IntegrationCmd represents the integrate command
25 | var IntegrationCmd = &cobra.Command{
26 | Use: "integration",
27 | Aliases: []string{"integrations"},
28 | Short: "Integrate another tool into K8sGPT",
29 | Long: `Integrate another tool into K8sGPT. For example:
30 |
31 | k8sgpt integration activate prometheus
32 |
33 | This would allow you to connect to prometheus running with your cluster.`,
34 | Run: func(cmd *cobra.Command, args []string) {
35 | _ = cmd.Help()
36 | },
37 | }
38 |
39 | func init() {
40 | IntegrationCmd.PersistentFlags().StringVarP(&namespace, "namespace", "n", "default", "The namespace to use for the integration")
41 | }
42 |
--------------------------------------------------------------------------------
/cmd/integration/list.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 | http://www.apache.org/licenses/LICENSE-2.0
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 | */
13 |
14 | package integration
15 |
16 | import (
17 | "fmt"
18 | "os"
19 |
20 | "github.com/fatih/color"
21 | "github.com/k8sgpt-ai/k8sgpt/pkg/integration"
22 | "github.com/spf13/cobra"
23 | )
24 |
25 | // listCmd represents the list command
26 | var listCmd = &cobra.Command{
27 | Use: "list",
28 | Short: "Lists built-in integrations",
29 | Long: ``,
30 | Run: func(cmd *cobra.Command, args []string) {
31 | integrationProvider := integration.NewIntegration()
32 | integrations := integrationProvider.List()
33 |
34 | fmt.Println(color.YellowString("Active:"))
35 | for _, i := range integrations {
36 | b, err := integrationProvider.IsActivate(i)
37 | if err != nil {
38 | fmt.Println(err)
39 | os.Exit(1)
40 | }
41 | if b {
42 | fmt.Printf("> %s\n", color.GreenString(i))
43 | }
44 | }
45 |
46 | fmt.Println(color.YellowString("Unused: "))
47 | for _, i := range integrations {
48 | b, err := integrationProvider.IsActivate(i)
49 | if err != nil {
50 | fmt.Println(err)
51 | os.Exit(1)
52 | }
53 | if !b {
54 | fmt.Printf("> %s\n", color.GreenString(i))
55 | }
56 | }
57 | },
58 | }
59 |
60 | func init() {
61 | IntegrationCmd.AddCommand(listCmd)
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/cmd/root_test.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 | http://www.apache.org/licenses/LICENSE-2.0
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 | */
13 |
14 | package cmd
15 |
16 | import (
17 | "testing"
18 |
19 | "github.com/spf13/viper"
20 | )
21 |
22 | // Test that verbose flag is correctly set in viper.
23 | func TestInitConfig_VerboseFlag(t *testing.T) {
24 | verbose = true
25 | viper.Reset()
26 | initConfig()
27 | if !viper.GetBool("verbose") {
28 | t.Error("Expected verbose flag to be true")
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/cmd/version.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 | http://www.apache.org/licenses/LICENSE-2.0
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 | */
13 |
14 | package cmd
15 |
16 | import (
17 | "fmt"
18 | "runtime/debug"
19 |
20 | "github.com/spf13/cobra"
21 | )
22 |
23 | // versionCmd represents the version command
24 | var versionCmd = &cobra.Command{
25 | Use: "version",
26 | Short: "Print the version number of k8sgpt",
27 | Long: `All software has versions. This is k8sgpt's`,
28 | Run: func(cmd *cobra.Command, args []string) {
29 | if Version == "dev" {
30 | details, ok := debug.ReadBuildInfo()
31 | if ok && details.Main.Version != "" && details.Main.Version != "(devel)" {
32 | Version = details.Main.Version
33 | for _, i := range details.Settings {
34 | if i.Key == "vcs.time" {
35 | Date = i.Value
36 | }
37 | if i.Key == "vcs.revision" {
38 | Commit = i.Value
39 | }
40 | }
41 | }
42 | }
43 | fmt.Printf("k8sgpt: %s (%s), built at: %s\n", Version, Commit, Date)
44 | },
45 | }
46 |
47 | func init() {
48 | rootCmd.AddCommand(versionCmd)
49 | }
50 |
--------------------------------------------------------------------------------
/container/Dockerfile:
--------------------------------------------------------------------------------
1 | # Copyright 2023 The K8sGPT Authors.
2 | # Licensed under the Apache License, Version 2.0 (the "License");
3 | # you may not use this file except in compliance with the License.
4 | # You may obtain a copy of the License at
5 | # http://www.apache.org/licenses/LICENSE-2.0
6 | # Unless required by applicable law or agreed to in writing, software
7 | # distributed under the License is distributed on an "AS IS" BASIS,
8 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9 | # See the License for the specific language governing permissions and
10 | # limitations under the License.
11 |
12 | FROM golang:1.23-alpine3.19 AS builder
13 |
14 | ENV CGO_ENABLED=0
15 | ARG VERSION
16 | ARG COMMIT
17 | ARG DATE
18 | WORKDIR /workspace
19 |
20 | COPY go.mod go.sum ./
21 | RUN go mod download
22 |
23 | COPY ./ ./
24 |
25 | RUN go build -o /workspace/k8sgpt -ldflags "-X main.version=${VERSION} -X main.commit=${COMMIT} -X main.date=${DATE}" ./
26 |
27 | FROM gcr.io/distroless/static AS production
28 |
29 | LABEL org.opencontainers.image.source="https://github.com/k8sgpt-ai/k8sgpt" \
30 | org.opencontainers.image.url="https://k8sgpt.ai" \
31 | org.opencontainers.image.title="k8sgpt" \
32 | org.opencontainers.image.vendor='The K8sGPT Authors' \
33 | org.opencontainers.image.licenses='Apache-2.0'
34 |
35 | WORKDIR /
36 | COPY --from=builder /workspace/k8sgpt .
37 | USER 65532:65532
38 |
39 | ENTRYPOINT ["/k8sgpt"]
40 |
--------------------------------------------------------------------------------
/images/banner-black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k8sgpt-ai/k8sgpt/1819e6f410d078fce2bda8bbdb22054dfb4fc092/images/banner-black.png
--------------------------------------------------------------------------------
/images/banner-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k8sgpt-ai/k8sgpt/1819e6f410d078fce2bda8bbdb22054dfb4fc092/images/banner-white.png
--------------------------------------------------------------------------------
/images/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k8sgpt-ai/k8sgpt/1819e6f410d078fce2bda8bbdb22054dfb4fc092/images/demo.gif
--------------------------------------------------------------------------------
/images/demo1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k8sgpt-ai/k8sgpt/1819e6f410d078fce2bda8bbdb22054dfb4fc092/images/demo1.gif
--------------------------------------------------------------------------------
/images/demo2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k8sgpt-ai/k8sgpt/1819e6f410d078fce2bda8bbdb22054dfb4fc092/images/demo2.gif
--------------------------------------------------------------------------------
/images/demo3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k8sgpt-ai/k8sgpt/1819e6f410d078fce2bda8bbdb22054dfb4fc092/images/demo3.png
--------------------------------------------------------------------------------
/images/demo4.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k8sgpt-ai/k8sgpt/1819e6f410d078fce2bda8bbdb22054dfb4fc092/images/demo4.gif
--------------------------------------------------------------------------------
/images/demo5.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k8sgpt-ai/k8sgpt/1819e6f410d078fce2bda8bbdb22054dfb4fc092/images/demo5.gif
--------------------------------------------------------------------------------
/images/image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k8sgpt-ai/k8sgpt/1819e6f410d078fce2bda8bbdb22054dfb4fc092/images/image.png
--------------------------------------------------------------------------------
/images/landing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k8sgpt-ai/k8sgpt/1819e6f410d078fce2bda8bbdb22054dfb4fc092/images/landing.png
--------------------------------------------------------------------------------
/images/nodes.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k8sgpt-ai/k8sgpt/1819e6f410d078fce2bda8bbdb22054dfb4fc092/images/nodes.gif
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 | http://www.apache.org/licenses/LICENSE-2.0
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 | */
13 |
14 | package main
15 |
16 | import "github.com/k8sgpt-ai/k8sgpt/cmd"
17 |
18 | var (
19 | version = "dev"
20 | commit = "HEAD"
21 | date = "unknown"
22 | )
23 |
24 | func main() {
25 | cmd.Execute(version, commit, date)
26 | }
27 |
--------------------------------------------------------------------------------
/pkg/ai/amazonbedrock_mock_test.go:
--------------------------------------------------------------------------------
1 | package ai
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | "github.com/aws/aws-sdk-go-v2/aws"
8 | "github.com/aws/aws-sdk-go-v2/service/bedrock"
9 | "github.com/aws/aws-sdk-go-v2/service/bedrock/types"
10 | "github.com/aws/aws-sdk-go-v2/service/bedrockruntime"
11 | "github.com/k8sgpt-ai/k8sgpt/pkg/ai/bedrock_support"
12 | "github.com/stretchr/testify/assert"
13 | "github.com/stretchr/testify/mock"
14 | )
15 |
16 | // Mock for Bedrock Management Client
17 | type MockBedrockClient struct {
18 | mock.Mock
19 | }
20 |
21 | func (m *MockBedrockClient) GetInferenceProfile(ctx context.Context, params *bedrock.GetInferenceProfileInput, optFns ...func(*bedrock.Options)) (*bedrock.GetInferenceProfileOutput, error) {
22 | args := m.Called(ctx, params)
23 |
24 | if args.Get(0) == nil {
25 | return nil, args.Error(1)
26 | }
27 |
28 | return args.Get(0).(*bedrock.GetInferenceProfileOutput), args.Error(1)
29 | }
30 |
31 | // Mock for Bedrock Runtime Client
32 | type MockBedrockRuntimeClient struct {
33 | mock.Mock
34 | }
35 |
36 | func (m *MockBedrockRuntimeClient) InvokeModel(ctx context.Context, params *bedrockruntime.InvokeModelInput, optFns ...func(*bedrockruntime.Options)) (*bedrockruntime.InvokeModelOutput, error) {
37 | args := m.Called(ctx, params)
38 |
39 | if args.Get(0) == nil {
40 | return nil, args.Error(1)
41 | }
42 |
43 | return args.Get(0).(*bedrockruntime.InvokeModelOutput), args.Error(1)
44 | }
45 |
46 | // TestBedrockInferenceProfileARNWithMocks tests the inference profile ARN validation with mocks
47 | func TestBedrockInferenceProfileARNWithMocks(t *testing.T) {
48 | // Create test models
49 | testModels := []bedrock_support.BedrockModel{
50 | {
51 | Name: "anthropic.claude-3-5-sonnet-20240620-v1:0",
52 | Completion: &bedrock_support.CohereMessagesCompletion{},
53 | Response: &bedrock_support.CohereMessagesResponse{},
54 | Config: bedrock_support.BedrockModelConfig{
55 | MaxTokens: 100,
56 | Temperature: 0.5,
57 | TopP: 0.9,
58 | ModelName: "anthropic.claude-3-5-sonnet-20240620-v1:0",
59 | },
60 | },
61 | }
62 |
63 | // Create a client with test models
64 | client := &AmazonBedRockClient{models: testModels}
65 |
66 | // Create mock clients
67 | mockMgmtClient := new(MockBedrockClient)
68 | mockRuntimeClient := new(MockBedrockRuntimeClient)
69 |
70 | // Inject mock clients into the AmazonBedRockClient
71 | client.mgmtClient = mockMgmtClient
72 | client.client = mockRuntimeClient
73 |
74 | // Test with a valid inference profile ARN
75 | inferenceProfileARN := "arn:aws:bedrock:us-east-1:123456789012:inference-profile/my-profile"
76 |
77 | // Setup mock response for GetInferenceProfile
78 | mockMgmtClient.On("GetInferenceProfile", mock.Anything, &bedrock.GetInferenceProfileInput{
79 | InferenceProfileIdentifier: aws.String("my-profile"),
80 | }).Return(&bedrock.GetInferenceProfileOutput{
81 | Models: []types.InferenceProfileModel{
82 | {
83 | ModelArn: aws.String("arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-5-sonnet-20240620-v1:0"),
84 | },
85 | },
86 | }, nil)
87 |
88 | // Configure the client with the inference profile ARN
89 | config := AIProvider{
90 | Model: inferenceProfileARN,
91 | ProviderRegion: "us-east-1",
92 | }
93 |
94 | // Test the Configure method with the inference profile ARN
95 | err := client.Configure(&config)
96 |
97 | // Verify that the configuration was successful
98 | assert.NoError(t, err)
99 | assert.Equal(t, inferenceProfileARN, client.model.Config.ModelName)
100 |
101 | // Verify that the mock was called
102 | mockMgmtClient.AssertExpectations(t)
103 | }
104 |
--------------------------------------------------------------------------------
/pkg/ai/azureopenai.go:
--------------------------------------------------------------------------------
1 | package ai
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "net/http"
7 | "net/url"
8 |
9 | "github.com/sashabaranov/go-openai"
10 | )
11 |
12 | const azureAIClientName = "azureopenai"
13 |
14 | type AzureAIClient struct {
15 | nopCloser
16 |
17 | client *openai.Client
18 | model string
19 | temperature float32
20 | // organizationId string
21 | }
22 |
23 | func (c *AzureAIClient) Configure(config IAIConfig) error {
24 | token := config.GetPassword()
25 | baseURL := config.GetBaseURL()
26 | engine := config.GetEngine()
27 | proxyEndpoint := config.GetProxyEndpoint()
28 | defaultConfig := openai.DefaultAzureConfig(token, baseURL)
29 | orgId := config.GetOrganizationId()
30 |
31 | defaultConfig.AzureModelMapperFunc = func(model string) string {
32 | // If you use a deployment name different from the model name, you can customize the AzureModelMapperFunc function
33 | azureModelMapping := map[string]string{
34 | model: engine,
35 | }
36 | return azureModelMapping[model]
37 |
38 | }
39 |
40 | if proxyEndpoint != "" {
41 | proxyUrl, err := url.Parse(proxyEndpoint)
42 | if err != nil {
43 | return err
44 | }
45 | transport := &http.Transport{
46 | Proxy: http.ProxyURL(proxyUrl),
47 | }
48 |
49 | defaultConfig.HTTPClient = &http.Client{
50 | Transport: transport,
51 | }
52 | }
53 | if orgId != "" {
54 | defaultConfig.OrgID = orgId
55 | }
56 |
57 | client := openai.NewClientWithConfig(defaultConfig)
58 | if client == nil {
59 | return errors.New("error creating Azure OpenAI client")
60 | }
61 | c.client = client
62 | c.model = config.GetModel()
63 | c.temperature = config.GetTemperature()
64 | return nil
65 | }
66 |
67 | func (c *AzureAIClient) GetCompletion(ctx context.Context, prompt string) (string, error) {
68 | // Create a completion request
69 | resp, err := c.client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{
70 | Model: c.model,
71 | Messages: []openai.ChatCompletionMessage{
72 | {
73 | Role: openai.ChatMessageRoleUser,
74 | Content: prompt,
75 | },
76 | },
77 | Temperature: c.temperature,
78 | })
79 | if err != nil {
80 | return "", err
81 | }
82 | return resp.Choices[0].Message.Content, nil
83 | }
84 |
85 | func (c *AzureAIClient) GetName() string {
86 | return azureAIClientName
87 | }
88 |
--------------------------------------------------------------------------------
/pkg/ai/bedrock_interfaces.go:
--------------------------------------------------------------------------------
1 | package ai
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/aws/aws-sdk-go-v2/service/bedrock"
7 | "github.com/aws/aws-sdk-go-v2/service/bedrockruntime"
8 | )
9 |
10 | // BedrockManagementAPI defines the interface for Bedrock management operations
11 | type BedrockManagementAPI interface {
12 | GetInferenceProfile(ctx context.Context, params *bedrock.GetInferenceProfileInput, optFns ...func(*bedrock.Options)) (*bedrock.GetInferenceProfileOutput, error)
13 | }
14 |
15 | // BedrockRuntimeAPI defines the interface for Bedrock runtime operations
16 | type BedrockRuntimeAPI interface {
17 | InvokeModel(ctx context.Context, params *bedrockruntime.InvokeModelInput, optFns ...func(*bedrockruntime.Options)) (*bedrockruntime.InvokeModelOutput, error)
18 | }
19 |
--------------------------------------------------------------------------------
/pkg/ai/bedrock_support/model.go:
--------------------------------------------------------------------------------
1 | package bedrock_support
2 |
3 | type BedrockModelConfig struct {
4 | MaxTokens int
5 | Temperature float32
6 | TopP float32
7 | ModelName string
8 | }
9 | type BedrockModel struct {
10 | Name string
11 | Completion ICompletion
12 | Response IResponse
13 | Config BedrockModelConfig
14 | }
15 |
--------------------------------------------------------------------------------
/pkg/ai/bedrock_support/model_test.go:
--------------------------------------------------------------------------------
1 | package bedrock_support
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func TestBedrockModelConfig(t *testing.T) {
11 | config := BedrockModelConfig{
12 | MaxTokens: 100,
13 | Temperature: 0.7,
14 | TopP: 0.9,
15 | ModelName: "test-model",
16 | }
17 |
18 | assert.Equal(t, 100, config.MaxTokens)
19 | assert.Equal(t, float32(0.7), config.Temperature)
20 | assert.Equal(t, float32(0.9), config.TopP)
21 | assert.Equal(t, "test-model", config.ModelName)
22 | }
23 |
24 | func TestBedrockModel(t *testing.T) {
25 | completion := &MockCompletion{}
26 | response := &MockResponse{}
27 | config := BedrockModelConfig{
28 | MaxTokens: 100,
29 | Temperature: 0.7,
30 | TopP: 0.9,
31 | ModelName: "test-model",
32 | }
33 |
34 | model := BedrockModel{
35 | Name: "Test Model",
36 | Completion: completion,
37 | Response: response,
38 | Config: config,
39 | }
40 |
41 | assert.Equal(t, "Test Model", model.Name)
42 | assert.Equal(t, completion, model.Completion)
43 | assert.Equal(t, response, model.Response)
44 | assert.Equal(t, config, model.Config)
45 | }
46 |
47 | // MockCompletion is a mock implementation of the ICompletion interface
48 | type MockCompletion struct{}
49 |
50 | func (m *MockCompletion) GetCompletion(ctx context.Context, prompt string, config BedrockModelConfig) ([]byte, error) {
51 | return []byte(`{"prompt": "mock prompt"}`), nil
52 | }
53 |
54 | // MockResponse is a mock implementation of the IResponse interface
55 | type MockResponse struct{}
56 |
57 | func (m *MockResponse) ParseResponse(body []byte) (string, error) {
58 | return "mock response", nil
59 | }
60 |
--------------------------------------------------------------------------------
/pkg/ai/bedrock_support/responses_test.go:
--------------------------------------------------------------------------------
1 | package bedrock_support
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestCohereResponse_ParseResponse(t *testing.T) {
10 | response := &CohereResponse{}
11 | rawResponse := []byte(`{"completion": "Test completion", "stop_reason": "max_tokens"}`)
12 |
13 | result, err := response.ParseResponse(rawResponse)
14 | assert.NoError(t, err)
15 | assert.Equal(t, "Test completion", result)
16 |
17 | invalidResponse := []byte(`{"completion": "Test completion", "invalid_json":]`)
18 | _, err = response.ParseResponse(invalidResponse)
19 | assert.Error(t, err)
20 | }
21 |
22 | func TestAI21Response_ParseResponse(t *testing.T) {
23 | response := &AI21Response{}
24 | rawResponse := []byte(`{"completions": [{"data": {"text": "AI21 test"}}], "id": "123"}`)
25 |
26 | result, err := response.ParseResponse(rawResponse)
27 | assert.NoError(t, err)
28 | assert.Equal(t, "AI21 test", result)
29 |
30 | invalidResponse := []byte(`{"completions": [{"data": {"text": "AI21 test"}}, "invalid_json":]`)
31 | _, err = response.ParseResponse(invalidResponse)
32 | assert.Error(t, err)
33 | }
34 |
35 | func TestAmazonResponse_ParseResponse(t *testing.T) {
36 | response := &AmazonResponse{}
37 | rawResponse := []byte(`{"inputTextTokenCount": 10, "results": [{"tokenCount": 20, "outputText": "Amazon test", "completionReason": "stop"}]}`)
38 |
39 | result, err := response.ParseResponse(rawResponse)
40 | assert.NoError(t, err)
41 | assert.Equal(t, "Amazon test", result)
42 |
43 | invalidResponse := []byte(`{"inputTextTokenCount": 10, "results": [{"tokenCount": 20, "outputText": "Amazon test", "invalid_json":]`)
44 | _, err = response.ParseResponse(invalidResponse)
45 | assert.Error(t, err)
46 | }
47 |
48 | func TestNovaResponse_ParseResponse(t *testing.T) {
49 | response := &NovaResponse{}
50 | rawResponse := []byte(`{"output": {"message": {"content": [{"text": "Nova test"}]}}, "stopReason": "stop", "usage": {"inputTokens": 10, "outputTokens": 20, "totalTokens": 30, "cacheReadInputTokenCount": 5}}`)
51 |
52 | result, err := response.ParseResponse(rawResponse)
53 | assert.NoError(t, err)
54 | assert.Equal(t, "Nova test", result)
55 |
56 | rawResponseEmptyContent := []byte(`{"output": {"message": {"content": []}}, "stopReason": "stop", "usage": {"inputTokens": 10, "outputTokens": 20, "totalTokens": 30, "cacheReadInputTokenCount": 5}}`)
57 |
58 | resultEmptyContent, errEmptyContent := response.ParseResponse(rawResponseEmptyContent)
59 | assert.NoError(t, errEmptyContent)
60 | assert.Equal(t, "", resultEmptyContent)
61 |
62 | invalidResponse := []byte(`{"output": {"message": {"content": [{"text": "Nova test"}}, "invalid_json":]`)
63 | _, err = response.ParseResponse(invalidResponse)
64 | assert.Error(t, err)
65 | }
66 |
--------------------------------------------------------------------------------
/pkg/ai/cohere.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 | http://www.apache.org/licenses/LICENSE-2.0
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 | */
13 |
14 | package ai
15 |
16 | import (
17 | "context"
18 | "errors"
19 |
20 | api "github.com/cohere-ai/cohere-go/v2"
21 | cohere "github.com/cohere-ai/cohere-go/v2/client"
22 | "github.com/cohere-ai/cohere-go/v2/option"
23 | )
24 |
25 | const cohereAIClientName = "cohere"
26 |
27 | type CohereClient struct {
28 | nopCloser
29 |
30 | client *cohere.Client
31 | model string
32 | temperature float32
33 | maxTokens int
34 | }
35 |
36 | func (c *CohereClient) Configure(config IAIConfig) error {
37 | token := config.GetPassword()
38 |
39 | opts := []option.RequestOption{
40 | cohere.WithToken(token),
41 | }
42 |
43 | baseURL := config.GetBaseURL()
44 | if baseURL != "" {
45 | opts = append(opts, cohere.WithBaseURL(baseURL))
46 | }
47 |
48 | client := cohere.NewClient(opts...)
49 | if client == nil {
50 | return errors.New("error creating Cohere client")
51 | }
52 |
53 | c.client = client
54 | c.model = config.GetModel()
55 | c.temperature = config.GetTemperature()
56 | c.maxTokens = config.GetMaxTokens()
57 |
58 | return nil
59 | }
60 |
61 | func (c *CohereClient) GetCompletion(ctx context.Context, prompt string) (string, error) {
62 | // Create a completion request
63 | response, err := c.client.Chat(ctx, &api.ChatRequest{
64 | Message: prompt,
65 | Model: &c.model,
66 | K: api.Int(0),
67 | Preamble: api.String(""),
68 | Temperature: api.Float64(float64(c.temperature)),
69 | RawPrompting: api.Bool(false),
70 | MaxTokens: api.Int(c.maxTokens),
71 | })
72 | if err != nil {
73 | return "", err
74 | }
75 | return response.Text, nil
76 | }
77 |
78 | func (c *CohereClient) GetName() string {
79 | return cohereAIClientName
80 | }
81 |
--------------------------------------------------------------------------------
/pkg/ai/factory.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 | http://www.apache.org/licenses/LICENSE-2.0
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 | */
13 |
14 | package ai
15 |
16 | import (
17 | "github.com/spf13/viper"
18 | )
19 |
20 | // AIClientFactory is an interface for creating AI clients
21 | type AIClientFactory interface {
22 | NewClient(provider string) IAI
23 | }
24 |
25 | // DefaultAIClientFactory is the default implementation of AIClientFactory
26 | type DefaultAIClientFactory struct{}
27 |
28 | // NewClient creates a new AI client using the default implementation
29 | func (f *DefaultAIClientFactory) NewClient(provider string) IAI {
30 | return NewClient(provider)
31 | }
32 |
33 | // ConfigProvider is an interface for accessing configuration
34 | type ConfigProvider interface {
35 | UnmarshalKey(key string, rawVal interface{}) error
36 | }
37 |
38 | // ViperConfigProvider is the default implementation of ConfigProvider using Viper
39 | type ViperConfigProvider struct{}
40 |
41 | // UnmarshalKey unmarshals a key from the configuration using Viper
42 | func (p *ViperConfigProvider) UnmarshalKey(key string, rawVal interface{}) error {
43 | return viper.UnmarshalKey(key, rawVal)
44 | }
45 |
46 | // Default instances to be used
47 | var (
48 | DefaultClientFactory = &DefaultAIClientFactory{}
49 | DefaultConfigProvider = &ViperConfigProvider{}
50 | )
51 |
52 | // For testing - these variables can be overridden in tests
53 | var (
54 | testAIClientFactory AIClientFactory = nil
55 | testConfigProvider ConfigProvider = nil
56 | )
57 |
58 | // GetAIClientFactory returns the test factory if set, otherwise the default
59 | func GetAIClientFactory() AIClientFactory {
60 | if testAIClientFactory != nil {
61 | return testAIClientFactory
62 | }
63 | return DefaultClientFactory
64 | }
65 |
66 | // GetConfigProvider returns the test provider if set, otherwise the default
67 | func GetConfigProvider() ConfigProvider {
68 | if testConfigProvider != nil {
69 | return testConfigProvider
70 | }
71 | return DefaultConfigProvider
72 | }
73 |
74 | // For testing - set the test implementations
75 | func SetTestAIClientFactory(factory AIClientFactory) {
76 | testAIClientFactory = factory
77 | }
78 |
79 | func SetTestConfigProvider(provider ConfigProvider) {
80 | testConfigProvider = provider
81 | }
82 |
83 | // Reset test implementations
84 | func ResetTestImplementations() {
85 | testAIClientFactory = nil
86 | testConfigProvider = nil
87 | }
--------------------------------------------------------------------------------
/pkg/ai/huggingface.go:
--------------------------------------------------------------------------------
1 | package ai
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/hupe1980/go-huggingface"
7 | "k8s.io/utils/ptr"
8 | )
9 |
10 | const huggingfaceAIClientName = "huggingface"
11 |
12 | type HuggingfaceClient struct {
13 | nopCloser
14 |
15 | client *huggingface.InferenceClient
16 | model string
17 | topP float32
18 | topK int32
19 | temperature float32
20 | maxTokens int
21 | }
22 |
23 | func (c *HuggingfaceClient) Configure(config IAIConfig) error {
24 | token := config.GetPassword()
25 |
26 | client := huggingface.NewInferenceClient(token)
27 |
28 | c.client = client
29 | c.model = config.GetModel()
30 | c.topP = config.GetTopP()
31 | c.topK = config.GetTopK()
32 | c.temperature = config.GetTemperature()
33 | if config.GetMaxTokens() > 500 {
34 | c.maxTokens = 500
35 | } else {
36 | c.maxTokens = config.GetMaxTokens()
37 | }
38 | return nil
39 | }
40 |
41 | func (c *HuggingfaceClient) GetCompletion(ctx context.Context, prompt string) (string, error) {
42 | resp, err := c.client.Conversational(ctx, &huggingface.ConversationalRequest{
43 | Inputs: huggingface.ConverstationalInputs{
44 | Text: prompt,
45 | },
46 | Model: c.model,
47 | Parameters: huggingface.ConversationalParameters{
48 | TopP: ptr.To[float64](float64(c.topP)),
49 | TopK: ptr.To[int](int(c.topK)),
50 | Temperature: ptr.To[float64](float64(c.temperature)),
51 | MaxLength: &c.maxTokens,
52 | },
53 | Options: huggingface.Options{
54 | WaitForModel: ptr.To[bool](true),
55 | },
56 | })
57 | if err != nil {
58 | return "", err
59 | }
60 | return resp.GeneratedText, nil
61 | }
62 |
63 | func (c *HuggingfaceClient) GetName() string { return huggingfaceAIClientName }
64 |
--------------------------------------------------------------------------------
/pkg/ai/interactive/interactive.go:
--------------------------------------------------------------------------------
1 | package interactive
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 |
7 | "github.com/fatih/color"
8 | "github.com/k8sgpt-ai/k8sgpt/pkg/analysis"
9 | "github.com/pterm/pterm"
10 | )
11 |
12 | type INTERACTIVE_STATE int
13 |
14 | const (
15 | prompt = "Given the following context: "
16 | )
17 |
18 | const (
19 | E_RUNNING INTERACTIVE_STATE = iota
20 | E_EXITED = iota
21 | )
22 |
23 | type InteractionRunner struct {
24 | config *analysis.Analysis
25 | State chan INTERACTIVE_STATE
26 | contextWindow []byte
27 | }
28 |
29 | func NewInteractionRunner(config *analysis.Analysis, contextWindow []byte) *InteractionRunner {
30 | return &InteractionRunner{
31 | config: config,
32 | contextWindow: contextWindow,
33 | State: make(chan INTERACTIVE_STATE),
34 | }
35 | }
36 |
37 | func (a *InteractionRunner) StartInteraction() {
38 | a.State <- E_RUNNING
39 | pterm.Println("Interactive mode enabled [type exit to close.]")
40 | for {
41 |
42 | query := pterm.DefaultInteractiveTextInput.WithMultiLine(false)
43 | queryString, err := query.Show()
44 | if err != nil {
45 | fmt.Println(err)
46 | }
47 | if queryString == "" {
48 | continue
49 | }
50 | if strings.Contains(queryString, "exit") {
51 | a.State <- E_EXITED
52 | continue
53 | }
54 | pterm.Println()
55 | contextWindow := fmt.Sprintf("%s %s %s", prompt, string(a.contextWindow),
56 | queryString)
57 |
58 | response, err := a.config.AIClient.GetCompletion(a.config.Context,
59 | contextWindow)
60 | if err != nil {
61 | color.Red("Error: %v", err)
62 | a.State <- E_EXITED
63 | continue
64 | }
65 | pterm.Println(response)
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/pkg/ai/localai.go:
--------------------------------------------------------------------------------
1 | package ai
2 |
3 | const localAIClientName = "localai"
4 |
5 | type LocalAIClient struct {
6 | OpenAIClient
7 | }
8 |
9 | func (a *LocalAIClient) GetName() string {
10 | return localAIClientName
11 | }
12 |
--------------------------------------------------------------------------------
/pkg/ai/noopai.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 | http://www.apache.org/licenses/LICENSE-2.0
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 | */
13 |
14 | package ai
15 |
16 | import (
17 | "context"
18 | )
19 |
20 | const noopAIClientName = "noopai"
21 |
22 | type NoOpAIClient struct {
23 | nopCloser
24 | }
25 |
26 | func (c *NoOpAIClient) Configure(_ IAIConfig) error {
27 | return nil
28 | }
29 |
30 | func (c *NoOpAIClient) GetCompletion(_ context.Context, prompt string) (string, error) {
31 | response := "I am a noop response to the prompt " + prompt
32 | return response, nil
33 | }
34 |
35 | func (c *NoOpAIClient) GetName() string {
36 | return noopAIClientName
37 | }
38 |
--------------------------------------------------------------------------------
/pkg/ai/ocigenai.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2024 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 | http://www.apache.org/licenses/LICENSE-2.0
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 | */
13 |
14 | package ai
15 |
16 | import (
17 | "context"
18 | "errors"
19 | "github.com/oracle/oci-go-sdk/v65/common"
20 | "github.com/oracle/oci-go-sdk/v65/generativeaiinference"
21 | "strings"
22 | )
23 |
24 | const ociClientName = "oci"
25 |
26 | type OCIGenAIClient struct {
27 | nopCloser
28 |
29 | client *generativeaiinference.GenerativeAiInferenceClient
30 | model string
31 | compartmentId string
32 | temperature float32
33 | topP float32
34 | maxTokens int
35 | }
36 |
37 | func (c *OCIGenAIClient) GetName() string {
38 | return ociClientName
39 | }
40 |
41 | func (c *OCIGenAIClient) Configure(config IAIConfig) error {
42 | config.GetEndpointName()
43 | c.model = config.GetModel()
44 | c.temperature = config.GetTemperature()
45 | c.topP = config.GetTopP()
46 | c.maxTokens = config.GetMaxTokens()
47 | c.compartmentId = config.GetCompartmentId()
48 | provider := common.DefaultConfigProvider()
49 | client, err := generativeaiinference.NewGenerativeAiInferenceClientWithConfigurationProvider(provider)
50 | if err != nil {
51 | return err
52 | }
53 | c.client = &client
54 | return nil
55 | }
56 |
57 | func (c *OCIGenAIClient) GetCompletion(ctx context.Context, prompt string) (string, error) {
58 | generateTextRequest := c.newGenerateTextRequest(prompt)
59 | generateTextResponse, err := c.client.GenerateText(ctx, generateTextRequest)
60 | if err != nil {
61 | return "", err
62 | }
63 | return extractGeneratedText(generateTextResponse.InferenceResponse)
64 | }
65 |
66 | func (c *OCIGenAIClient) newGenerateTextRequest(prompt string) generativeaiinference.GenerateTextRequest {
67 | temperatureF64 := float64(c.temperature)
68 | topPF64 := float64(c.topP)
69 | return generativeaiinference.GenerateTextRequest{
70 | GenerateTextDetails: generativeaiinference.GenerateTextDetails{
71 | CompartmentId: &c.compartmentId,
72 | ServingMode: generativeaiinference.OnDemandServingMode{
73 | ModelId: &c.model,
74 | },
75 | InferenceRequest: generativeaiinference.CohereLlmInferenceRequest{
76 | Prompt: &prompt,
77 | MaxTokens: &c.maxTokens,
78 | Temperature: &temperatureF64,
79 | TopP: &topPF64,
80 | },
81 | },
82 | }
83 | }
84 |
85 | func extractGeneratedText(llmInferenceResponse generativeaiinference.LlmInferenceResponse) (string, error) {
86 | response, ok := llmInferenceResponse.(generativeaiinference.CohereLlmInferenceResponse)
87 | if !ok {
88 | return "", errors.New("failed to extract generated text from backed response")
89 | }
90 | sb := strings.Builder{}
91 | for _, text := range response.GeneratedTexts {
92 | if text.Text != nil {
93 | sb.WriteString(*text.Text)
94 | }
95 | }
96 | return sb.String(), nil
97 | }
98 |
--------------------------------------------------------------------------------
/pkg/ai/ollama.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 | http://www.apache.org/licenses/LICENSE-2.0
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 | */
13 |
14 | package ai
15 |
16 | import (
17 | "context"
18 | "errors"
19 | "net/http"
20 | "net/url"
21 |
22 | ollama "github.com/ollama/ollama/api"
23 | )
24 |
25 | const ollamaClientName = "ollama"
26 |
27 | type OllamaClient struct {
28 | nopCloser
29 |
30 | client *ollama.Client
31 | model string
32 | temperature float32
33 | topP float32
34 | }
35 |
36 | const (
37 | defaultBaseURL = "http://localhost:11434"
38 | defaultModel = "llama3"
39 | )
40 |
41 | func (c *OllamaClient) Configure(config IAIConfig) error {
42 | baseURL := config.GetBaseURL()
43 | if baseURL == "" {
44 | baseURL = defaultBaseURL
45 | }
46 | baseClientURL, err := url.Parse(baseURL)
47 | if err != nil {
48 | return err
49 | }
50 |
51 | proxyEndpoint := config.GetProxyEndpoint()
52 | httpClient := http.DefaultClient
53 | if proxyEndpoint != "" {
54 | proxyUrl, err := url.Parse(proxyEndpoint)
55 | if err != nil {
56 | return err
57 | }
58 | transport := &http.Transport{
59 | Proxy: http.ProxyURL(proxyUrl),
60 | }
61 |
62 | httpClient = &http.Client{
63 | Transport: transport,
64 | }
65 | }
66 |
67 | c.client = ollama.NewClient(baseClientURL, httpClient)
68 | if c.client == nil {
69 | return errors.New("error creating Ollama client")
70 | }
71 | c.model = config.GetModel()
72 | if c.model == "" {
73 | c.model = defaultModel
74 | }
75 | c.temperature = config.GetTemperature()
76 | c.topP = config.GetTopP()
77 | return nil
78 | }
79 | func (c *OllamaClient) GetCompletion(ctx context.Context, prompt string) (string, error) {
80 | req := &ollama.GenerateRequest{
81 | Model: c.model,
82 | Prompt: prompt,
83 | Stream: new(bool),
84 | Options: map[string]interface{}{
85 | "temperature": c.temperature,
86 | "top_p": c.topP,
87 | },
88 | }
89 | completion := ""
90 | respFunc := func(resp ollama.GenerateResponse) error {
91 | completion = resp.Response
92 | return nil
93 | }
94 | err := c.client.Generate(ctx, req, respFunc)
95 | if err != nil {
96 | return "", err
97 | }
98 | return completion, nil
99 | }
100 | func (a *OllamaClient) GetName() string {
101 | return ollamaClientName
102 | }
103 |
--------------------------------------------------------------------------------
/pkg/ai/openai_header_transport_test.go:
--------------------------------------------------------------------------------
1 | package ai
2 |
3 | import (
4 | "context"
5 | "net/http"
6 | "net/http/httptest"
7 | "testing"
8 |
9 | "github.com/stretchr/testify/assert"
10 | )
11 |
12 | // Mock configuration
13 | type mockConfig struct {
14 | baseURL string
15 | }
16 |
17 | func (m *mockConfig) GetPassword() string {
18 | return ""
19 | }
20 |
21 | func (m *mockConfig) GetOrganizationId() string {
22 | return ""
23 | }
24 |
25 | func (m *mockConfig) GetProxyEndpoint() string {
26 | return ""
27 | }
28 |
29 | func (m *mockConfig) GetBaseURL() string {
30 | return m.baseURL
31 | }
32 |
33 | func (m *mockConfig) GetCustomHeaders() []http.Header {
34 | return []http.Header{
35 | {"X-Custom-Header-1": []string{"Value1"}},
36 | {"X-Custom-Header-2": []string{"Value2"}},
37 | {"X-Custom-Header-2": []string{"Value3"}}, // Testing multiple values for the same header
38 | }
39 | }
40 |
41 | func (m *mockConfig) GetModel() string {
42 | return ""
43 | }
44 |
45 | func (m *mockConfig) GetTemperature() float32 {
46 | return 0.0
47 | }
48 |
49 | func (m *mockConfig) GetTopP() float32 {
50 | return 0.0
51 | }
52 | func (m *mockConfig) GetCompartmentId() string {
53 | return ""
54 | }
55 |
56 | func (m *mockConfig) GetTopK() int32 {
57 | return 0.0
58 | }
59 |
60 | func (m *mockConfig) GetMaxTokens() int {
61 | return 0
62 | }
63 |
64 | func (m *mockConfig) GetEndpointName() string {
65 | return ""
66 | }
67 | func (m *mockConfig) GetEngine() string {
68 | return ""
69 | }
70 |
71 | func (m *mockConfig) GetProviderId() string {
72 | return ""
73 | }
74 |
75 | func (m *mockConfig) GetProviderRegion() string {
76 | return ""
77 | }
78 |
79 | func TestOpenAIClient_CustomHeaders(t *testing.T) {
80 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
81 | assert.Equal(t, "Value1", r.Header.Get("X-Custom-Header-1"))
82 | assert.ElementsMatch(t, []string{"Value2", "Value3"}, r.Header["X-Custom-Header-2"])
83 | w.WriteHeader(http.StatusOK)
84 | // Mock response for openai completion
85 | mockResponse := `{"choices": [{"message": {"content": "test"}}]}`
86 | n, err := w.Write([]byte(mockResponse))
87 | if err != nil {
88 | t.Fatalf("error writing response: %v", err)
89 | }
90 | if n != len(mockResponse) {
91 | t.Fatalf("expected to write %d bytes but wrote %d bytes", len(mockResponse), n)
92 | }
93 | }))
94 | defer server.Close()
95 |
96 | config := &mockConfig{baseURL: server.URL}
97 |
98 | client := &OpenAIClient{}
99 | err := client.Configure(config)
100 | assert.NoError(t, err)
101 |
102 | // Make a completion request to trigger the headers
103 | ctx := context.Background()
104 | _, err = client.GetCompletion(ctx, "foo prompt")
105 | assert.NoError(t, err)
106 | }
107 |
--------------------------------------------------------------------------------
/pkg/ai/prompts.go:
--------------------------------------------------------------------------------
1 | package ai
2 |
3 | const (
4 | default_prompt = `Simplify the following Kubernetes error message delimited by triple dashes written in --- %s --- language; --- %s ---.
5 | Provide the most possible solution in a step by step style in no more than 280 characters. Write the output in the following format:
6 | Error: {Explain error here}
7 | Solution: {Step by step solution here}
8 | `
9 |
10 | prom_conf_prompt = `Simplify the following Prometheus error message delimited by triple dashes written in --- %s --- language; --- %s ---.
11 | This error came when validating the Prometheus configuration file.
12 | Provide step by step instructions to fix, with suggestions, referencing Prometheus documentation if relevant.
13 | Write the output in the following format in no more than 300 characters:
14 | Error: {Explain error here}
15 | Solution: {Step by step solution here}
16 | `
17 |
18 | prom_relabel_prompt = `
19 | Return your prompt in this language: %s, beginning with
20 | The following is a list of the form:
21 | job_name:
22 | {Prometheus job_name}
23 | relabel_configs:
24 | {Prometheus relabel_configs}
25 | kubernetes_sd_configs:
26 | {Prometheus service discovery config}
27 | ---
28 | %s
29 | ---
30 | For each job_name, describe the Kubernetes service and pod labels,
31 | namespaces, ports, and containers they match.
32 | Return the message:
33 | Discovered and parsed Prometheus scrape configurations.
34 | For targets to be scraped by Prometheus, ensure they are running with
35 | at least one of the following label sets:
36 | Then for each job, write this format:
37 | - Job: {job_name}
38 | - Service Labels:
39 | - {list of service labels}
40 | - Pod Labels:
41 | - {list of pod labels}
42 | - Namespaces:
43 | - {list of namespaces}
44 | - Ports:
45 | - {list of ports}
46 | - Containers:
47 | - {list of container names}
48 | `
49 |
50 | kyverno_prompt = `Simplify the following Kyverno warnings message delimited by triple dashes written in --- %s --- language; --- %s ---.
51 | Provide the most probable solution as a kubectl command.
52 |
53 | Write the output in the following format, for the solution, only show the kubectl command:
54 |
55 | Error: {Explain error here}
56 |
57 | Solution: {kubectl command}
58 | `
59 | raw_promt = `{"language": "%s","message": "%s","prompt": "%s"}`
60 | )
61 |
62 | var PromptMap = map[string]string{
63 | "raw": raw_promt,
64 | "default": default_prompt,
65 | "PrometheusConfigValidate": prom_conf_prompt,
66 | "PrometheusConfigRelabelReport": prom_relabel_prompt,
67 | "PolicyReport": kyverno_prompt,
68 | "ClusterPolicyReport": kyverno_prompt,
69 | }
70 |
--------------------------------------------------------------------------------
/pkg/ai/watsonxai.go:
--------------------------------------------------------------------------------
1 | package ai
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 |
8 | wx "github.com/IBM/watsonx-go/pkg/models"
9 | )
10 |
11 | const ibmWatsonxAIClientName = "ibmwatsonxai"
12 |
13 | type IBMWatsonxAIClient struct {
14 | nopCloser
15 |
16 | client *wx.Client
17 | model string
18 | temperature float32
19 | topP float32
20 | topK int32
21 | maxNewTokens int
22 | }
23 |
24 | const (
25 | modelMetallama = "ibm/granite-13b-chat-v2"
26 | maxTokens = 2048
27 | )
28 |
29 | func (c *IBMWatsonxAIClient) Configure(config IAIConfig) error {
30 | if config.GetModel() == "" {
31 | c.model = modelMetallama
32 | } else {
33 | c.model = config.GetModel()
34 | }
35 | if config.GetMaxTokens() == 0 {
36 | c.maxNewTokens = maxTokens
37 | } else {
38 | c.maxNewTokens = config.GetMaxTokens()
39 | }
40 | c.temperature = config.GetTemperature()
41 | c.topP = config.GetTopP()
42 | c.topK = config.GetTopK()
43 |
44 | apiKey := config.GetPassword()
45 | if apiKey == "" {
46 | return errors.New("No watsonx API key provided")
47 | }
48 |
49 | projectId := config.GetProviderId()
50 | if projectId == "" {
51 | return errors.New("No watsonx project ID provided")
52 | }
53 |
54 | client, err := wx.NewClient(
55 | wx.WithWatsonxAPIKey(apiKey),
56 | wx.WithWatsonxProjectID(projectId),
57 | )
58 | if err != nil {
59 | return fmt.Errorf("Failed to create client for testing. Error: %v", err)
60 | }
61 | c.client = client
62 |
63 | return nil
64 | }
65 |
66 | func (c *IBMWatsonxAIClient) GetCompletion(ctx context.Context, prompt string) (string, error) {
67 | result, err := c.client.GenerateText(
68 | c.model,
69 | prompt,
70 | wx.WithTemperature((float64)(c.temperature)),
71 | wx.WithTopP((float64)(c.topP)),
72 | wx.WithTopK((uint)(c.topK)),
73 | wx.WithMaxNewTokens((uint)(c.maxNewTokens)),
74 | )
75 | if err != nil {
76 | return "", fmt.Errorf("Expected no error, but got an error: %v", err)
77 | }
78 | if result.Text == "" {
79 | return "", errors.New("Expected a result, but got an empty string")
80 | }
81 | return result.Text, nil
82 | }
83 |
84 | func (c *IBMWatsonxAIClient) GetName() string {
85 | return ibmWatsonxAIClientName
86 | }
87 |
--------------------------------------------------------------------------------
/pkg/analysis/output.go:
--------------------------------------------------------------------------------
1 | package analysis
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "strings"
7 |
8 | "github.com/fatih/color"
9 | )
10 |
11 | var outputFormats = map[string]func(*Analysis) ([]byte, error){
12 | "json": (*Analysis).jsonOutput,
13 | "text": (*Analysis).textOutput,
14 | }
15 |
16 | func getOutputFormats() []string {
17 | formats := make([]string, 0, len(outputFormats))
18 | for format := range outputFormats {
19 | formats = append(formats, format)
20 | }
21 | return formats
22 | }
23 |
24 | func (a *Analysis) PrintOutput(format string) ([]byte, error) {
25 | outputFunc, ok := outputFormats[format]
26 | if !ok {
27 | return nil, fmt.Errorf("unsupported output format: %s. Available format %s", format, strings.Join(getOutputFormats(), ","))
28 | }
29 | return outputFunc(a)
30 | }
31 |
32 | func (a *Analysis) jsonOutput() ([]byte, error) {
33 | var problems int
34 | var status AnalysisStatus
35 | for _, result := range a.Results {
36 | problems += len(result.Error)
37 | }
38 | if problems > 0 {
39 | status = StateProblemDetected
40 | } else {
41 | status = StateOK
42 | }
43 |
44 | result := JsonOutput{
45 | Provider: a.AnalysisAIProvider,
46 | Problems: problems,
47 | Results: a.Results,
48 | Errors: a.Errors,
49 | Status: status,
50 | }
51 | output, err := json.MarshalIndent(result, "", " ")
52 | if err != nil {
53 | return nil, fmt.Errorf("error marshalling json: %v", err)
54 | }
55 | return output, nil
56 | }
57 |
58 | func (a *Analysis) PrintStats() []byte {
59 | var output strings.Builder
60 |
61 | output.WriteString(color.YellowString("The stats mode allows for debugging and understanding the time taken by an analysis by displaying the statistics of each analyzer.\n"))
62 |
63 | for _, stat := range a.Stats {
64 | output.WriteString(fmt.Sprintf("- Analyzer %s took %s \n", color.YellowString(stat.Analyzer), stat.DurationTime))
65 | }
66 |
67 | return []byte(output.String())
68 | }
69 |
70 | func (a *Analysis) textOutput() ([]byte, error) {
71 | var output strings.Builder
72 |
73 | // Print the AI provider used for this analysis (if explain was enabled).
74 | if a.Explain {
75 | output.WriteString(fmt.Sprintf("AI Provider: %s\n", color.YellowString(a.AnalysisAIProvider)))
76 | } else {
77 | output.WriteString(fmt.Sprintf("AI Provider: %s\n", color.YellowString("AI not used; --explain not set")))
78 | }
79 |
80 | if len(a.Errors) != 0 {
81 | output.WriteString("\n")
82 | output.WriteString(color.YellowString("Warnings : \n"))
83 | for _, aerror := range a.Errors {
84 | output.WriteString(fmt.Sprintf("- %s\n", color.YellowString(aerror)))
85 | }
86 | }
87 | output.WriteString("\n")
88 | if len(a.Results) == 0 {
89 | output.WriteString(color.GreenString("No problems detected\n"))
90 | return []byte(output.String()), nil
91 | }
92 | for n, result := range a.Results {
93 | output.WriteString(fmt.Sprintf("%s: %s %s(%s)\n", color.CyanString("%d", n),
94 | color.HiYellowString(result.Kind),
95 | color.YellowString(result.Name),
96 | color.CyanString(result.ParentObject)))
97 | for _, err := range result.Error {
98 | output.WriteString(fmt.Sprintf("- %s %s\n", color.RedString("Error:"), color.RedString(err.Text)))
99 | if err.KubernetesDoc != "" {
100 | output.WriteString(fmt.Sprintf(" %s %s\n", color.RedString("Kubernetes Doc:"), color.RedString(err.KubernetesDoc)))
101 | }
102 | }
103 | output.WriteString(color.GreenString(result.Details + "\n"))
104 | }
105 | return []byte(output.String()), nil
106 | }
107 |
--------------------------------------------------------------------------------
/pkg/analysis/output_test.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2024 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 | http://www.apache.org/licenses/LICENSE-2.0
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 | */
13 |
14 | package analysis
15 |
16 | import (
17 | "testing"
18 |
19 | "github.com/stretchr/testify/require"
20 | )
21 |
22 | func TestPrintOutput(t *testing.T) {
23 | require.NotEmpty(t, getOutputFormats())
24 |
25 | tests := []struct {
26 | name string
27 | a *Analysis
28 | format string
29 | expectedOutput string
30 | expectedErr string
31 | }{
32 | {
33 | name: "json format",
34 | a: &Analysis{},
35 | format: "json",
36 | expectedOutput: "{\n \"provider\": \"\",\n \"errors\": null,\n \"status\": \"OK\",\n \"problems\": 0,\n \"results\": null\n}",
37 | },
38 | {
39 | name: "text format",
40 | a: &Analysis{},
41 | format: "text",
42 | expectedOutput: "AI Provider: AI not used; --explain not set\n\nNo problems detected\n",
43 | },
44 | {
45 | name: "unsupported format",
46 | a: &Analysis{},
47 | format: "unsupported",
48 | expectedErr: "unsupported output format: unsupported. Available format",
49 | },
50 | }
51 | for _, tt := range tests {
52 | tt := tt
53 | t.Run(tt.name, func(t *testing.T) {
54 | output, err := tt.a.PrintOutput(tt.format)
55 | if tt.expectedErr == "" {
56 | require.NoError(t, err)
57 | require.Contains(t, string(output), tt.expectedOutput)
58 | } else {
59 | require.ErrorContains(t, err, tt.expectedErr)
60 | require.Nil(t, output)
61 | }
62 | })
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/pkg/analyzer/gatewayclass.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 | http://www.apache.org/licenses/LICENSE-2.0
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 | */
13 |
14 | package analyzer
15 |
16 | import (
17 | "fmt"
18 |
19 | "github.com/k8sgpt-ai/k8sgpt/pkg/common"
20 | "github.com/k8sgpt-ai/k8sgpt/pkg/util"
21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
22 | ctrl "sigs.k8s.io/controller-runtime/pkg/client"
23 | gtwapi "sigs.k8s.io/gateway-api/apis/v1"
24 | )
25 |
26 | type GatewayClassAnalyzer struct{}
27 |
28 | // Gateway analyser will analyse all different Kinds and search for missing object dependencies
29 | func (GatewayClassAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
30 |
31 | kind := "GatewayClass"
32 | AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
33 | "analyzer_name": kind,
34 | })
35 |
36 | gcList := >wapi.GatewayClassList{}
37 | client := a.Client.CtrlClient
38 | err := gtwapi.AddToScheme(client.Scheme())
39 | if err != nil {
40 | return nil, err
41 | }
42 |
43 | labelSelector := util.LabelStrToSelector(a.LabelSelector)
44 | if err := client.List(a.Context, gcList, &ctrl.ListOptions{LabelSelector: labelSelector}); err != nil {
45 | return nil, err
46 | }
47 | var preAnalysis = map[string]common.PreAnalysis{}
48 |
49 | // Find all unhealthy gateway Classes
50 |
51 | for _, gc := range gcList.Items {
52 | var failures []common.Failure
53 |
54 | gcName := gc.GetName()
55 | // Check only the current condition
56 | if gc.Status.Conditions[0].Status != metav1.ConditionTrue {
57 | failures = append(failures, common.Failure{
58 | Text: fmt.Sprintf(
59 | "GatewayClass '%s' with a controller name '%s' is not accepted. Message: '%s'.",
60 | gcName,
61 | gc.Spec.ControllerName,
62 | gc.Status.Conditions[0].Message,
63 | ),
64 | Sensitive: []common.Sensitive{
65 | {
66 | Unmasked: gcName,
67 | Masked: util.MaskString(gcName),
68 | },
69 | },
70 | })
71 | }
72 | if len(failures) > 0 {
73 | preAnalysis[gcName] = common.PreAnalysis{
74 | GatewayClass: gc,
75 | FailureDetails: failures,
76 | }
77 | AnalyzerErrorsMetric.WithLabelValues(kind, gcName, "").Set(float64(len(failures)))
78 | }
79 | }
80 | for key, value := range preAnalysis {
81 | var currentAnalysis = common.Result{
82 | Kind: kind,
83 | Name: key,
84 | Error: value.FailureDetails,
85 | }
86 | a.Results = append(a.Results, currentAnalysis)
87 | }
88 | return a.Results, nil
89 | }
90 |
--------------------------------------------------------------------------------
/pkg/analyzer/gatewayclass_test.go:
--------------------------------------------------------------------------------
1 | package analyzer
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | "github.com/k8sgpt-ai/k8sgpt/pkg/common"
8 | "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
9 | "github.com/stretchr/testify/assert"
10 | apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
12 | "k8s.io/client-go/kubernetes/scheme"
13 | fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
14 | gtwapi "sigs.k8s.io/gateway-api/apis/v1"
15 | )
16 |
17 | // Testing with the fake dynamic client if GatewayClasses have an accepted status
18 | func TestGatewayClassAnalyzer(t *testing.T) {
19 | GatewayClass := >wapi.GatewayClass{}
20 | GatewayClass.Name = "foobar"
21 | GatewayClass.Spec.ControllerName = "gateway.fooproxy.io/gatewayclass-controller"
22 | // Initialize Conditions slice before setting properties
23 | BadCondition := metav1.Condition{
24 | Type: "Accepted",
25 | Status: "Uknown",
26 | Message: "Waiting for controller",
27 | Reason: "Pending",
28 | }
29 | GatewayClass.Status.Conditions = []metav1.Condition{BadCondition}
30 | // Create a GatewayClassAnalyzer instance with the fake client
31 | scheme := scheme.Scheme
32 | err := gtwapi.Install(scheme)
33 | if err != nil {
34 | t.Error(err)
35 | }
36 | err = apiextensionsv1.AddToScheme(scheme)
37 | if err != nil {
38 | t.Error(err)
39 | }
40 |
41 | fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(GatewayClass).Build()
42 |
43 | analyzerInstance := GatewayClassAnalyzer{}
44 | config := common.Analyzer{
45 | Client: &kubernetes.Client{
46 | CtrlClient: fakeClient,
47 | },
48 | Context: context.Background(),
49 | Namespace: "default",
50 | }
51 | analysisResults, err := analyzerInstance.Analyze(config)
52 | if err != nil {
53 | t.Error(err)
54 | }
55 | assert.Equal(t, len(analysisResults), 1)
56 |
57 | }
58 |
59 | func TestGatewayClassAnalyzerLabelSelectorFiltering(t *testing.T) {
60 | condition := metav1.Condition{
61 | Type: "Accepted",
62 | Status: "Ready",
63 | Message: "Ready",
64 | Reason: "Ready",
65 | }
66 |
67 | // Create two GatewayClasses with different labels
68 | GatewayClass := >wapi.GatewayClass{}
69 | GatewayClass.Name = "foobar"
70 | GatewayClass.Spec.ControllerName = "gateway.fooproxy.io/gatewayclass-controller"
71 | GatewayClass.Labels = map[string]string{"app": "gatewayclass"}
72 | GatewayClass.Status.Conditions = []metav1.Condition{condition}
73 |
74 | GatewayClass2 := >wapi.GatewayClass{}
75 | GatewayClass2.Name = "foobar2"
76 | GatewayClass2.Spec.ControllerName = "gateway.fooproxy.io/gatewayclass-controller"
77 | GatewayClass2.Status.Conditions = []metav1.Condition{condition}
78 |
79 | scheme := scheme.Scheme
80 | err := gtwapi.Install(scheme)
81 | if err != nil {
82 | t.Error(err)
83 | }
84 | err = apiextensionsv1.AddToScheme(scheme)
85 | if err != nil {
86 | t.Error(err)
87 | }
88 |
89 | fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(GatewayClass, GatewayClass2).Build()
90 |
91 | analyzerInstance := GatewayClassAnalyzer{}
92 | config := common.Analyzer{
93 | Client: &kubernetes.Client{
94 | CtrlClient: fakeClient,
95 | },
96 | Context: context.Background(),
97 | Namespace: "default",
98 | LabelSelector: "app=gatewayclass",
99 | }
100 | analysisResults, err := analyzerInstance.Analyze(config)
101 | if err != nil {
102 | t.Error(err)
103 | }
104 | assert.Equal(t, len(analysisResults), 1)
105 | }
106 |
--------------------------------------------------------------------------------
/pkg/analyzer/job.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2025 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 | http://www.apache.org/licenses/LICENSE-2.0
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 | */
13 |
14 | package analyzer
15 |
16 | import (
17 | "fmt"
18 |
19 | "github.com/k8sgpt-ai/k8sgpt/pkg/common"
20 | "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
21 | "github.com/k8sgpt-ai/k8sgpt/pkg/util"
22 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
23 | "k8s.io/apimachinery/pkg/runtime/schema"
24 | )
25 |
26 | type JobAnalyzer struct{}
27 |
28 | func (analyzer JobAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
29 |
30 | kind := "Job"
31 | apiDoc := kubernetes.K8sApiReference{
32 | Kind: kind,
33 | ApiVersion: schema.GroupVersion{
34 | Group: "batch",
35 | Version: "v1",
36 | },
37 | OpenapiSchema: a.OpenapiSchema,
38 | }
39 |
40 | AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
41 | "analyzer_name": kind,
42 | })
43 |
44 | JobList, err := a.Client.GetClient().BatchV1().Jobs(a.Namespace).List(a.Context, v1.ListOptions{LabelSelector: a.LabelSelector})
45 | if err != nil {
46 | return nil, err
47 | }
48 |
49 | var preAnalysis = map[string]common.PreAnalysis{}
50 |
51 | for _, Job := range JobList.Items {
52 | var failures []common.Failure
53 | if Job.Spec.Suspend != nil && *Job.Spec.Suspend {
54 | doc := apiDoc.GetApiDocV2("spec.suspend")
55 |
56 | failures = append(failures, common.Failure{
57 | Text: fmt.Sprintf("Job %s is suspended", Job.Name),
58 | KubernetesDoc: doc,
59 | Sensitive: []common.Sensitive{
60 | {
61 | Unmasked: Job.Namespace,
62 | Masked: util.MaskString(Job.Namespace),
63 | },
64 | {
65 | Unmasked: Job.Name,
66 | Masked: util.MaskString(Job.Name),
67 | },
68 | },
69 | })
70 | }
71 | if Job.Status.Failed > 0 {
72 | doc := apiDoc.GetApiDocV2("status.failed")
73 | failures = append(failures, common.Failure{
74 | Text: fmt.Sprintf("Job %s has failed", Job.Name),
75 | KubernetesDoc: doc,
76 | Sensitive: []common.Sensitive{
77 | {
78 | Unmasked: Job.Namespace,
79 | Masked: util.MaskString(Job.Namespace),
80 | },
81 | {
82 | Unmasked: Job.Name,
83 | Masked: util.MaskString(Job.Name),
84 | },
85 | },
86 | })
87 | }
88 |
89 | if len(failures) > 0 {
90 | preAnalysis[fmt.Sprintf("%s/%s", Job.Namespace, Job.Name)] = common.PreAnalysis{
91 | FailureDetails: failures,
92 | }
93 | AnalyzerErrorsMetric.WithLabelValues(kind, Job.Name, Job.Namespace).Set(float64(len(failures)))
94 | }
95 | }
96 |
97 | for key, value := range preAnalysis {
98 | currentAnalysis := common.Result{
99 | Kind: kind,
100 | Name: key,
101 | Error: value.FailureDetails,
102 | }
103 | a.Results = append(a.Results, currentAnalysis)
104 | }
105 |
106 | return a.Results, nil
107 | }
108 |
--------------------------------------------------------------------------------
/pkg/analyzer/netpol.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 | http://www.apache.org/licenses/LICENSE-2.0
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 | */
13 |
14 | package analyzer
15 |
16 | import (
17 | "fmt"
18 |
19 | "github.com/k8sgpt-ai/k8sgpt/pkg/common"
20 | "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
21 | "github.com/k8sgpt-ai/k8sgpt/pkg/util"
22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
23 | "k8s.io/apimachinery/pkg/runtime/schema"
24 | )
25 |
26 | type NetworkPolicyAnalyzer struct{}
27 |
28 | func (NetworkPolicyAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
29 |
30 | kind := "NetworkPolicy"
31 | apiDoc := kubernetes.K8sApiReference{
32 | Kind: kind,
33 | ApiVersion: schema.GroupVersion{
34 | Group: "networking",
35 | Version: "v1",
36 | },
37 | OpenapiSchema: a.OpenapiSchema,
38 | }
39 |
40 | AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
41 | "analyzer_name": kind,
42 | })
43 |
44 | // get all network policies in the namespace
45 | policies, err := a.Client.GetClient().NetworkingV1().
46 | NetworkPolicies(a.Namespace).List(a.Context, metav1.ListOptions{LabelSelector: a.LabelSelector})
47 | if err != nil {
48 | return nil, err
49 | }
50 |
51 | var preAnalysis = map[string]common.PreAnalysis{}
52 |
53 | for _, policy := range policies.Items {
54 | var failures []common.Failure
55 |
56 | // Check if policy allows traffic to all pods in the namespace
57 | if len(policy.Spec.PodSelector.MatchLabels) == 0 {
58 | doc := apiDoc.GetApiDocV2("spec.podSelector.matchLabels")
59 |
60 | failures = append(failures, common.Failure{
61 | Text: fmt.Sprintf("Network policy allows traffic to all pods: %s", policy.Name),
62 | KubernetesDoc: doc,
63 | Sensitive: []common.Sensitive{
64 | {
65 | Unmasked: policy.Name,
66 | Masked: util.MaskString(policy.Name),
67 | },
68 | },
69 | })
70 | } else {
71 | // Check if policy is not applied to any pods
72 | podList, err := util.GetPodListByLabels(a.Client.GetClient(), a.Namespace, policy.Spec.PodSelector.MatchLabels)
73 | if err != nil {
74 | return nil, err
75 | }
76 | if len(podList.Items) == 0 {
77 | failures = append(failures, common.Failure{
78 | Text: fmt.Sprintf("Network policy is not applied to any pods: %s", policy.Name),
79 | Sensitive: []common.Sensitive{
80 | {
81 | Unmasked: policy.Name,
82 | Masked: util.MaskString(policy.Name),
83 | },
84 | },
85 | })
86 | }
87 | }
88 |
89 | if len(failures) > 0 {
90 | preAnalysis[fmt.Sprintf("%s/%s", policy.Namespace, policy.Name)] = common.PreAnalysis{
91 | FailureDetails: failures,
92 | NetworkPolicy: policy,
93 | }
94 | AnalyzerErrorsMetric.WithLabelValues(kind, policy.Name, policy.Namespace).Set(float64(len(failures)))
95 |
96 | }
97 | }
98 |
99 | for key, value := range preAnalysis {
100 | currentAnalysis := common.Result{
101 | Kind: kind,
102 | Name: key,
103 | Error: value.FailureDetails,
104 | }
105 | a.Results = append(a.Results, currentAnalysis)
106 | }
107 |
108 | return a.Results, nil
109 | }
110 |
--------------------------------------------------------------------------------
/pkg/analyzer/pdb.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 | http://www.apache.org/licenses/LICENSE-2.0
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 | */
13 |
14 | package analyzer
15 |
16 | import (
17 | "fmt"
18 |
19 | "github.com/k8sgpt-ai/k8sgpt/pkg/common"
20 | "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
21 | "github.com/k8sgpt-ai/k8sgpt/pkg/util"
22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
23 | "k8s.io/apimachinery/pkg/runtime/schema"
24 | )
25 |
26 | type PdbAnalyzer struct{}
27 |
28 | func (PdbAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
29 |
30 | kind := "PodDisruptionBudget"
31 | apiDoc := kubernetes.K8sApiReference{
32 | Kind: kind,
33 | ApiVersion: schema.GroupVersion{
34 | Group: "policy",
35 | Version: "v1",
36 | },
37 | OpenapiSchema: a.OpenapiSchema,
38 | }
39 |
40 | AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
41 | "analyzer_name": kind,
42 | })
43 |
44 | list, err := a.Client.GetClient().PolicyV1().PodDisruptionBudgets(a.Namespace).List(a.Context, metav1.ListOptions{LabelSelector: a.LabelSelector})
45 | if err != nil {
46 | return nil, err
47 | }
48 |
49 | var preAnalysis = map[string]common.PreAnalysis{}
50 |
51 | for _, pdb := range list.Items {
52 | var failures []common.Failure
53 |
54 | // Before accessing the Conditions, check if they exist or not.
55 | if len(pdb.Status.Conditions) == 0 {
56 | continue
57 | }
58 | if pdb.Status.Conditions[0].Type == "DisruptionAllowed" && pdb.Status.Conditions[0].Status == "False" {
59 | var doc string
60 | if pdb.Spec.MaxUnavailable != nil {
61 | doc = apiDoc.GetApiDocV2("spec.maxUnavailable")
62 | }
63 | if pdb.Spec.MinAvailable != nil {
64 | doc = apiDoc.GetApiDocV2("spec.minAvailable")
65 | }
66 | if pdb.Spec.Selector != nil && pdb.Spec.Selector.MatchLabels != nil {
67 | for k, v := range pdb.Spec.Selector.MatchLabels {
68 | failures = append(failures, common.Failure{
69 | Text: fmt.Sprintf("%s, expected pdb pod label %s=%s", pdb.Status.Conditions[0].Reason, k, v),
70 | KubernetesDoc: doc,
71 | Sensitive: []common.Sensitive{
72 | {
73 | Unmasked: k,
74 | Masked: util.MaskString(k),
75 | },
76 | {
77 | Unmasked: v,
78 | Masked: util.MaskString(v),
79 | },
80 | },
81 | })
82 | }
83 | }
84 | }
85 |
86 | if len(failures) > 0 {
87 | preAnalysis[fmt.Sprintf("%s/%s", pdb.Namespace, pdb.Name)] = common.PreAnalysis{
88 | PodDisruptionBudget: pdb,
89 | FailureDetails: failures,
90 | }
91 | AnalyzerErrorsMetric.WithLabelValues(kind, pdb.Name, pdb.Namespace).Set(float64(len(failures)))
92 | }
93 | }
94 |
95 | for key, value := range preAnalysis {
96 | var currentAnalysis = common.Result{
97 | Kind: kind,
98 | Name: key,
99 | Error: value.FailureDetails,
100 | }
101 |
102 | parent, found := util.GetParent(a.Client, value.PodDisruptionBudget.ObjectMeta)
103 | if found {
104 | currentAnalysis.ParentObject = parent
105 | }
106 | a.Results = append(a.Results, currentAnalysis)
107 | }
108 |
109 | return a.Results, err
110 | }
111 |
--------------------------------------------------------------------------------
/pkg/analyzer/pvc.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 | http://www.apache.org/licenses/LICENSE-2.0
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 | */
13 |
14 | package analyzer
15 |
16 | import (
17 | "fmt"
18 |
19 | "github.com/k8sgpt-ai/k8sgpt/pkg/common"
20 | "github.com/k8sgpt-ai/k8sgpt/pkg/util"
21 | appsv1 "k8s.io/api/core/v1"
22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
23 | )
24 |
25 | type PvcAnalyzer struct{}
26 |
27 | func (PvcAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
28 |
29 | kind := "PersistentVolumeClaim"
30 |
31 | AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
32 | "analyzer_name": kind,
33 | })
34 |
35 | // search all namespaces for pods that are not running
36 | list, err := a.Client.GetClient().CoreV1().PersistentVolumeClaims(a.Namespace).List(a.Context, metav1.ListOptions{LabelSelector: a.LabelSelector})
37 | if err != nil {
38 | return nil, err
39 | }
40 |
41 | var preAnalysis = map[string]common.PreAnalysis{}
42 |
43 | for _, pvc := range list.Items {
44 | var failures []common.Failure
45 |
46 | // Check for empty rs
47 | if pvc.Status.Phase == appsv1.ClaimPending {
48 |
49 | // parse the event log and append details
50 | evt, err := util.FetchLatestEvent(a.Context, a.Client, pvc.Namespace, pvc.Name)
51 | if err != nil || evt == nil {
52 | continue
53 | }
54 | if evt.Reason == "ProvisioningFailed" && evt.Message != "" {
55 | failures = append(failures, common.Failure{
56 | Text: evt.Message,
57 | Sensitive: []common.Sensitive{},
58 | })
59 | }
60 | }
61 | if len(failures) > 0 {
62 | preAnalysis[fmt.Sprintf("%s/%s", pvc.Namespace, pvc.Name)] = common.PreAnalysis{
63 | PersistentVolumeClaim: pvc,
64 | FailureDetails: failures,
65 | }
66 | AnalyzerErrorsMetric.WithLabelValues(kind, pvc.Name, pvc.Namespace).Set(float64(len(failures)))
67 | }
68 | }
69 |
70 | for key, value := range preAnalysis {
71 | var currentAnalysis = common.Result{
72 | Kind: kind,
73 | Name: key,
74 | Error: value.FailureDetails,
75 | }
76 |
77 | parent, found := util.GetParent(a.Client, value.PersistentVolumeClaim.ObjectMeta)
78 | if found {
79 | currentAnalysis.ParentObject = parent
80 | }
81 | a.Results = append(a.Results, currentAnalysis)
82 | }
83 |
84 | return a.Results, nil
85 | }
86 |
--------------------------------------------------------------------------------
/pkg/analyzer/rs.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 | http://www.apache.org/licenses/LICENSE-2.0
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 | */
13 |
14 | package analyzer
15 |
16 | import (
17 | "fmt"
18 |
19 | "github.com/k8sgpt-ai/k8sgpt/pkg/common"
20 | "github.com/k8sgpt-ai/k8sgpt/pkg/util"
21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
22 | )
23 |
24 | type ReplicaSetAnalyzer struct{}
25 |
26 | func (ReplicaSetAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
27 |
28 | kind := "ReplicaSet"
29 |
30 | AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
31 | "analyzer_name": kind,
32 | })
33 |
34 | // search all namespaces for pods that are not running
35 | list, err := a.Client.GetClient().AppsV1().ReplicaSets(a.Namespace).List(a.Context, metav1.ListOptions{LabelSelector: a.LabelSelector})
36 | if err != nil {
37 | return nil, err
38 | }
39 |
40 | var preAnalysis = map[string]common.PreAnalysis{}
41 |
42 | for _, rs := range list.Items {
43 | var failures []common.Failure
44 |
45 | // Check for empty rs
46 | if rs.Status.Replicas == 0 {
47 |
48 | // Check through container status to check for crashes
49 | for _, rsStatus := range rs.Status.Conditions {
50 | if rsStatus.Type == "ReplicaFailure" && rsStatus.Reason == "FailedCreate" {
51 | failures = append(failures, common.Failure{
52 | Text: rsStatus.Message,
53 | Sensitive: []common.Sensitive{},
54 | })
55 |
56 | }
57 | }
58 | }
59 | if len(failures) > 0 {
60 | preAnalysis[fmt.Sprintf("%s/%s", rs.Namespace, rs.Name)] = common.PreAnalysis{
61 | ReplicaSet: rs,
62 | FailureDetails: failures,
63 | }
64 | AnalyzerErrorsMetric.WithLabelValues(kind, rs.Name, rs.Namespace).Set(float64(len(failures)))
65 | }
66 | }
67 |
68 | for key, value := range preAnalysis {
69 | var currentAnalysis = common.Result{
70 | Kind: kind,
71 | Name: key,
72 | Error: value.FailureDetails,
73 | }
74 |
75 | parent, found := util.GetParent(a.Client, value.ReplicaSet.ObjectMeta)
76 | if found {
77 | currentAnalysis.ParentObject = parent
78 | }
79 | a.Results = append(a.Results, currentAnalysis)
80 | }
81 | return a.Results, nil
82 | }
83 |
--------------------------------------------------------------------------------
/pkg/analyzer/test_utils.go:
--------------------------------------------------------------------------------
1 | package analyzer
2 |
3 | // Helper functions for tests
4 | func boolPtr(b bool) *bool {
5 | return &b
6 | }
7 |
8 | func int64Ptr(i int64) *int64 {
9 | return &i
10 | }
11 |
--------------------------------------------------------------------------------
/pkg/cache/cache.go:
--------------------------------------------------------------------------------
1 | package cache
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/spf13/viper"
7 | "google.golang.org/grpc/codes"
8 | "google.golang.org/grpc/status"
9 | )
10 |
11 | var (
12 | types = []ICache{
13 | &AzureCache{},
14 | &FileBasedCache{},
15 | &GCSCache{},
16 | &S3Cache{},
17 | &InterplexCache{},
18 | }
19 | )
20 |
21 | type ICache interface {
22 | Configure(cacheInfo CacheProvider) error
23 | Store(key string, data string) error
24 | Load(key string) (string, error)
25 | List() ([]CacheObjectDetails, error)
26 | Remove(key string) error
27 | Exists(key string) bool
28 | IsCacheDisabled() bool
29 | GetName() string
30 | DisableCache()
31 | }
32 |
33 | func New(cacheType string) ICache {
34 | for _, t := range types {
35 | if cacheType == t.GetName() {
36 | return t
37 | }
38 | }
39 | return &FileBasedCache{}
40 | }
41 |
42 | func ParseCacheConfiguration() (CacheProvider, error) {
43 | var cacheInfo CacheProvider
44 | err := viper.UnmarshalKey("cache", &cacheInfo)
45 | if err != nil {
46 | return cacheInfo, err
47 | }
48 | return cacheInfo, nil
49 | }
50 |
51 | func NewCacheProvider(cacheType, bucketname, region, endpoint, storageAccount, containerName, projectId string, insecure bool) (CacheProvider, error) {
52 | cProvider := CacheProvider{}
53 |
54 | switch {
55 | case cacheType == "azure":
56 | cProvider.Azure.ContainerName = containerName
57 | cProvider.Azure.StorageAccount = storageAccount
58 | cProvider.CurrentCacheType = "azure"
59 | case cacheType == "gcs":
60 | cProvider.GCS.BucketName = bucketname
61 | cProvider.GCS.ProjectId = projectId
62 | cProvider.GCS.Region = region
63 | cProvider.CurrentCacheType = "gcs"
64 | case cacheType == "s3":
65 | cProvider.S3.BucketName = bucketname
66 | cProvider.S3.Region = region
67 | cProvider.S3.Endpoint = endpoint
68 | cProvider.S3.InsecureSkipVerify = insecure
69 | cProvider.CurrentCacheType = "s3"
70 | case cacheType == "interplex":
71 | cProvider.Interplex.ConnectionString = endpoint
72 | cProvider.CurrentCacheType = "interplex"
73 | default:
74 | return CacheProvider{}, status.Error(codes.Internal, fmt.Sprintf("%s is not a valid option", cacheType))
75 | }
76 |
77 | cache := New(cacheType)
78 | err := cache.Configure(cProvider)
79 | if err != nil {
80 | return CacheProvider{}, err
81 | }
82 | return cProvider, nil
83 | }
84 |
85 | // If we have set a remote cache, return the remote cache configuration
86 | func GetCacheConfiguration() (ICache, error) {
87 | cacheInfo, err := ParseCacheConfiguration()
88 | if err != nil {
89 | return nil, err
90 | }
91 |
92 | var cache ICache
93 | switch {
94 | case cacheInfo.CurrentCacheType == "gcs":
95 | cache = &GCSCache{}
96 | case cacheInfo.CurrentCacheType == "azure":
97 | cache = &AzureCache{}
98 | case cacheInfo.CurrentCacheType == "s3":
99 | cache = &S3Cache{}
100 | case cacheInfo.CurrentCacheType == "interplex":
101 | cache = &InterplexCache{}
102 | default:
103 | cache = &FileBasedCache{}
104 | }
105 | err_config := cache.Configure(cacheInfo)
106 | return cache, err_config
107 | }
108 |
109 | func AddRemoteCache(cacheInfo CacheProvider) error {
110 |
111 | viper.Set("cache", cacheInfo)
112 |
113 | err := viper.WriteConfig()
114 | if err != nil {
115 | return err
116 | }
117 | return nil
118 | }
119 |
120 | func RemoveRemoteCache() error {
121 | var cacheInfo CacheProvider
122 | err := viper.UnmarshalKey("cache", &cacheInfo)
123 | if err != nil {
124 | return status.Error(codes.Internal, "cache unmarshal")
125 | }
126 |
127 | cacheInfo = CacheProvider{}
128 | viper.Set("cache", cacheInfo)
129 | err = viper.WriteConfig()
130 | if err != nil {
131 | return status.Error(codes.Internal, "unable to write config")
132 | }
133 |
134 | return nil
135 |
136 | }
137 |
--------------------------------------------------------------------------------
/pkg/cache/file_based.go:
--------------------------------------------------------------------------------
1 | package cache
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "path/filepath"
7 |
8 | "github.com/adrg/xdg"
9 | "github.com/k8sgpt-ai/k8sgpt/pkg/util"
10 | )
11 |
12 | var _ (ICache) = (*FileBasedCache)(nil)
13 |
14 | type FileBasedCache struct {
15 | noCache bool
16 | }
17 |
18 | func (f *FileBasedCache) Configure(cacheInfo CacheProvider) error {
19 | return nil
20 | }
21 |
22 | func (f *FileBasedCache) IsCacheDisabled() bool {
23 | return f.noCache
24 | }
25 |
26 | func (*FileBasedCache) List() ([]CacheObjectDetails, error) {
27 | path, err := xdg.CacheFile("k8sgpt")
28 | if err != nil {
29 | return nil, err
30 | }
31 |
32 | files, err := os.ReadDir(path)
33 | if err != nil {
34 | return nil, err
35 | }
36 |
37 | var result []CacheObjectDetails
38 | for _, file := range files {
39 | info, err := file.Info()
40 | if err != nil {
41 | return nil, err
42 | }
43 | result = append(result, CacheObjectDetails{
44 | Name: file.Name(),
45 | UpdatedAt: info.ModTime(),
46 | })
47 | }
48 |
49 | return result, nil
50 | }
51 |
52 | func (*FileBasedCache) Exists(key string) bool {
53 | path, err := xdg.CacheFile(filepath.Join("k8sgpt", key))
54 |
55 | if err != nil {
56 | fmt.Fprintln(os.Stderr, "warning: error while testing if cache key exists:", err)
57 | return false
58 | }
59 |
60 | exists, err := util.FileExists(path)
61 |
62 | if err != nil {
63 | fmt.Fprintln(os.Stderr, "warning: error while testing if cache key exists:", err)
64 | return false
65 | }
66 |
67 | return exists
68 | }
69 |
70 | func (*FileBasedCache) Load(key string) (string, error) {
71 | path, err := xdg.CacheFile(filepath.Join("k8sgpt", key))
72 |
73 | if err != nil {
74 | return "", err
75 | }
76 |
77 | data, err := os.ReadFile(path)
78 |
79 | if err != nil {
80 | return "", err
81 | }
82 |
83 | return string(data), nil
84 | }
85 |
86 | func (*FileBasedCache) Remove(key string) error {
87 | path, err := xdg.CacheFile(filepath.Join("k8sgpt", key))
88 |
89 | if err != nil {
90 | return err
91 | }
92 |
93 | if err := os.Remove(path); err != nil {
94 | return err
95 | }
96 |
97 | return nil
98 | }
99 |
100 | func (*FileBasedCache) Store(key string, data string) error {
101 | path, err := xdg.CacheFile(filepath.Join("k8sgpt", key))
102 |
103 | if err != nil {
104 | return err
105 | }
106 |
107 | return os.WriteFile(path, []byte(data), 0600)
108 | }
109 |
110 | func (s *FileBasedCache) GetName() string {
111 | return "file"
112 | }
113 |
114 | func (s *FileBasedCache) DisableCache() {
115 | s.noCache = true
116 | }
117 |
--------------------------------------------------------------------------------
/pkg/cache/gcs_based.go:
--------------------------------------------------------------------------------
1 | package cache
2 |
3 | import (
4 | "context"
5 | "io"
6 | "log"
7 |
8 | "cloud.google.com/go/storage"
9 | "google.golang.org/api/iterator"
10 | )
11 |
12 | type GCSCache struct {
13 | ctx context.Context
14 | noCache bool
15 | bucketName string
16 | projectId string
17 | region string
18 | session *storage.Client
19 | }
20 |
21 | type GCSCacheConfiguration struct {
22 | ProjectId string `mapstructure:"projectid" yaml:"projectid,omitempty"`
23 | Region string `mapstructure:"region" yaml:"region,omitempty"`
24 | BucketName string `mapstructure:"bucketname" yaml:"bucketname,omitempty"`
25 | }
26 |
27 | func (s *GCSCache) Configure(cacheInfo CacheProvider) error {
28 | s.ctx = context.Background()
29 | if cacheInfo.GCS.BucketName == "" {
30 | log.Fatal("Bucket name not configured")
31 | }
32 | if cacheInfo.GCS.Region == "" {
33 | log.Fatal("Region not configured")
34 | }
35 | if cacheInfo.GCS.ProjectId == "" {
36 | log.Fatal("ProjectID not configured")
37 | }
38 | s.bucketName = cacheInfo.GCS.BucketName
39 | s.projectId = cacheInfo.GCS.ProjectId
40 | s.region = cacheInfo.GCS.Region
41 | storageClient, err := storage.NewClient(s.ctx)
42 | if err != nil {
43 | log.Fatal(err)
44 | }
45 |
46 | _, err = storageClient.Bucket(s.bucketName).Attrs(s.ctx)
47 | if err == storage.ErrBucketNotExist {
48 | err = storageClient.Bucket(s.bucketName).Create(s.ctx, s.projectId, &storage.BucketAttrs{
49 | Location: s.region,
50 | })
51 | if err != nil {
52 | return err
53 | }
54 | }
55 | s.session = storageClient
56 | return nil
57 | }
58 |
59 | func (s *GCSCache) Store(key string, data string) error {
60 | wc := s.session.Bucket(s.bucketName).Object(key).NewWriter(s.ctx)
61 |
62 | if _, err := wc.Write([]byte(data)); err != nil {
63 | return err
64 | }
65 |
66 | if err := wc.Close(); err != nil {
67 | return err
68 | }
69 |
70 | return nil
71 | }
72 |
73 | func (s *GCSCache) Load(key string) (string, error) {
74 | reader, err := s.session.Bucket(s.bucketName).Object(key).NewReader(s.ctx)
75 | if err != nil {
76 | return "", err
77 | }
78 | defer reader.Close()
79 |
80 | data, err := io.ReadAll(reader)
81 | if err != nil {
82 | return "", err
83 | }
84 |
85 | return string(data), nil
86 | }
87 |
88 | func (s *GCSCache) Remove(key string) error {
89 | bucketClient := s.session.Bucket(s.bucketName)
90 | obj := bucketClient.Object(key)
91 | if err := obj.Delete(s.ctx); err != nil {
92 | return err
93 | }
94 | return nil
95 | }
96 |
97 | func (s *GCSCache) List() ([]CacheObjectDetails, error) {
98 | var files []CacheObjectDetails
99 |
100 | items := s.session.Bucket(s.bucketName).Objects(s.ctx, nil)
101 | for {
102 | attrs, err := items.Next()
103 | if err == iterator.Done {
104 | break
105 | }
106 | if err != nil {
107 | return nil, err
108 | }
109 | files = append(files, CacheObjectDetails{
110 | Name: attrs.Name,
111 | UpdatedAt: attrs.Updated,
112 | })
113 | }
114 | return files, nil
115 | }
116 |
117 | func (s *GCSCache) Exists(key string) bool {
118 | obj := s.session.Bucket(s.bucketName).Object(key)
119 | _, err := obj.Attrs(s.ctx)
120 | return err == nil
121 | }
122 |
123 | func (s *GCSCache) IsCacheDisabled() bool {
124 | return s.noCache
125 | }
126 |
127 | func (s *GCSCache) GetName() string {
128 | return "gcs"
129 | }
130 |
131 | func (s *GCSCache) DisableCache() {
132 | s.noCache = true
133 | }
134 |
--------------------------------------------------------------------------------
/pkg/cache/interplex_based_test.go:
--------------------------------------------------------------------------------
1 | package cache
2 |
3 | import (
4 | rpc "buf.build/gen/go/interplex-ai/schemas/grpc/go/protobuf/schema/v1/schemav1grpc"
5 | schemav1 "buf.build/gen/go/interplex-ai/schemas/protocolbuffers/go/protobuf/schema/v1"
6 | "context"
7 | "errors"
8 | "google.golang.org/grpc"
9 | "net"
10 | "testing"
11 | )
12 |
13 | func TestInterplexCache(t *testing.T) {
14 | cache := &InterplexCache{
15 | configuration: InterplexCacheConfiguration{
16 | ConnectionString: "localhost:50051",
17 | },
18 | }
19 |
20 | // Mock GRPC server setup
21 | go func() {
22 | lis, err := net.Listen("tcp", ":50051")
23 | if err != nil {
24 | t.Fatalf("failed to listen: %v", err)
25 | }
26 | s := grpc.NewServer()
27 | rpc.RegisterCacheServiceServer(s, &mockCacheService{})
28 | if err := s.Serve(lis); err != nil {
29 | t.Fatalf("failed to serve: %v", err)
30 | }
31 | }()
32 |
33 | t.Run("TestStore", func(t *testing.T) {
34 | err := cache.Store("key1", "value1")
35 | if err != nil {
36 | t.Errorf("Error storing value: %v", err)
37 | }
38 | })
39 |
40 | t.Run("TestLoad", func(t *testing.T) {
41 | value, err := cache.Load("key1")
42 | if err != nil {
43 | t.Errorf("Error loading value: %v", err)
44 | }
45 | if value != "value1" {
46 | t.Errorf("Expected value1, got %v", value)
47 | }
48 | })
49 |
50 | t.Run("TestExists", func(t *testing.T) {
51 | exists := cache.Exists("key1")
52 | if !exists {
53 | t.Errorf("Expected key1 to exist")
54 | }
55 | })
56 | }
57 |
58 | type mockCacheService struct {
59 | rpc.UnimplementedCacheServiceServer
60 | data map[string]string
61 | }
62 |
63 | func (m *mockCacheService) Set(ctx context.Context, req *schemav1.SetRequest) (*schemav1.SetResponse, error) {
64 | if m.data == nil {
65 | m.data = make(map[string]string)
66 | }
67 | m.data[req.Key] = req.Value
68 | return &schemav1.SetResponse{}, nil
69 | }
70 |
71 | func (m *mockCacheService) Get(ctx context.Context, req *schemav1.GetRequest) (*schemav1.GetResponse, error) {
72 | value, exists := m.data[req.Key]
73 | if !exists {
74 | return nil, errors.New("key not found")
75 | }
76 | return &schemav1.GetResponse{Value: value}, nil
77 | }
78 |
--------------------------------------------------------------------------------
/pkg/cache/types.go:
--------------------------------------------------------------------------------
1 | package cache
2 |
3 | import "time"
4 |
5 | type CacheProvider struct {
6 | CurrentCacheType string `mapstructure:"currentCacheType" yaml:"currentCacheType"`
7 | GCS GCSCacheConfiguration `mapstructure:"gcs" yaml:"gcs,omitempty"`
8 | Azure AzureCacheConfiguration `mapstructure:"azure" yaml:"azure,omitempty"`
9 | S3 S3CacheConfiguration `mapstructure:"s3" yaml:"s3,omitempty"`
10 | Interplex InterplexCacheConfiguration `mapstructure:"interplex" yaml:"interplex,omitempty"`
11 | }
12 |
13 | type CacheObjectDetails struct {
14 | Name string
15 | UpdatedAt time.Time
16 | }
17 |
--------------------------------------------------------------------------------
/pkg/common/types.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 | http://www.apache.org/licenses/LICENSE-2.0
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 | */
13 |
14 | package common
15 |
16 | import (
17 | "context"
18 | "time"
19 |
20 | openapi_v2 "github.com/google/gnostic/openapiv2"
21 | "github.com/k8sgpt-ai/k8sgpt/pkg/ai"
22 | "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
23 | keda "github.com/kedacore/keda/v2/apis/keda/v1alpha1"
24 | kyverno "github.com/kyverno/policy-reporter-kyverno-plugin/pkg/crd/api/policyreport/v1alpha2"
25 | regv1 "k8s.io/api/admissionregistration/v1"
26 | appsv1 "k8s.io/api/apps/v1"
27 | autov2 "k8s.io/api/autoscaling/v2"
28 | v1 "k8s.io/api/core/v1"
29 | networkv1 "k8s.io/api/networking/v1"
30 | policyv1 "k8s.io/api/policy/v1"
31 | gtwapi "sigs.k8s.io/gateway-api/apis/v1"
32 | )
33 |
34 | type IAnalyzer interface {
35 | Analyze(analysis Analyzer) ([]Result, error)
36 | }
37 |
38 | type Analyzer struct {
39 | Client *kubernetes.Client
40 | Context context.Context
41 | Namespace string
42 | LabelSelector string
43 | AIClient ai.IAI
44 | PreAnalysis map[string]PreAnalysis
45 | Results []Result
46 | OpenapiSchema *openapi_v2.Document
47 | }
48 |
49 | type PreAnalysis struct {
50 | Pod v1.Pod
51 | FailureDetails []Failure
52 | Deployment appsv1.Deployment
53 | ReplicaSet appsv1.ReplicaSet
54 | PersistentVolumeClaim v1.PersistentVolumeClaim
55 | Endpoint v1.Endpoints
56 | Ingress networkv1.Ingress
57 | HorizontalPodAutoscalers autov2.HorizontalPodAutoscaler
58 | PodDisruptionBudget policyv1.PodDisruptionBudget
59 | StatefulSet appsv1.StatefulSet
60 | NetworkPolicy networkv1.NetworkPolicy
61 | Node v1.Node
62 | ValidatingWebhook regv1.ValidatingWebhookConfiguration
63 | MutatingWebhook regv1.MutatingWebhookConfiguration
64 | GatewayClass gtwapi.GatewayClass
65 | Gateway gtwapi.Gateway
66 | HTTPRoute gtwapi.HTTPRoute
67 | // Integrations
68 | ScaledObject keda.ScaledObject
69 | KyvernoPolicyReport kyverno.PolicyReport
70 | KyvernoClusterPolicyReport kyverno.ClusterPolicyReport
71 | }
72 |
73 | type Result struct {
74 | Kind string `json:"kind"`
75 | Name string `json:"name"`
76 | Error []Failure `json:"error"`
77 | Details string `json:"details"`
78 | ParentObject string `json:"parentObject"`
79 | }
80 |
81 | type AnalysisStats struct {
82 | Analyzer string `json:"analyzer"`
83 | DurationTime time.Duration `json:"durationTime"`
84 | }
85 |
86 | type Failure struct {
87 | Text string
88 | KubernetesDoc string
89 | Sensitive []Sensitive
90 | }
91 |
92 | type Sensitive struct {
93 | Unmasked string
94 | Masked string
95 | }
96 |
--------------------------------------------------------------------------------
/pkg/custom/client.go:
--------------------------------------------------------------------------------
1 | package custom
2 |
3 | import (
4 | rpc "buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go/schema/v1/schemav1grpc"
5 | schemav1 "buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go/schema/v1"
6 | "context"
7 | "fmt"
8 | "github.com/k8sgpt-ai/k8sgpt/pkg/common"
9 | "google.golang.org/grpc"
10 | "google.golang.org/grpc/credentials/insecure"
11 | )
12 |
13 | type Client struct {
14 | c *grpc.ClientConn
15 | analyzerClient rpc.CustomAnalyzerServiceClient
16 | }
17 |
18 | func NewClient(c Connection) (*Client, error) {
19 |
20 | //nolint:staticcheck // Ignoring SA1019 for compatibility reasons
21 | conn, err := grpc.Dial(fmt.Sprintf("%s:%s", c.Url, c.Port), grpc.WithTransportCredentials(insecure.NewCredentials()))
22 |
23 | if err != nil {
24 | return nil, err
25 | }
26 | client := rpc.NewCustomAnalyzerServiceClient(conn)
27 | return &Client{
28 | c: conn,
29 | analyzerClient: client,
30 | }, nil
31 | }
32 |
33 | func (cli *Client) Run() (common.Result, error) {
34 | var result common.Result
35 | req := &schemav1.RunRequest{}
36 | res, err := cli.analyzerClient.Run(context.Background(), req)
37 | if err != nil {
38 | return result, err
39 | }
40 | if res.Result != nil {
41 |
42 | // We should refactor this, because Error and Failure do not map 1:1 from K8sGPT/schema
43 | var errorsFound []common.Failure
44 | for _, e := range res.Result.Error {
45 | errorsFound = append(errorsFound, common.Failure{
46 | Text: e.Text,
47 | // TODO: Support sensitive data
48 | })
49 | }
50 |
51 | result.Name = res.Result.Name
52 | result.Kind = res.Result.Kind
53 | result.Details = res.Result.Details
54 | result.ParentObject = res.Result.ParentObject
55 | result.Error = errorsFound
56 | }
57 | return result, nil
58 | }
59 |
--------------------------------------------------------------------------------
/pkg/custom/types.go:
--------------------------------------------------------------------------------
1 | package custom
2 |
3 | type Connection struct {
4 | Url string `json:"url"`
5 | Port string `json:"port"`
6 | }
7 | type CustomAnalyzer struct {
8 | Name string `json:"name"`
9 | Connection Connection `json:"connection"`
10 | }
11 |
--------------------------------------------------------------------------------
/pkg/custom_analyzer/customAnalyzer.go:
--------------------------------------------------------------------------------
1 | package custom_analyzer
2 |
3 | import (
4 | "fmt"
5 | "reflect"
6 | "regexp"
7 | )
8 |
9 | type CustomAnalyzerConfiguration struct {
10 | Name string `mapstructure:"name"`
11 | Connection Connection `mapstructure:"connection"`
12 | }
13 |
14 | type Connection struct {
15 | Url string `mapstructure:"url"`
16 | Port int `mapstructure:"port"`
17 | }
18 |
19 | type CustomAnalyzer struct{}
20 |
21 | func NewCustomAnalyzer() *CustomAnalyzer {
22 | return &CustomAnalyzer{}
23 | }
24 |
25 | func (*CustomAnalyzer) Check(actualConfig []CustomAnalyzerConfiguration, name, url string, port int) error {
26 | validNameRegex := `^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*
27 | validName := regexp.MustCompile(validNameRegex)
28 | if !validName.MatchString(name) {
29 | return fmt.Errorf("invalid name format. Must match %s", validNameRegex)
30 | }
31 |
32 | for _, analyzer := range actualConfig {
33 | if analyzer.Name == name {
34 | return fmt.Errorf("custom analyzer with the name '%s' already exists. Please use a different name", name)
35 | }
36 |
37 | if reflect.DeepEqual(analyzer.Connection, Connection{
38 | Url: url,
39 | Port: port,
40 | }) {
41 | return fmt.Errorf("custom analyzer with the same connection configuration (URL: '%s', Port: %d) already exists. Please use a different URL or port", url, port)
42 | }
43 | }
44 |
45 | return nil
46 | }
47 |
--------------------------------------------------------------------------------
/pkg/integration/aws/aws.go:
--------------------------------------------------------------------------------
1 | package aws
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/aws/aws-sdk-go/aws"
7 | "github.com/aws/aws-sdk-go/aws/session"
8 | "github.com/k8sgpt-ai/k8sgpt/pkg/common"
9 | "github.com/spf13/viper"
10 | )
11 |
12 | type AWS struct {
13 | sess *session.Session
14 | }
15 |
16 | func (a *AWS) Deploy(namespace string) error {
17 |
18 | return nil
19 | }
20 |
21 | func (a *AWS) UnDeploy(namespace string) error {
22 | a.sess = nil
23 | return nil
24 | }
25 |
26 | func (a *AWS) AddAnalyzer(mergedMap *map[string]common.IAnalyzer) {
27 | // Retrieve AWS credentials from the environment
28 | accessKeyID := os.Getenv("AWS_ACCESS_KEY_ID")
29 | secretAccessKey := os.Getenv("AWS_SECRET_ACCESS_KEY")
30 | awsProfile := os.Getenv("AWS_PROFILE")
31 |
32 | var sess *session.Session
33 | if accessKeyID != "" && secretAccessKey != "" {
34 | // Use access keys if both are provided
35 | sess = session.Must(session.NewSessionWithOptions(session.Options{
36 | Config: aws.Config{},
37 | }))
38 | } else {
39 | // Use AWS profile, default to "default" if not set
40 | if awsProfile == "" {
41 | awsProfile = "default"
42 | }
43 | sess = session.Must(session.NewSessionWithOptions(session.Options{
44 | Profile: awsProfile,
45 | SharedConfigState: session.SharedConfigEnable,
46 | }))
47 | }
48 |
49 | a.sess = sess
50 | (*mergedMap)["EKS"] = &EKSAnalyzer{
51 | session: a.sess,
52 | }
53 | }
54 |
55 | func (a *AWS) GetAnalyzerName() []string {
56 |
57 | return []string{"EKS"}
58 | }
59 |
60 | func (a *AWS) GetNamespace() (string, error) {
61 |
62 | return "", nil
63 | }
64 |
65 | func (a *AWS) OwnsAnalyzer(s string) bool {
66 | for _, az := range a.GetAnalyzerName() {
67 | if s == az {
68 | return true
69 | }
70 | }
71 | return false
72 | }
73 |
74 | func (a *AWS) isFilterActive() bool {
75 | activeFilters := viper.GetStringSlice("active_filters")
76 |
77 | for _, filter := range a.GetAnalyzerName() {
78 | for _, af := range activeFilters {
79 | if af == filter {
80 | return true
81 | }
82 | }
83 | }
84 |
85 | return false
86 | }
87 |
88 | func (a *AWS) IsActivate() bool {
89 | if a.isFilterActive() {
90 | return true
91 | } else {
92 | return false
93 | }
94 | }
95 |
96 | func NewAWS() *AWS {
97 | return &AWS{}
98 | }
99 |
--------------------------------------------------------------------------------
/pkg/integration/aws/eks.go:
--------------------------------------------------------------------------------
1 | package aws
2 |
3 | import (
4 | "errors"
5 | "github.com/spf13/viper"
6 | "os"
7 | "path/filepath"
8 | "strings"
9 |
10 | "github.com/aws/aws-sdk-go/aws/session"
11 | "github.com/aws/aws-sdk-go/service/eks"
12 | "github.com/k8sgpt-ai/k8sgpt/pkg/common"
13 | "k8s.io/client-go/tools/clientcmd"
14 | )
15 |
16 | type EKSAnalyzer struct {
17 | session *session.Session
18 | }
19 |
20 | func (e *EKSAnalyzer) Analyze(analysis common.Analyzer) ([]common.Result, error) {
21 | var cr []common.Result = []common.Result{}
22 | _ = map[string]common.PreAnalysis{}
23 | svc := eks.New(e.session)
24 | // Get the name of the current cluster
25 | var kubeconfig string
26 | kubeconfigFromPath := viper.GetString("kubeconfig")
27 | if kubeconfigFromPath != "" {
28 | kubeconfig = kubeconfigFromPath
29 | } else {
30 | kubeconfig = filepath.Join(os.Getenv("HOME"), ".kube", "config")
31 | }
32 | config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
33 | &clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfig},
34 | &clientcmd.ConfigOverrides{
35 | CurrentContext: "",
36 | }).RawConfig()
37 | if err != nil {
38 | return cr, err
39 | }
40 | currentConfig := config.CurrentContext
41 |
42 | if !strings.Contains(currentConfig, "eks") {
43 | return cr, errors.New("EKS cluster was not detected")
44 | }
45 |
46 | input := &eks.ListClustersInput{}
47 | result, err := svc.ListClusters(input)
48 | if err != nil {
49 | return cr, err
50 | }
51 | for _, cluster := range result.Clusters {
52 | // describe the cluster
53 | if !strings.Contains(currentConfig, *cluster) {
54 | continue
55 | }
56 | input := &eks.DescribeClusterInput{
57 | Name: cluster,
58 | }
59 | result, err := svc.DescribeCluster(input)
60 | if err != nil {
61 | return cr, err
62 | }
63 | if len(result.Cluster.Health.Issues) > 0 {
64 | for _, issue := range result.Cluster.Health.Issues {
65 | err := make([]common.Failure, 0)
66 | err = append(err, common.Failure{
67 | Text: issue.String(),
68 | KubernetesDoc: "",
69 | Sensitive: nil,
70 | })
71 | cr = append(cr, common.Result{
72 | Kind: "EKS",
73 | Name: "AWS/EKS",
74 | Error: err,
75 | })
76 | }
77 | }
78 | }
79 | return cr, nil
80 | }
81 |
--------------------------------------------------------------------------------
/pkg/integration/kyverno/analyzer_test.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 | http://www.apache.org/licenses/LICENSE-2.0
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 | */
13 |
14 | package kyverno
15 |
16 | import (
17 | "context"
18 | "testing"
19 |
20 | "github.com/k8sgpt-ai/k8sgpt/pkg/common"
21 | "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
22 | "github.com/kyverno/policy-reporter-kyverno-plugin/pkg/crd/api/policyreport/v1alpha2"
23 |
24 | "github.com/stretchr/testify/assert"
25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26 | "k8s.io/apimachinery/pkg/runtime"
27 | "sigs.k8s.io/controller-runtime/pkg/client"
28 | "sigs.k8s.io/controller-runtime/pkg/client/fake"
29 | )
30 |
31 | func buildFakeClient(t *testing.T) client.Client {
32 | objects := []client.Object{
33 | &v1alpha2.PolicyReport{
34 | ObjectMeta: metav1.ObjectMeta{
35 | Name: "policy-1",
36 | Namespace: "test-ns",
37 | },
38 | Results: []v1alpha2.PolicyReportResult{
39 | {
40 | Category: "Other",
41 | Message: "validation failure: Images built more than 6 months ago are prohibited.",
42 | Policy: "block-stale-images",
43 | Result: "fail",
44 | },
45 | },
46 | },
47 | &v1alpha2.PolicyReport{
48 | ObjectMeta: metav1.ObjectMeta{
49 | Name: "policy-2",
50 | Namespace: "other-ns",
51 | },
52 | Results: []v1alpha2.PolicyReportResult{
53 | {
54 | Category: "Other",
55 | Message: "validation failure: Images built more than 6 months ago are prohibited.",
56 | Policy: "block-stale-images",
57 | Result: "fail",
58 | },
59 | },
60 | },
61 | }
62 |
63 | scheme := runtime.NewScheme()
64 | err := v1alpha2.AddToScheme(scheme)
65 | if err != nil {
66 | t.Error(err)
67 | }
68 | return fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build()
69 | }
70 |
71 | func TestAnalyzerNamespaceFiltering(t *testing.T) {
72 |
73 | config := common.Analyzer{
74 | Client: &kubernetes.Client{
75 | CtrlClient: buildFakeClient(t),
76 | },
77 | Context: context.Background(),
78 | Namespace: "test-ns",
79 | }
80 |
81 | // Create and run analyzer
82 | analyzer := KyvernoAnalyzer{
83 | policyReportAnalysis: true,
84 | }
85 | results, err := analyzer.Analyze(config)
86 | if err != nil {
87 | t.Error(err)
88 | }
89 |
90 | // Verify results
91 | assert.Equal(t, len(results), 1)
92 | assert.Equal(t, results[0].Kind, "PolicyReport")
93 | assert.Equal(t, results[0].Name, "test-ns/policy-1")
94 | }
95 |
96 | func TestAnalyzerAllNamespace(t *testing.T) {
97 |
98 | config := common.Analyzer{
99 | Client: &kubernetes.Client{
100 | CtrlClient: buildFakeClient(t),
101 | },
102 | Context: context.Background(),
103 | }
104 |
105 | // Create and run analyzer
106 | analyzer := KyvernoAnalyzer{
107 | policyReportAnalysis: true,
108 | }
109 | results, err := analyzer.Analyze(config)
110 | if err != nil {
111 | t.Error(err)
112 | }
113 |
114 | // Verify results
115 | assert.Equal(t, len(results), 2)
116 |
117 | }
118 |
--------------------------------------------------------------------------------
/pkg/integration/kyverno/kyverno.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 | http://www.apache.org/licenses/LICENSE-2.0
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 | */
13 |
14 | package kyverno
15 |
16 | import (
17 | "os"
18 |
19 | "github.com/fatih/color"
20 | "github.com/k8sgpt-ai/k8sgpt/pkg/common"
21 | "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
22 | "github.com/spf13/viper"
23 | )
24 |
25 | type Kyverno struct{}
26 |
27 | func NewKyverno() *Kyverno {
28 | return &Kyverno{}
29 | }
30 |
31 | func (k *Kyverno) GetAnalyzerName() []string {
32 | return []string{
33 | //from wgpolicyk8s.io/v1alpha2
34 | "PolicyReport",
35 | "ClusterPolicyReport",
36 | }
37 | }
38 |
39 | func (k *Kyverno) OwnsAnalyzer(analyzer string) bool {
40 |
41 | for _, a := range k.GetAnalyzerName() {
42 | if analyzer == a {
43 | return true
44 | }
45 | }
46 | return false
47 | }
48 |
49 | func (k *Kyverno) isDeployed() bool {
50 | // check if wgpolicyk8s apigroup is available as a marker if new policy resource available is installed on the cluster
51 | kubecontext := viper.GetString("kubecontext")
52 | kubeconfig := viper.GetString("kubeconfig")
53 | client, err := kubernetes.NewClient(kubecontext, kubeconfig)
54 | if err != nil {
55 | // TODO: better error handling
56 | color.Red("Error initialising kubernetes client: %v", err)
57 | os.Exit(1)
58 | }
59 | groups, _, err := client.Client.Discovery().ServerGroupsAndResources()
60 | if err != nil {
61 | // TODO: better error handling
62 | color.Red("Error initialising discovery client: %v", err)
63 | os.Exit(1)
64 | }
65 |
66 | for _, group := range groups {
67 | if group.Name == "kyverno.io" {
68 | return true
69 | }
70 | }
71 |
72 | return false
73 | }
74 |
75 | func (k *Kyverno) isFilterActive() bool {
76 | activeFilters := viper.GetStringSlice("active_filters")
77 |
78 | for _, filter := range k.GetAnalyzerName() {
79 | for _, af := range activeFilters {
80 | if af == filter {
81 | return true
82 | }
83 | }
84 | }
85 |
86 | return false
87 | }
88 |
89 | func (k *Kyverno) IsActivate() bool {
90 | if k.isFilterActive() && k.isDeployed() {
91 | return true
92 | } else {
93 | return false
94 | }
95 | }
96 |
97 | func (k *Kyverno) AddAnalyzer(mergedMap *map[string]common.IAnalyzer) {
98 |
99 | (*mergedMap)["PolicyReport"] = &KyvernoAnalyzer{
100 | policyReportAnalysis: true,
101 | }
102 | (*mergedMap)["ClusterPolicyReport"] = &KyvernoAnalyzer{
103 | clusterReportAnalysis: true,
104 | }
105 | }
106 |
107 | func (k *Kyverno) Deploy(namespace string) error {
108 | return nil
109 | }
110 |
111 | func (k *Kyverno) UnDeploy(_ string) error {
112 | return nil
113 | }
114 |
115 | func (t *Kyverno) GetNamespace() (string, error) {
116 | return "", nil
117 | }
118 |
--------------------------------------------------------------------------------
/pkg/integration/prometheus/prometheus.go:
--------------------------------------------------------------------------------
1 | package prometheus
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "os"
8 |
9 | "github.com/fatih/color"
10 | "github.com/k8sgpt-ai/k8sgpt/pkg/common"
11 | "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
12 | "github.com/spf13/viper"
13 | )
14 |
15 | const (
16 | ConfigValidate = "PrometheusConfigValidate"
17 | ConfigRelabel = "PrometheusConfigRelabelReport"
18 | )
19 |
20 | type Prometheus struct {
21 | }
22 |
23 | func NewPrometheus() *Prometheus {
24 | return &Prometheus{}
25 | }
26 |
27 | func (p *Prometheus) Deploy(namespace string) error {
28 | // no-op
29 | color.Green("Activating prometheus integration...")
30 | // TODO(pintohutch): add timeout or inherit an upstream context
31 | // for better signal management.
32 | ctx := context.Background()
33 | kubecontext := viper.GetString("kubecontext")
34 | kubeconfig := viper.GetString("kubeconfig")
35 | client, err := kubernetes.NewClient(kubecontext, kubeconfig)
36 | if err != nil {
37 | color.Red("Error initialising kubernetes client: %v", err)
38 | os.Exit(1)
39 | }
40 |
41 | // We just care about existing deployments.
42 | // Try and find Prometheus configurations in the cluster using the provided namespace.
43 | //
44 | // Note: We could cache this state and inject it into the various analyzers
45 | // to save additional parsing later.
46 | // However, the state of the cluster can change from activation to analysis,
47 | // so we would want to run this again on each analyze call anyway.
48 | //
49 | // One consequence of this is one can run `activate` in one namespace
50 | // and run `analyze` in another, without issues, as long as Prometheus
51 | // is found in both.
52 | // We accept this as a trade-off for the time-being to avoid having the tool
53 | // manage Prometheus on the behalf of users.
54 | podConfigs, err := findPrometheusPodConfigs(ctx, client.GetClient(), namespace)
55 | if err != nil {
56 | color.Red("Error discovering Prometheus workloads: %v", err)
57 | os.Exit(1)
58 | }
59 | if len(podConfigs) == 0 {
60 | color.Yellow(fmt.Sprintf(`Prometheus installation not found in namespace: %s.
61 | Please ensure Prometheus is deployed to analyze.`, namespace))
62 | return errors.New("no prometheus installation found")
63 | }
64 | // Prime state of the analyzer so
65 | color.Green("Found existing installation")
66 | return nil
67 | }
68 |
69 | func (p *Prometheus) UnDeploy(_ string) error {
70 | // no-op
71 | // We just care about existing deployments.
72 | color.Yellow("Integration will leave Prometheus resources deployed. This is an effective no-op in the cluster.")
73 | return nil
74 | }
75 |
76 | func (p *Prometheus) AddAnalyzer(mergedMap *map[string]common.IAnalyzer) {
77 | (*mergedMap)[ConfigValidate] = &ConfigAnalyzer{}
78 | (*mergedMap)[ConfigRelabel] = &RelabelAnalyzer{}
79 | }
80 |
81 | func (p *Prometheus) GetAnalyzerName() []string {
82 | return []string{ConfigValidate, ConfigRelabel}
83 | }
84 |
85 | func (p *Prometheus) GetNamespace() (string, error) {
86 | return "", nil
87 | }
88 |
89 | func (p *Prometheus) OwnsAnalyzer(analyzer string) bool {
90 | return (analyzer == ConfigValidate) || (analyzer == ConfigRelabel)
91 | }
92 |
93 | func (t *Prometheus) IsActivate() bool {
94 | activeFilters := viper.GetStringSlice("active_filters")
95 |
96 | for _, filter := range t.GetAnalyzerName() {
97 | for _, af := range activeFilters {
98 | if af == filter {
99 | return true
100 | }
101 | }
102 | }
103 |
104 | return false
105 | }
106 |
--------------------------------------------------------------------------------
/pkg/integration/prometheus/relabel_analyzer.go:
--------------------------------------------------------------------------------
1 | package prometheus
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/k8sgpt-ai/k8sgpt/pkg/common"
7 | "github.com/k8sgpt-ai/k8sgpt/pkg/util"
8 | discoverykube "github.com/prometheus/prometheus/discovery/kubernetes"
9 | "gopkg.in/yaml.v2"
10 | )
11 |
12 | type RelabelAnalyzer struct {
13 | }
14 |
15 | func (r *RelabelAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
16 | ctx := a.Context
17 | client := a.Client.GetClient()
18 | namespace := a.Namespace
19 | kind := ConfigRelabel
20 |
21 | podConfigs, err := findPrometheusPodConfigs(ctx, client, namespace)
22 | if err != nil {
23 | return nil, err
24 | }
25 |
26 | var preAnalysis = map[string]common.PreAnalysis{}
27 | for _, pc := range podConfigs {
28 | var failures []common.Failure
29 | pod := pc.pod
30 |
31 | // Check upstream validation.
32 | // The Prometheus configuration structs do not generally have validation
33 | // methods and embed their validation logic in the UnmarshalYAML methods.
34 | config, _ := unmarshalPromConfigBytes(pc.b)
35 | // Limit output for brevity.
36 | limit := 6
37 | i := 0
38 | for _, sc := range config.ScrapeConfigs {
39 | if i == limit {
40 | break
41 | }
42 | if sc == nil {
43 | continue
44 | }
45 | brc, _ := yaml.Marshal(sc.RelabelConfigs)
46 | var bsd []byte
47 | for _, cfg := range sc.ServiceDiscoveryConfigs {
48 | ks, ok := cfg.(*discoverykube.SDConfig)
49 | if !ok {
50 | continue
51 | }
52 | bsd, _ = yaml.Marshal(ks)
53 | }
54 | // Don't bother with relabel analysis if the scrape config
55 | // or service discovery config are empty.
56 | if len(brc) == 0 || len(bsd) == 0 {
57 | continue
58 | }
59 | failures = append(failures, common.Failure{
60 | Text: fmt.Sprintf("job_name:\n%s\nrelabel_configs:\n%s\nkubernetes_sd_configs:\n%s\n", sc.JobName, string(brc), string(bsd)),
61 | })
62 | i++
63 | }
64 |
65 | if len(failures) > 0 {
66 | preAnalysis[fmt.Sprintf("%s/%s", pod.Namespace, pod.Name)] = common.PreAnalysis{
67 | Pod: *pod,
68 | FailureDetails: failures,
69 | }
70 | }
71 | }
72 |
73 | for key, value := range preAnalysis {
74 | var currentAnalysis = common.Result{
75 | Kind: kind,
76 | Name: key,
77 | Error: value.FailureDetails,
78 | }
79 | parent, _ := util.GetParent(a.Client, value.Pod.ObjectMeta)
80 | currentAnalysis.ParentObject = parent
81 | a.Results = append(a.Results, currentAnalysis)
82 | }
83 |
84 | return a.Results, nil
85 | }
86 |
--------------------------------------------------------------------------------
/pkg/kubernetes/apireference.go:
--------------------------------------------------------------------------------
1 | package kubernetes
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 |
7 | openapi_v2 "github.com/google/gnostic/openapiv2"
8 | )
9 |
10 | func (k *K8sApiReference) GetApiDocV2(field string) string {
11 | startPoint := ""
12 | // the path must be formated like "path1.path2.path3"
13 | paths := strings.Split(field, ".")
14 | group := strings.Split(k.ApiVersion.Group, ".")
15 | definitions := k.OpenapiSchema.GetDefinitions().GetAdditionalProperties()
16 |
17 | // extract the startpoint by searching the highest leaf corresponding to the requested group qnd kind
18 | for _, prop := range definitions {
19 | if strings.HasSuffix(prop.GetName(), fmt.Sprintf("%s.%s.%s", group[0], k.ApiVersion.Version, k.Kind)) {
20 | startPoint = prop.GetName()
21 |
22 | break
23 | }
24 | }
25 |
26 | // recursively parse the definitions to find the description of the latest part of the given path
27 | description := k.recursePath(definitions, startPoint, paths)
28 |
29 | return description
30 | }
31 |
32 | func (k *K8sApiReference) recursePath(definitions []*openapi_v2.NamedSchema, leaf string, paths []string) string {
33 | description := ""
34 |
35 | for _, prop := range definitions {
36 | // search the requested leaf
37 | if prop.GetName() == leaf {
38 | for _, addProp := range prop.GetValue().GetProperties().GetAdditionalProperties() {
39 | // search the additional property of the leaf corresponding the current path
40 | if addProp.GetName() == paths[0] {
41 | // the last path or the path is string, we get the description and we go out
42 | if len(paths) == 1 || addProp.GetValue().GetType().String() == "value:\"string\"" {
43 | // extract the path description as we are at the end of the paths
44 | description = addProp.GetValue().Description
45 | } else {
46 | // the path is an object, we extract the xref
47 | if addProp.GetValue().GetXRef() != "" {
48 | splitRef := strings.Split(addProp.GetValue().GetXRef(), "/")
49 | reducedPaths := paths[1:]
50 | description = k.recursePath(definitions, splitRef[len(splitRef)-1], reducedPaths)
51 | }
52 |
53 | // the path is an array, we take the first xref from the items
54 | if len(addProp.GetValue().GetItems().GetSchema()) == 1 {
55 | splitRef := strings.Split(addProp.GetValue().GetItems().GetSchema()[0].GetXRef(), "/")
56 | reducedPaths := paths[1:]
57 | description = k.recursePath(definitions, splitRef[len(splitRef)-1], reducedPaths)
58 | }
59 | }
60 |
61 | break
62 | }
63 | }
64 |
65 | break
66 | }
67 | }
68 |
69 | return description
70 | }
71 |
--------------------------------------------------------------------------------
/pkg/kubernetes/apireference_test.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2024 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 | http://www.apache.org/licenses/LICENSE-2.0
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 | */
13 |
14 | package kubernetes
15 |
16 | import (
17 | "testing"
18 |
19 | openapi_v2 "github.com/google/gnostic/openapiv2"
20 | "github.com/stretchr/testify/require"
21 | "k8s.io/apimachinery/pkg/runtime/schema"
22 | )
23 |
24 | func TestGetApiDocV2(t *testing.T) {
25 | k8s := &K8sApiReference{
26 | ApiVersion: schema.GroupVersion{
27 | Group: "group.v1",
28 | Version: "v1",
29 | },
30 | OpenapiSchema: &openapi_v2.Document{
31 | Definitions: &openapi_v2.Definitions{
32 | AdditionalProperties: []*openapi_v2.NamedSchema{
33 | {
34 | Name: "group.v1.kind",
35 | Value: &openapi_v2.Schema{
36 | Title: "test",
37 | Properties: &openapi_v2.Properties{
38 | AdditionalProperties: []*openapi_v2.NamedSchema{
39 | {
40 | Name: "schema1",
41 | Value: &openapi_v2.Schema{
42 | Title: "test",
43 | Description: "schema1 description",
44 | Type: &openapi_v2.TypeItem{
45 | Value: []string{"string"},
46 | },
47 | },
48 | },
49 | {
50 | Name: "schema2",
51 | Value: &openapi_v2.Schema{
52 | Items: &openapi_v2.ItemsItem{
53 | Schema: []*openapi_v2.Schema{
54 | {
55 | Title: "random-schema",
56 | },
57 | },
58 | },
59 | Title: "test",
60 | XRef: "xref",
61 | Description: "schema2 description",
62 | Type: &openapi_v2.TypeItem{
63 | Value: []string{"bool"},
64 | },
65 | },
66 | },
67 | },
68 | },
69 | },
70 | },
71 | {
72 | Name: "group",
73 | },
74 | },
75 | },
76 | },
77 | Kind: "kind",
78 | }
79 |
80 | tests := []struct {
81 | name string
82 | field string
83 | expectedOutput string
84 | }{
85 | {
86 | name: "empty field",
87 | },
88 | {
89 | name: "2 schemas",
90 | field: "schema2.schema1",
91 | expectedOutput: "",
92 | },
93 | {
94 | name: "schema1 description",
95 | field: "schema1",
96 | expectedOutput: "schema1 description",
97 | },
98 | }
99 |
100 | for _, tt := range tests {
101 | t.Run(tt.name, func(t *testing.T) {
102 | output := k8s.GetApiDocV2(tt.field)
103 | require.Equal(t, tt.expectedOutput, output)
104 | })
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/pkg/kubernetes/kubernetes.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 | http://www.apache.org/licenses/LICENSE-2.0
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 | */
13 |
14 | package kubernetes
15 |
16 | import (
17 | "k8s.io/client-go/kubernetes"
18 | _ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
19 | "k8s.io/client-go/rest"
20 | "k8s.io/client-go/tools/clientcmd"
21 | ctrl "sigs.k8s.io/controller-runtime/pkg/client"
22 | )
23 |
24 | func (c *Client) GetConfig() *rest.Config {
25 | return c.Config
26 | }
27 |
28 | func (c *Client) GetClient() kubernetes.Interface {
29 | return c.Client
30 | }
31 |
32 | func (c *Client) GetCtrlClient() ctrl.Client {
33 | return c.CtrlClient
34 | }
35 |
36 | func NewClient(kubecontext string, kubeconfig string) (*Client, error) {
37 | var config *rest.Config
38 | config, err := rest.InClusterConfig()
39 | if kubeconfig != "" || err != nil {
40 | loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
41 |
42 | if kubeconfig != "" {
43 | loadingRules.ExplicitPath = kubeconfig
44 | }
45 |
46 | clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
47 | loadingRules,
48 | &clientcmd.ConfigOverrides{
49 | CurrentContext: kubecontext,
50 | })
51 | // create the clientset
52 | config, err = clientConfig.ClientConfig()
53 | if err != nil {
54 | return nil, err
55 | }
56 | }
57 | clientSet, err := kubernetes.NewForConfig(config)
58 | if err != nil {
59 | return nil, err
60 | }
61 |
62 | ctrlClient, err := ctrl.New(config, ctrl.Options{})
63 | if err != nil {
64 | return nil, err
65 | }
66 |
67 | serverVersion, err := clientSet.ServerVersion()
68 | if err != nil {
69 | return nil, err
70 | }
71 |
72 | return &Client{
73 | Client: clientSet,
74 | CtrlClient: ctrlClient,
75 | Config: config,
76 | ServerVersion: serverVersion,
77 | }, nil
78 | }
79 |
--------------------------------------------------------------------------------
/pkg/kubernetes/types.go:
--------------------------------------------------------------------------------
1 | package kubernetes
2 |
3 | import (
4 | openapi_v2 "github.com/google/gnostic/openapiv2"
5 | "k8s.io/apimachinery/pkg/runtime/schema"
6 | "k8s.io/apimachinery/pkg/version"
7 | "k8s.io/client-go/kubernetes"
8 | "k8s.io/client-go/rest"
9 | ctrl "sigs.k8s.io/controller-runtime/pkg/client"
10 | )
11 |
12 | type Client struct {
13 | Client kubernetes.Interface
14 | CtrlClient ctrl.Client
15 | Config *rest.Config
16 | ServerVersion *version.Info
17 | }
18 |
19 | type K8sApiReference struct {
20 | ApiVersion schema.GroupVersion
21 | Kind string
22 | OpenapiSchema *openapi_v2.Document
23 | }
24 |
--------------------------------------------------------------------------------
/pkg/server/README.md:
--------------------------------------------------------------------------------
1 | # K8sGPT MCP Server
2 |
3 | This directory contains the implementation of the Mission Control Protocol (MCP) server for K8sGPT. The MCP server allows K8sGPT to be integrated with other tools that support the MCP protocol.
4 |
5 | ## Components
6 |
7 | - `mcp.go`: The main MCP server implementation
8 | - `server.go`: The HTTP server implementation
9 | - `tools.go`: Tool definitions for the MCP server
10 |
11 | ## Features
12 |
13 | The MCP server provides the following features:
14 |
15 | 1. **Analyze Kubernetes Resources**: Analyze Kubernetes resources in a cluster
16 | 2. **Get Cluster Information**: Retrieve information about the Kubernetes cluster
17 |
18 | ## Usage
19 |
20 | To use the MCP server, you need to:
21 |
22 | 1. Initialize the MCP server with a Kubernetes client
23 | 2. Start the server
24 | 3. Connect to the server using an MCP client
25 |
26 | Example:
27 |
28 | ```go
29 | client, err := kubernetes.NewForConfig(config)
30 | if err != nil {
31 | log.Fatalf("Failed to create Kubernetes client: %v", err)
32 | }
33 |
34 | mcpServer := server.NewMCPServer(client)
35 | if err := mcpServer.Start(); err != nil {
36 | log.Fatalf("Failed to start MCP server: %v", err)
37 | }
38 | ```
39 |
40 | ## Integration
41 |
42 | The MCP server can be integrated with other tools that support the MCP protocol, such as:
43 |
44 | - Mission Control
45 | - Other MCP-compatible tools
46 |
47 | ## License
48 |
49 | This code is licensed under the Apache License 2.0.
50 |
--------------------------------------------------------------------------------
/pkg/server/analyze/analyze.go:
--------------------------------------------------------------------------------
1 | package analyze
2 |
3 | import (
4 | "context"
5 | json "encoding/json"
6 |
7 | schemav1 "buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go/schema/v1"
8 | "github.com/k8sgpt-ai/k8sgpt/pkg/analysis"
9 | )
10 |
11 | func (h *Handler) Analyze(ctx context.Context, i *schemav1.AnalyzeRequest) (
12 | *schemav1.AnalyzeResponse,
13 | error,
14 | ) {
15 | if i.Output == "" {
16 | i.Output = "json"
17 | }
18 |
19 | if int(i.MaxConcurrency) == 0 {
20 | i.MaxConcurrency = 10
21 | }
22 |
23 | config, err := analysis.NewAnalysis(
24 | i.Backend,
25 | i.Language,
26 | i.Filters,
27 | i.Namespace,
28 | i.LabelSelector,
29 | i.Nocache,
30 | i.Explain,
31 | int(i.MaxConcurrency),
32 | false, // Kubernetes Doc disabled in server mode
33 | false, // Interactive mode disabled in server mode
34 | []string{}, //TODO: add custom http headers in server mode
35 | false, // with stats disable
36 | )
37 | if err != nil {
38 | return &schemav1.AnalyzeResponse{}, err
39 | }
40 | config.Context = ctx // Replace context for correct timeouts.
41 | defer config.Close()
42 |
43 | if config.CustomAnalyzersAreAvailable() {
44 | config.RunCustomAnalysis()
45 | }
46 | config.RunAnalysis()
47 |
48 | if i.Explain {
49 | err := config.GetAIResults(i.Output, i.Anonymize)
50 | if err != nil {
51 | return &schemav1.AnalyzeResponse{}, err
52 | }
53 | }
54 |
55 | out, err := config.PrintOutput(i.Output)
56 | if err != nil {
57 | return &schemav1.AnalyzeResponse{}, err
58 | }
59 | var obj schemav1.AnalyzeResponse
60 |
61 | err = json.Unmarshal(out, &obj)
62 | if err != nil {
63 | return &schemav1.AnalyzeResponse{}, err
64 | }
65 |
66 | return &obj, nil
67 | }
68 |
--------------------------------------------------------------------------------
/pkg/server/analyze/handler.go:
--------------------------------------------------------------------------------
1 | package analyze
2 |
3 | import rpc "buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go/schema/v1/schemav1grpc"
4 |
5 | type Handler struct {
6 | rpc.UnimplementedServerAnalyzerServiceServer
7 | }
8 |
--------------------------------------------------------------------------------
/pkg/server/client_example/README.md:
--------------------------------------------------------------------------------
1 | # K8sGPT MCP Client Example
2 |
3 | This directory contains an example of how to use the K8sGPT MCP client in a real-world scenario.
4 |
5 | ## Prerequisites
6 |
7 | - Go 1.16 or later
8 | - Access to a Kubernetes cluster
9 | - `kubectl` configured to access your cluster
10 |
11 | ## Building the Example
12 |
13 | To build the example, run:
14 |
15 | ```bash
16 | go build -o mcp-client-example
17 | ```
18 |
19 | ## Running the Example
20 |
21 | To run the example, use the following command:
22 |
23 | ```bash
24 | ./mcp-client-example --kubeconfig=/path/to/kubeconfig --namespace=default
25 | ```
26 |
27 | ### Command-line Flags
28 |
29 | - `--kubeconfig`: Path to the kubeconfig file (optional, defaults to the standard location)
30 | - `--namespace`: Kubernetes namespace to analyze (optional)
31 |
32 | ## Example Output
33 |
34 | When you run the example, you should see output similar to the following:
35 |
36 | ```
37 | Starting MCP client...
38 | ```
39 |
40 | The client will continue running until you press Ctrl+C to stop it.
41 |
42 | ## Integration with Mission Control
43 |
44 | To integrate this example with Mission Control, you need to:
45 |
46 | 1. Start the MCP client using the example
47 | 2. Configure Mission Control to connect to the MCP client
48 | 3. Use Mission Control to analyze your Kubernetes cluster
49 |
50 | ## Troubleshooting
51 |
52 | If you encounter any issues, check the following:
53 |
54 | 1. Ensure that your Kubernetes cluster is accessible
55 | 2. Verify that your kubeconfig file is valid
56 | 3. Check that the namespace you specified exists
57 |
58 | ## License
59 |
60 | This code is licensed under the Apache License 2.0.
--------------------------------------------------------------------------------
/pkg/server/client_example/main.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2024 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 | http://www.apache.org/licenses/LICENSE-2.0
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 | */
13 |
14 | package main
15 |
16 | import (
17 | "bytes"
18 | "encoding/json"
19 | "flag"
20 | "fmt"
21 | "io"
22 | "log"
23 | "net/http"
24 | "time"
25 | )
26 |
27 | // AnalyzeRequest represents the input parameters for the analyze tool
28 | type AnalyzeRequest struct {
29 | Namespace string `json:"namespace,omitempty"`
30 | Backend string `json:"backend,omitempty"`
31 | Language string `json:"language,omitempty"`
32 | Filters []string `json:"filters,omitempty"`
33 | LabelSelector string `json:"labelSelector,omitempty"`
34 | NoCache bool `json:"noCache,omitempty"`
35 | Explain bool `json:"explain,omitempty"`
36 | MaxConcurrency int `json:"maxConcurrency,omitempty"`
37 | WithDoc bool `json:"withDoc,omitempty"`
38 | InteractiveMode bool `json:"interactiveMode,omitempty"`
39 | CustomHeaders []string `json:"customHeaders,omitempty"`
40 | WithStats bool `json:"withStats,omitempty"`
41 | }
42 |
43 | // AnalyzeResponse represents the output of the analyze tool
44 | type AnalyzeResponse struct {
45 | Content []struct {
46 | Text string `json:"text"`
47 | Type string `json:"type"`
48 | } `json:"content"`
49 | }
50 |
51 | func main() {
52 | // Parse command line flags
53 | serverPort := flag.String("port", "8089", "Port of the MCP server")
54 | namespace := flag.String("namespace", "", "Kubernetes namespace to analyze")
55 | backend := flag.String("backend", "", "AI backend to use")
56 | language := flag.String("language", "english", "Language for analysis")
57 | flag.Parse()
58 |
59 | // Create analyze request
60 | req := AnalyzeRequest{
61 | Namespace: *namespace,
62 | Backend: *backend,
63 | Language: *language,
64 | Explain: true,
65 | MaxConcurrency: 10,
66 | }
67 |
68 | // Convert request to JSON
69 | reqJSON, err := json.Marshal(req)
70 | if err != nil {
71 | log.Fatalf("Failed to marshal request: %v", err)
72 | }
73 |
74 | // Create HTTP client with timeout
75 | client := &http.Client{
76 | Timeout: 5 * time.Minute,
77 | }
78 |
79 | // Send request to MCP server
80 | resp, err := client.Post(
81 | fmt.Sprintf("http://localhost:%s/mcp/analyze", *serverPort),
82 | "application/json",
83 | bytes.NewBuffer(reqJSON),
84 | )
85 | if err != nil {
86 | log.Fatalf("Failed to send request: %v", err)
87 | }
88 | defer func() {
89 | if err := resp.Body.Close(); err != nil {
90 | log.Printf("Error closing response body: %v", err)
91 | }
92 | }()
93 |
94 | // Read and print raw response for debugging
95 | body, err := io.ReadAll(resp.Body)
96 | if err != nil {
97 | log.Fatalf("Failed to read response body: %v", err)
98 | }
99 | fmt.Printf("Raw response: %s\n", string(body))
100 |
101 | // Parse response
102 | var analyzeResp AnalyzeResponse
103 | if err := json.Unmarshal(body, &analyzeResp); err != nil {
104 | log.Fatalf("Failed to decode response: %v", err)
105 | }
106 |
107 | // Print results
108 | fmt.Println("Analysis Results:")
109 | if len(analyzeResp.Content) > 0 {
110 | fmt.Println(analyzeResp.Content[0].Text)
111 | } else {
112 | fmt.Println("No results returned")
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/pkg/server/config/handler.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | rpc "buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go/schema/v1/schemav1grpc"
5 | schemav1 "buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go/schema/v1"
6 | "context"
7 | )
8 |
9 | type Handler struct {
10 | rpc.UnimplementedServerConfigServiceServer
11 | }
12 |
13 | func (h *Handler) Shutdown(ctx context.Context, request *schemav1.ShutdownRequest) (*schemav1.ShutdownResponse, error) {
14 | //TODO implement me
15 | panic("implement me")
16 | }
17 |
--------------------------------------------------------------------------------
/pkg/server/example/main.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2024 The K8sGPT Authors.
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 | http://www.apache.org/licenses/LICENSE-2.0
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 | */
13 |
14 | package main
15 |
16 | import (
17 | "flag"
18 | "log"
19 | "os"
20 | "os/signal"
21 | "syscall"
22 |
23 | "github.com/k8sgpt-ai/k8sgpt/pkg/ai"
24 | "github.com/k8sgpt-ai/k8sgpt/pkg/server"
25 | "go.uber.org/zap"
26 | )
27 |
28 | func main() {
29 | // Parse command line flags
30 | port := flag.String("port", "8089", "Port to run the MCP server on")
31 | useHTTP := flag.Bool("http", false, "Enable HTTP mode for MCP server")
32 | flag.Parse()
33 |
34 | // Initialize zap logger
35 | logger, err := zap.NewProduction()
36 | if err != nil {
37 | log.Fatalf("Error creating logger: %v", err)
38 | }
39 | defer func() {
40 | if err := logger.Sync(); err != nil {
41 | log.Printf("Error syncing logger: %v", err)
42 | }
43 | }()
44 |
45 | // Create AI provider
46 | aiProvider := &ai.AIProvider{
47 | Name: "openai",
48 | Password: os.Getenv("OPENAI_API_KEY"),
49 | Model: "gpt-3.5-turbo",
50 | }
51 |
52 | // Create and start MCP server
53 | mcpServer, err := server.NewMCPServer(*port, aiProvider, *useHTTP, logger)
54 | if err != nil {
55 | log.Fatalf("Error creating MCP server: %v", err)
56 | }
57 |
58 | // Start the server in a goroutine
59 | go func() {
60 | if err := mcpServer.Start(); err != nil {
61 | log.Fatalf("Error starting MCP server: %v", err)
62 | }
63 | }()
64 |
65 | // Handle graceful shutdown
66 | sigChan := make(chan os.Signal, 1)
67 | signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
68 | <-sigChan
69 |
70 | // Cleanup
71 | if err := mcpServer.Close(); err != nil {
72 | log.Printf("Error closing MCP server: %v", err)
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/pkg/server/log.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "time"
7 |
8 | "go.uber.org/zap"
9 | "google.golang.org/grpc"
10 | "google.golang.org/grpc/peer"
11 | "google.golang.org/grpc/status"
12 | )
13 |
14 | func LogInterceptor(logger *zap.Logger) grpc.UnaryServerInterceptor {
15 | return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
16 | start := time.Now()
17 |
18 | // Call the handler to execute the gRPC request
19 | response, err := handler(ctx, req)
20 |
21 | duration := time.Since(start).Milliseconds()
22 | fields := []zap.Field{
23 | zap.Int64("duration_ms", duration),
24 | zap.String("method", info.FullMethod),
25 | zap.Any("request", req),
26 | }
27 | // Get the remote address from the context
28 | peer, ok := peer.FromContext(ctx)
29 | if ok {
30 | fields = append(fields, zap.String("remote_addr", peer.Addr.String()))
31 | }
32 |
33 | if err != nil {
34 | fields = append(fields, zap.Int32("status_code", int32(status.Code(err))))
35 | }
36 | message := "request completed"
37 | if err != nil {
38 | message = fmt.Sprintf("request failed. %s", err.Error())
39 | }
40 | logRequest(logger, fields, int(status.Code(err)), message)
41 |
42 | return response, err
43 | }
44 | }
45 |
46 | func logRequest(logger *zap.Logger, fields []zap.Field, statusCode int, message string) {
47 | if statusCode >= 400 {
48 | logger.Error(message, fields...)
49 | } else {
50 | logger.Info(message, fields...)
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/pkg/server/query/handler.go:
--------------------------------------------------------------------------------
1 | package query
2 |
3 | import rpc "buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go/schema/v1/schemav1grpc"
4 |
5 | type Handler struct {
6 | rpc.UnimplementedServerQueryServiceServer
7 | }
8 |
--------------------------------------------------------------------------------
/pkg/server/query/query.go:
--------------------------------------------------------------------------------
1 | package query
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | schemav1 "buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go/schema/v1"
8 | "github.com/k8sgpt-ai/k8sgpt/pkg/ai"
9 | )
10 |
11 | func (h *Handler) Query(ctx context.Context, i *schemav1.QueryRequest) (
12 | *schemav1.QueryResponse,
13 | error,
14 | ) {
15 | // Create client factory and config provider
16 | factory := ai.GetAIClientFactory()
17 | configProvider := ai.GetConfigProvider()
18 |
19 | // Use the factory to create the client
20 | aiClient := factory.NewClient(i.Backend)
21 | defer aiClient.Close()
22 |
23 | var configAI ai.AIConfiguration
24 | if err := configProvider.UnmarshalKey("ai", &configAI); err != nil {
25 | return &schemav1.QueryResponse{
26 | Response: "",
27 | Error: &schemav1.QueryError{
28 | Message: fmt.Sprintf("Failed to unmarshal AI configuration: %v", err),
29 | },
30 | }, nil
31 | }
32 |
33 | var aiProvider ai.AIProvider
34 | for _, provider := range configAI.Providers {
35 | if i.Backend == provider.Name {
36 | aiProvider = provider
37 | break
38 | }
39 | }
40 | if aiProvider.Name == "" {
41 | return &schemav1.QueryResponse{
42 | Response: "",
43 | Error: &schemav1.QueryError{
44 | Message: fmt.Sprintf("AI provider %s not found in configuration", i.Backend),
45 | },
46 | }, nil
47 | }
48 |
49 | // Configure the AI client
50 | if err := aiClient.Configure(&aiProvider); err != nil {
51 | return &schemav1.QueryResponse{
52 | Response: "",
53 | Error: &schemav1.QueryError{
54 | Message: fmt.Sprintf("Failed to configure AI client: %v", err),
55 | },
56 | }, nil
57 | }
58 |
59 | resp, err := aiClient.GetCompletion(ctx, i.Query)
60 | var errMessage string = ""
61 | if err != nil {
62 | errMessage = err.Error()
63 | }
64 | return &schemav1.QueryResponse{
65 | Response: resp,
66 | Error: &schemav1.QueryError{
67 | Message: errMessage,
68 | },
69 | }, nil
70 | }
71 |
--------------------------------------------------------------------------------
/pkg/server/server_test.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "context"
5 | "net"
6 | "testing"
7 | "time"
8 |
9 | "github.com/stretchr/testify/assert"
10 | "go.uber.org/zap"
11 | "google.golang.org/grpc"
12 | "google.golang.org/grpc/reflection/grpc_reflection_v1alpha"
13 | )
14 |
15 | func TestServe(t *testing.T) {
16 | logger, _ := zap.NewDevelopment()
17 | defer logger.Sync()
18 |
19 | s := &Config{
20 | Port: "50059",
21 | Logger: logger,
22 | EnableHttp: false,
23 | }
24 |
25 | go func() {
26 | err := s.Serve()
27 | time.Sleep(time.Second * 2)
28 | assert.NoError(t, err, "Serve should not return an error")
29 | }()
30 |
31 | // Wait until the server is ready to accept connections
32 | err := waitForPort("localhost:50059", 10*time.Second)
33 | assert.NoError(t, err, "Server should start without error")
34 |
35 | conn, err := grpc.Dial("localhost:50059", grpc.WithInsecure())
36 | assert.NoError(t, err, "Should be able to dial the server")
37 | defer conn.Close()
38 |
39 | // Test a simple gRPC reflection request
40 | cli := grpc_reflection_v1alpha.NewServerReflectionClient(conn)
41 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
42 | defer cancel()
43 | resp, err := cli.ServerReflectionInfo(ctx)
44 | assert.NoError(t, err, "Should be able to get server reflection info")
45 | assert.NotNil(t, resp, "Response should not be nil")
46 |
47 | // Cleanup
48 | err = s.Shutdown()
49 | assert.NoError(t, err, "Shutdown should not return an error")
50 | }
51 |
52 | func waitForPort(address string, timeout time.Duration) error {
53 | start := time.Now()
54 | for {
55 | conn, err := net.Dial("tcp", address)
56 | if err == nil {
57 | conn.Close()
58 | return nil
59 | }
60 | if time.Since(start) > timeout {
61 | return err
62 | }
63 | time.Sleep(100 * time.Millisecond)
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/release-please-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "packages": {
3 | ".": {
4 | "changelog-path": "CHANGELOG.md",
5 | "release-type": "go",
6 | "prerelease": false,
7 | "bump-minor-pre-major": true,
8 | "bump-patch-for-minor-pre-major": true,
9 | "draft": false,
10 | "extra-files": [
11 | "README.md",
12 | "deploy/manifest.yaml",
13 | "chart/Chart.yaml",
14 | "chart/values.yaml",
15 | "container/manifests/deployment.yaml"
16 | ],
17 | "changelog-sections": [
18 | {
19 | "type": "feat",
20 | "section": "Features"
21 | },
22 | {
23 | "type": "fix",
24 | "section": "Bug Fixes"
25 | },
26 | {
27 | "type": "chore",
28 | "section": "Other"
29 | },
30 | {
31 | "type": "docs",
32 | "section": "Docs"
33 | },
34 | {
35 | "type": "perf",
36 | "section": "Performance"
37 | },
38 | {
39 | "type": "build",
40 | "hidden": true,
41 | "section": "Build"
42 | },
43 | {
44 | "type": "deps",
45 | "section": "Dependency Updates"
46 | },
47 | {
48 | "type": "ci",
49 | "hidden": true,
50 | "section": "CI"
51 | },
52 | {
53 | "type": "refactor",
54 | "section": "Refactoring"
55 | },
56 | {
57 | "type": "revert",
58 | "hidden": true,
59 | "section": "Reverts"
60 | },
61 | {
62 | "type": "style",
63 | "hidden": true,
64 | "section": "Styling"
65 | },
66 | {
67 | "type": "test",
68 | "hidden": true,
69 | "section": "Tests"
70 | }
71 | ]
72 | }
73 | },
74 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json"
75 | }
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:base",
5 | "helpers:pinGitHubActionDigests",
6 | ":gitSignOff",
7 | "group:allNonMajor"
8 | ],
9 | "addLabels": ["dependencies"],
10 | "postUpdateOptions": [
11 | "gomodTidy",
12 | "gomodMassage"
13 | ],
14 | "automerge": true,
15 | "automergeType": "pr",
16 | "schedule": [
17 | "at any time"
18 | ],
19 | "platformAutomerge": true,
20 | "packageRules": [
21 | {
22 | "matchPackageNames": ["azure-sdk-for-go"],
23 | "enabled": true,
24 | "groupName": "azure-group"
25 | },
26 | {
27 | "matchPackageNames": ["prometheus"],
28 | "enabled": true,
29 | "groupName": "prometheus-group"
30 | },
31 | {
32 | "matchPackageNames": ["k8s.io", "sigs.k8s.io"],
33 | "enabled": true,
34 | "groupName": "kubernetes-group"
35 | },
36 | {
37 | "matchPackageNames": ["golang"],
38 | "enabled": true,
39 | "groupName": "golang-group"
40 | },
41 | {
42 | "matchUpdateTypes": ["minor", "patch"],
43 | "matchCurrentVersion": "!/^0/",
44 | "automerge": true
45 | },
46 | {
47 | "matchManagers": ["gomod"],
48 | "addLabels": ["go"]
49 | },
50 | {
51 | "matchManagers": ["github-actions"],
52 | "addLabels": ["github_actions"]
53 | },
54 | {
55 | "matchManagers": ["dockerfile"],
56 | "addLabels": ["docker"]
57 | }
58 | ],
59 | "regexManagers": [
60 | {
61 | "fileMatch": [
62 | "(^|\\/)Makefile
quot;,
63 | "(^|\\/)Dockerfile",
64 | "(^|\\/).*\\.ya?ml
quot;,
65 | "(^|\\/).*\\.toml
quot;,
66 | "(^|\\/).*\\.sh
quot;
67 | ],
68 | "matchStrings": [
69 | "# renovate: datasource=(?<datasource>.+?) depName=(?<depName>.+?)\\s.*?_VERSION ?(\\??=|\\: ?) ?\\\"?(?<currentValue>.+?)?\\\"?\\s"
70 | ]
71 | }
72 | ]
73 | }
74 |
--------------------------------------------------------------------------------